mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4eb4e8c1f8 | ||
|
|
984df15949 | ||
|
|
aa3bd24332 | ||
|
|
250889623d | ||
|
|
862fa55986 | ||
|
|
0f2889904a | ||
|
|
49f14c7929 | ||
|
|
c3f3c5d5df | ||
|
|
274259f305 | ||
|
|
a15d7a9cf3 | ||
|
|
7d154fc28d | ||
|
|
6727d3be83 | ||
|
|
4bbd5c3bcb | ||
|
|
5995f2e427 | ||
|
|
0910c8e100 | ||
|
|
8df76dfa90 | ||
|
|
78e9e66cb0 | ||
|
|
71c61fc4b3 | ||
|
|
a728aa8689 | ||
|
|
5a3174a720 | ||
|
|
652072d660 | ||
|
|
323f5c0c15 | ||
|
|
229d78c78b | ||
|
|
9d9a955149 | ||
|
|
b40a047190 | ||
|
|
ff9e415823 | ||
|
|
2f7a4fb2b3 | ||
|
|
f8a654a5b6 | ||
|
|
4449475467 | ||
|
|
bcbbf0dd42 | ||
|
|
19b9ba52d4 | ||
|
|
bdc30db2dc | ||
|
|
74634ba96b | ||
|
|
77feb05bad | ||
|
|
a1f05d006e | ||
|
|
cc3dae1335 | ||
|
|
d9debcfb0d | ||
|
|
8770d16d52 | ||
|
|
84953481e4 | ||
|
|
df7aa59f6f | ||
|
|
008b83119d | ||
|
|
15dafcb6ff | ||
|
|
7d291ed888 | ||
|
|
a7221f0750 | ||
|
|
9a5b2a5d30 | ||
|
|
7361a00d42 | ||
|
|
e07d402782 | ||
|
|
a34354bee1 | ||
|
|
84b762b529 | ||
|
|
2a839951b4 | ||
|
|
26d7c2f047 | ||
|
|
151a5b85f3 | ||
|
|
3f506f53b4 | ||
|
|
95f734e4b8 | ||
|
|
ff74574e84 | ||
|
|
843f6c405f | ||
|
|
f8b24ebfce | ||
|
|
d1d26b5424 | ||
|
|
8aa6f45da8 | ||
|
|
48d5bee95b | ||
|
|
98c79d2dec | ||
|
|
0f51238f62 | ||
|
|
b7323b929f | ||
|
|
05eb1537f7 | ||
|
|
bc9f27cfed | ||
|
|
8f457be719 | ||
|
|
6d590e13c9 | ||
|
|
c987ec172a | ||
|
|
49f1637c47 | ||
|
|
13379bd699 | ||
|
|
5ee762c08c | ||
|
|
edb6d561f1 | ||
|
|
1b90f77e82 | ||
|
|
c37785ecbc | ||
|
|
c67188edf8 | ||
|
|
8630c68631 | ||
|
|
ddcb5c5d40 | ||
|
|
07f0fbb50f | ||
|
|
3781b0aa72 | ||
|
|
42a5cea521 | ||
|
|
fb6000ca2c | ||
|
|
59f98db754 | ||
|
|
6add842a27 | ||
|
|
43ca4a6365 | ||
|
|
68196d6af0 | ||
|
|
a582d39bb9 | ||
|
|
7a3e89c4d8 | ||
|
|
87f036ad88 | ||
|
|
5738b8dd58 | ||
|
|
8d73371e0f | ||
|
|
3d870b2305 | ||
|
|
27d8cd57bd | ||
|
|
353b2e7a94 | ||
|
|
d77bd87acb | ||
|
|
bae7b924ae | ||
|
|
89867fdde3 | ||
|
|
c9a1f067ac | ||
|
|
eb26835d57 | ||
|
|
7301661468 | ||
|
|
e4fe984a70 | ||
|
|
12ccf622c1 | ||
|
|
6c35ff6d1c | ||
|
|
1d7c84302e | ||
|
|
f3e8889710 | ||
|
|
4b2f9ff306 | ||
|
|
b4ba642f8e | ||
|
|
5c69b965c0 | ||
|
|
cba196385c | ||
|
|
00abad56cb | ||
|
|
d1070c4955 | ||
|
|
e84be523c6 | ||
|
|
5d598e8268 | ||
|
|
0e9d922204 | ||
|
|
80c1b04e83 | ||
|
|
b2fca3edf3 | ||
|
|
2a292c8e80 | ||
|
|
e6052ad0d8 | ||
|
|
8646e17d5a | ||
|
|
e6785b0afd | ||
|
|
009033a411 | ||
|
|
fdc3d7742a | ||
|
|
f9ccca1376 | ||
|
|
0a00fe6ab8 | ||
|
|
6253e80a70 | ||
|
|
3dcb7c813f | ||
|
|
39cdc41a67 | ||
|
|
d3000487f4 | ||
|
|
80254da1c1 | ||
|
|
73fc2d92f7 | ||
|
|
f69aa33f3d | ||
|
|
e16202c9f7 | ||
|
|
7453d0d274 | ||
|
|
c102828c93 | ||
|
|
afeb6dd318 | ||
|
|
6156623f0e | ||
|
|
ccc06fcc3d | ||
|
|
bbe3942056 | ||
|
|
49cea59b94 | ||
|
|
ce1323e849 | ||
|
|
fc3c1e65e3 | ||
|
|
f1ebd8f912 | ||
|
|
98f5bba832 | ||
|
|
e864e8a47f | ||
|
|
a573545ead | ||
|
|
15ff4386e5 | ||
|
|
7a4aa3bf87 | ||
|
|
c1ba6288c8 | ||
|
|
d0ba180729 | ||
|
|
2edc76414b | ||
|
|
452db5dc39 | ||
|
|
21c4c53feb | ||
|
|
6292789cac | ||
|
|
d445bbc02d | ||
|
|
8627f58cbc | ||
|
|
66ab0059bb | ||
|
|
3f06932277 | ||
|
|
59cf1b435e | ||
|
|
c9a5b0e66e | ||
|
|
bcd5e1da98 | ||
|
|
f74ad050b4 | ||
|
|
b6f43c9b23 | ||
|
|
298c64a119 | ||
|
|
8b80679b3a | ||
|
|
82a2bc561a | ||
|
|
e019542d3f | ||
|
|
9356247ef0 | ||
|
|
4db77c18e3 | ||
|
|
0d0b248988 | ||
|
|
44f3910266 | ||
|
|
35c6798a80 | ||
|
|
0995c7ee63 | ||
|
|
0a25f78329 | ||
|
|
bb8fffc491 | ||
|
|
6f6c51fb32 | ||
|
|
39d14c15d6 | ||
|
|
17de28965b | ||
|
|
ae7865e508 | ||
|
|
dc8e8cc5c9 | ||
|
|
5208470c38 | ||
|
|
e7ef7b552f | ||
|
|
a478112b07 | ||
|
|
7d0f32e1f9 | ||
|
|
d35d8bead3 | ||
|
|
ab9cc52bcc | ||
|
|
7dc342a6f9 | ||
|
|
80a3ef9193 | ||
|
|
bb723d20cb | ||
|
|
0c24a2016c | ||
|
|
2124965996 | ||
|
|
6f1e104067 | ||
|
|
b20f40b5a3 | ||
|
|
1093ca5fc4 | ||
|
|
0d99c55d3f | ||
|
|
86dc1a88ed | ||
|
|
968c20d8ca | ||
|
|
1b2d978449 | ||
|
|
30582b2d75 | ||
|
|
38a8037a4d | ||
|
|
67de45f787 | ||
|
|
f358f24abb | ||
|
|
fd4e15003a | ||
|
|
81df249d5c | ||
|
|
ba60997992 | ||
|
|
8961294a29 | ||
|
|
bc2e0a000a | ||
|
|
1a693fba2d | ||
|
|
05b7acf6aa | ||
|
|
de13d8d397 | ||
|
|
6d7d8ae87c | ||
|
|
cfb1bdcd52 | ||
|
|
48d41833f1 | ||
|
|
01535f1afc | ||
|
|
fe845bfa36 | ||
|
|
b94dbc75b7 | ||
|
|
bdf40640d0 | ||
|
|
496f6c706b | ||
|
|
79f97d5555 | ||
|
|
82f679c84c | ||
|
|
9b789889fa | ||
|
|
dd8dd118da | ||
|
|
8222a4b6a4 | ||
|
|
31f8057bfa | ||
|
|
1d8e0417bd | ||
|
|
8100ca46c2 | ||
|
|
94d745cf1c | ||
|
|
a92bd0ad0f | ||
|
|
94a13ffca2 | ||
|
|
64c8707957 | ||
|
|
d421a73988 | ||
|
|
f7eef1f8d6 | ||
|
|
08c780d309 | ||
|
|
baaf1f510f | ||
|
|
8898dcc054 | ||
|
|
33521c8aeb | ||
|
|
9db03bd7b9 | ||
|
|
bd600c1fba | ||
|
|
82b18b08f1 | ||
|
|
0aefe616b3 | ||
|
|
59a66bbd16 | ||
|
|
155c708e4f | ||
|
|
5502c2ab1b | ||
|
|
e89b42de40 | ||
|
|
2562c4f432 | ||
|
|
17323074ca | ||
|
|
8a4c5c5e09 | ||
|
|
3033faff0d | ||
|
|
0e6aab0e8d | ||
|
|
75e3dd839a | ||
|
|
830e6eb88d | ||
|
|
3c8fae78d1 | ||
|
|
441c1d7044 | ||
|
|
203f4633f7 | ||
|
|
e558f4d9fb | ||
|
|
990e4020e9 | ||
|
|
1ad3e45519 | ||
|
|
442ced8381 | ||
|
|
ba46202a4d | ||
|
|
1c77a1ee90 | ||
|
|
ea74b59535 | ||
|
|
0e9343d9e2 | ||
|
|
af4cf7257f | ||
|
|
c8d4ee58f8 | ||
|
|
65e57c22e5 | ||
|
|
7351ae0a77 | ||
|
|
f1e0d5d137 | ||
|
|
8a1b71688c | ||
|
|
8ef1c309e0 | ||
|
|
a32dcc0dc5 | ||
|
|
16a2fde4b7 | ||
|
|
7856f7a134 | ||
|
|
c545924fe9 | ||
|
|
b1510bb88c | ||
|
|
31d2024ce5 | ||
|
|
23c49f8706 | ||
|
|
62eb0cda6b | ||
|
|
9f48c6e625 | ||
|
|
83ce401cf2 | ||
|
|
541b025ad0 | ||
|
|
5fd018df46 | ||
|
|
2822b0086a | ||
|
|
e27606f443 | ||
|
|
6c95cd5945 | ||
|
|
d723146060 | ||
|
|
3b03e61ae3 | ||
|
|
172513b96c | ||
|
|
68be42a48d | ||
|
|
0370e363bc | ||
|
|
ced88476cc | ||
|
|
c5d71505ef | ||
|
|
489f9859be | ||
|
|
a87ca65839 | ||
|
|
fa78d0976d | ||
|
|
4aaab2563b | ||
|
|
e073cf2c27 | ||
|
|
173d406b1f | ||
|
|
3bcbad6f04 | ||
|
|
e67854fe56 | ||
|
|
7510e0cb6b | ||
|
|
30c913b54f | ||
|
|
2cda9cd736 | ||
|
|
342d29f272 |
No files matched your search
@@ -8,7 +8,8 @@
|
|||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"vue/no-deprecated-html-element-is": 0,
|
"vue/no-deprecated-html-element-is": 0,
|
||||||
"vue/no-mutating-props": 0
|
"vue/no-mutating-props": 0,
|
||||||
|
"vue/no-v-html": 0
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2022,
|
"ecmaVersion": 2022,
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Create .version file
|
||||||
|
run: echo ${{ github.ref_name }} > .version
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
+2
-2
@@ -30,7 +30,7 @@ Metrics/ParameterLists:
|
|||||||
Max: 10
|
Max: 10
|
||||||
|
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Max: 25
|
Max: 30
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'db/migrate/**'
|
- 'db/migrate/**'
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ Layout/LineLength:
|
|||||||
AllowedPatterns: ['\A\s*#']
|
AllowedPatterns: ['\A\s*#']
|
||||||
|
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Max: 35
|
Max: 40
|
||||||
|
|
||||||
Metrics/ModuleLength:
|
Metrics/ModuleLength:
|
||||||
Max: 500
|
Max: 500
|
||||||
|
|||||||
+12
-5
@@ -1,4 +1,10 @@
|
|||||||
FROM ruby:3.2.2-alpine as webpack
|
FROM ruby:3.2.2-alpine3.18 as fonts
|
||||||
|
|
||||||
|
WORKDIR /fonts
|
||||||
|
|
||||||
|
RUN apk --no-cache add 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://github.com/impallari/DancingScript/raw/master/OFL.txt
|
||||||
|
|
||||||
|
FROM ruby:3.2.2-alpine3.18 as webpack
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
@@ -24,18 +30,18 @@ COPY ./app/views ./app/views
|
|||||||
|
|
||||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||||
|
|
||||||
FROM ruby:3.2.2-alpine as app
|
FROM ruby:3.2.2-alpine3.18 as app
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV BUNDLE_WITHOUT="development:test"
|
ENV BUNDLE_WITHOUT="development:test"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont ttf-liberation && mkdir /fonts && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf /fonts && apk del ttf-liberation && wget -O /fonts/DancingScript.otf "https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf" && wget -O /fonts/DancingScript-License.txt https://github.com/impallari/DancingScript/blob/master/OFL.txt
|
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont && mkdir /fonts
|
||||||
|
|
||||||
COPY ./Gemfile ./Gemfile.lock ./
|
COPY ./Gemfile ./Gemfile.lock ./
|
||||||
|
|
||||||
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle
|
RUN bundle update --bundler && bundle install && 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
|
||||||
@@ -45,8 +51,9 @@ COPY ./log ./log
|
|||||||
COPY ./lib ./lib
|
COPY ./lib ./lib
|
||||||
COPY ./public ./public
|
COPY ./public ./public
|
||||||
COPY ./tmp ./tmp
|
COPY ./tmp ./tmp
|
||||||
COPY LICENSE README.md Rakefile config.ru ./
|
COPY LICENSE README.md Rakefile config.ru .version ./
|
||||||
|
|
||||||
|
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||||
COPY --from=webpack /app/public/packs ./public/packs
|
COPY --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
RUN ln -s /fonts /app/public/fonts
|
RUN ln -s /fonts /app/public/fonts
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ source 'https://rubygems.org'
|
|||||||
|
|
||||||
ruby '3.2.2'
|
ruby '3.2.2'
|
||||||
|
|
||||||
|
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
|
||||||
@@ -21,13 +22,13 @@ gem 'jwt'
|
|||||||
gem 'lograge'
|
gem 'lograge'
|
||||||
gem 'mysql2', require: false
|
gem 'mysql2', require: false
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
gem 'omniauth-google-oauth2'
|
|
||||||
gem 'omniauth-rails_csrf_protection'
|
|
||||||
gem 'pagy'
|
gem 'pagy'
|
||||||
gem 'pdf-reader'
|
gem 'pdf-reader'
|
||||||
gem 'pg', require: false
|
gem 'pg', require: false
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
|
gem 'pretender'
|
||||||
gem 'puma'
|
gem 'puma'
|
||||||
|
gem 'rack'
|
||||||
gem 'rails'
|
gem 'rails'
|
||||||
gem 'rails_autolink'
|
gem 'rails_autolink'
|
||||||
gem 'rails-i18n'
|
gem 'rails-i18n'
|
||||||
@@ -41,6 +42,7 @@ gem 'sidekiq', require: ENV.key?('REDIS_URL')
|
|||||||
gem 'sqlite3', require: false
|
gem 'sqlite3', require: false
|
||||||
gem 'strip_attributes'
|
gem 'strip_attributes'
|
||||||
gem 'turbo-rails'
|
gem 'turbo-rails'
|
||||||
|
gem 'twitter_cldr', require: false
|
||||||
gem 'tzinfo-data'
|
gem 'tzinfo-data'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
|
|||||||
+199
-193
@@ -2,93 +2,103 @@ GEM
|
|||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
Ascii85 (1.1.0)
|
Ascii85 (1.1.0)
|
||||||
actioncable (7.0.7)
|
actioncable (7.1.2)
|
||||||
actionpack (= 7.0.7)
|
actionpack (= 7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (7.0.7)
|
zeitwerk (~> 2.6)
|
||||||
actionpack (= 7.0.7)
|
actionmailbox (7.1.2)
|
||||||
activejob (= 7.0.7)
|
actionpack (= 7.1.2)
|
||||||
activerecord (= 7.0.7)
|
activejob (= 7.1.2)
|
||||||
activestorage (= 7.0.7)
|
activerecord (= 7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activestorage (= 7.1.2)
|
||||||
|
activesupport (= 7.1.2)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
actionmailer (7.0.7)
|
actionmailer (7.1.2)
|
||||||
actionpack (= 7.0.7)
|
actionpack (= 7.1.2)
|
||||||
actionview (= 7.0.7)
|
actionview (= 7.1.2)
|
||||||
activejob (= 7.0.7)
|
activejob (= 7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.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.0)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (7.0.7)
|
actionpack (7.1.2)
|
||||||
actionview (= 7.0.7)
|
actionview (= 7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.2)
|
||||||
rack (~> 2.0, >= 2.2.4)
|
nokogiri (>= 1.8.5)
|
||||||
|
racc
|
||||||
|
rack (>= 2.2.4)
|
||||||
|
rack-session (>= 1.0.1)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.6)
|
||||||
actiontext (7.0.7)
|
actiontext (7.1.2)
|
||||||
actionpack (= 7.0.7)
|
actionpack (= 7.1.2)
|
||||||
activerecord (= 7.0.7)
|
activerecord (= 7.1.2)
|
||||||
activestorage (= 7.0.7)
|
activestorage (= 7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.2)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (7.0.7)
|
actionview (7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.2)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.6)
|
||||||
activejob (7.0.7)
|
activejob (7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.2)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (7.0.7)
|
activemodel (7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.2)
|
||||||
activerecord (7.0.7)
|
activerecord (7.1.2)
|
||||||
activemodel (= 7.0.7)
|
activemodel (= 7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.2)
|
||||||
activestorage (7.0.7)
|
timeout (>= 0.4.0)
|
||||||
actionpack (= 7.0.7)
|
activestorage (7.1.2)
|
||||||
activejob (= 7.0.7)
|
actionpack (= 7.1.2)
|
||||||
activerecord (= 7.0.7)
|
activejob (= 7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activerecord (= 7.1.2)
|
||||||
|
activesupport (= 7.1.2)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
activesupport (7.1.2)
|
||||||
activesupport (7.0.7)
|
base64
|
||||||
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
|
connection_pool (>= 2.2.5)
|
||||||
|
drb
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
|
mutex_m
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
addressable (2.8.5)
|
addressable (2.8.6)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
afm (0.2.2)
|
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)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.807.0)
|
aws-partitions (1.874.0)
|
||||||
aws-sdk-core (3.180.3)
|
aws-sdk-core (3.190.1)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.8)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.71.0)
|
aws-sdk-kms (1.75.0)
|
||||||
aws-sdk-core (~> 3, >= 3.177.0)
|
aws-sdk-core (~> 3, >= 3.188.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.132.1)
|
aws-sdk-s3 (1.142.0)
|
||||||
aws-sdk-core (~> 3, >= 3.179.0)
|
aws-sdk-core (~> 3, >= 3.189.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.6)
|
aws-sigv4 (~> 1.8)
|
||||||
aws-sigv4 (1.6.0)
|
aws-sigv4 (1.8.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
azure-storage-blob (2.0.3)
|
azure-storage-blob (2.0.3)
|
||||||
azure-storage-common (~> 2.0)
|
azure-storage-common (~> 2.0)
|
||||||
@@ -98,7 +108,8 @@ GEM
|
|||||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
||||||
net-http-persistent (~> 4.0)
|
net-http-persistent (~> 4.0)
|
||||||
nokogiri (~> 1, >= 1.10.8)
|
nokogiri (~> 1, >= 1.10.8)
|
||||||
bcrypt (3.1.19)
|
base64 (0.2.0)
|
||||||
|
bcrypt (3.1.20)
|
||||||
better_html (2.0.2)
|
better_html (2.0.2)
|
||||||
actionview (>= 6.0)
|
actionview (>= 6.0)
|
||||||
activesupport (>= 6.0)
|
activesupport (>= 6.0)
|
||||||
@@ -106,13 +117,15 @@ GEM
|
|||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
parser (>= 2.4)
|
parser (>= 2.4)
|
||||||
smart_properties
|
smart_properties
|
||||||
|
bigdecimal (3.1.5)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.16.0)
|
bootsnap (1.17.0)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
bullet (7.0.7)
|
bullet (7.1.4)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11)
|
uniform_notifier (~> 1.11)
|
||||||
|
camertron-eprun (1.1.1)
|
||||||
cancancan (3.5.0)
|
cancancan (3.5.0)
|
||||||
capybara (3.39.2)
|
capybara (3.39.2)
|
||||||
addressable
|
addressable
|
||||||
@@ -124,6 +137,7 @@ GEM
|
|||||||
regexp_parser (>= 1.5, < 3.0)
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
chunky_png (1.4.0)
|
chunky_png (1.4.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.2)
|
||||||
@@ -131,17 +145,17 @@ GEM
|
|||||||
crack (0.4.5)
|
crack (0.4.5)
|
||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
css_parser (1.14.0)
|
css_parser (1.16.0)
|
||||||
addressable
|
addressable
|
||||||
cuprite (0.14.3)
|
cuprite (0.15)
|
||||||
capybara (~> 3.0)
|
capybara (~> 3.0)
|
||||||
ferrum (~> 0.13.0)
|
ferrum (~> 0.14.0)
|
||||||
date (3.3.3)
|
date (3.3.4)
|
||||||
debug (1.8.0)
|
debug (1.9.1)
|
||||||
irb (>= 1.5.0)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.1)
|
reline (>= 0.3.8)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
devise (4.9.2)
|
devise (4.9.3)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
@@ -157,8 +171,10 @@ GEM
|
|||||||
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 (2.8.1)
|
||||||
|
drb (2.2.0)
|
||||||
|
ruby2_keywords
|
||||||
email_typo (0.2.3)
|
email_typo (0.2.3)
|
||||||
erb_lint (0.4.0)
|
erb_lint (0.5.0)
|
||||||
activesupport
|
activesupport
|
||||||
better_html (>= 2.0.1)
|
better_html (>= 2.0.1)
|
||||||
parser (>= 2.7.1.4)
|
parser (>= 2.7.1.4)
|
||||||
@@ -166,12 +182,12 @@ GEM
|
|||||||
rubocop
|
rubocop
|
||||||
smart_properties
|
smart_properties
|
||||||
erubi (1.12.0)
|
erubi (1.12.0)
|
||||||
factory_bot (6.2.1)
|
factory_bot (6.4.4)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
factory_bot_rails (6.2.0)
|
factory_bot_rails (6.4.2)
|
||||||
factory_bot (~> 6.2.0)
|
factory_bot (~> 6.4)
|
||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
faker (3.2.1)
|
faker (3.2.2)
|
||||||
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)
|
||||||
@@ -200,16 +216,16 @@ GEM
|
|||||||
faraday-retry (1.0.3)
|
faraday-retry (1.0.3)
|
||||||
faraday_middleware (1.2.0)
|
faraday_middleware (1.2.0)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
ferrum (0.13)
|
ferrum (0.14)
|
||||||
addressable (~> 2.5)
|
addressable (~> 2.5)
|
||||||
concurrent-ruby (~> 1.1)
|
concurrent-ruby (~> 1.1)
|
||||||
webrick (~> 1.7)
|
webrick (~> 1.7)
|
||||||
websocket-driver (>= 0.6, < 0.8)
|
websocket-driver (>= 0.6, < 0.8)
|
||||||
ffi (1.15.5)
|
ffi (1.16.3)
|
||||||
geom2d (0.4.1)
|
geom2d (0.4.1)
|
||||||
globalid (1.1.0)
|
globalid (1.2.1)
|
||||||
activesupport (>= 5.0)
|
activesupport (>= 6.1)
|
||||||
google-apis-core (0.11.1)
|
google-apis-core (0.11.2)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
httpclient (>= 2.8.1, < 3.a)
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
@@ -220,33 +236,32 @@ GEM
|
|||||||
webrick
|
webrick
|
||||||
google-apis-iamcredentials_v1 (0.17.0)
|
google-apis-iamcredentials_v1 (0.17.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-apis-storage_v1 (0.19.0)
|
google-apis-storage_v1 (0.29.0)
|
||||||
google-apis-core (>= 0.9.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-cloud-core (1.6.0)
|
google-cloud-core (1.6.1)
|
||||||
google-cloud-env (~> 1.0)
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (1.6.0)
|
google-cloud-env (2.1.0)
|
||||||
faraday (>= 0.17.3, < 3.0)
|
faraday (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (1.3.1)
|
google-cloud-errors (1.3.1)
|
||||||
google-cloud-storage (1.44.0)
|
google-cloud-storage (1.45.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-iamcredentials_v1 (~> 0.1)
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
google-apis-storage_v1 (~> 0.19.0)
|
google-apis-storage_v1 (~> 0.29.0)
|
||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
googleauth (1.7.0)
|
googleauth (1.9.1)
|
||||||
faraday (>= 0.17.3, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
|
google-cloud-env (~> 2.1)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
memoist (~> 0.16)
|
|
||||||
multi_json (~> 1.11)
|
multi_json (~> 1.11)
|
||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
hashdiff (1.0.1)
|
hashdiff (1.1.0)
|
||||||
hashery (2.1.2)
|
hashery (2.1.2)
|
||||||
hashie (5.0.0)
|
hexapdf (0.37.1)
|
||||||
hexapdf (0.34.1)
|
|
||||||
cmdparse (~> 3.0, >= 3.0.3)
|
cmdparse (~> 3.0, >= 3.0.3)
|
||||||
geom2d (~> 0.4, >= 0.4.1)
|
geom2d (~> 0.4, >= 0.4.1)
|
||||||
openssl (>= 2.2.1)
|
openssl (>= 2.2.1)
|
||||||
@@ -257,11 +272,12 @@ GEM
|
|||||||
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.6.0)
|
io-console (0.7.1)
|
||||||
irb (1.7.4)
|
irb (1.11.0)
|
||||||
reline (>= 0.3.6)
|
rdoc
|
||||||
|
reline (>= 0.3.8)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.6.3)
|
json (2.7.1)
|
||||||
jwt (2.7.1)
|
jwt (2.7.1)
|
||||||
language_server-protocol (3.17.0.3)
|
language_server-protocol (3.17.0.3)
|
||||||
launchy (2.5.2)
|
launchy (2.5.2)
|
||||||
@@ -273,12 +289,12 @@ GEM
|
|||||||
letter_opener (~> 1.7)
|
letter_opener (~> 1.7)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml
|
rexml
|
||||||
lograge (0.13.0)
|
lograge (0.14.0)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.21.3)
|
loofah (2.22.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.8.1)
|
mail (2.8.1)
|
||||||
@@ -288,72 +304,48 @@ GEM
|
|||||||
net-smtp
|
net-smtp
|
||||||
marcel (1.0.2)
|
marcel (1.0.2)
|
||||||
matrix (0.4.2)
|
matrix (0.4.2)
|
||||||
memoist (0.16.2)
|
|
||||||
method_source (1.0.0)
|
method_source (1.0.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.4)
|
mini_portile2 (2.8.5)
|
||||||
minitest (5.20.0)
|
minitest (5.20.0)
|
||||||
msgpack (1.7.2)
|
msgpack (1.7.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multi_xml (0.6.0)
|
|
||||||
multipart-post (2.3.0)
|
multipart-post (2.3.0)
|
||||||
|
mutex_m (0.2.0)
|
||||||
mysql2 (0.5.5)
|
mysql2 (0.5.5)
|
||||||
net-http-persistent (4.0.2)
|
net-http-persistent (4.0.2)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-imap (0.3.7)
|
net-imap (0.4.9)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.1)
|
net-protocol (0.2.2)
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.3.3)
|
net-smtp (0.4.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.5.9)
|
nio4r (2.7.0)
|
||||||
nokogiri (1.15.4)
|
nokogiri (1.15.5)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.15.4-arm64-darwin)
|
oj (3.16.3)
|
||||||
racc (~> 1.4)
|
bigdecimal (>= 3.0)
|
||||||
oauth2 (2.0.9)
|
|
||||||
faraday (>= 0.17.3, < 3.0)
|
|
||||||
jwt (>= 1.0, < 3.0)
|
|
||||||
multi_xml (~> 0.5)
|
|
||||||
rack (>= 1.2, < 4)
|
|
||||||
snaky_hash (~> 2.0)
|
|
||||||
version_gem (~> 1.1)
|
|
||||||
oj (3.16.0)
|
|
||||||
omniauth (2.1.1)
|
|
||||||
hashie (>= 3.4.6)
|
|
||||||
rack (>= 2.2.3)
|
|
||||||
rack-protection
|
|
||||||
omniauth-google-oauth2 (1.1.1)
|
|
||||||
jwt (>= 2.0)
|
|
||||||
oauth2 (~> 2.0.6)
|
|
||||||
omniauth (~> 2.0)
|
|
||||||
omniauth-oauth2 (~> 1.8.0)
|
|
||||||
omniauth-oauth2 (1.8.0)
|
|
||||||
oauth2 (>= 1.4, < 3)
|
|
||||||
omniauth (~> 2.0)
|
|
||||||
omniauth-rails_csrf_protection (1.0.1)
|
|
||||||
actionpack (>= 4.2)
|
|
||||||
omniauth (~> 2.0)
|
|
||||||
openssl (3.2.0)
|
openssl (3.2.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
pagy (6.0.4)
|
pagy (6.2.0)
|
||||||
parallel (1.23.0)
|
parallel (1.24.0)
|
||||||
parser (3.2.2.4)
|
parser (3.2.2.4)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pdf-reader (2.11.0)
|
pdf-reader (2.12.0)
|
||||||
Ascii85 (~> 1.0)
|
Ascii85 (~> 1.0)
|
||||||
afm (~> 0.2.1)
|
afm (~> 0.2.1)
|
||||||
hashery (~> 2.0)
|
hashery (~> 2.0)
|
||||||
ruby-rc4
|
ruby-rc4
|
||||||
ttfunk
|
ttfunk
|
||||||
pg (1.5.3)
|
pg (1.5.4)
|
||||||
premailer (1.21.0)
|
premailer (1.21.0)
|
||||||
addressable
|
addressable
|
||||||
css_parser (>= 1.12.0)
|
css_parser (>= 1.12.0)
|
||||||
@@ -362,36 +354,43 @@ GEM
|
|||||||
actionmailer (>= 3)
|
actionmailer (>= 3)
|
||||||
net-smtp
|
net-smtp
|
||||||
premailer (~> 1.7, >= 1.7.9)
|
premailer (~> 1.7, >= 1.7.9)
|
||||||
|
pretender (0.5.0)
|
||||||
|
actionpack (>= 6.1)
|
||||||
pry (0.14.2)
|
pry (0.14.2)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (5.0.3)
|
psych (5.1.2)
|
||||||
puma (6.3.1)
|
stringio
|
||||||
|
public_suffix (5.0.4)
|
||||||
|
puma (6.4.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.7.1)
|
racc (1.7.3)
|
||||||
rack (2.2.8)
|
rack (2.2.8)
|
||||||
rack-protection (3.1.0)
|
rack-proxy (0.7.7)
|
||||||
rack (~> 2.2, >= 2.2.4)
|
|
||||||
rack-proxy (0.7.6)
|
|
||||||
rack
|
rack
|
||||||
|
rack-session (1.0.2)
|
||||||
|
rack (< 3)
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rails (7.0.7)
|
rackup (1.0.0)
|
||||||
actioncable (= 7.0.7)
|
rack (< 3)
|
||||||
actionmailbox (= 7.0.7)
|
webrick
|
||||||
actionmailer (= 7.0.7)
|
rails (7.1.2)
|
||||||
actionpack (= 7.0.7)
|
actioncable (= 7.1.2)
|
||||||
actiontext (= 7.0.7)
|
actionmailbox (= 7.1.2)
|
||||||
actionview (= 7.0.7)
|
actionmailer (= 7.1.2)
|
||||||
activejob (= 7.0.7)
|
actionpack (= 7.1.2)
|
||||||
activemodel (= 7.0.7)
|
actiontext (= 7.1.2)
|
||||||
activerecord (= 7.0.7)
|
actionview (= 7.1.2)
|
||||||
activestorage (= 7.0.7)
|
activejob (= 7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activemodel (= 7.1.2)
|
||||||
|
activerecord (= 7.1.2)
|
||||||
|
activestorage (= 7.1.2)
|
||||||
|
activesupport (= 7.1.2)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.0.7)
|
railties (= 7.1.2)
|
||||||
rails-dom-testing (2.2.0)
|
rails-dom-testing (2.2.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
@@ -406,19 +405,22 @@ GEM
|
|||||||
actionview (> 3.1)
|
actionview (> 3.1)
|
||||||
activesupport (> 3.1)
|
activesupport (> 3.1)
|
||||||
railties (> 3.1)
|
railties (> 3.1)
|
||||||
railties (7.0.7)
|
railties (7.1.2)
|
||||||
actionpack (= 7.0.7)
|
actionpack (= 7.1.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.2)
|
||||||
method_source
|
irb
|
||||||
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0, >= 1.2.2)
|
||||||
zeitwerk (~> 2.5)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.0.6)
|
rake (13.1.0)
|
||||||
redis-client (0.16.0)
|
rdoc (6.6.2)
|
||||||
|
psych (>= 4.0.0)
|
||||||
|
redis-client (0.19.1)
|
||||||
connection_pool
|
connection_pool
|
||||||
regexp_parser (2.8.2)
|
regexp_parser (2.8.3)
|
||||||
reline (0.3.8)
|
reline (0.4.1)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
@@ -426,12 +428,12 @@ GEM
|
|||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
request_store (1.5.1)
|
request_store (1.5.1)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.1.0)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.2.6)
|
rexml (3.2.6)
|
||||||
rollbar (3.4.0)
|
rollbar (3.4.2)
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rqrcode (2.2.0)
|
rqrcode (2.2.0)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
@@ -445,7 +447,7 @@ GEM
|
|||||||
rspec-mocks (3.12.6)
|
rspec-mocks (3.12.6)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.12.0)
|
||||||
rspec-rails (6.0.3)
|
rspec-rails (6.1.0)
|
||||||
actionpack (>= 6.1)
|
actionpack (>= 6.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
railties (>= 6.1)
|
railties (>= 6.1)
|
||||||
@@ -454,7 +456,7 @@ GEM
|
|||||||
rspec-mocks (~> 3.12)
|
rspec-mocks (~> 3.12)
|
||||||
rspec-support (~> 3.12)
|
rspec-support (~> 3.12)
|
||||||
rspec-support (3.12.1)
|
rspec-support (3.12.1)
|
||||||
rubocop (1.57.2)
|
rubocop (1.59.0)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
@@ -462,7 +464,7 @@ GEM
|
|||||||
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.28.1, < 2.0)
|
rubocop-ast (>= 1.30.0, < 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.30.0)
|
||||||
@@ -471,20 +473,21 @@ GEM
|
|||||||
rubocop (~> 1.41)
|
rubocop (~> 1.41)
|
||||||
rubocop-factory_bot (2.24.0)
|
rubocop-factory_bot (2.24.0)
|
||||||
rubocop (~> 1.33)
|
rubocop (~> 1.33)
|
||||||
rubocop-performance (1.19.1)
|
rubocop-performance (1.20.1)
|
||||||
rubocop (>= 1.7.0, < 2.0)
|
rubocop (>= 1.48.1, < 2.0)
|
||||||
rubocop-ast (>= 0.4.0)
|
rubocop-ast (>= 1.30.0, < 2.0)
|
||||||
rubocop-rails (2.22.1)
|
rubocop-rails (2.23.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-rspec (2.25.0)
|
rubocop-rspec (2.25.0)
|
||||||
rubocop (~> 1.40)
|
rubocop (~> 1.40)
|
||||||
rubocop-capybara (~> 2.17)
|
rubocop-capybara (~> 2.17)
|
||||||
rubocop-factory_bot (~> 2.22)
|
rubocop-factory_bot (~> 2.22)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby-rc4 (0.1.5)
|
ruby-rc4 (0.1.5)
|
||||||
ruby-vips (2.1.4)
|
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.25)
|
||||||
@@ -492,17 +495,17 @@ GEM
|
|||||||
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.0.3)
|
shakapacker (7.1.0)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
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.1.2)
|
sidekiq (7.2.0)
|
||||||
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.14.0)
|
||||||
signet (0.17.0)
|
signet (0.18.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)
|
||||||
@@ -514,38 +517,39 @@ GEM
|
|||||||
simplecov-html (0.12.3)
|
simplecov-html (0.12.3)
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
smart_properties (1.17.0)
|
smart_properties (1.17.0)
|
||||||
snaky_hash (2.0.1)
|
sqlite3 (1.7.0)
|
||||||
hashie
|
|
||||||
version_gem (~> 1.1, >= 1.1.1)
|
|
||||||
sqlite3 (1.6.3)
|
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
sqlite3 (1.6.3-arm64-darwin)
|
sqlite3 (1.7.0-arm64-darwin)
|
||||||
|
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.2.2)
|
thor (1.3.0)
|
||||||
timeout (0.4.0)
|
timeout (0.4.1)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
ttfunk (1.7.0)
|
ttfunk (1.7.0)
|
||||||
turbo-rails (1.4.0)
|
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)
|
||||||
|
twitter_cldr (6.12.0)
|
||||||
|
camertron-eprun
|
||||||
|
cldr-plurals-runtime-rb (~> 1.1)
|
||||||
|
tzinfo
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
tzinfo-data (1.2023.3)
|
tzinfo-data (1.2023.4)
|
||||||
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)
|
||||||
uniform_notifier (1.16.0)
|
uniform_notifier (1.16.0)
|
||||||
version_gem (1.1.3)
|
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
web-console (4.2.0)
|
web-console (4.2.1)
|
||||||
actionview (>= 6.0.0)
|
actionview (>= 6.0.0)
|
||||||
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.18.1)
|
webmock (3.19.1)
|
||||||
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)
|
||||||
@@ -555,7 +559,7 @@ 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.11)
|
zeitwerk (2.6.12)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin-22
|
arm64-darwin-22
|
||||||
@@ -563,6 +567,7 @@ PLATFORMS
|
|||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
annotate
|
annotate
|
||||||
|
arabic-letter-connector
|
||||||
aws-sdk-s3
|
aws-sdk-s3
|
||||||
azure-storage-blob
|
azure-storage-blob
|
||||||
better_html
|
better_html
|
||||||
@@ -589,14 +594,14 @@ DEPENDENCIES
|
|||||||
lograge
|
lograge
|
||||||
mysql2
|
mysql2
|
||||||
oj
|
oj
|
||||||
omniauth-google-oauth2
|
|
||||||
omniauth-rails_csrf_protection
|
|
||||||
pagy
|
pagy
|
||||||
pdf-reader
|
pdf-reader
|
||||||
pg
|
pg
|
||||||
premailer-rails
|
premailer-rails
|
||||||
|
pretender
|
||||||
pry-rails
|
pry-rails
|
||||||
puma
|
puma
|
||||||
|
rack
|
||||||
rails
|
rails
|
||||||
rails-i18n
|
rails-i18n
|
||||||
rails_autolink
|
rails_autolink
|
||||||
@@ -616,6 +621,7 @@ DEPENDENCIES
|
|||||||
sqlite3
|
sqlite3
|
||||||
strip_attributes
|
strip_attributes
|
||||||
turbo-rails
|
turbo-rails
|
||||||
|
twitter_cldr
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
web-console
|
web-console
|
||||||
webmock
|
webmock
|
||||||
@@ -624,4 +630,4 @@ RUBY VERSION
|
|||||||
ruby 3.2.2p53
|
ruby 3.2.2p53
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.4.10
|
2.5.3
|
||||||
@@ -33,17 +33,29 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
[](https://demo.docuseal.co)
|
[](https://demo.docuseal.co)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- [x] PDF form fields builder (WYSIWYG)
|
- PDF form fields builder (WYSIWYG)
|
||||||
- [x] 11 field types available (Signature, Date, File, Checkbox etc.)
|
- 12 field types available (Signature, Date, File, Checkbox etc.)
|
||||||
- [x] Multiple submitters per document
|
- Multiple submitters per document
|
||||||
- [x] Automated emails via SMTP
|
- Automated emails via SMTP
|
||||||
- [x] Files storage on disk or AWS S3, Google Storage, Azure Cloud
|
- Files storage on disk or AWS S3, Google Storage, Azure Cloud
|
||||||
- [x] Automatic PDF eSignature
|
- Automatic PDF eSignature
|
||||||
- [x] PDF signature verification
|
- PDF signature verification
|
||||||
- [x] Users management
|
- Users management
|
||||||
- [x] Mobile-optimized
|
- Mobile-optimized
|
||||||
- [x] API and Webhooks for integrations
|
- API and Webhooks for integrations
|
||||||
- [x] Easy to deploy in minutes
|
- Easy to deploy in minutes
|
||||||
|
|
||||||
|
## Pro Features
|
||||||
|
- Company logo and white-label
|
||||||
|
- User roles
|
||||||
|
- Automated reminders
|
||||||
|
- Invitation and identify verification via SMS
|
||||||
|
- SSO / SAML
|
||||||
|
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
|
||||||
|
- Template creation with PDF or DOCX and text tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
|
||||||
|
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||||
|
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||||
|
- [Learn more](https://www.docuseal.co/pricing)
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
@@ -73,15 +85,15 @@ curl https://raw.githubusercontent.com/docusealco/docuseal/master/docker-compose
|
|||||||
|
|
||||||
Run the app under a custom domain over https using docker compose (make sure your DNS points to the server to automatically issue ssl certs with Caddy):
|
Run the app under a custom domain over https using docker compose (make sure your DNS points to the server to automatically issue ssl certs with Caddy):
|
||||||
```sh
|
```sh
|
||||||
HOST=your-domain-name.com docker-compose up
|
sudo HOST=your-domain-name.com docker compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
## For Businesses
|
## For Businesses
|
||||||
### Integrate seamless document signing into your web or mobile apps with DocuSeal!
|
### Integrate seamless document signing into your web or mobile apps with DocuSeal
|
||||||
|
|
||||||
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
||||||
|
|
||||||
[Book a Meeting](https://calendly.com/kriti-docuseal/30min)
|
[Book a Meeting](https://www.docuseal.co/contact)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Reporting a Vulnerability
|
||||||
|
|
||||||
|
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.**
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AccountConfigsController < ApplicationController
|
||||||
|
before_action :load_account_config
|
||||||
|
authorize_resource :account_config
|
||||||
|
|
||||||
|
ALLOWED_KEYS = [
|
||||||
|
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||||
|
AccountConfig::FORCE_MFA,
|
||||||
|
AccountConfig::ALLOW_TO_RESUBMIT,
|
||||||
|
AccountConfig::ESIGNING_PREFERENCE_KEY
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
def create
|
||||||
|
@account_config.update!(account_config_params)
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_account_config
|
||||||
|
return head :not_found unless ALLOWED_KEYS.include?(account_config_params[:key])
|
||||||
|
|
||||||
|
@account_config =
|
||||||
|
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_config_params
|
||||||
|
params.required(:account_config).permit!.tap do |attrs|
|
||||||
|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -37,6 +37,18 @@ class AccountsController < ApplicationController
|
|||||||
render :show, status: :unprocessable_entity
|
render :show, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize!(:manage, current_account)
|
||||||
|
|
||||||
|
true_user.update!(locked_at: Time.current)
|
||||||
|
|
||||||
|
render turbo_stream: turbo_stream.replace(
|
||||||
|
:account_delete_button,
|
||||||
|
html: helpers.tag.p('Your account removal request will be processed within 2 weeks. ' \
|
||||||
|
'Please contact us if you want to keep your account.')
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_account
|
def load_account
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class ActiveStorageBlobsProxyController < ApiBaseController
|
||||||
|
include ActiveStorage::Streaming
|
||||||
|
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
before_action :set_cors_headers
|
||||||
|
|
||||||
|
def show
|
||||||
|
blob_uuid, = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
||||||
|
|
||||||
|
if blob_uuid.blank?
|
||||||
|
Rollbar.error('Blob not found') if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
||||||
|
|
||||||
|
if request.headers['Range'].present?
|
||||||
|
send_blob_byte_range_data blob, request.headers['Range']
|
||||||
|
else
|
||||||
|
http_cache_forever public: true do
|
||||||
|
response.headers['Accept-Ranges'] = 'bytes'
|
||||||
|
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||||
|
|
||||||
|
send_blob_stream blob, disposition: params[:disposition]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class ActiveStorageBlobsProxyLegacyController < ApiBaseController
|
||||||
|
include ActiveStorage::Streaming
|
||||||
|
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
# rubocop:disable Metrics
|
||||||
|
def show
|
||||||
|
Rollbar.info('Blob legacy') if defined?(Rollbar)
|
||||||
|
|
||||||
|
blob = ActiveStorage::Blob.find_signed!(params[:signed_blob_id] || params[:signed_id])
|
||||||
|
|
||||||
|
is_permitted = blob.attachments.any? do |a|
|
||||||
|
(current_user && a.record.account.id == current_user.account_id) ||
|
||||||
|
a.record.account.account_configs.any? { |e| e.key == 'legacy_blob_proxy' } ||
|
||||||
|
a.name == 'logo'
|
||||||
|
end
|
||||||
|
|
||||||
|
unless is_permitted
|
||||||
|
Rollbar.error("Blob account not found: #{blob.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: 'Not authenticated' }, status: :unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
if request.headers['Range'].present?
|
||||||
|
send_blob_byte_range_data blob, request.headers['Range']
|
||||||
|
else
|
||||||
|
http_cache_forever public: true do
|
||||||
|
response.headers['Accept-Ranges'] = 'bytes'
|
||||||
|
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||||
|
|
||||||
|
send_blob_stream blob, disposition: params[:disposition]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,17 +8,29 @@ module Api
|
|||||||
DEFAULT_LIMIT = 10
|
DEFAULT_LIMIT = 10
|
||||||
MAX_LIMIT = 100
|
MAX_LIMIT = 100
|
||||||
|
|
||||||
|
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
||||||
|
|
||||||
wrap_parameters false
|
wrap_parameters false
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
check_authorization
|
check_authorization
|
||||||
|
|
||||||
|
rescue_from Params::BaseValidator::InvalidParameterError do |e|
|
||||||
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
rescue_from CanCan::AccessDenied do |e|
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :forbidden
|
render json: { error: e.message }, status: :forbidden
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rescue_from JSON::ParserError do |e|
|
||||||
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -27,14 +39,33 @@ module Api
|
|||||||
result = relation.order(id: :desc)
|
result = relation.order(id: :desc)
|
||||||
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
||||||
|
|
||||||
result = result.where('id < ?', params[:after]) if params[:after].present?
|
result = result.where(relation.arel_table[:id].lt(params[:after])) if params[:after].present?
|
||||||
result = result.where('id > ?', params[:before]) if params[:before].present?
|
result = result.where(relation.arel_table[:id].gt(params[:before])) if params[:before].present?
|
||||||
|
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
current_user&.account
|
current_user&.account
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_cors_headers
|
||||||
|
headers['Access-Control-Allow-Origin'] = '*'
|
||||||
|
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
||||||
|
headers['Access-Control-Allow-Headers'] = '*'
|
||||||
|
headers['Access-Control-Max-Age'] = '1728000'
|
||||||
|
headers['Access-Control-Allow-Credentials'] = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -13,6 +13,10 @@ module Api
|
|||||||
submissions = Submissions.search(@submissions, params[:q])
|
submissions = Submissions.search(@submissions, params[:q])
|
||||||
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
||||||
|
|
||||||
|
if params[:template_folder].present?
|
||||||
|
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
|
||||||
|
end
|
||||||
|
|
||||||
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters,
|
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters,
|
||||||
audit_trail_attachment: :blob))
|
audit_trail_attachment: :blob))
|
||||||
|
|
||||||
@@ -27,29 +31,54 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
serialized_subbmitters =
|
submitters = @submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
|
||||||
@submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob).map do |submitter|
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter) if submitter.completed_at?
|
|
||||||
|
|
||||||
Submitters::SerializeForApi.call(submitter)
|
submitters.each do |submitter|
|
||||||
|
if submitter.completed_at? && submitter.documents_attachments.blank?
|
||||||
|
submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?)
|
||||||
|
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
|
||||||
|
end
|
||||||
|
|
||||||
|
serialized_submitters = submitters.map { |submitter| Submitters::SerializeForApi.call(submitter) }
|
||||||
|
|
||||||
json = @submission.as_json(
|
json = @submission.as_json(
|
||||||
serialize_params.deep_merge(
|
serialize_params.deep_merge(
|
||||||
include: {
|
include: { submission_events: { only: %i[id submitter_id event_type event_timestamp] } }
|
||||||
submission_events: {
|
|
||||||
only: %i[id submitter_id event_type event_timestamp]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
json[:submitters] = serialized_subbmitters
|
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:
|
render json:
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
Params::SubmissionCreateValidator.call(params, dry_run: !Docuseal.multitenant?)
|
||||||
|
|
||||||
|
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
|
||||||
|
|
||||||
|
if @template.fields.blank?
|
||||||
|
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
params[:send_email] = true unless params.key?(:send_email)
|
params[:send_email] = true unless params.key?(:send_email)
|
||||||
params[:send_sms] = false unless params.key?(:send_sms)
|
params[:send_sms] = false unless params.key?(:send_sms)
|
||||||
|
|
||||||
@@ -57,15 +86,23 @@ module Api
|
|||||||
|
|
||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
|
submissions.each do |submission|
|
||||||
|
if submission.submitters.all?(&:completed_at?) && submission.submitters.last
|
||||||
|
ProcessSubmitterCompletionJob.perform_later(submission.submitters.last)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
render json: submissions.flat_map(&:submitters)
|
render json: submissions.flat_map(&:submitters)
|
||||||
rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e
|
rescue Submitters::NormalizeValues::BaseError => e
|
||||||
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@submission.update!(deleted_at: Time.current)
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|
||||||
render json: @submission.as_json(only: %i[id deleted_at])
|
render json: @submission.as_json(only: %i[id], methods: %i[archived_at])
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -73,7 +110,8 @@ module Api
|
|||||||
def create_submissions(template, params)
|
def create_submissions(template, params)
|
||||||
is_send_email = !params[:send_email].in?(['false', false])
|
is_send_email = !params[:send_email].in?(['false', false])
|
||||||
|
|
||||||
if (emails = (params[:emails] || params[:email]).presence) && params[:submission].blank?
|
if (emails = (params[:emails] || params[:email]).presence) &&
|
||||||
|
(params[:submission].blank? && params[:submitters].blank?)
|
||||||
Submissions.create_from_emails(template:,
|
Submissions.create_from_emails(template:,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :api,
|
source: :api,
|
||||||
@@ -81,7 +119,8 @@ module Api
|
|||||||
emails:,
|
emails:,
|
||||||
params:)
|
params:)
|
||||||
else
|
else
|
||||||
submissions_attrs, attachments = normalize_submissions_params!(submissions_params, template)
|
submissions_attrs, attachments =
|
||||||
|
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template)
|
||||||
|
|
||||||
submissions = Submissions.create_from_submitters(
|
submissions = Submissions.create_from_submitters(
|
||||||
template:,
|
template:,
|
||||||
@@ -93,7 +132,13 @@ module Api
|
|||||||
params:
|
params:
|
||||||
)
|
)
|
||||||
|
|
||||||
save_default_value_attachments!(attachments, submissions.flat_map(&:submitters))
|
submitters = submissions.flat_map(&:submitters)
|
||||||
|
|
||||||
|
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters)
|
||||||
|
|
||||||
|
submitters.each do |submitter|
|
||||||
|
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) if submitter.completed_at?
|
||||||
|
end
|
||||||
|
|
||||||
submissions
|
submissions
|
||||||
end
|
end
|
||||||
@@ -101,13 +146,13 @@ module Api
|
|||||||
|
|
||||||
def serialize_params
|
def serialize_params
|
||||||
{
|
{
|
||||||
only: %i[id source submitters_order created_at updated_at],
|
only: %i[id source submitters_order created_at updated_at archived_at],
|
||||||
methods: %i[audit_log_url],
|
methods: %i[audit_log_url],
|
||||||
include: {
|
include: {
|
||||||
submitters: { only: %i[id slug uuid name email phone
|
submitters: { only: %i[id slug uuid name email phone
|
||||||
completed_at opened_at sent_at
|
completed_at opened_at sent_at
|
||||||
created_at updated_at],
|
created_at updated_at external_id metadata],
|
||||||
methods: %i[status] },
|
methods: %i[status application_key] },
|
||||||
template: { only: %i[id name created_at updated_at] },
|
template: { only: %i[id name created_at updated_at] },
|
||||||
created_by_user: { only: %i[id email first_name last_name] }
|
created_by_user: { only: %i[id email first_name last_name] }
|
||||||
}
|
}
|
||||||
@@ -115,61 +160,26 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
key = params.key?(:submission) ? :submission : :submissions
|
permitted_attrs = [
|
||||||
|
:send_email, :send_sms, :bcc_completed, :completed_redirect_url,
|
||||||
|
{
|
||||||
|
message: %i[subject body],
|
||||||
|
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
|
:completed, :phone, :application_key, :external_id,
|
||||||
|
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
|
fields: [%i[name default_value title description
|
||||||
|
readonly validation_pattern invalid_message]] }]]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
params.permit(
|
if params.key?(:submitters)
|
||||||
key => [
|
params.permit(*permitted_attrs)
|
||||||
[:send_email, :send_sms, {
|
else
|
||||||
message: %i[subject body],
|
key = params.key?(:submission) ? :submission : :submissions
|
||||||
submitters: [[:send_email, :send_sms, :uuid, :name, :email, :role,
|
|
||||||
:completed, :phone, :application_key,
|
|
||||||
{ values: {}, readonly_fields: [], message: %i[subject body],
|
|
||||||
fields: [%i[name default_value readonly validation_pattern invalid_message]] }]]
|
|
||||||
}]
|
|
||||||
]
|
|
||||||
).fetch(key, [])
|
|
||||||
end
|
|
||||||
|
|
||||||
def normalize_submissions_params!(submissions_params, template)
|
params.permit(
|
||||||
attachments = []
|
key => [permitted_attrs]
|
||||||
|
).fetch(key, [])
|
||||||
Array.wrap(submissions_params).each do |submission|
|
|
||||||
submission[:submitters].each_with_index do |submitter, index|
|
|
||||||
default_values = submitter[:values] || {}
|
|
||||||
|
|
||||||
submitter[:fields]&.each { |f| default_values[f[:name]] = f[:default_value] if f[:default_value].present? }
|
|
||||||
|
|
||||||
next if default_values.blank?
|
|
||||||
|
|
||||||
values, new_attachments =
|
|
||||||
Submitters::NormalizeValues.call(template,
|
|
||||||
default_values,
|
|
||||||
submitter[:role] || template.submitters[index]['name'])
|
|
||||||
|
|
||||||
attachments.push(*new_attachments)
|
|
||||||
|
|
||||||
submitter[:values] = values
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
[submissions_params, attachments]
|
|
||||||
end
|
|
||||||
|
|
||||||
def save_default_value_attachments!(attachments, submitters)
|
|
||||||
return if attachments.blank?
|
|
||||||
|
|
||||||
attachments_index = attachments.index_by(&:uuid)
|
|
||||||
|
|
||||||
submitters.each do |submitter|
|
|
||||||
submitter.values.to_a.each do |_, value|
|
|
||||||
attachment = attachments_index[value]
|
|
||||||
|
|
||||||
next unless attachment
|
|
||||||
|
|
||||||
attachment.record = submitter
|
|
||||||
|
|
||||||
attachment.save!
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ module Api
|
|||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
|
submitter.opened_at = Time.current
|
||||||
|
submitter.save
|
||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
||||||
|
|
||||||
SendFormViewedWebhookRequestJob.perform_later(submitter)
|
SendFormViewedWebhookRequestJob.perform_later(submitter)
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ module Api
|
|||||||
def index
|
def index
|
||||||
submitters = Submitters.search(@submitters, params[:q])
|
submitters = Submitters.search(@submitters, params[:q])
|
||||||
|
|
||||||
submitters = submitters.where(application_key: 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(submission_id: params[:submission_id]) if params[:submission_id].present?
|
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
||||||
|
|
||||||
submitters = paginate(
|
submitters = paginate(
|
||||||
@@ -30,5 +31,92 @@ module Api
|
|||||||
|
|
||||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true)
|
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @submitter.completed_at?
|
||||||
|
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
role = @submitter.submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
|
||||||
|
|
||||||
|
normalized_params, new_attachments =
|
||||||
|
Submissions::NormalizeParamUtils.normalize_submitter_params!(submitter_params.merge(role:), @submitter.template,
|
||||||
|
for_submitter: @submitter)
|
||||||
|
|
||||||
|
Submissions::CreateFromSubmitters.maybe_set_template_fields(@submitter.submission,
|
||||||
|
[normalized_params],
|
||||||
|
default_submitter_uuid: @submitter.uuid)
|
||||||
|
|
||||||
|
assign_submitter_attrs(@submitter, normalized_params)
|
||||||
|
|
||||||
|
ApplicationRecord.transaction do
|
||||||
|
Submissions::NormalizeParamUtils.save_default_value_attachments!(new_attachments, [@submitter])
|
||||||
|
|
||||||
|
@submitter.save!
|
||||||
|
|
||||||
|
@submitter.submission.save!
|
||||||
|
|
||||||
|
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) if @submitter.completed_at?
|
||||||
|
end
|
||||||
|
|
||||||
|
if @submitter.completed_at?
|
||||||
|
ProcessSubmitterCompletionJob.perform_later(@submitter)
|
||||||
|
elsif normalized_params[:send_email] || normalized_params[:send_sms]
|
||||||
|
Submitters.send_signature_requests([@submitter])
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_events: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def submitter_params
|
||||||
|
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
|
||||||
|
|
||||||
|
submitter_params.permit(
|
||||||
|
:send_email, :send_sms, :uuid, :name, :email, :role,
|
||||||
|
:completed, :phone, :application_key, :external_id,
|
||||||
|
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
|
fields: [%i[name default_value readonly validation_pattern invalid_message]] }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def assign_submitter_attrs(submitter, attrs)
|
||||||
|
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
||||||
|
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
|
||||||
|
submitter.values = submitter.values.merge(attrs[:values].to_unsafe_h) if attrs[:values].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_preferences(submitter, attrs)
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_submission_fields(submission)
|
||||||
|
submission.template_fields ||= submission.template.fields
|
||||||
|
submission.template_schema ||= submission.template.schema
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_preferences(submitter, attrs)
|
||||||
|
submitter_preferences = Submitters.normalize_preferences(submitter.account, current_user, attrs)
|
||||||
|
|
||||||
|
submitter.preferences['default_values'] = attrs[:values].to_unsafe_h if attrs[:values].present?
|
||||||
|
|
||||||
|
if submitter_preferences.key?('send_email')
|
||||||
|
submitter.preferences['send_email'] = submitter_preferences['send_email']
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
||||||
|
|
||||||
|
return unless submitter_preferences.key?('email_message_uuid')
|
||||||
|
|
||||||
|
submitter.preferences['email_message_uuid'] = submitter_preferences['email_message_uuid']
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -7,10 +7,10 @@ module Api
|
|||||||
LIMIT = 100
|
LIMIT = 100
|
||||||
|
|
||||||
def index
|
def index
|
||||||
template_folders = @template_folders.joins(:templates).where(templates: { deleted_at: nil }).distinct
|
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
|
||||||
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
||||||
|
|
||||||
render json: template_folders.as_json(only: %i[name deleted_at])
|
render json: template_folders.as_json(only: %i[name archived_at])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class TemplatesCloneController < ApiBaseController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:manage, @template)
|
||||||
|
|
||||||
|
cloned_template = Templates::Clone.call(
|
||||||
|
@template,
|
||||||
|
author: current_user,
|
||||||
|
name: params[:name],
|
||||||
|
external_id: params[:external_id].presence || params[:application_key],
|
||||||
|
folder_name: params[:folder_name]
|
||||||
|
)
|
||||||
|
|
||||||
|
cloned_template.source = :api
|
||||||
|
cloned_template.save!
|
||||||
|
|
||||||
|
Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
||||||
|
|
||||||
|
render json: cloned_template.as_json(serialize_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def serialize_params
|
||||||
|
{
|
||||||
|
include: { author: { only: %i[id email first_name last_name] },
|
||||||
|
documents: { only: %i[id uuid], methods: %i[url filename] } }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,10 +5,7 @@ module Api
|
|||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def index
|
def index
|
||||||
templates = Templates.search(@templates, params[:q])
|
templates = filter_templates(@templates, params)
|
||||||
|
|
||||||
templates = params[:archived] ? templates.archived : templates.active
|
|
||||||
templates = templates.where(application_key: params[:application_key]) if params[:application_key].present?
|
|
||||||
|
|
||||||
templates = paginate(templates.preload(:author, documents_attachments: :blob))
|
templates = paginate(templates.preload(:author, documents_attachments: :blob))
|
||||||
|
|
||||||
@@ -27,38 +24,67 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
if (folder_name = params.dig(:template, :folder_name))
|
if (folder_name = params[:folder_name] || params.dig(:template, :folder_name))
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
|
||||||
|
|
||||||
|
if archived.in?([true, false])
|
||||||
|
@template.archived_at = archived == true ? Time.current : nil
|
||||||
|
end
|
||||||
|
|
||||||
@template.update!(template_params)
|
@template.update!(template_params)
|
||||||
|
|
||||||
render json: @template.as_json(only: %i[id updated_at])
|
render json: @template.as_json(only: %i[id updated_at])
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@template.update!(deleted_at: Time.current)
|
if params[:permanently] == 'true' && !Docuseal.multitenant?
|
||||||
|
@template.destroy!
|
||||||
|
else
|
||||||
|
@template.update!(archived_at: Time.current)
|
||||||
|
end
|
||||||
|
|
||||||
render json: @template.as_json(only: %i[id deleted_at])
|
render json: @template.as_json(only: %i[id archived_at])
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def filter_templates(templates, params)
|
||||||
|
templates = Templates.search(templates, params[:q])
|
||||||
|
templates = params[:archived] ? templates.archived : templates.active
|
||||||
|
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
|
||||||
|
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
|
||||||
|
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
|
||||||
|
|
||||||
|
templates
|
||||||
|
end
|
||||||
|
|
||||||
def serialize_params
|
def serialize_params
|
||||||
{
|
{
|
||||||
|
methods: %i[application_key],
|
||||||
include: { author: { only: %i[id email first_name last_name] },
|
include: { author: { only: %i[id email first_name last_name] },
|
||||||
documents: { only: %i[id uuid], methods: %i[url filename] } }
|
documents: { only: %i[id uuid], methods: %i[url preview_image_url filename] } }
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(
|
permit_params = [
|
||||||
:name,
|
:name,
|
||||||
schema: [%i[attachment_uuid name]],
|
{ schema: [%i[attachment_uuid name]],
|
||||||
submitters: [%i[name uuid]],
|
submitters: [%i[name uuid]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
|
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
|
||||||
{ options: [%i[value uuid]], areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
{ preferences: {},
|
||||||
)
|
options: [%i[value uuid]],
|
||||||
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||||
|
]
|
||||||
|
|
||||||
|
if params.key?(:template)
|
||||||
|
params.require(:template).permit(*permit_params)
|
||||||
|
else
|
||||||
|
params.permit(*permit_params)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -16,12 +16,14 @@ module Api
|
|||||||
render json: {
|
render json: {
|
||||||
schema:,
|
schema:,
|
||||||
documents: documents.as_json(
|
documents: documents.as_json(
|
||||||
methods: [:metadata],
|
methods: %i[metadata signed_uuid],
|
||||||
include: {
|
include: {
|
||||||
preview_images: { methods: %i[url metadata filename] }
|
preview_images: { methods: %i[url metadata filename] }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
|
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -4,4 +4,14 @@ class ApiSettingsController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
authorize!(:read, current_user.access_token)
|
authorize!(:read, current_user.access_token)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:manage, current_user.access_token)
|
||||||
|
|
||||||
|
current_user.access_token.token = SecureRandom.base58(AccessToken::TOKEN_LENGTH)
|
||||||
|
|
||||||
|
current_user.access_token.save!
|
||||||
|
|
||||||
|
redirect_back(fallback_location: settings_api_index_path, notice: 'API token as been updated.')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
|
BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/
|
||||||
|
|
||||||
include ActiveStorage::SetCurrent
|
include ActiveStorage::SetCurrent
|
||||||
include Pagy::Backend
|
include Pagy::Backend
|
||||||
|
|
||||||
@@ -14,15 +16,17 @@ class ApplicationController < ActionController::Base
|
|||||||
:current_account,
|
:current_account,
|
||||||
:svg_icon
|
:svg_icon
|
||||||
|
|
||||||
|
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
||||||
|
|
||||||
rescue_from Pagy::OverflowError do
|
rescue_from Pagy::OverflowError do
|
||||||
redirect_to request.path
|
redirect_to request.path
|
||||||
end
|
end
|
||||||
|
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
rescue_from CanCan::AccessDenied do |e|
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
redirect_back(fallback_location: root_path, alert: e.message)
|
redirect_to root_path, alert: e.message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -30,8 +34,32 @@ class ApplicationController < ActionController::Base
|
|||||||
Docuseal.default_url_options
|
Docuseal.default_url_options
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def impersonate_user(user)
|
||||||
|
raise ArgumentError unless user
|
||||||
|
raise Pretender::Error unless true_user
|
||||||
|
|
||||||
|
@impersonated_user = user
|
||||||
|
|
||||||
|
request.session[:impersonated_user_id] = user.uuid
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def with_browser_locale(&)
|
||||||
|
locale = request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
|
||||||
|
|
||||||
|
locale =
|
||||||
|
if locale.starts_with?('en-') && locale != 'en-US'
|
||||||
|
'en-GB'
|
||||||
|
else
|
||||||
|
locale.split('-').first.presence || 'en-GB'
|
||||||
|
end
|
||||||
|
|
||||||
|
locale = 'en-GB' unless I18n.locale_available?(locale)
|
||||||
|
|
||||||
|
I18n.with_locale(locale, &)
|
||||||
|
end
|
||||||
|
|
||||||
def sign_in_for_demo
|
def sign_in_for_demo
|
||||||
sign_in(User.active.order('random()').take) unless signed_in?
|
sign_in(User.active.order('random()').take) unless signed_in?
|
||||||
end
|
end
|
||||||
@@ -44,8 +72,9 @@ class ApplicationController < ActionController::Base
|
|||||||
redirect_to setup_index_path unless User.exists?
|
redirect_to setup_index_path unless User.exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil, icon_disabled: nil)
|
def button_title(title: 'Submit', disabled_with: 'Submitting', title_class: '', icon: nil, icon_disabled: nil)
|
||||||
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon:, icon_disabled: })
|
render_to_string(partial: 'shared/button_title',
|
||||||
|
locals: { title:, disabled_with:, title_class:, icon:, icon_disabled: })
|
||||||
end
|
end
|
||||||
|
|
||||||
def svg_icon(icon_name, class: '')
|
def svg_icon(icon_name, class: '')
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ class ConsoleRedirectController < ApplicationController
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def index
|
def index
|
||||||
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if current_user.blank?
|
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
|
||||||
|
|
||||||
auth = JsonWebToken.encode(uuid: current_user.uuid,
|
auth = JsonWebToken.encode(uuid: true_user.uuid,
|
||||||
scope: :console,
|
scope: :console,
|
||||||
exp: 1.minute.from_now.to_i)
|
exp: 1.minute.from_now.to_i)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ class DashboardController < ApplicationController
|
|||||||
|
|
||||||
before_action :maybe_redirect_product_url
|
before_action :maybe_redirect_product_url
|
||||||
before_action :maybe_render_landing
|
before_action :maybe_render_landing
|
||||||
|
before_action :maybe_redirect_mfa_setup
|
||||||
|
|
||||||
load_and_authorize_resource :template_folder, parent: false
|
load_and_authorize_resource :template_folder, parent: false
|
||||||
load_and_authorize_resource :template, parent: false
|
load_and_authorize_resource :template, parent: false
|
||||||
@@ -51,7 +52,13 @@ class DashboardController < ApplicationController
|
|||||||
|
|
||||||
def filter_templates(templates)
|
def filter_templates(templates)
|
||||||
rel = templates.active.preload(:author).order(id: :desc)
|
rel = templates.active.preload(:author).order(id: :desc)
|
||||||
rel = rel.where(folder_id: current_account.default_template_folder.id) if params[:q].blank?
|
|
||||||
|
if params[:q].blank?
|
||||||
|
shared_template_ids =
|
||||||
|
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
|
||||||
|
|
||||||
|
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
|
||||||
|
end
|
||||||
|
|
||||||
Templates.search(rel, params[:q])
|
Templates.search(rel, params[:q])
|
||||||
end
|
end
|
||||||
@@ -62,6 +69,17 @@ class DashboardController < ApplicationController
|
|||||||
redirect_to Docuseal::PRODUCT_URL, allow_other_host: true
|
redirect_to Docuseal::PRODUCT_URL, allow_other_host: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def maybe_redirect_mfa_setup
|
||||||
|
return unless signed_in?
|
||||||
|
return if current_user.otp_required_for_login
|
||||||
|
|
||||||
|
return if !current_user.otp_required_for_login && !AccountConfig.exists?(value: true,
|
||||||
|
account_id: current_user.account_id,
|
||||||
|
key: AccountConfig::FORCE_MFA)
|
||||||
|
|
||||||
|
redirect_to mfa_setup_path, notice: 'Setup 2FA to continue'
|
||||||
|
end
|
||||||
|
|
||||||
def maybe_render_landing
|
def maybe_render_landing
|
||||||
return if signed_in?
|
return if signed_in?
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class EncryptedUserConfigsController < ApplicationController
|
||||||
|
load_and_authorize_resource :encrypted_user_config
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@encrypted_user_config.destroy!
|
||||||
|
|
||||||
|
redirect_back(fallback_location: root_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,17 +2,21 @@
|
|||||||
|
|
||||||
class ErrorsController < ActionController::Base
|
class ErrorsController < ActionController::Base
|
||||||
ENTERPRISE_FEATURE_MESSAGE =
|
ENTERPRISE_FEATURE_MESSAGE =
|
||||||
'This feature is available in Enterprise Edition: https://www.docuseal.co/pricing'
|
'This feature is available in Pro Edition: https://www.docuseal.co/pricing'
|
||||||
|
|
||||||
ENTERPRISE_PATHS = [
|
ENTERPRISE_PATHS = [
|
||||||
'/templates/html',
|
'/templates/html',
|
||||||
'/api/templates/html',
|
'/api/templates/html',
|
||||||
'/templates/pdf',
|
'/templates/pdf',
|
||||||
'/api/templates/pdf'
|
'/api/templates/pdf',
|
||||||
|
'/templates/doc',
|
||||||
|
'/api/templates/doc',
|
||||||
|
'/templates/docx',
|
||||||
|
'/api/templates/docx'
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
if request.original_fullpath.in?(ENTERPRISE_PATHS)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ class MfaSetupController < ApplicationController
|
|||||||
authorize!(:update, current_user)
|
authorize!(:update, current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
before_action :set_provision_url, only: %i[show new]
|
||||||
current_user.otp_secret ||= User.generate_otp_secret
|
|
||||||
|
|
||||||
current_user.save!
|
def show; end
|
||||||
|
|
||||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
def new; end
|
||||||
end
|
|
||||||
|
|
||||||
def edit; end
|
def edit; end
|
||||||
|
|
||||||
@@ -26,7 +24,7 @@ class MfaSetupController < ApplicationController
|
|||||||
|
|
||||||
@error_message = 'Code is invalid'
|
@error_message = 'Code is invalid'
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/new'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -41,4 +39,16 @@ class MfaSetupController < ApplicationController
|
|||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_provision_url
|
||||||
|
return redirect_to root_path, alert: '2FA has been set up already' if current_user.otp_required_for_login
|
||||||
|
|
||||||
|
current_user.otp_secret ||= User.generate_otp_secret
|
||||||
|
|
||||||
|
current_user.save!
|
||||||
|
|
||||||
|
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|
||||||
def google_oauth2
|
|
||||||
@user = Users.from_omniauth(request.env['omniauth.auth'])
|
|
||||||
|
|
||||||
if @user.persisted?
|
|
||||||
flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: 'Google')
|
|
||||||
|
|
||||||
sign_in_and_redirect @user, event: :authentication
|
|
||||||
else
|
|
||||||
redirect_to new_registration_path(oauth_callback: true, user: @user.slice(:email, :first_name, :last_name)),
|
|
||||||
notice: 'Please complete registration with Google auth'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -7,18 +7,28 @@ class PersonalizationSettingsController < ApplicationController
|
|||||||
|
|
||||||
def create
|
def create
|
||||||
account_config =
|
account_config =
|
||||||
current_account.account_configs.find_or_initialize_by(key: encrypted_config_params[:key])
|
current_account.account_configs.find_or_initialize_by(key: account_config_params[:key])
|
||||||
|
|
||||||
authorize!(:create, account_config)
|
authorize!(:create, account_config)
|
||||||
|
|
||||||
account_config.update!(encrypted_config_params)
|
account_config.update!(account_config_params)
|
||||||
|
|
||||||
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
|
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def encrypted_config_params
|
def account_config_params
|
||||||
params.require(:account_config).permit!
|
attrs = params.require(:account_config).permit!
|
||||||
|
|
||||||
|
attrs[:value]&.transform_values! do |value|
|
||||||
|
if value.in?(%w[true false])
|
||||||
|
value == 'true'
|
||||||
|
else
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attrs
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -6,7 +6,9 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
FORMAT = Templates::ProcessDocument::FORMAT
|
FORMAT = Templates::ProcessDocument::FORMAT
|
||||||
|
|
||||||
def show
|
def show
|
||||||
attachment = ActiveStorage::Attachment.find_by(uuid: params[:attachment_uuid])
|
attachment_uuid = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid], purpose: :attachment)
|
||||||
|
|
||||||
|
attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid, name: :preview_images) if attachment_uuid
|
||||||
|
|
||||||
return head :not_found unless attachment
|
return head :not_found unless attachment
|
||||||
|
|
||||||
@@ -21,9 +23,10 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
find_or_create_document_tempfile_path(attachment)
|
find_or_create_document_tempfile_path(attachment)
|
||||||
end
|
end
|
||||||
|
|
||||||
io = Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
preview_image =
|
||||||
|
Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
||||||
|
|
||||||
render plain: io.tap(&:rewind).read, content_type: 'image/jpeg'
|
redirect_to preview_image.url, allow_other_host: true
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_document_tempfile_path(attachment)
|
def find_or_create_document_tempfile_path(attachment)
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ class SessionsController < Devise::SessionsController
|
|||||||
before_action :configure_permitted_parameters
|
before_action :configure_permitted_parameters
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if Docuseal.multitenant? && !User.exists?(email: sign_in_params[:email])
|
email = sign_in_params[:email].to_s.downcase
|
||||||
|
|
||||||
|
if Docuseal.multitenant? && !User.exists?(email:)
|
||||||
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
|
||||||
|
|
||||||
if User.exists?(email: sign_in_params[:email], otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
||||||
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_entity
|
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -6,30 +6,32 @@ class StartFormController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
around_action :with_browser_locale, only: %i[show completed]
|
||||||
before_action :load_template
|
before_action :load_template
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submitter = @template.submissions.new.submitters.new(uuid: @template.submitters.first['uuid'])
|
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||||
|
.submitters.new(uuid: @template.submitters.first['uuid'])
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@submitter = Submitter.where(submission: @template.submissions.where(deleted_at: nil))
|
return redirect_to start_form_path(@template.slug) if @template.archived_at?
|
||||||
|
|
||||||
|
@submitter = Submitter.where(submission: @template.submissions.where(archived_at: nil))
|
||||||
|
.order(id: :desc)
|
||||||
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
||||||
.find_or_initialize_by(**submitter_params.compact_blank)
|
.find_or_initialize_by(**submitter_params.compact_blank)
|
||||||
|
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
|
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
|
||||||
else
|
else
|
||||||
@submitter.assign_attributes(
|
if @template.submitters.to_a.size > 1 && @submitter.new_record?
|
||||||
uuid: @template.submitters.first['uuid'],
|
@error_message = 'Not found'
|
||||||
ip: request.remote_ip,
|
|
||||||
ua: request.user_agent,
|
|
||||||
preferences: { 'send_email' => true }
|
|
||||||
)
|
|
||||||
|
|
||||||
@submitter.submission ||= Submission.new(template: @template,
|
return render :show
|
||||||
template_submitters: @template.submitters,
|
end
|
||||||
source: :link)
|
|
||||||
|
assign_submission_attributes(@submitter, @template) if @submitter.new_record?
|
||||||
|
|
||||||
if @submitter.save
|
if @submitter.save
|
||||||
redirect_to submit_form_path(@submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
@@ -47,6 +49,35 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def assign_submission_attributes(submitter, template)
|
||||||
|
resubmit_submitter =
|
||||||
|
if params[:resubmit].present?
|
||||||
|
Submitter.where(submission: @template.submissions).find_by(slug: params[:resubmit])
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter.assign_attributes(
|
||||||
|
uuid: template.submitters.first['uuid'],
|
||||||
|
ip: request.remote_ip,
|
||||||
|
ua: request.user_agent,
|
||||||
|
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
|
||||||
|
preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true },
|
||||||
|
metadata: resubmit_submitter&.metadata.presence || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
if submitter.values.present?
|
||||||
|
resubmit_submitter.attachments.each do |attachment|
|
||||||
|
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter.submission ||= Submission.new(template:,
|
||||||
|
account_id: template.account_id,
|
||||||
|
template_submitters: template.submitters,
|
||||||
|
source: :link)
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
|
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
|
||||||
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class SubmissionsController < ApplicationController
|
|||||||
source: :invite,
|
source: :invite,
|
||||||
mark_as_sent: params[:send_email] == '1',
|
mark_as_sent: params[:send_email] == '1',
|
||||||
emails: params[:emails],
|
emails: params[:emails],
|
||||||
params:)
|
params: params.merge('send_completed_email' => true))
|
||||||
else
|
else
|
||||||
Submissions.create_from_submitters(template: @template,
|
Submissions.create_from_submitters(template: @template,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
@@ -54,7 +54,7 @@ class SubmissionsController < ApplicationController
|
|||||||
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||||
mark_as_sent: params[:send_email] == '1',
|
mark_as_sent: params[:send_email] == '1',
|
||||||
submissions_attrs: submissions_params[:submission].to_h.values,
|
submissions_attrs: submissions_params[:submission].to_h.values,
|
||||||
params:)
|
params: params.merge('send_completed_email' => true))
|
||||||
end
|
end
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
@@ -63,7 +63,7 @@ class SubmissionsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@submission.update!(deleted_at: Time.current)
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|
||||||
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
|
||||||
@@ -75,6 +75,6 @@ class SubmissionsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def load_template
|
def load_template
|
||||||
@template = current_account.templates.find(params[:template_id])
|
@template = Template.accessible_by(current_ability).find(params[:template_id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -4,8 +4,10 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
TTL = 20.minutes
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitter = Submitter.find_by(slug: params[:submitter_slug])
|
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
@@ -13,9 +15,18 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
|
return head :not_found unless last_submitter.completed_at?
|
||||||
|
|
||||||
|
if last_submitter.completed_at < TTL.ago &&
|
||||||
|
(current_user.nil? || !current_user.account.submitters.exists?(id: last_submitter.id))
|
||||||
|
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
|
||||||
urls =
|
urls =
|
||||||
Submitters.select_attachments_for_download(last_submitter).map do |attachment|
|
Submitters.select_attachments_for_download(last_submitter).map do |attachment|
|
||||||
helpers.rails_blob_url(attachment)
|
ActiveStorage::Blob.proxy_url(attachment.blob)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: urls
|
render json: urls
|
||||||
|
|||||||
@@ -6,9 +6,21 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
PRELOAD_ALL_PAGES_AMOUNT = 200
|
PRELOAD_ALL_PAGES_AMOUNT = 200
|
||||||
|
|
||||||
|
TTL = 20.minutes
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submission = Submission.find_by!(slug: params[:slug])
|
@submission = Submission.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
|
if !@submission.submitters.all?(&:completed_at?) && current_user.blank?
|
||||||
|
raise ActionController::RoutingError, 'Not Found'
|
||||||
|
end
|
||||||
|
|
||||||
|
unless submission_valid_ttl?(@submission)
|
||||||
|
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return redirect_to submissions_preview_completed_path(@submission.slug)
|
||||||
|
end
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@submission],
|
records: [@submission],
|
||||||
associations: [:template, { template_schema_documents: :blob }]
|
associations: [:template, { template_schema_documents: :blob }]
|
||||||
@@ -26,4 +38,20 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
render 'submissions/show', layout: 'plain'
|
render 'submissions/show', layout: 'plain'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def completed
|
||||||
|
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
||||||
|
|
||||||
|
render :completed, layout: 'plain'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def submission_valid_ttl?(submission)
|
||||||
|
return true if current_user && current_user.account.submissions.exists?(id: submission.id)
|
||||||
|
|
||||||
|
last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
|
||||||
|
|
||||||
|
last_submitter && last_submitter.completed_at > TTL.ago
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
class SubmitFormController < ApplicationController
|
class SubmitFormController < ApplicationController
|
||||||
layout 'form'
|
layout 'form'
|
||||||
|
|
||||||
|
around_action :with_browser_locale, only: %i[show completed success]
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
@@ -32,12 +33,22 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
cookies[:submitter_sid] = @submitter.signed_id
|
cookies[:submitter_sid] = @submitter.signed_id
|
||||||
|
|
||||||
render @submitter.submission.template.deleted_at? ? :archived : :show
|
render(@submitter.submission.template.archived_at? || @submitter.submission.archived_at? ? :archived : :show)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
submitter = Submitter.find_by!(slug: params[:slug])
|
submitter = Submitter.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
|
if submitter.completed_at?
|
||||||
|
return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
if submitter.template.archived_at? || submitter.submission.archived_at?
|
||||||
|
Rollbar.info("Archived template: #{submitter.template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
Submitters::SubmitValues.call(submitter, params, request)
|
Submitters::SubmitValues.call(submitter, params, request)
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
@@ -48,4 +59,6 @@ class SubmitFormController < ApplicationController
|
|||||||
def completed
|
def completed
|
||||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def success; end
|
||||||
end
|
end
|
||||||
@@ -11,9 +11,7 @@ class SubmittersSendEmailController < ApplicationController
|
|||||||
alert: 'Email has been sent already.')
|
alert: 'Email has been sent already.')
|
||||||
end
|
end
|
||||||
|
|
||||||
SubmitterMailer.invitation_email(@submitter).deliver_later!
|
SendSubmitterInvitationEmailJob.perform_later('submitter_id' => @submitter.id)
|
||||||
|
|
||||||
SubmissionEvent.create!(submitter: @submitter, event_type: 'send_email')
|
|
||||||
|
|
||||||
@submitter.sent_at ||= Time.current
|
@submitter.sent_at ||= Time.current
|
||||||
@submitter.save!
|
@submitter.save!
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplateSharingsTestingController < ApplicationController
|
||||||
|
load_and_authorize_resource :template, parent: true
|
||||||
|
|
||||||
|
before_action do
|
||||||
|
authorize!(:manage, TemplateSharing.new(template: @template))
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
testing_account = Accounts.find_or_create_testing_user(true_user.account).account
|
||||||
|
|
||||||
|
if params[:value] == '1'
|
||||||
|
TemplateSharing.create!(ability: :manage, account: testing_account, template: @template)
|
||||||
|
else
|
||||||
|
TemplateSharing.find_by(template: @template, account: testing_account)&.destroy!
|
||||||
|
end
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,7 +4,7 @@ class TemplatesArchivedController < ApplicationController
|
|||||||
load_and_authorize_resource :template, parent: false
|
load_and_authorize_resource :template, parent: false
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@templates = @templates.where.not(deleted_at: nil).preload(:author, :folder).order(id: :desc)
|
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder).order(id: :desc)
|
||||||
@templates = Templates.search(@templates, params[:q])
|
@templates = Templates.search(@templates, params[:q])
|
||||||
|
|
||||||
@pagy, @templates = pagy(@templates, items: 12)
|
@pagy, @templates = pagy(@templates, items: 12)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
|
|||||||
load_and_authorize_resource :submission, through: :template, parent: false
|
load_and_authorize_resource :submission, through: :template, parent: false
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@submissions = @submissions.where.not(deleted_at: nil)
|
@submissions = @submissions.where.not(archived_at: nil)
|
||||||
@submissions = Submissions.search(@submissions, params[:q])
|
@submissions = Submissions.search(@submissions, params[:q])
|
||||||
|
|
||||||
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesCodeModalController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
end
|
||||||
@@ -6,8 +6,8 @@ class TemplatesController < ApplicationController
|
|||||||
before_action :load_base_template, only: %i[new create]
|
before_action :load_base_template, only: %i[new create]
|
||||||
|
|
||||||
def show
|
def show
|
||||||
submissions = @template.submissions
|
submissions = @template.submissions.accessible_by(current_ability)
|
||||||
submissions = submissions.active if @template.deleted_at.blank?
|
submissions = submissions.active if @template.archived_at.blank?
|
||||||
submissions = Submissions.search(submissions, params[:q])
|
submissions = Submissions.search(submissions, params[:q])
|
||||||
|
|
||||||
@base_submissions = submissions
|
@base_submissions = submissions
|
||||||
@@ -33,7 +33,7 @@ class TemplatesController < ApplicationController
|
|||||||
@template_data =
|
@template_data =
|
||||||
@template.as_json.merge(
|
@template.as_json.merge(
|
||||||
documents: @template.schema_documents.as_json(
|
documents: @template.schema_documents.as_json(
|
||||||
methods: [:metadata],
|
methods: %i[metadata signed_uuid],
|
||||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||||
)
|
)
|
||||||
).to_json
|
).to_json
|
||||||
@@ -42,15 +42,26 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@template.account = current_account
|
if @base_template
|
||||||
@template.author = current_user
|
@template = Templates::Clone.call(@base_template, author: current_user,
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
name: params.dig(:template, :name),
|
||||||
@template.assign_attributes(@base_template.slice(:fields, :schema, :submitters)) if @base_template
|
folder_name: params[:folder_name])
|
||||||
|
else
|
||||||
|
@template.author = current_user
|
||||||
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:account_id].present? && authorized_clone_account_id?(params[:account_id])
|
||||||
|
@template.account_id = params[:account_id]
|
||||||
|
@template.folder = @template.account.default_template_folder
|
||||||
|
else
|
||||||
|
@template.account = current_account
|
||||||
|
end
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
redirect_to edit_template_path(@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
|
||||||
end
|
end
|
||||||
@@ -58,12 +69,14 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
notice =
|
notice =
|
||||||
if !Docuseal.multitenant? && params[:permanently].present?
|
if params[:permanently].present?
|
||||||
@template.destroy!
|
@template.destroy!
|
||||||
|
|
||||||
|
Rollbar.info("Remove template: #{@template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
'Template has been removed.'
|
'Template has been removed.'
|
||||||
else
|
else
|
||||||
@template.update!(deleted_at: Time.current)
|
@template.update!(archived_at: Time.current)
|
||||||
|
|
||||||
'Template has been archived.'
|
'Template has been archived.'
|
||||||
end
|
end
|
||||||
@@ -73,6 +86,18 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def authorized_clone_account_id?(account_id)
|
||||||
|
true_user.account_id.to_s == account_id.to_s || true_user.account.linked_accounts.exists?(id: account_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_redirect_to_template(template)
|
||||||
|
if template.account == current_account
|
||||||
|
redirect_to(edit_template_path(@template))
|
||||||
|
else
|
||||||
|
redirect_back(fallback_location: root_path, notice: 'Template has been clonned')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(:name)
|
params.require(:template).permit(:name)
|
||||||
end
|
end
|
||||||
@@ -80,6 +105,6 @@ class TemplatesController < ApplicationController
|
|||||||
def load_base_template
|
def load_base_template
|
||||||
return if params[:base_template_id].blank?
|
return if params[:base_template_id].blank?
|
||||||
|
|
||||||
@base_template = current_account.templates.find_by(id: params[:base_template_id])
|
@base_template = Template.accessible_by(current_ability).find_by(id: params[:base_template_id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesPreviewController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def show
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [@template],
|
||||||
|
associations: [schema_documents: { preview_images_attachments: :blob }]
|
||||||
|
).call
|
||||||
|
|
||||||
|
@template_data =
|
||||||
|
@template.as_json.merge(
|
||||||
|
documents: @template.schema_documents.as_json(
|
||||||
|
methods: %i[metadata signed_uuid],
|
||||||
|
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||||
|
)
|
||||||
|
).to_json
|
||||||
|
|
||||||
|
render :show, layout: 'plain'
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,7 +4,7 @@ class TemplatesRestoreController < ApplicationController
|
|||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@template.update!(deleted_at: nil)
|
@template.update!(archived_at: nil)
|
||||||
|
|
||||||
redirect_to template_path(@template), notice: 'Template has been unarchived'
|
redirect_to template_path(@template), notice: 'Template has been unarchived'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,12 +10,7 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
url_params = create_file_params_from_url if params[:url].present?
|
url_params = create_file_params_from_url if params[:url].present?
|
||||||
|
|
||||||
@template.account = current_account
|
save_template!(@template, url_params)
|
||||||
@template.author = current_user
|
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
|
||||||
@template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
|
||||||
|
|
||||||
@template.save!
|
|
||||||
|
|
||||||
documents = Templates::CreateAttachments.call(@template, url_params || params)
|
documents = Templates::CreateAttachments.call(@template, url_params || params)
|
||||||
|
|
||||||
@@ -24,6 +19,8 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
@template.update!(schema:)
|
@template.update!(schema:)
|
||||||
|
|
||||||
redirect_to edit_template_path(@template)
|
redirect_to edit_template_path(@template)
|
||||||
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
|
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)
|
||||||
|
|
||||||
@@ -32,6 +29,17 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def save_template!(template, url_params)
|
||||||
|
template.account = current_account
|
||||||
|
template.author = current_user
|
||||||
|
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
|
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
||||||
|
|
||||||
|
template.save!
|
||||||
|
|
||||||
|
template
|
||||||
|
end
|
||||||
|
|
||||||
def create_file_params_from_url
|
def create_file_params_from_url
|
||||||
tempfile = Tempfile.new
|
tempfile = Tempfile.new
|
||||||
tempfile.binmode
|
tempfile.binmode
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TestingAccountsController < ApplicationController
|
||||||
|
skip_authorization_check only: :destroy
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize!(:manage, current_account)
|
||||||
|
authorize!(:manage, current_user)
|
||||||
|
|
||||||
|
impersonate_user(Accounts.find_or_create_testing_user(true_user.account))
|
||||||
|
|
||||||
|
redirect_back(fallback_location: root_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
stop_impersonating_user
|
||||||
|
|
||||||
|
redirect_back(fallback_location: root_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TestingApiSettingsController < ApplicationController
|
||||||
|
def index
|
||||||
|
authorize!(:manage, current_user.access_token)
|
||||||
|
|
||||||
|
@webhook_config =
|
||||||
|
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||||
|
|
||||||
|
authorize!(:manage, @webhook_config)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TimestampServerController < ApplicationController
|
||||||
|
before_action :build_encrypted_config
|
||||||
|
authorize_resource :encrypted_config
|
||||||
|
|
||||||
|
def create
|
||||||
|
return head :not_found if Docuseal.multitenant?
|
||||||
|
|
||||||
|
test_timeserver_url(@encrypted_config.value) if @encrypted_config.value.present?
|
||||||
|
|
||||||
|
if @encrypted_config.value.present? ? @encrypted_config.save : @encrypted_config.delete
|
||||||
|
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
|
||||||
|
else
|
||||||
|
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
||||||
|
end
|
||||||
|
rescue HexaPDF::Error, SocketError, Submissions::TimestampHandler::TimestampError, OpenSSL::Timestamp::TimestampError
|
||||||
|
redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def test_timeserver_url(url)
|
||||||
|
pdf = HexaPDF::Document.new
|
||||||
|
pdf.pages.add
|
||||||
|
|
||||||
|
pkcs = Accounts.load_signing_pkcs(current_account)
|
||||||
|
|
||||||
|
pdf.sign(StringIO.new,
|
||||||
|
reason: 'Test',
|
||||||
|
certificate: pkcs.certificate,
|
||||||
|
key: pkcs.key,
|
||||||
|
signature_size: 10_000,
|
||||||
|
certificate_chain: pkcs.ca_certs || [],
|
||||||
|
timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url))
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_encrypted_config
|
||||||
|
@encrypted_config
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_encrypted_config
|
||||||
|
@encrypted_config =
|
||||||
|
EncryptedConfig.find_or_initialize_by(account: current_account,
|
||||||
|
key: EncryptedConfig::TIMESTAMP_SERVER_URL_KEY)
|
||||||
|
|
||||||
|
@encrypted_config.assign_attributes(encrypted_config_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def encrypted_config_params
|
||||||
|
params.require(:encrypted_config).permit(:value)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -28,6 +28,12 @@ class UserSignaturesController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@user_config.destroy
|
||||||
|
|
||||||
|
redirect_to settings_profile_index_path, notice: 'Signature has been removed'
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_user_config
|
def load_user_config
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ class UsersController < ApplicationController
|
|||||||
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
|
||||||
attrs.delete(:role) if User::ROLES.exclude?(attrs[:role])
|
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
|
||||||
|
|
||||||
if @user.update(attrs.except(current_user == @user ? :role : nil))
|
if @user.update(attrs)
|
||||||
redirect_to settings_users_path, notice: 'User has been updated'
|
redirect_to settings_users_path, notice: 'User has been updated'
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
|
||||||
@@ -42,17 +42,21 @@ class UsersController < ApplicationController
|
|||||||
return redirect_to settings_users_path, notice: 'Unable to remove user'
|
return redirect_to settings_users_path, notice: 'Unable to remove user'
|
||||||
end
|
end
|
||||||
|
|
||||||
@user.update!(deleted_at: Time.current)
|
@user.update!(archived_at: Time.current)
|
||||||
|
|
||||||
redirect_to settings_users_path, notice: 'User has been removed'
|
redirect_to settings_users_path, notice: 'User has been removed'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def role_valid?(role)
|
||||||
|
User::ROLES.include?(role)
|
||||||
|
end
|
||||||
|
|
||||||
def build_user
|
def build_user
|
||||||
@user = current_account.users.find_by(email: user_params[:email])&.tap do |user|
|
@user = current_account.users.find_by(email: user_params[:email])&.tap do |user|
|
||||||
user.assign_attributes(user_params)
|
user.assign_attributes(user_params)
|
||||||
user.deleted_at = nil
|
user.archived_at = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@user ||= current_account.users.new(user_params)
|
@user ||= current_account.users.new(user_params)
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ class WebhookSettingsController < ApplicationController
|
|||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@encrypted_config.update!(encrypted_config_params)
|
@encrypted_config.assign_attributes(encrypted_config_params)
|
||||||
|
|
||||||
|
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
|
||||||
|
|
||||||
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
|
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import SubmittersAutocomplete from './elements/submitter_autocomplete'
|
|||||||
import FolderAutocomplete from './elements/folder_autocomplete'
|
import FolderAutocomplete from './elements/folder_autocomplete'
|
||||||
import SignatureForm from './elements/signature_form'
|
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 * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ window.customElements.define('submitters-autocomplete', SubmittersAutocomplete)
|
|||||||
window.customElements.define('folder-autocomplete', FolderAutocomplete)
|
window.customElements.define('folder-autocomplete', FolderAutocomplete)
|
||||||
window.customElements.define('signature-form', SignatureForm)
|
window.customElements.define('signature-form', SignatureForm)
|
||||||
window.customElements.define('submit-form', SubmitForm)
|
window.customElements.define('submit-form', SubmitForm)
|
||||||
|
window.customElements.define('prompt-password', PromptPassword)
|
||||||
|
|
||||||
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) => {
|
||||||
@@ -86,6 +88,9 @@ window.customElements.define('template-builder', class extends HTMLElement {
|
|||||||
backgroundColor: '#faf7f5',
|
backgroundColor: '#faf7f5',
|
||||||
withPhone: this.dataset.withPhone === 'true',
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
withLogo: this.dataset.withLogo !== 'false',
|
withLogo: this.dataset.withLogo !== 'false',
|
||||||
|
editable: this.dataset.editable !== 'false',
|
||||||
|
withPayment: this.dataset.withPayment === 'true',
|
||||||
|
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -75,16 +75,16 @@ button[disabled] .enabled {
|
|||||||
@apply select base-input w-full font-normal;
|
@apply select base-input w-full font-normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-bottom-end:before {
|
.tooltip-bottom-start:before {
|
||||||
transform: translateX(-95%);
|
transform: translateX(-30%);
|
||||||
top: var(--tooltip-offset);
|
top: var(--tooltip-offset);
|
||||||
left: 100%;
|
left: 100%;
|
||||||
right: auto;
|
right: auto;
|
||||||
bottom: auto;
|
bottom: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-bottom-end:after {
|
.tooltip-bottom-start:after {
|
||||||
transform: translateX(-25%);
|
transform: translateX(-75%);
|
||||||
border-color: transparent transparent var(--tooltip-color) transparent;
|
border-color: transparent transparent var(--tooltip-color) transparent;
|
||||||
top: var(--tooltip-tail-offset);
|
top: var(--tooltip-tail-offset);
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|||||||
@@ -18,27 +18,64 @@ 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((response) => response.json()).then((urls) => {
|
||||||
const fileRequests = urls.map((url) => {
|
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||||
return () => {
|
|
||||||
return fetch(url).then(async (resp) => {
|
|
||||||
const blobUrl = URL.createObjectURL(await resp.blob())
|
|
||||||
const link = document.createElement('a')
|
|
||||||
|
|
||||||
link.href = blobUrl
|
if (isSafariIos && urls.length > 1) {
|
||||||
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
this.downloadSafariIos(urls)
|
||||||
|
} else {
|
||||||
|
this.downloadUrls(urls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
link.click()
|
downloadUrls (urls) {
|
||||||
|
const fileRequests = urls.map((url) => {
|
||||||
|
return () => {
|
||||||
|
return fetch(url).then(async (resp) => {
|
||||||
|
const blobUrl = URL.createObjectURL(await resp.blob())
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
URL.revokeObjectURL(url)
|
link.href = blobUrl
|
||||||
})
|
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
||||||
}
|
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
URL.revokeObjectURL(blobUrl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fileRequests.reduce(
|
||||||
|
(prevPromise, request) => prevPromise.then(() => request()),
|
||||||
|
Promise.resolve()
|
||||||
|
)
|
||||||
|
|
||||||
|
this.toggleState()
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadSafariIos (urls) {
|
||||||
|
const fileRequests = urls.map((url) => {
|
||||||
|
return fetch(url).then(async (resp) => {
|
||||||
|
const blob = await resp.blob()
|
||||||
|
const blobUrl = URL.createObjectURL(blob.slice(0, blob.size, 'application/octet-stream'))
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
|
link.href = blobUrl
|
||||||
|
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
||||||
|
|
||||||
|
return link
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
fileRequests.reduce(
|
Promise.all(fileRequests).then((links) => {
|
||||||
(prevPromise, request) => prevPromise.then(() => request()),
|
links.forEach((link, index) => {
|
||||||
Promise.resolve()
|
setTimeout(() => {
|
||||||
)
|
link.click()
|
||||||
|
|
||||||
|
URL.revokeObjectURL(link.href)
|
||||||
|
}, index * 50)
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
this.toggleState()
|
this.toggleState()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export default class extends HTMLElement {
|
|||||||
render = (item) => {
|
render = (item) => {
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
|
|
||||||
|
div.setAttribute('dir', 'auto')
|
||||||
|
|
||||||
div.textContent = item.name
|
div.textContent = item.name
|
||||||
|
|
||||||
return div
|
return div
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
|
||||||
|
input.type = 'hidden'
|
||||||
|
input.name = 'password'
|
||||||
|
input.value = prompt('Enter PDF password')
|
||||||
|
|
||||||
|
this.form.append(input)
|
||||||
|
|
||||||
|
this.form.requestSubmit()
|
||||||
|
|
||||||
|
this.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.closest('form')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,8 @@ export default class extends HTMLElement {
|
|||||||
render = (item) => {
|
render = (item) => {
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
|
|
||||||
|
div.setAttribute('dir', 'auto')
|
||||||
|
|
||||||
div.textContent = item[this.dataset.field]
|
div.textContent = item[this.dataset.field]
|
||||||
|
|
||||||
return div
|
return div
|
||||||
|
|||||||
@@ -10,15 +10,16 @@ window.customElements.define('submission-form', class extends HTMLElement {
|
|||||||
|
|
||||||
this.app = createApp(Form, {
|
this.app = createApp(Form, {
|
||||||
submitter: JSON.parse(this.dataset.submitter),
|
submitter: JSON.parse(this.dataset.submitter),
|
||||||
authenticityToken: this.dataset.authenticityToken,
|
|
||||||
canSendEmail: this.dataset.canSendEmail === 'true',
|
canSendEmail: this.dataset.canSendEmail === 'true',
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
||||||
goToLast: this.dataset.goToLast === 'true',
|
goToLast: this.dataset.goToLast === 'true',
|
||||||
isDemo: this.dataset.isDemo === 'true',
|
isDemo: this.dataset.isDemo === 'true',
|
||||||
attribution: this.dataset.attribution !== 'false',
|
attribution: this.dataset.attribution !== 'false',
|
||||||
withConfetti: true,
|
withConfetti: this.dataset.withConfetti !== 'false',
|
||||||
|
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
||||||
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),
|
||||||
|
completedRedirectUrl: this.dataset.completedRedirectUrl,
|
||||||
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
||||||
fields: JSON.parse(this.dataset.fields)
|
fields: JSON.parse(this.dataset.fields)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="uniqueAreas.length > 1"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
<div class="flex space-x-1 text-sm ml-1 -mt-3">
|
||||||
|
<span>
|
||||||
|
{{ t('appears_on') }}:
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
v-for="area in uniqueAreas"
|
||||||
|
:key="area.uuid"
|
||||||
|
href="#"
|
||||||
|
class="link pr-0.5"
|
||||||
|
@click.prevent="scrollIntoArea(area)"
|
||||||
|
>
|
||||||
|
{{ t('page') }} <template v-if="isMultipleDocs">{{ attachmentUuids.indexOf(area.attachment_uuid) + 1 }}-</template>{{ area.page + 1 }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'AppearsOn',
|
||||||
|
inject: ['t', 'scrollIntoArea'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isMultipleDocs () {
|
||||||
|
return this.attachmentUuids.length > 1
|
||||||
|
},
|
||||||
|
attachmentUuids () {
|
||||||
|
return [...new Set(this.uniqueAreas.map((e) => e.attachment_uuid))]
|
||||||
|
},
|
||||||
|
uniqueAreas () {
|
||||||
|
const areas = {}
|
||||||
|
|
||||||
|
this.field.areas?.forEach((area) => {
|
||||||
|
areas[area.attachment_uuid + area.page] ||= area
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.values(areas).slice(0, 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex absolute lg:text-base"
|
class="flex absolute lg:text-base -outline-offset-1"
|
||||||
|
dir="auto"
|
||||||
:style="computedStyle"
|
:style="computedStyle"
|
||||||
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-70': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-30': (isActive || isValueSet) && submittable }"
|
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, '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 }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
||||||
@@ -21,15 +22,20 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isActive && withLabel && !area.option_uuid"
|
v-if="isActive && withLabel && (!area.option_uuid || !option.value)"
|
||||||
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap"
|
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap pointer-events-none"
|
||||||
>
|
>
|
||||||
{{ field.name || fieldNames[field.type] }}
|
<template v-if="area.option_uuid && !option.value">
|
||||||
<template v-if="field.type === 'checkbox' && !field.name">
|
{{ optionValue(option) }}
|
||||||
{{ fieldIndex + 1 }}
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
<template v-else>
|
||||||
(optional)
|
{{ field.name || fieldNames[field.type] }}
|
||||||
|
<template v-if="field.type === 'checkbox' && !field.name">
|
||||||
|
{{ fieldIndex + 1 }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
||||||
|
(optional)
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -42,6 +48,11 @@
|
|||||||
class="object-contain mx-auto"
|
class="object-contain mx-auto"
|
||||||
:src="image.url"
|
:src="image.url"
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
v-else-if="field.type === 'stamp' && stamp"
|
||||||
|
class="object-contain mx-auto"
|
||||||
|
:src="stamp.url"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
v-else-if="field.type === 'signature' && signature"
|
v-else-if="field.type === 'signature' && signature"
|
||||||
class="object-contain mx-auto"
|
class="object-contain mx-auto"
|
||||||
@@ -53,7 +64,7 @@
|
|||||||
:src="initials.url"
|
:src="initials.url"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'file'"
|
v-else-if="field.type === 'file' || field.type === 'payment'"
|
||||||
class="px-0.5 flex flex-col justify-center"
|
class="px-0.5 flex flex-col justify-center"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -70,7 +81,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'checkbox'"
|
v-else-if="field.type === 'checkbox'"
|
||||||
class="w-full p-[0.2vw] flex items-center justify-center"
|
class="w-full p-[1px] flex items-center justify-center"
|
||||||
|
@click="$event.target.querySelector('input')?.click()"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-if="submittable"
|
v-if="submittable"
|
||||||
@@ -89,26 +101,28 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'radio' && area.option_uuid"
|
v-else-if="field.type === 'radio' && area.option_uuid"
|
||||||
class="w-full p-[0.2vw] flex items-center justify-center"
|
class="w-full p-[1px] flex items-center justify-center"
|
||||||
|
@click="$event.target.querySelector('input')?.click()"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-if="submittable"
|
v-if="submittable"
|
||||||
type="radio"
|
type="radio"
|
||||||
:value="false"
|
:value="false"
|
||||||
class="aspect-square base-radio"
|
class="aspect-square checked:checkbox checked:checkbox-xs"
|
||||||
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
:class="{ 'base-radio': !modelValue || modelValue !== optionValue(option), '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
:checked="!!modelValue && modelValue === field.options.find((o) => o.uuid === area.option_uuid)?.value"
|
:checked="!!modelValue && modelValue === optionValue(option)"
|
||||||
@click="$emit('update:model-value', field.options.find((o) => o.uuid === area.option_uuid)?.value)"
|
@click="$emit('update:model-value', optionValue(option))"
|
||||||
>
|
>
|
||||||
<IconCheck
|
<IconCheck
|
||||||
v-else-if="!!modelValue && modelValue === field.options.find((o) => o.uuid === area.option_uuid)?.value"
|
v-else-if="!!modelValue && modelValue === optionValue(option)"
|
||||||
class="aspect-square"
|
class="aspect-square"
|
||||||
: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 }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'multiple' && area.option_uuid"
|
v-else-if="field.type === 'multiple' && area.option_uuid"
|
||||||
class="w-full p-[0.2vw] flex items-center justify-center"
|
class="w-full p-[1px] flex items-center justify-center"
|
||||||
|
@click="$event.target.querySelector('input')?.click()"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-if="submittable"
|
v-if="submittable"
|
||||||
@@ -116,11 +130,11 @@
|
|||||||
:value="false"
|
:value="false"
|
||||||
class="aspect-square base-checkbox"
|
class="aspect-square base-checkbox"
|
||||||
: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 }"
|
||||||
:checked="!!modelValue && modelValue.includes(field.options.find((o) => o.uuid === area.option_uuid)?.value)"
|
:checked="!!modelValue && modelValue.includes(optionValue(option))"
|
||||||
@change="updateMultipleSelectValue(field.options.find((o) => o.uuid === area.option_uuid)?.value)"
|
@change="updateMultipleSelectValue(optionValue(option))"
|
||||||
>
|
>
|
||||||
<IconCheck
|
<IconCheck
|
||||||
v-else-if="!!modelValue && modelValue.includes(field.options.find((o) => o.uuid === area.option_uuid)?.value)"
|
v-else-if="!!modelValue && modelValue.includes(optionValue(option))"
|
||||||
class="aspect-square"
|
class="aspect-square"
|
||||||
: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 }"
|
||||||
/>
|
/>
|
||||||
@@ -141,7 +155,8 @@
|
|||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
ref="textContainer"
|
ref="textContainer"
|
||||||
class="flex items-center px-0.5"
|
dir="auto"
|
||||||
|
class="flex items-center px-0.5 w-full"
|
||||||
>
|
>
|
||||||
<span v-if="Array.isArray(modelValue)">
|
<span v-if="Array.isArray(modelValue)">
|
||||||
{{ modelValue.join(', ') }}
|
{{ modelValue.join(', ') }}
|
||||||
@@ -158,7 +173,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
@@ -166,6 +181,7 @@ export default {
|
|||||||
IconPaperclip,
|
IconPaperclip,
|
||||||
IconCheck
|
IconCheck
|
||||||
},
|
},
|
||||||
|
inject: ['t'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -220,20 +236,25 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
fieldNames () {
|
fieldNames () {
|
||||||
return {
|
return {
|
||||||
text: 'Text',
|
text: this.t('text'),
|
||||||
cells: 'Text',
|
signature: this.t('signature'),
|
||||||
signature: 'Signature',
|
initials: this.t('initials'),
|
||||||
date: 'Date',
|
date: this.t('date'),
|
||||||
image: 'Image',
|
image: this.t('image'),
|
||||||
initials: 'Initials',
|
file: this.t('file'),
|
||||||
file: 'File',
|
select: this.t('select'),
|
||||||
select: 'Select',
|
checkbox: this.t('checkbox'),
|
||||||
checkbox: 'Checkbox',
|
multiple: this.t('multiple'),
|
||||||
radio: 'Radio',
|
radio: this.t('radio'),
|
||||||
multiple: 'Multiple Select',
|
cells: this.t('cells'),
|
||||||
phone: 'Phone'
|
stamp: this.t('stamp'),
|
||||||
|
payment: this.t('payment'),
|
||||||
|
phone: this.t('phone')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
option () {
|
||||||
|
return this.field.options.find((o) => o.uuid === this.area.option_uuid)
|
||||||
|
},
|
||||||
fieldIcons () {
|
fieldIcons () {
|
||||||
return {
|
return {
|
||||||
text: IconTextSize,
|
text: IconTextSize,
|
||||||
@@ -245,9 +266,11 @@ export default {
|
|||||||
select: IconSelect,
|
select: IconSelect,
|
||||||
checkbox: IconCheckbox,
|
checkbox: IconCheckbox,
|
||||||
radio: IconCircleDot,
|
radio: IconCircleDot,
|
||||||
|
stamp: IconRubberStamp,
|
||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
phone: IconPhoneCheck
|
phone: IconPhoneCheck,
|
||||||
|
payment: IconCreditCard
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
image () {
|
image () {
|
||||||
@@ -257,6 +280,13 @@ export default {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
stamp () {
|
||||||
|
if (this.field.type === 'stamp') {
|
||||||
|
return this.attachmentsIndex[this.modelValue]
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
signature () {
|
signature () {
|
||||||
if (this.field.type === 'signature') {
|
if (this.field.type === 'signature') {
|
||||||
return this.attachmentsIndex[this.modelValue]
|
return this.attachmentsIndex[this.modelValue]
|
||||||
@@ -271,9 +301,15 @@ export default {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
locale () {
|
||||||
|
return Intl.DateTimeFormat().resolvedOptions()?.locale
|
||||||
|
},
|
||||||
formattedDate () {
|
formattedDate () {
|
||||||
if (this.field.type === 'date' && this.modelValue) {
|
if (this.field.type === 'date' && this.modelValue) {
|
||||||
return new Intl.DateTimeFormat([], { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(new Date(this.modelValue))
|
return this.formatDate(
|
||||||
|
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
|
||||||
|
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -281,6 +317,8 @@ export default {
|
|||||||
attachments () {
|
attachments () {
|
||||||
if (this.field.type === 'file') {
|
if (this.field.type === 'file') {
|
||||||
return (this.modelValue || []).map((uuid) => this.attachmentsIndex[uuid])
|
return (this.modelValue || []).map((uuid) => this.attachmentsIndex[uuid])
|
||||||
|
} else if (this.field.type === 'payment') {
|
||||||
|
return [this.attachmentsIndex[this.modelValue]].filter(Boolean)
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -311,6 +349,47 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
optionValue (option) {
|
||||||
|
if (option) {
|
||||||
|
if (option.value) {
|
||||||
|
return option.value
|
||||||
|
} else {
|
||||||
|
const index = this.field.options.indexOf(option)
|
||||||
|
|
||||||
|
return `${this.t('option')} ${index + 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+/)],
|
||||||
|
timeZone: 'UTC'
|
||||||
|
}).formatToParts(date)
|
||||||
|
|
||||||
|
return format
|
||||||
|
.replace(/D+/, parts.find((p) => p.type === 'day').value)
|
||||||
|
.replace(/M+/, parts.find((p) => p.type === 'month').value)
|
||||||
|
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
|
||||||
|
},
|
||||||
updateMultipleSelectValue (value) {
|
updateMultipleSelectValue (value) {
|
||||||
if (this.modelValue?.includes(value)) {
|
if (this.modelValue?.includes(value)) {
|
||||||
const newValue = [...this.modelValue]
|
const newValue = [...this.modelValue]
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
:key="areaIndex"
|
:key="areaIndex"
|
||||||
>
|
>
|
||||||
<Teleport
|
<Teleport
|
||||||
|
v-if="findPageElementForArea(area)"
|
||||||
:to="findPageElementForArea(area)"
|
:to="findPageElementForArea(area)"
|
||||||
>
|
>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
@@ -82,22 +83,25 @@ export default {
|
|||||||
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
|
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
|
||||||
},
|
},
|
||||||
scrollIntoField (field) {
|
scrollIntoField (field) {
|
||||||
this.areaRefs.find((area) => {
|
if (field?.areas) {
|
||||||
if (area.field === field) {
|
this.scrollIntoArea(field.areas[0])
|
||||||
const root = this.$root.$el.parentNode.getRootNode()
|
}
|
||||||
const container = root.body || root.querySelector('div')
|
},
|
||||||
|
scrollIntoArea (area) {
|
||||||
|
const areaRef = this.areaRefs.find((a) => a.area === area)
|
||||||
|
|
||||||
if (container.style.overflow === 'hidden') {
|
if (areaRef) {
|
||||||
this.scrollInContainer(area.$el)
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
} else {
|
const container = root.body || root.querySelector('div')
|
||||||
area.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
if (container.style.overflow === 'hidden') {
|
||||||
|
this.scrollInContainer(areaRef.$el)
|
||||||
} else {
|
} else {
|
||||||
return null
|
areaRef.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
scrollInContainer (target) {
|
scrollInContainer (target) {
|
||||||
const root = this.$root.$el.parentNode.getRootNode()
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
<FileDropzone
|
<FileDropzone
|
||||||
:message="`Upload ${field.name || 'Attachments'}${field.required ? '' : ' (optional)'}`"
|
:message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
@@ -62,6 +62,7 @@ export default {
|
|||||||
IconPaperclip,
|
IconPaperclip,
|
||||||
IconTrashX
|
IconTrashX
|
||||||
},
|
},
|
||||||
|
inject: ['t'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mx-auto max-w-md flex flex-col">
|
<div
|
||||||
|
class="mx-auto max-w-md flex flex-col"
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
<p class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
|
<p class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
|
||||||
<IconCircleCheck
|
<IconCircleCheck
|
||||||
class="inline text-green-600"
|
class="inline text-green-600"
|
||||||
@@ -21,7 +24,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
v-if="canSendEmail && !isDemo"
|
v-if="canSendEmail && !isDemo && withSendCopyButton"
|
||||||
class="white-button !h-auto flex items-center space-x-1 w-full"
|
class="white-button !h-auto flex items-center space-x-1 w-full"
|
||||||
:disabled="isSendingCopy"
|
:disabled="isSendingCopy"
|
||||||
@click.prevent="sendCopyToEmail"
|
@click.prevent="sendCopyToEmail"
|
||||||
@@ -36,7 +39,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!isWebView"
|
v-if="!isWebView && withDownloadButton"
|
||||||
class="base-button flex items-center space-x-1 w-full"
|
class="base-button flex items-center space-x-1 w-full"
|
||||||
:disabled="isDownloading"
|
:disabled="isDownloading"
|
||||||
@click.prevent="download"
|
@click.prevent="download"
|
||||||
@@ -115,6 +118,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withDownloadButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withSendCopyButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withConfetti: {
|
withConfetti: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -169,27 +182,62 @@ export default {
|
|||||||
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((response) => response.json()).then((urls) => {
|
||||||
const fileRequests = urls.map((url) => {
|
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||||
return () => {
|
|
||||||
return fetch(url).then(async (resp) => {
|
|
||||||
const blobUrl = URL.createObjectURL(await resp.blob())
|
|
||||||
const link = document.createElement('a')
|
|
||||||
|
|
||||||
link.href = blobUrl
|
if (isSafariIos && urls.length > 1) {
|
||||||
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
this.downloadSafariIos(urls)
|
||||||
|
} else {
|
||||||
|
this.downloadUrls(urls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
downloadUrls (urls) {
|
||||||
|
const fileRequests = urls.map((url) => {
|
||||||
|
return () => {
|
||||||
|
return fetch(url).then(async (resp) => {
|
||||||
|
const blobUrl = URL.createObjectURL(await resp.blob())
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
link.click()
|
link.href = blobUrl
|
||||||
|
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
||||||
|
|
||||||
URL.revokeObjectURL(url)
|
link.click()
|
||||||
})
|
|
||||||
}
|
URL.revokeObjectURL(blobUrl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fileRequests.reduce(
|
||||||
|
(prevPromise, request) => prevPromise.then(() => request()),
|
||||||
|
Promise.resolve()
|
||||||
|
)
|
||||||
|
|
||||||
|
this.isDownloading = false
|
||||||
|
},
|
||||||
|
downloadSafariIos (urls) {
|
||||||
|
const fileRequests = urls.map((url) => {
|
||||||
|
return fetch(url).then(async (resp) => {
|
||||||
|
const blob = await resp.blob()
|
||||||
|
const blobUrl = URL.createObjectURL(blob.slice(0, blob.size, 'application/octet-stream'))
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
|
link.href = blobUrl
|
||||||
|
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
||||||
|
|
||||||
|
return link
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
fileRequests.reduce(
|
Promise.all(fileRequests).then((links) => {
|
||||||
(prevPromise, request) => prevPromise.then(() => request()),
|
links.forEach((link, index) => {
|
||||||
Promise.resolve()
|
setTimeout(() => {
|
||||||
)
|
link.click()
|
||||||
|
|
||||||
|
URL.revokeObjectURL(link.href)
|
||||||
|
}, index * 50)
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
this.isDownloading = false
|
this.isDownloading = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div dir="auto">
|
||||||
<div class="flex justify-between items-center w-full mb-2">
|
<div class="flex justify-between items-center w-full mb-2">
|
||||||
<label
|
<label
|
||||||
:for="field.uuid"
|
:for="field.uuid"
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
>{{ field.name || t('date') }}
|
>{{ field.name && showFieldNames ? field.name : t('date') }}
|
||||||
<template v-if="!field.required">({{ t('optional') }})</template>
|
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
{{ t('set_today') }}
|
{{ t('set_today') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<AppearsOn :field="field" />
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<input
|
<input
|
||||||
ref="input"
|
ref="input"
|
||||||
@@ -31,11 +32,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconCalendarCheck } from '@tabler/icons-vue'
|
import { IconCalendarCheck } from '@tabler/icons-vue'
|
||||||
|
import AppearsOn from './appears_on'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DateStep',
|
name: 'DateStep',
|
||||||
components: {
|
components: {
|
||||||
IconCalendarCheck
|
IconCalendarCheck,
|
||||||
|
AppearsOn
|
||||||
},
|
},
|
||||||
inject: ['t'],
|
inject: ['t'],
|
||||||
props: {
|
props: {
|
||||||
@@ -43,6 +46,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
showFieldNames: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -4,12 +4,13 @@
|
|||||||
:steps="stepFields"
|
:steps="stepFields"
|
||||||
:values="values"
|
:values="values"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:with-label="!isAnonymousChecboxes"
|
:with-label="!isAnonymousChecboxes && showFieldNames"
|
||||||
:current-step="currentStepFields"
|
:current-step="currentStepFields"
|
||||||
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
|
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="!isFormVisible"
|
v-if="!isFormVisible"
|
||||||
|
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 rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
|
||||||
@click.prevent="isFormVisible = true"
|
@click.prevent="isFormVisible = true"
|
||||||
>
|
>
|
||||||
@@ -23,13 +24,15 @@
|
|||||||
<div
|
<div
|
||||||
v-show="isFormVisible"
|
v-show="isFormVisible"
|
||||||
id="form_container"
|
id="form_container"
|
||||||
class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border-base-200 border p-4 rounded"
|
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded"
|
||||||
|
:class="{ 'md:bottom-4': isBreakpointMd }"
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="!isCompleted"
|
v-if="!isCompleted"
|
||||||
|
id="minimize_form_button"
|
||||||
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
|
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
|
||||||
title="Minimize"
|
:title="t('minimize')"
|
||||||
@click.prevent="isFormVisible = false"
|
@click.prevent="isFormVisible = false"
|
||||||
>
|
>
|
||||||
<IconArrowsDiagonalMinimize2
|
<IconArrowsDiagonalMinimize2
|
||||||
@@ -37,13 +40,16 @@
|
|||||||
:height="20"
|
:height="20"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div
|
||||||
|
:class="{ 'md:px-4': isBreakpointMd }"
|
||||||
|
>
|
||||||
<form
|
<form
|
||||||
v-if="!isCompleted"
|
v-if="!isCompleted"
|
||||||
ref="form"
|
ref="form"
|
||||||
:action="submitPath"
|
:action="submitPath"
|
||||||
method="post"
|
method="post"
|
||||||
class="md:mx-16"
|
class="mx-auto"
|
||||||
|
:style="{ maxWidth: isBreakpointMd ? '582px' : '' }"
|
||||||
@submit.prevent="submitStep"
|
@submit.prevent="submitStep"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -61,21 +67,24 @@
|
|||||||
<TextStep
|
<TextStep
|
||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
|
:show-field-names="showFieldNames"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DateStep
|
<DateStep
|
||||||
v-else-if="currentField.type === 'date'"
|
v-else-if="currentField.type === 'date'"
|
||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
|
:show-field-names="showFieldNames"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
/>
|
/>
|
||||||
<div v-else-if="currentField.type === 'select'">
|
<div v-else-if="currentField.type === 'select'">
|
||||||
<label
|
<label
|
||||||
v-if="currentField.name"
|
v-if="showFieldNames && currentField.name"
|
||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
|
dir="auto"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl mb-2"
|
||||||
>{{ currentField.name }}
|
>{{ currentField.name }}
|
||||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||||
@@ -84,13 +93,15 @@
|
|||||||
v-else
|
v-else
|
||||||
class="py-1"
|
class="py-1"
|
||||||
/>
|
/>
|
||||||
|
<AppearsOn :field="currentField" />
|
||||||
<select
|
<select
|
||||||
:id="currentField.uuid"
|
:id="currentField.uuid"
|
||||||
|
dir="auto"
|
||||||
:required="currentField.required"
|
:required="currentField.required"
|
||||||
class="select base-input !text-2xl w-full text-center font-normal"
|
class="select base-input !text-2xl w-full text-center font-normal"
|
||||||
:name="`values[${currentField.uuid}]`"
|
:name="`values[${currentField.uuid}]`"
|
||||||
@change="values[currentField.uuid] = $event.target.value"
|
@change="values[currentField.uuid] = $event.target.value"
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
value=""
|
value=""
|
||||||
@@ -110,16 +121,30 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else-if="currentField.type === 'radio'">
|
<div v-else-if="currentField.type === 'radio'">
|
||||||
<label
|
<label
|
||||||
v-if="currentField.name"
|
v-if="showFieldNames && currentField.name"
|
||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
|
dir="auto"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl mb-2"
|
||||||
>{{ currentField.name }}
|
>{{ currentField.name }}
|
||||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||||
</label>
|
</label>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full max-h-44 overflow-y-auto">
|
||||||
<div class="space-y-3.5 mx-auto">
|
<div
|
||||||
|
v-if="!showFieldNames || (currentField.options.every((e) => !e.value) && currentField.options.length > 4)"
|
||||||
|
class="text-xl px-1"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
@click="scrollIntoField(currentField)"
|
||||||
|
>
|
||||||
|
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="space-y-3.5 mx-auto"
|
||||||
|
:class="{ hidden: !showFieldNames || (currentField.options.every((e) => !e.value) && currentField.options.length > 4) }"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="option in currentField.options"
|
v-for="(option, index) in currentField.options"
|
||||||
:key="option.uuid"
|
:key="option.uuid"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
@@ -132,11 +157,11 @@
|
|||||||
type="radio"
|
type="radio"
|
||||||
class="base-radio !h-7 !w-7"
|
class="base-radio !h-7 !w-7"
|
||||||
:name="`values[${currentField.uuid}]`"
|
:name="`values[${currentField.uuid}]`"
|
||||||
:value="option.value"
|
:value="option.value || `${t('option')} ${index + 1}`"
|
||||||
:required="currentField.required"
|
:required="currentField.required"
|
||||||
>
|
>
|
||||||
<span class="text-xl">
|
<span class="text-xl">
|
||||||
{{ option.value }}
|
{{ option.value || `${t('option')} ${index + 1}` }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,6 +172,8 @@
|
|||||||
v-else-if="currentField.type === 'multiple'"
|
v-else-if="currentField.type === 'multiple'"
|
||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
|
:show-field-names="showFieldNames"
|
||||||
|
:is-last-step="stepFields.length === currentStep + 1"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@@ -161,7 +188,7 @@
|
|||||||
<div
|
<div
|
||||||
class="space-y-3.5 mx-auto"
|
class="space-y-3.5 mx-auto"
|
||||||
>
|
>
|
||||||
<template v-if="isAnonymousChecboxes">
|
<template v-if="isAnonymousChecboxes || !showFieldNames">
|
||||||
<span class="text-xl">
|
<span class="text-xl">
|
||||||
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
|
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
|
||||||
</span>
|
</span>
|
||||||
@@ -195,9 +222,17 @@
|
|||||||
:onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`"
|
:onchange="`this.setCustomValidity(validity.valueMissing ? '${t('please_check_the_box_to_continue')}' : '');`"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
:checked="!!values[field.uuid]"
|
:checked="!!values[field.uuid]"
|
||||||
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="field.title"
|
||||||
|
class="text-xl"
|
||||||
|
v-html="field.title"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-xl"
|
||||||
>
|
>
|
||||||
<span class="text-xl">
|
|
||||||
{{ field.name || field.type + ' ' + (index + 1) }}
|
{{ field.name || field.type + ' ' + (index + 1) }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -206,14 +241,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ImageStep
|
<ImageStep
|
||||||
v-else-if="currentField.type === 'image'"
|
v-else-if="currentField.type === 'image' || currentField.type === 'stamp'"
|
||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]"
|
:show-field-names="showFieldNames"
|
||||||
|
@attached="[attachments.push($event), scrollIntoField(currentField)]"
|
||||||
/>
|
/>
|
||||||
<SignatureStep
|
<SignatureStep
|
||||||
v-else-if="currentField.type === 'signature'"
|
v-else-if="currentField.type === 'signature'"
|
||||||
@@ -221,12 +257,14 @@
|
|||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:previous-value="previousSignatureValue"
|
:previous-value="previousSignatureValueFor(currentField)"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
|
:with-typed-signature="withTypedSignature"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
|
:show-field-names="showFieldNames"
|
||||||
@attached="attachments.push($event)"
|
@attached="attachments.push($event)"
|
||||||
@start="$refs.areas.scrollIntoField(currentField)"
|
@start="scrollIntoField(currentField)"
|
||||||
@minimize="isFormVisible = false"
|
@minimize="isFormVisible = false"
|
||||||
/>
|
/>
|
||||||
<InitialsStep
|
<InitialsStep
|
||||||
@@ -238,10 +276,11 @@
|
|||||||
:previous-value="previousInitialsValue"
|
:previous-value="previousInitialsValue"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
|
:show-field-names="showFieldNames"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@attached="attachments.push($event)"
|
@attached="attachments.push($event)"
|
||||||
@start="$refs.areas.scrollIntoField(currentField)"
|
@start="scrollIntoField(currentField)"
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
@minimize="isFormVisible = false"
|
@minimize="isFormVisible = false"
|
||||||
/>
|
/>
|
||||||
<AttachmentStep
|
<AttachmentStep
|
||||||
@@ -252,7 +291,7 @@
|
|||||||
:field="currentField"
|
:field="currentField"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]"
|
@attached="[attachments.push($event), scrollIntoField(currentField)]"
|
||||||
/>
|
/>
|
||||||
<PhoneStep
|
<PhoneStep
|
||||||
v-else-if="currentField.type === 'phone'"
|
v-else-if="currentField.type === 'phone'"
|
||||||
@@ -260,14 +299,30 @@
|
|||||||
:key="currentField.uuid"
|
:key="currentField.uuid"
|
||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
|
:show-field-names="showFieldNames"
|
||||||
:default-value="submitter.phone"
|
:default-value="submitter.phone"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
|
@submit="submitStep"
|
||||||
|
/>
|
||||||
|
<PaymentStep
|
||||||
|
v-else-if="currentField.type === 'payment'"
|
||||||
|
ref="currentStep"
|
||||||
|
:key="currentField.uuid"
|
||||||
|
v-model="values[currentField.uuid]"
|
||||||
|
:field="currentField"
|
||||||
|
:submitter-slug="submitterSlug"
|
||||||
|
@attached="attachments.push($event)"
|
||||||
|
@focus="scrollIntoField(currentField)"
|
||||||
@submit="submitStep"
|
@submit="submitStep"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6 md:mt-8">
|
<div
|
||||||
|
v-if="currentField.type !== 'payment' || submittedValues[currentField.uuid]"
|
||||||
|
class="mt-6 md:mt-8"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
|
id="submit_form_button"
|
||||||
ref="submitButton"
|
ref="submitButton"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="base-button w-full flex justify-center"
|
class="base-button w-full flex justify-center"
|
||||||
@@ -301,23 +356,32 @@
|
|||||||
v-else
|
v-else
|
||||||
:is-demo="isDemo"
|
:is-demo="isDemo"
|
||||||
:attribution="attribution"
|
:attribution="attribution"
|
||||||
:completed-button="completedButton"
|
:completed-button="completedRedirectUrl ? {} : completedButton"
|
||||||
|
:with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
|
||||||
|
:with-download-button="withDownloadButton && !completedRedirectUrl"
|
||||||
:with-confetti="withConfetti"
|
:with-confetti="withConfetti"
|
||||||
:can-send-email="canSendEmail && !!submitter.email"
|
:can-send-email="canSendEmail && !!submitter.email"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
/>
|
/>
|
||||||
<div class="flex justify-center">
|
<div
|
||||||
<div class="flex items-center mt-5 mb-1">
|
v-if="stepFields.length < 80"
|
||||||
|
class="flex justify-center"
|
||||||
|
>
|
||||||
|
<div class="flex items-center mt-4 mb-1 flex-wrap">
|
||||||
<a
|
<a
|
||||||
v-for="(step, index) in stepFields"
|
v-for="(step, index) in stepFields"
|
||||||
:key="step[0].uuid"
|
:key="step[0].uuid"
|
||||||
href="#"
|
href="#"
|
||||||
class="inline border border-base-300 h-3 w-3 rounded-full mx-1"
|
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1"
|
||||||
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
|
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
|
||||||
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(step, true)]"
|
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(step, true)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="mt-5"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -330,11 +394,13 @@ import InitialsStep from './initials_step'
|
|||||||
import AttachmentStep from './attachment_step'
|
import AttachmentStep from './attachment_step'
|
||||||
import MultiSelectStep from './multi_select_step'
|
import MultiSelectStep from './multi_select_step'
|
||||||
import PhoneStep from './phone_step'
|
import PhoneStep from './phone_step'
|
||||||
|
import PaymentStep from './payment_step'
|
||||||
import TextStep from './text_step'
|
import TextStep from './text_step'
|
||||||
import DateStep from './date_step'
|
import DateStep from './date_step'
|
||||||
import FormCompleted from './completed'
|
import FormCompleted from './completed'
|
||||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import { t } from './i18n'
|
import AppearsOn from './appears_on'
|
||||||
|
import i18n from './i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SubmissionForm',
|
name: 'SubmissionForm',
|
||||||
@@ -342,6 +408,7 @@ export default {
|
|||||||
FieldAreas,
|
FieldAreas,
|
||||||
ImageStep,
|
ImageStep,
|
||||||
SignatureStep,
|
SignatureStep,
|
||||||
|
AppearsOn,
|
||||||
AttachmentStep,
|
AttachmentStep,
|
||||||
InitialsStep,
|
InitialsStep,
|
||||||
MultiSelectStep,
|
MultiSelectStep,
|
||||||
@@ -350,12 +417,15 @@ export default {
|
|||||||
IconArrowsDiagonal,
|
IconArrowsDiagonal,
|
||||||
TextStep,
|
TextStep,
|
||||||
PhoneStep,
|
PhoneStep,
|
||||||
|
PaymentStep,
|
||||||
IconArrowsDiagonalMinimize2,
|
IconArrowsDiagonalMinimize2,
|
||||||
FormCompleted
|
FormCompleted
|
||||||
},
|
},
|
||||||
provide () {
|
provide () {
|
||||||
return {
|
return {
|
||||||
baseUrl: this.baseUrl,
|
baseUrl: this.baseUrl,
|
||||||
|
scrollIntoArea: this.scrollIntoArea,
|
||||||
|
scrollIntoField: this.scrollIntoField,
|
||||||
t: this.t
|
t: this.t
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -386,6 +456,21 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
showFieldNames: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withTypedSignature: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
isBreakpointMd: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
baseUrl: {
|
baseUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -396,11 +481,6 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
authenticityToken: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -431,11 +511,36 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
language: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
values: {
|
values: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
i18n: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
withSendCopyButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withDownloadButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
completedRedirectUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
completedButton: {
|
completedButton: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -457,14 +562,18 @@ export default {
|
|||||||
currentStepFields () {
|
currentStepFields () {
|
||||||
return this.stepFields[this.currentStep]
|
return this.stepFields[this.currentStep]
|
||||||
},
|
},
|
||||||
|
browserLanguage () {
|
||||||
|
return (navigator.language || navigator.userLanguage || 'en').split('-')[0]
|
||||||
|
},
|
||||||
|
queryParams () {
|
||||||
|
return new URLSearchParams(window.location.search)
|
||||||
|
},
|
||||||
|
authenticityToken () {
|
||||||
|
return document.querySelector('meta[name="csrf-token"]')?.content
|
||||||
|
},
|
||||||
submitterSlug () {
|
submitterSlug () {
|
||||||
return this.submitter.slug
|
return this.submitter.slug
|
||||||
},
|
},
|
||||||
previousSignatureValue () {
|
|
||||||
const signatureField = [...this.fields].reverse().find((field) => field.type === 'signature' && !!this.values[field.uuid])
|
|
||||||
|
|
||||||
return this.values[signatureField?.uuid]
|
|
||||||
},
|
|
||||||
previousInitialsValue () {
|
previousInitialsValue () {
|
||||||
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||||
|
|
||||||
@@ -476,7 +585,7 @@ export default {
|
|||||||
isButtonDisabled () {
|
isButtonDisabled () {
|
||||||
if (this.recalculateButtonDisabledKey) {
|
if (this.recalculateButtonDisabledKey) {
|
||||||
return this.isSubmitting ||
|
return this.isSubmitting ||
|
||||||
(this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
|
(this.currentField.required && ['image', 'file', 'multiple'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
|
||||||
(this.currentField.required && this.currentField.type === 'signature' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isSignatureStarted) ||
|
(this.currentField.required && this.currentField.type === 'signature' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isSignatureStarted) ||
|
||||||
(this.currentField.required && this.currentField.type === 'initials' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isInitialsStarted)
|
(this.currentField.required && this.currentField.type === 'initials' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isInitialsStarted)
|
||||||
} else {
|
} else {
|
||||||
@@ -515,11 +624,17 @@ export default {
|
|||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.goToLast) {
|
if (this.queryParams.get('field_uuid')) {
|
||||||
|
const stepIndex = this.stepFields.findIndex((fields) => {
|
||||||
|
return fields.some((f) => f.uuid === this.queryParams.get('field_uuid'))
|
||||||
|
})
|
||||||
|
|
||||||
|
this.currentStep = Math.max(stepIndex, 0)
|
||||||
|
} else if (this.goToLast) {
|
||||||
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.submittedValues[f.uuid])))
|
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.submittedValues[f.uuid])))
|
||||||
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1
|
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1
|
||||||
|
|
||||||
@@ -562,9 +677,11 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
t,
|
t (key) {
|
||||||
|
return this.i18n[key] || i18n[this.language?.toLowerCase()]?.[key] || i18n[this.browserLanguage]?.[key] || i18n.en[key] || key
|
||||||
|
},
|
||||||
maybeTrackEmailClick () {
|
maybeTrackEmailClick () {
|
||||||
const queryParams = new URLSearchParams(window.location.search)
|
const { queryParams } = this
|
||||||
|
|
||||||
if (queryParams.has('t')) {
|
if (queryParams.has('t')) {
|
||||||
const t = queryParams.get('t')
|
const t = queryParams.get('t')
|
||||||
@@ -588,7 +705,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
maybeTrackSmsClick () {
|
maybeTrackSmsClick () {
|
||||||
const queryParams = new URLSearchParams(window.location.search)
|
const { queryParams } = this
|
||||||
|
|
||||||
if (queryParams.has('c')) {
|
if (queryParams.has('c')) {
|
||||||
const c = queryParams.get('c')
|
const c = queryParams.get('c')
|
||||||
@@ -622,6 +739,13 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
previousSignatureValueFor (field) {
|
||||||
|
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]
|
||||||
|
},
|
||||||
goToStep (step, scrollToArea = false, clickUpload = false) {
|
goToStep (step, scrollToArea = false, clickUpload = false) {
|
||||||
this.currentStep = this.stepFields.indexOf(step)
|
this.currentStep = this.stepFields.indexOf(step)
|
||||||
this.showFillAllRequiredFields = false
|
this.showFillAllRequiredFields = false
|
||||||
@@ -630,7 +754,7 @@ export default {
|
|||||||
this.recalculateButtonDisabledKey = Math.random()
|
this.recalculateButtonDisabledKey = Math.random()
|
||||||
|
|
||||||
if (scrollToArea) {
|
if (scrollToArea) {
|
||||||
this.$refs.areas.scrollIntoField(step[0])
|
this.scrollIntoField(step[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus()
|
this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus()
|
||||||
@@ -658,16 +782,22 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
scrollIntoField (field) {
|
||||||
|
return this.$refs.areas.scrollIntoField(field)
|
||||||
|
},
|
||||||
|
scrollIntoArea (area) {
|
||||||
|
return this.$refs.areas.scrollIntoArea(area)
|
||||||
|
},
|
||||||
async submitStep () {
|
async submitStep () {
|
||||||
this.isSubmitting = true
|
this.isSubmitting = true
|
||||||
|
|
||||||
const stepPromise = ['signature', 'phone', 'initials'].includes(this.currentField.type)
|
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
|
||||||
? this.$refs.currentStep.submit
|
? this.$refs.currentStep.submit
|
||||||
: () => Promise.resolve({})
|
: () => Promise.resolve({})
|
||||||
|
|
||||||
stepPromise().then(async () => {
|
stepPromise().then(async () => {
|
||||||
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
||||||
return index < this.currentStep && fields[0].required && (fields[0].type === 'phone' || !this.allowToSkip) && !this.submittedValues[fields[0].uuid]
|
return index < this.currentStep && fields[0].required && (fields[0].type === 'phone' || !this.allowToSkip) && (this.submittedValues[fields[0].uuid] === true ? false : !this.submittedValues[fields[0].uuid]?.length)
|
||||||
})
|
})
|
||||||
|
|
||||||
const formData = new FormData(this.$refs.form)
|
const formData = new FormData(this.$refs.form)
|
||||||
@@ -678,7 +808,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.saveStep(formData).then(async (response) => {
|
await this.saveStep(formData).then(async (response) => {
|
||||||
if (response.status === 422) {
|
if (response.status === 422 || response.status === 500) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
alert(data.error || 'Value is invalid')
|
alert(data.error || 'Value is invalid')
|
||||||
@@ -706,9 +836,13 @@ export default {
|
|||||||
if (respData) {
|
if (respData) {
|
||||||
this.onComplete(JSON.parse(respData))
|
this.onComplete(JSON.parse(respData))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.completedRedirectUrl) {
|
||||||
|
window.location.href = this.completedRedirectUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('Error submitting form:', error)
|
console.error(error)
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isSubmitting = false
|
this.isSubmitting = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,26 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
text: 'Text',
|
||||||
|
signature: 'Signature',
|
||||||
|
initials: 'Initials',
|
||||||
|
date: 'Date',
|
||||||
|
image: 'Image',
|
||||||
|
take_photo: 'Take photo',
|
||||||
|
file: 'File',
|
||||||
|
select: 'Select',
|
||||||
|
checkbox: 'Checkbox',
|
||||||
|
multiple: 'Multiple',
|
||||||
|
radio: 'Radio',
|
||||||
|
cells: 'Cells',
|
||||||
|
stamp: 'Stamp',
|
||||||
|
minimize: 'Minimize',
|
||||||
|
payment: 'Payment',
|
||||||
|
phone: 'Phone',
|
||||||
submit_form: 'Submit Form',
|
submit_form: 'Submit Form',
|
||||||
type_here: 'Type here',
|
type_here_: 'Type here...',
|
||||||
optional: 'optional',
|
optional: 'optional',
|
||||||
|
option: 'Option',
|
||||||
|
appears_on: 'Appears on',
|
||||||
|
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 hightlighted checkboxes and click',
|
||||||
submit: 'submit',
|
submit: 'submit',
|
||||||
@@ -10,8 +29,6 @@ const en = {
|
|||||||
or_drag_and_drop_files: 'or drag and drop files',
|
or_drag_and_drop_files: 'or drag and drop files',
|
||||||
send_copy_via_email: 'Send copy via email',
|
send_copy_via_email: 'Send copy via email',
|
||||||
download: 'Download',
|
download: 'Download',
|
||||||
signature: 'Signature',
|
|
||||||
initials: 'Initials',
|
|
||||||
clear: 'Clear',
|
clear: 'Clear',
|
||||||
redraw: 'Redraw',
|
redraw: 'Redraw',
|
||||||
draw_initials: 'Draw initials',
|
draw_initials: 'Draw initials',
|
||||||
@@ -37,14 +54,36 @@ const en = {
|
|||||||
draw: 'Draw',
|
draw: 'Draw',
|
||||||
type: 'Type',
|
type: 'Type',
|
||||||
type_text: 'Type text',
|
type_text: 'Type text',
|
||||||
date: 'Date',
|
email_has_been_sent: 'Email has been sent',
|
||||||
email_has_been_sent: 'Email has been sent'
|
processing: 'Processing',
|
||||||
|
pay_with_strip: 'Pay with Stripe',
|
||||||
|
reupload: 'Reupload',
|
||||||
|
upload: 'Upload',
|
||||||
|
files: 'Files'
|
||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
minimize: 'Minimizar',
|
||||||
|
text: 'Texto',
|
||||||
|
signature: 'Firma',
|
||||||
|
initials: 'Iniciales',
|
||||||
|
date: 'Fecha',
|
||||||
|
image: 'Imagen',
|
||||||
|
file: 'Archivo',
|
||||||
|
select: 'Seleccionar',
|
||||||
|
checkbox: 'Casilla',
|
||||||
|
multiple: 'Múltiple',
|
||||||
|
radio: 'Radio',
|
||||||
|
cells: 'Celdas',
|
||||||
|
stamp: 'Sello',
|
||||||
|
payment: 'Pago',
|
||||||
|
phone: 'Teléfono',
|
||||||
|
take_photo: 'Tomar foto',
|
||||||
submit_form: 'Enviar Formulario',
|
submit_form: 'Enviar Formulario',
|
||||||
type_here: 'Escribe aquí',
|
type_here_: 'Escribe aquí...',
|
||||||
optional: 'opcional',
|
optional: 'opcional',
|
||||||
|
appears_on: 'Aparece en',
|
||||||
|
page: 'Página',
|
||||||
select_your_option: 'Selecciona tu opción',
|
select_your_option: 'Selecciona tu opción',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Completa las casillas resaltadas y haz clic',
|
complete_hightlighted_checkboxes_and_click: 'Completa las casillas resaltadas y haz clic',
|
||||||
submit: 'enviar',
|
submit: 'enviar',
|
||||||
@@ -53,8 +92,6 @@ const es = {
|
|||||||
or_drag_and_drop_files: 'o arrastra y suelta archivos',
|
or_drag_and_drop_files: 'o arrastra y suelta archivos',
|
||||||
send_copy_via_email: 'Enviar copia por correo electrónico',
|
send_copy_via_email: 'Enviar copia por correo electrónico',
|
||||||
download: 'Descargar',
|
download: 'Descargar',
|
||||||
signature: 'Firma',
|
|
||||||
initials: 'Iniciales',
|
|
||||||
clear: 'Borrar',
|
clear: 'Borrar',
|
||||||
redraw: 'Redibujar',
|
redraw: 'Redibujar',
|
||||||
draw_initials: 'Dibujar iniciales',
|
draw_initials: 'Dibujar iniciales',
|
||||||
@@ -74,20 +111,42 @@ const es = {
|
|||||||
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS',
|
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS',
|
||||||
please_fill_all_required_fields: 'Por favor, complete todos los campos obligatorios',
|
please_fill_all_required_fields: 'Por favor, complete todos los campos obligatorios',
|
||||||
set_today: 'Establecer Hoy',
|
set_today: 'Establecer Hoy',
|
||||||
date: 'Fecha',
|
|
||||||
toggle_multiline_text: 'Alternar Texto Multilínea',
|
toggle_multiline_text: 'Alternar Texto Multilínea',
|
||||||
draw_signature: 'Dibujar firma',
|
draw_signature: 'Dibujar firma',
|
||||||
type_initial: 'Escribir iniciales',
|
type_initial: 'Escribir iniciales',
|
||||||
draw: 'Dibujar',
|
draw: 'Dibujar',
|
||||||
type: 'Escribir',
|
type: 'Escribir',
|
||||||
type_text: 'Escribir texto',
|
type_text: 'Escribir texto',
|
||||||
email_has_been_sent: 'El correo electrónico ha sido enviado'
|
email_has_been_sent: 'El correo electrónico ha sido enviado',
|
||||||
|
processing: 'Procesando',
|
||||||
|
pay_with_strip: 'Pagar con Stripe',
|
||||||
|
reupload: 'Volver a subir',
|
||||||
|
upload: 'Subir',
|
||||||
|
files: 'Archivos'
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
minimize: 'Minimizza',
|
||||||
|
text: 'Testo',
|
||||||
|
signature: 'Firma',
|
||||||
|
initials: 'Iniziali',
|
||||||
|
date: 'Data',
|
||||||
|
image: 'Immagine',
|
||||||
|
file: 'File',
|
||||||
|
select: 'Seleziona',
|
||||||
|
checkbox: 'Casella di controllo',
|
||||||
|
multiple: 'Multiplo',
|
||||||
|
radio: 'Radio',
|
||||||
|
cells: 'Celle',
|
||||||
|
stamp: 'Timbro',
|
||||||
|
payment: 'Pagamento',
|
||||||
|
phone: 'Telefono',
|
||||||
submit_form: 'Invia Modulo',
|
submit_form: 'Invia Modulo',
|
||||||
type_here: 'Digita qui',
|
type_here_: 'Digita qui...',
|
||||||
optional: 'opzionale',
|
optional: 'opzionale',
|
||||||
|
appears_on: 'Compare su',
|
||||||
|
page: 'Pagina',
|
||||||
|
take_photo: 'Scattare una foto',
|
||||||
select_your_option: 'Seleziona la tua opzione',
|
select_your_option: 'Seleziona la tua opzione',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic',
|
complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic',
|
||||||
submit: 'invia',
|
submit: 'invia',
|
||||||
@@ -96,8 +155,6 @@ const it = {
|
|||||||
or_drag_and_drop_files: 'oppure trascina e rilascia i file',
|
or_drag_and_drop_files: 'oppure trascina e rilascia i file',
|
||||||
send_copy_via_email: 'Invia copia via email',
|
send_copy_via_email: 'Invia copia via email',
|
||||||
download: 'Scarica',
|
download: 'Scarica',
|
||||||
signature: 'Firma',
|
|
||||||
initials: 'Iniziali',
|
|
||||||
clear: 'Cancella',
|
clear: 'Cancella',
|
||||||
redraw: 'Ridisegna',
|
redraw: 'Ridisegna',
|
||||||
draw_initials: 'Disegna iniziali',
|
draw_initials: 'Disegna iniziali',
|
||||||
@@ -117,20 +174,42 @@ const it = {
|
|||||||
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS',
|
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS',
|
||||||
please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori',
|
please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori',
|
||||||
set_today: 'Imposta Oggi',
|
set_today: 'Imposta Oggi',
|
||||||
date: 'Data',
|
|
||||||
draw_signature: 'Disegna firma',
|
draw_signature: 'Disegna firma',
|
||||||
type_initial: 'Inserisci iniziali',
|
type_initial: 'Inserisci iniziali',
|
||||||
draw: 'Disegna',
|
draw: 'Disegna',
|
||||||
type: 'Inserisci',
|
type: 'Inserisci',
|
||||||
type_text: 'Inserisci testo',
|
type_text: 'Inserisci testo',
|
||||||
toggle_multiline_text: 'Attiva Testo Multilinea',
|
toggle_multiline_text: 'Attiva Testo Multilinea',
|
||||||
email_has_been_sent: "L'email è stata inviata"
|
email_has_been_sent: "L'email è stata inviata",
|
||||||
|
processing: 'Elaborazione',
|
||||||
|
pay_with_strip: 'Paga con Stripe',
|
||||||
|
reupload: 'Ricarica',
|
||||||
|
upload: 'Carica',
|
||||||
|
files: 'File'
|
||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
minimize: 'Minimieren',
|
||||||
|
text: 'Text',
|
||||||
|
signature: 'Unterschrift',
|
||||||
|
initials: 'Initialen',
|
||||||
|
date: 'Datum',
|
||||||
|
image: 'Bild',
|
||||||
|
file: 'Datei',
|
||||||
|
select: 'Auswählen',
|
||||||
|
checkbox: 'Checkbox',
|
||||||
|
multiple: 'Mehrere',
|
||||||
|
radio: 'Radio',
|
||||||
|
cells: 'Zellen',
|
||||||
|
stamp: 'Stempel',
|
||||||
|
payment: 'Zahlung',
|
||||||
|
phone: 'Telefon',
|
||||||
submit_form: 'Formular absenden',
|
submit_form: 'Formular absenden',
|
||||||
type_here: 'Hier eingeben',
|
type_here_: 'Hier eingeben...',
|
||||||
optional: 'optional',
|
optional: 'optional',
|
||||||
|
appears_on: 'Erscheint auf',
|
||||||
|
page: 'Seite',
|
||||||
|
take_photo: 'Foto aufnehmen',
|
||||||
select_your_option: 'Wähle deine Option',
|
select_your_option: 'Wähle deine Option',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken',
|
complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken',
|
||||||
submit: 'absenden',
|
submit: 'absenden',
|
||||||
@@ -139,8 +218,6 @@ const de = {
|
|||||||
or_drag_and_drop_files: 'oder Dateien hierher ziehen und ablegen',
|
or_drag_and_drop_files: 'oder Dateien hierher ziehen und ablegen',
|
||||||
send_copy_via_email: 'Kopie per E-Mail senden',
|
send_copy_via_email: 'Kopie per E-Mail senden',
|
||||||
download: 'Herunterladen',
|
download: 'Herunterladen',
|
||||||
signature: 'Unterschrift',
|
|
||||||
initials: 'Initialen',
|
|
||||||
clear: 'Löschen',
|
clear: 'Löschen',
|
||||||
redraw: 'Neu zeichnen',
|
redraw: 'Neu zeichnen',
|
||||||
draw_initials: 'Initialen zeichnen',
|
draw_initials: 'Initialen zeichnen',
|
||||||
@@ -160,20 +237,42 @@ const de = {
|
|||||||
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet',
|
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet',
|
||||||
please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus',
|
please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus',
|
||||||
set_today: 'Heute einstellen',
|
set_today: 'Heute einstellen',
|
||||||
date: 'Datum',
|
|
||||||
draw_signature: 'Unterschrift zeichnen',
|
draw_signature: 'Unterschrift zeichnen',
|
||||||
type_initial: 'Initialen eingeben',
|
type_initial: 'Initialen eingeben',
|
||||||
draw: 'Zeichnen',
|
draw: 'Zeichnen',
|
||||||
type: 'Eingeben',
|
type: 'Eingeben',
|
||||||
type_text: 'Text eingeben',
|
type_text: 'Text eingeben',
|
||||||
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
||||||
email_has_been_sent: 'Die E-Mail wurde gesendet'
|
email_has_been_sent: 'Die E-Mail wurde gesendet',
|
||||||
|
processing: 'Verarbeitung',
|
||||||
|
pay_with_strip: 'Mit Stripe bezahlen',
|
||||||
|
reupload: 'Erneut hochladen',
|
||||||
|
upload: 'Hochladen',
|
||||||
|
files: 'Dateien'
|
||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
minimize: 'Réduire',
|
||||||
|
text: 'Texte',
|
||||||
|
signature: 'Signature',
|
||||||
|
initials: 'Initiales',
|
||||||
|
date: 'Date',
|
||||||
|
image: 'Image',
|
||||||
|
file: 'Fichier',
|
||||||
|
select: 'Choisir',
|
||||||
|
checkbox: 'Coche',
|
||||||
|
multiple: 'Multiple',
|
||||||
|
radio: 'Radio',
|
||||||
|
cells: 'Cellules',
|
||||||
|
stamp: 'Tampon',
|
||||||
|
payment: 'Paiement',
|
||||||
|
phone: 'Téléphone',
|
||||||
submit_form: 'Envoyer le Formulaire',
|
submit_form: 'Envoyer le Formulaire',
|
||||||
type_here: 'Tapez ici',
|
type_here_: 'Tapez ici...',
|
||||||
optional: 'facultatif',
|
optional: 'facultatif',
|
||||||
|
appears_on: 'Apparaît sur',
|
||||||
|
page: 'Page',
|
||||||
|
take_photo: 'Prendre une photo',
|
||||||
select_your_option: 'Sélectionnez votre option',
|
select_your_option: 'Sélectionnez votre option',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez',
|
complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez',
|
||||||
submit: 'envoyer',
|
submit: 'envoyer',
|
||||||
@@ -182,8 +281,6 @@ const fr = {
|
|||||||
or_drag_and_drop_files: 'ou faites glisser-déposer les fichiers',
|
or_drag_and_drop_files: 'ou faites glisser-déposer les fichiers',
|
||||||
send_copy_via_email: 'Envoyer une copie par e-mail',
|
send_copy_via_email: 'Envoyer une copie par e-mail',
|
||||||
download: 'Télécharger',
|
download: 'Télécharger',
|
||||||
signature: 'Signature',
|
|
||||||
initials: 'Initiales',
|
|
||||||
clear: 'Effacer',
|
clear: 'Effacer',
|
||||||
redraw: 'Redessiner',
|
redraw: 'Redessiner',
|
||||||
draw_initials: 'Dessiner les initiales',
|
draw_initials: 'Dessiner les initiales',
|
||||||
@@ -203,30 +300,50 @@ const fr = {
|
|||||||
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
|
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
|
||||||
please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
|
please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
|
||||||
set_today: "Définir Aujourd'hui",
|
set_today: "Définir Aujourd'hui",
|
||||||
date: 'Date',
|
|
||||||
draw_signature: 'Dessiner une signature',
|
draw_signature: 'Dessiner une signature',
|
||||||
type_initial: 'Saisir les initiales',
|
type_initial: 'Saisir les initiales',
|
||||||
draw: 'Dessiner',
|
draw: 'Dessiner',
|
||||||
type: 'Saisir',
|
type: 'Saisir',
|
||||||
type_text: 'Saisir du texte',
|
type_text: 'Saisir du texte',
|
||||||
toggle_multiline_text: 'Basculer le Texte Multiligne',
|
toggle_multiline_text: 'Basculer le Texte Multiligne',
|
||||||
email_has_been_sent: "L'email a été envoyé"
|
email_has_been_sent: "L'email a été envoyé",
|
||||||
|
processing: 'Traitement',
|
||||||
|
pay_with_strip: 'Paiement avec Stripe',
|
||||||
|
reupload: 'Recharger',
|
||||||
|
upload: 'Télécharger',
|
||||||
|
files: 'Fichiers'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
|
minimize: 'Zminimalizuj',
|
||||||
|
text: 'Tekst',
|
||||||
|
signature: 'Podpis',
|
||||||
|
initials: 'Inicjały',
|
||||||
|
date: 'Data',
|
||||||
|
image: 'Obraz',
|
||||||
|
file: 'Plik',
|
||||||
|
select: 'Wybierz',
|
||||||
|
checkbox: 'Pole wyboru',
|
||||||
|
multiple: 'Wielokrotne',
|
||||||
|
radio: 'Radio',
|
||||||
|
cells: 'Komórki',
|
||||||
|
stamp: 'Pieczątka',
|
||||||
|
payment: 'Płatność',
|
||||||
|
phone: 'Telefon',
|
||||||
submit_form: 'Wyślij Formularz',
|
submit_form: 'Wyślij Formularz',
|
||||||
type_here: 'Wpisz tutaj',
|
type_here_: 'Wpisz tutaj...',
|
||||||
optional: 'opcjonalny',
|
optional: 'opcjonalny',
|
||||||
|
appears_on: 'Pojawia się na',
|
||||||
|
page: 'Strona',
|
||||||
select_your_option: 'Wybierz swoją opcję',
|
select_your_option: 'Wybierz swoją opcję',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij',
|
complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij',
|
||||||
submit: 'wyślij',
|
submit: 'wyślij',
|
||||||
next: 'dalej',
|
next: 'dalej',
|
||||||
|
take_photo: 'Zrób zdjęcie',
|
||||||
click_to_upload: 'Kliknij, aby przesłać',
|
click_to_upload: 'Kliknij, aby przesłać',
|
||||||
or_drag_and_drop_files: 'lub przeciągnij i upuść pliki',
|
or_drag_and_drop_files: 'lub przeciągnij i upuść pliki',
|
||||||
send_copy_via_email: 'Wyślij kopię drogą mailową',
|
send_copy_via_email: 'Wyślij kopię drogą mailową',
|
||||||
download: 'Pobierz',
|
download: 'Pobierz',
|
||||||
signature: 'Podpis',
|
|
||||||
initials: 'Inicjały',
|
|
||||||
clear: 'Wyczyść',
|
clear: 'Wyczyść',
|
||||||
redraw: 'Przerysuj',
|
redraw: 'Przerysuj',
|
||||||
draw_initials: 'Narysuj inicjały',
|
draw_initials: 'Narysuj inicjały',
|
||||||
@@ -246,20 +363,42 @@ const pl = {
|
|||||||
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany',
|
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany',
|
||||||
please_fill_all_required_fields: 'Proszę wypełnić wszystkie wymagane pola',
|
please_fill_all_required_fields: 'Proszę wypełnić wszystkie wymagane pola',
|
||||||
set_today: 'Ustaw Dziś',
|
set_today: 'Ustaw Dziś',
|
||||||
date: 'Data',
|
|
||||||
draw_signature: 'Rysuj podpis',
|
draw_signature: 'Rysuj podpis',
|
||||||
type_initial: 'Wprowadź inicjały',
|
type_initial: 'Wprowadź inicjały',
|
||||||
draw: 'Rysuj',
|
draw: 'Rysuj',
|
||||||
type: 'Wprowadź',
|
type: 'Wprowadź',
|
||||||
type_text: 'Wprowadź tekst',
|
type_text: 'Wprowadź tekst',
|
||||||
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
||||||
email_has_been_sent: 'E-mail został wysłany'
|
email_has_been_sent: 'E-mail został wysłany',
|
||||||
|
processing: 'Przetwarzanie',
|
||||||
|
pay_with_strip: 'Płatność za pomocą Stripe',
|
||||||
|
reupload: 'Ponowne przesłanie',
|
||||||
|
upload: 'Przesyłanie',
|
||||||
|
files: 'Pliki'
|
||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
|
minimize: 'Зменшити',
|
||||||
|
text: 'Текст',
|
||||||
|
signature: 'Підпис',
|
||||||
|
initials: 'Ініціали',
|
||||||
|
date: 'Дата',
|
||||||
|
image: 'Зображення',
|
||||||
|
file: 'Файл',
|
||||||
|
select: 'Вибрати',
|
||||||
|
checkbox: 'Прапорець',
|
||||||
|
multiple: 'Множинний',
|
||||||
|
radio: 'Радіо',
|
||||||
|
cells: 'Клітинки',
|
||||||
|
stamp: 'Штамп',
|
||||||
|
payment: 'Платіж',
|
||||||
|
phone: 'Телефон',
|
||||||
submit_form: 'Надіслати Форму',
|
submit_form: 'Надіслати Форму',
|
||||||
type_here: 'Введіть тут',
|
type_here_: 'Введіть тут',
|
||||||
optional: 'необов’язково',
|
optional: 'необов’язково',
|
||||||
|
appears_on: "З'являється на",
|
||||||
|
page: 'Сторінка',
|
||||||
|
take_photo: 'Зробити фото',
|
||||||
select_your_option: 'Виберіть варіант',
|
select_your_option: 'Виберіть варіант',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть',
|
complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть',
|
||||||
submit: 'надіслати',
|
submit: 'надіслати',
|
||||||
@@ -268,8 +407,6 @@ const uk = {
|
|||||||
or_drag_and_drop_files: 'або перетягніть файли сюди',
|
or_drag_and_drop_files: 'або перетягніть файли сюди',
|
||||||
send_copy_via_email: 'Надіслати копію електронною поштою',
|
send_copy_via_email: 'Надіслати копію електронною поштою',
|
||||||
download: 'Завантажити',
|
download: 'Завантажити',
|
||||||
signature: 'Підпис',
|
|
||||||
initials: 'Ініціали',
|
|
||||||
clear: 'Очистити',
|
clear: 'Очистити',
|
||||||
redraw: 'Перемалювати',
|
redraw: 'Перемалювати',
|
||||||
draw_initials: 'Намалювати ініціали',
|
draw_initials: 'Намалювати ініціали',
|
||||||
@@ -289,21 +426,43 @@ const uk = {
|
|||||||
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий',
|
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий',
|
||||||
please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля",
|
please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля",
|
||||||
set_today: 'Задати Сьогодні',
|
set_today: 'Задати Сьогодні',
|
||||||
date: 'Дата',
|
|
||||||
draw_signature: 'Намалюйте підпис',
|
draw_signature: 'Намалюйте підпис',
|
||||||
type_initial: 'Введіть ініціали',
|
type_initial: 'Введіть ініціали',
|
||||||
draw: 'Підпис',
|
draw: 'Підпис',
|
||||||
type: 'Текст',
|
type: 'Текст',
|
||||||
type_text: 'Введіть текст',
|
type_text: 'Введіть текст',
|
||||||
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
||||||
email_has_been_sent: 'Електронний лист був відправлений'
|
email_has_been_sent: 'Електронний лист був відправлений',
|
||||||
|
processing: 'Обробка',
|
||||||
|
pay_with_strip: 'Сплатити за допомогою Stripe',
|
||||||
|
reupload: 'Перезавантажити',
|
||||||
|
upload: 'Завантажити',
|
||||||
|
files: 'Файли'
|
||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
|
minimize: 'Minimalizovat',
|
||||||
|
text: 'Text',
|
||||||
|
signature: 'Podpis',
|
||||||
|
initials: 'Iniciály',
|
||||||
|
date: 'Datum',
|
||||||
|
image: 'Obrázek',
|
||||||
|
file: 'Soubor',
|
||||||
|
select: 'Vybrat',
|
||||||
|
checkbox: 'Zaškrtávací políčko',
|
||||||
|
multiple: 'Více možností',
|
||||||
|
radio: 'Rádio',
|
||||||
|
cells: 'Buňky',
|
||||||
|
stamp: 'Razítko',
|
||||||
|
payment: 'Platba',
|
||||||
|
phone: 'Telefon',
|
||||||
submit_form: 'Odeslat formulář',
|
submit_form: 'Odeslat formulář',
|
||||||
type_here: 'Zadejte zde',
|
type_here_: 'Zadejte zde',
|
||||||
optional: 'volitelné',
|
optional: 'volitelné',
|
||||||
|
appears_on: 'Zobrazuje se na',
|
||||||
|
page: 'Stránka',
|
||||||
select_your_option: 'Vyberte svou volbu',
|
select_your_option: 'Vyberte svou volbu',
|
||||||
|
take_photo: 'Fotit',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na',
|
complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na',
|
||||||
submit: 'odeslat',
|
submit: 'odeslat',
|
||||||
next: 'další',
|
next: 'další',
|
||||||
@@ -311,8 +470,6 @@ const cs = {
|
|||||||
or_drag_and_drop_files: 'nebo přetáhněte soubory sem',
|
or_drag_and_drop_files: 'nebo přetáhněte soubory sem',
|
||||||
send_copy_via_email: 'Odeslat kopii e-mailem',
|
send_copy_via_email: 'Odeslat kopii e-mailem',
|
||||||
download: 'Stáhnout',
|
download: 'Stáhnout',
|
||||||
signature: 'Podpis',
|
|
||||||
initials: 'Iniciály',
|
|
||||||
clear: 'Smazat',
|
clear: 'Smazat',
|
||||||
redraw: 'Překreslit',
|
redraw: 'Překreslit',
|
||||||
draw_initials: 'Nakreslit iniciály',
|
draw_initials: 'Nakreslit iniciály',
|
||||||
@@ -332,20 +489,42 @@ const cs = {
|
|||||||
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán',
|
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán',
|
||||||
please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky',
|
please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky',
|
||||||
set_today: 'Nastavit Dnes',
|
set_today: 'Nastavit Dnes',
|
||||||
date: 'Datum',
|
|
||||||
draw_signature: 'Nakreslit podpis',
|
draw_signature: 'Nakreslit podpis',
|
||||||
type_initial: 'Zadat iniciály',
|
type_initial: 'Zadat iniciály',
|
||||||
draw: 'Kreslit',
|
draw: 'Kreslit',
|
||||||
type: 'Zadat',
|
type: 'Zadat',
|
||||||
type_text: 'Zadat text',
|
type_text: 'Zadat text',
|
||||||
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
||||||
email_has_been_sent: 'E-mail byl odeslán'
|
email_has_been_sent: 'E-mail byl odeslán',
|
||||||
|
processing: 'Zpracování',
|
||||||
|
pay_with_strip: 'Zaplacení přes Stripe',
|
||||||
|
reupload: 'Znovu nahrát',
|
||||||
|
upload: 'Nahrát',
|
||||||
|
files: 'Soubory'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
minimize: 'Minimizar',
|
||||||
|
text: 'Texto',
|
||||||
|
signature: 'Assinatura',
|
||||||
|
initials: 'Iniciais',
|
||||||
|
date: 'Data',
|
||||||
|
image: 'Imagem',
|
||||||
|
file: 'Arquivo',
|
||||||
|
select: 'Selecionar',
|
||||||
|
checkbox: 'Caixa',
|
||||||
|
multiple: 'Múltiplo',
|
||||||
|
radio: 'Rádio',
|
||||||
|
cells: 'Células',
|
||||||
|
stamp: 'Carimbo',
|
||||||
|
payment: 'Pagamento',
|
||||||
|
phone: 'Telefone',
|
||||||
submit_form: 'Enviar Formulário',
|
submit_form: 'Enviar Formulário',
|
||||||
type_here: 'Digite aqui',
|
type_here_: 'Digite aqui',
|
||||||
optional: 'opcional',
|
optional: 'opcional',
|
||||||
|
appears_on: 'Aparece em',
|
||||||
|
page: 'Página',
|
||||||
|
take_photo: 'Tirar foto',
|
||||||
select_your_option: 'Selecione sua opção',
|
select_your_option: 'Selecione sua opção',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique',
|
complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique',
|
||||||
submit: 'enviar',
|
submit: 'enviar',
|
||||||
@@ -354,8 +533,6 @@ const pt = {
|
|||||||
or_drag_and_drop_files: 'ou arraste e solte arquivos',
|
or_drag_and_drop_files: 'ou arraste e solte arquivos',
|
||||||
send_copy_via_email: 'Enviar cópia por e-mail',
|
send_copy_via_email: 'Enviar cópia por e-mail',
|
||||||
download: 'Baixar',
|
download: 'Baixar',
|
||||||
signature: 'Assinatura',
|
|
||||||
initials: 'Iniciais',
|
|
||||||
clear: 'Limpar',
|
clear: 'Limpar',
|
||||||
redraw: 'Redesenhar',
|
redraw: 'Redesenhar',
|
||||||
draw_initials: 'Desenhar iniciais',
|
draw_initials: 'Desenhar iniciais',
|
||||||
@@ -375,21 +552,148 @@ const pt = {
|
|||||||
verification_code_has_been_resent: 'O código de verificação foi reenviado via SMS',
|
verification_code_has_been_resent: 'O código de verificação foi reenviado via SMS',
|
||||||
please_fill_all_required_fields: 'Por favor, preencha todos os campos obrigatórios',
|
please_fill_all_required_fields: 'Por favor, preencha todos os campos obrigatórios',
|
||||||
set_today: 'Definir Hoje',
|
set_today: 'Definir Hoje',
|
||||||
date: 'Data',
|
|
||||||
draw_signature: 'Desenhar assinatura',
|
draw_signature: 'Desenhar assinatura',
|
||||||
type_initial: 'Inserir iniciais',
|
type_initial: 'Inserir iniciais',
|
||||||
draw: 'Desenhar',
|
draw: 'Desenhar',
|
||||||
type: 'Inserir',
|
type: 'Inserir',
|
||||||
type_text: 'Inserir texto',
|
type_text: 'Inserir texto',
|
||||||
toggle_multiline_text: 'Alternar Texto Multilinha',
|
toggle_multiline_text: 'Alternar Texto Multilinha',
|
||||||
email_has_been_sent: 'Email enviado'
|
email_has_been_sent: 'Email enviado',
|
||||||
|
processing: 'Processamento',
|
||||||
|
pay_with_strip: 'Pagar com Stripe',
|
||||||
|
reupload: 'Reenviar',
|
||||||
|
upload: 'Carregar',
|
||||||
|
files: 'Arquivos'
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = { en, es, it, de, fr, pl, uk, cs, pt }
|
const he = {
|
||||||
|
minimize: 'לקטן',
|
||||||
|
text: 'טקסט',
|
||||||
|
signature: 'חתימה',
|
||||||
|
initials: 'ראשי תיקיות',
|
||||||
|
date: 'תאריך',
|
||||||
|
image: 'תמונה',
|
||||||
|
file: 'קובץ',
|
||||||
|
select: 'בחר',
|
||||||
|
checkbox: 'תיק בחירה',
|
||||||
|
multiple: 'רב ערכים',
|
||||||
|
radio: 'רדיו',
|
||||||
|
cells: 'תאים',
|
||||||
|
stamp: 'חותמת',
|
||||||
|
payment: 'תשלום',
|
||||||
|
phone: 'טלפון',
|
||||||
|
submit_form: 'שלח טופס',
|
||||||
|
type_here_: 'הקלד כאן',
|
||||||
|
optional: 'אופציונלי',
|
||||||
|
option: 'אפשרות',
|
||||||
|
appears_on: 'מופיע ב',
|
||||||
|
page: 'עמוד',
|
||||||
|
take_photo: 'צלם תמונה',
|
||||||
|
select_your_option: 'בחר את האפשרות שלך',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'השלם את תיבות הסימון המודגשות ולחץ',
|
||||||
|
submit: 'שליחה',
|
||||||
|
next: 'הבא',
|
||||||
|
click_to_upload: 'לחץ להעלאה',
|
||||||
|
or_drag_and_drop_files: 'או גרור ושחרר קבצים',
|
||||||
|
send_copy_via_email: 'שלח עותק באמצעות אימייל',
|
||||||
|
download: 'הורדה',
|
||||||
|
clear: 'נקה',
|
||||||
|
redraw: 'צייר מחדש',
|
||||||
|
draw_initials: 'צייר ציוני ראשי תיבות',
|
||||||
|
type_signature_here: 'הקלד חתימה כאן',
|
||||||
|
type_initial_here: 'הקלד ציוני ראשי תיבות כאן',
|
||||||
|
form_has_been_completed: 'הטופס הושלם',
|
||||||
|
create_a_free_account: 'צור חשבון חינם',
|
||||||
|
signed_with: 'חתום באמצעות',
|
||||||
|
please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך',
|
||||||
|
open_source_documents_software: 'תוכנה פתוחה למסמכים',
|
||||||
|
verified_phone_number: 'אימות מספר טלפון',
|
||||||
|
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
|
||||||
|
six_digits_code: 'קוד משתמש שש ספרות',
|
||||||
|
change_phone_number: 'שינוי מספר טלפון',
|
||||||
|
sending: 'שולח',
|
||||||
|
resend_code: 'שלח מחדש קוד',
|
||||||
|
verification_code_has_been_resent: 'קוד האימות נשלח מחדש בSMS',
|
||||||
|
please_fill_all_required_fields: 'אנא מלא את כל השדות הדרושים',
|
||||||
|
set_today: 'קבע היום',
|
||||||
|
toggle_multiline_text: 'שנה בין טקסט במספר שורות לטקסט בשורה אחת',
|
||||||
|
draw_signature: 'צייר חתימה',
|
||||||
|
type_initial: 'הקלד ציוני ראשי תיבות',
|
||||||
|
draw: 'צייר',
|
||||||
|
type: 'הקלד',
|
||||||
|
type_text: 'הקלד טקסט',
|
||||||
|
email_has_been_sent: 'האימייל נשלח',
|
||||||
|
processing: 'מעבד',
|
||||||
|
pay_with_strip: 'שלם עם סטרייפ',
|
||||||
|
reupload: 'העלה שוב',
|
||||||
|
upload: 'העלאה',
|
||||||
|
files: 'קבצים'
|
||||||
|
}
|
||||||
|
|
||||||
const browserLanguage = (navigator.language || navigator.userLanguage || 'en').split('-')[0]
|
const nl = {
|
||||||
|
minimize: 'Minimaliseren',
|
||||||
|
text: 'Tekst',
|
||||||
|
signature: 'Handtekening',
|
||||||
|
initials: 'Initialen',
|
||||||
|
date: 'Datum',
|
||||||
|
image: 'Afbeelding',
|
||||||
|
take_photo: 'Maak een foto',
|
||||||
|
file: 'Bestand',
|
||||||
|
select: 'Selecteer',
|
||||||
|
checkbox: 'Aankruisvakje',
|
||||||
|
multiple: 'Meerdere',
|
||||||
|
radio: 'Radio',
|
||||||
|
cells: 'Cellen',
|
||||||
|
stamp: 'Stempel',
|
||||||
|
payment: 'Betaling',
|
||||||
|
phone: 'Telefoon',
|
||||||
|
submit_form: 'Formulier verzenden',
|
||||||
|
type_here_: 'Typ hier...',
|
||||||
|
optional: 'Optioneel',
|
||||||
|
option: 'Optie',
|
||||||
|
appears_on: 'Verschijnt op',
|
||||||
|
page: 'Pagina',
|
||||||
|
select_your_option: 'Selecteer uw optie',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Voltooi de gemarkeerde selectievakjes en klik',
|
||||||
|
submit: 'Voltooien',
|
||||||
|
next: 'Volgende',
|
||||||
|
click_to_upload: 'Klik om te uploaden',
|
||||||
|
or_drag_and_drop_files: 'of sleep en plaats bestanden',
|
||||||
|
send_copy_via_email: 'Stuur kopie per e-mail',
|
||||||
|
download: 'Downloaden',
|
||||||
|
clear: 'Wissen',
|
||||||
|
redraw: 'Opnieuw tekenen',
|
||||||
|
draw_initials: 'Initialen tekenen',
|
||||||
|
type_signature_here: 'Typ hier uw handtekening',
|
||||||
|
type_initial_here: 'Typ hier uw initialen',
|
||||||
|
form_has_been_completed: 'Formulier is voltooid!',
|
||||||
|
create_a_free_account: 'Maak een gratis account aan',
|
||||||
|
signed_with: 'Ondertekend met',
|
||||||
|
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan',
|
||||||
|
open_source_documents_software: 'Open source documenten software',
|
||||||
|
verified_phone_number: 'Geverifieerd telefoonnummer',
|
||||||
|
use_international_format: 'Gebruik internationaal formaat: +1xxx',
|
||||||
|
six_digits_code: '6-cijferige code',
|
||||||
|
change_phone_number: 'Wijzig telefoonnummer',
|
||||||
|
sending: 'Voltooien...',
|
||||||
|
resend_code: 'Code opnieuw verzenden',
|
||||||
|
verification_code_has_been_resent: 'Verificatiecode is opnieuw verzonden via SMS',
|
||||||
|
please_fill_all_required_fields: 'Vul alle verplichte velden in',
|
||||||
|
set_today: 'Vandaag',
|
||||||
|
toggle_multiline_text: 'Schakel meerregelige tekst in/uit',
|
||||||
|
draw_signature: 'Handtekening tekenen',
|
||||||
|
type_initial: 'Typ initialen',
|
||||||
|
draw: 'Tekenen',
|
||||||
|
type: 'Typen',
|
||||||
|
type_text: 'Typ tekst',
|
||||||
|
email_has_been_sent: 'E-mail is verzonden',
|
||||||
|
processing: 'Verwerken',
|
||||||
|
pay_with_strip: 'Betalen met Stripe',
|
||||||
|
reupload: 'Opnieuw uploaden',
|
||||||
|
upload: 'Uploaden',
|
||||||
|
files: 'Bestanden'
|
||||||
|
}
|
||||||
|
|
||||||
const t = (key) => i18n[browserLanguage]?.[key] || i18n.en[key] || key
|
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl }
|
||||||
|
|
||||||
export default i18n
|
export default i18n
|
||||||
export { t }
|
|
||||||
@@ -3,13 +3,13 @@
|
|||||||
<div class="flex justify-between items-center w-full mb-2">
|
<div class="flex justify-between items-center w-full mb-2">
|
||||||
<label
|
<label
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
>{{ field.name || 'Image' }}</label>
|
>{{ showFieldNames && field.name ? field.name : t('image') }}</label>
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline btn-sm"
|
class="btn btn-outline btn-sm"
|
||||||
@click.prevent="remove"
|
@click.prevent="remove"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
Reupload
|
{{ t('reupload') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<FileDropzone
|
<FileDropzone
|
||||||
v-if="!modelValue"
|
v-if="!modelValue"
|
||||||
:message="`Upload ${field.name || 'Image'}${field.required ? '' : ' (optional)'}`"
|
:message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:accept="'image/*'"
|
:accept="'image/*'"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
@@ -46,6 +46,7 @@ export default {
|
|||||||
FileDropzone,
|
FileDropzone,
|
||||||
IconReload
|
IconReload
|
||||||
},
|
},
|
||||||
|
inject: ['t'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -56,6 +57,11 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
showFieldNames: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
submitterSlug: {
|
submitterSlug: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div dir="auto">
|
||||||
<div class="flex justify-between items-center w-full mb-2">
|
<div class="flex justify-between items-center w-full mb-2">
|
||||||
<label
|
<label
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
>{{ field.name || t('initials') }}</label>
|
>{{ showFieldNames && field.name ? field.name : t('initials') }}</label>
|
||||||
<div class="space-x-2 flex">
|
<div class="space-x-2 flex">
|
||||||
<span
|
<span
|
||||||
v-if="isDrawInitials"
|
v-if="isDrawInitials"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
:data-tip="t('type_initials')"
|
:data-tip="t('type_initial')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="tooltip"
|
class="tooltip ml-2"
|
||||||
:data-tip="t('draw_initials')"
|
:data-tip="t('draw_initials')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
title="Minimize"
|
:title="t('minimize')"
|
||||||
href="#"
|
href="#"
|
||||||
class="py-1.5 inline md:hidden"
|
class="py-1.5 inline md:hidden"
|
||||||
@click.prevent="$emit('minimize')"
|
@click.prevent="$emit('minimize')"
|
||||||
@@ -70,6 +70,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<AppearsOn :field="field" />
|
||||||
<input
|
<input
|
||||||
:value="modelValue || computedPreviousValue"
|
:value="modelValue || computedPreviousValue"
|
||||||
type="hidden"
|
type="hidden"
|
||||||
@@ -83,7 +84,7 @@
|
|||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue && !computedPreviousValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
class="bg-white border border-base-300 rounded-2xl"
|
class="bg-white border border-base-300 rounded-2xl w-full"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
||||||
@@ -103,10 +104,14 @@
|
|||||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||||
import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import SignaturePad from 'signature_pad'
|
import SignaturePad from 'signature_pad'
|
||||||
|
import AppearsOn from './appears_on'
|
||||||
|
|
||||||
|
const scale = 3
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'InitialsStep',
|
name: 'InitialsStep',
|
||||||
components: {
|
components: {
|
||||||
|
AppearsOn,
|
||||||
IconReload,
|
IconReload,
|
||||||
IconTextSize,
|
IconTextSize,
|
||||||
IconSignature,
|
IconSignature,
|
||||||
@@ -122,6 +127,11 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
showFieldNames: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
isDirectUpload: {
|
isDirectUpload: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -163,8 +173,10 @@ export default {
|
|||||||
async mounted () {
|
async mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth
|
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
||||||
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth / 5
|
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 3) * scale
|
||||||
|
|
||||||
|
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$refs.textInput?.focus()
|
this.$refs.textInput?.focus()
|
||||||
@@ -182,8 +194,26 @@ export default {
|
|||||||
|
|
||||||
this.$emit('start')
|
this.$emit('start')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
||||||
|
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 3) * scale
|
||||||
|
|
||||||
|
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||||
|
|
||||||
|
this.intersectionObserver?.disconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.intersectionObserver.observe(this.$refs.canvas)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
this.intersectionObserver?.disconnect()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
remove () {
|
remove () {
|
||||||
this.$emit('update:model-value', '')
|
this.$emit('update:model-value', '')
|
||||||
@@ -201,7 +231,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateWrittenInitials (e) {
|
updateWrittenInitials (e) {
|
||||||
this.isInitialsStarted = true
|
this.isInitialsStarted = !!e.target.value
|
||||||
|
|
||||||
const canvas = this.$refs.canvas
|
const canvas = this.$refs.canvas
|
||||||
const context = canvas.getContext('2d')
|
const context = canvas.getContext('2d')
|
||||||
@@ -214,8 +244,8 @@ export default {
|
|||||||
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
|
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
|
||||||
context.textAlign = 'center'
|
context.textAlign = 'center'
|
||||||
|
|
||||||
context.clearRect(0, 0, canvas.width, canvas.height)
|
context.clearRect(0, 0, canvas.width / scale, canvas.height / scale)
|
||||||
context.fillText(e.target.value, canvas.width / 2, canvas.height / 2 + 11)
|
context.fillText(e.target.value, canvas.width / 2 / scale, canvas.height / 2 / scale + 11)
|
||||||
},
|
},
|
||||||
toggleTextInput () {
|
toggleTextInput () {
|
||||||
this.remove()
|
this.remove()
|
||||||
|
|||||||
@@ -1,13 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<label
|
<label
|
||||||
v-if="field.name"
|
v-if="showFieldNames && field.name"
|
||||||
:for="field.uuid"
|
:for="field.uuid"
|
||||||
|
dir="auto"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl mb-2"
|
||||||
>{{ field.name }}</label>
|
>{{ field.name }}</label>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full max-h-44 overflow-y-auto">
|
||||||
<div class="space-y-3.5 mx-auto">
|
<input
|
||||||
|
v-if="modelValue.length === 0"
|
||||||
|
type="text"
|
||||||
|
:name="`values[${field.uuid}][]`"
|
||||||
|
:value="''"
|
||||||
|
class="hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!showOptions"
|
||||||
|
class="text-xl px-1"
|
||||||
|
>
|
||||||
|
<span @click="scrollIntoField(field)">
|
||||||
|
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ isLastStep ? t('submit') : t('next') }}</span>.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="space-y-3.5 mx-auto"
|
||||||
|
:class="{ hidden: !showOptions }"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="option in field.options"
|
v-for="(option, index) in field.options"
|
||||||
:key="option.uuid"
|
:key="option.uuid"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
@@ -19,13 +38,13 @@
|
|||||||
:ref="setInputRef"
|
:ref="setInputRef"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:name="`values[${field.uuid}][]`"
|
:name="`values[${field.uuid}][]`"
|
||||||
:value="option.value"
|
:value="optionValue(option, index)"
|
||||||
class="base-checkbox !h-7 !w-7"
|
class="base-checkbox !h-7 !w-7"
|
||||||
:checked="(modelValue || []).includes(option.value)"
|
:checked="(modelValue || []).includes(optionValue(option, index))"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
>
|
>
|
||||||
<span class="text-xl">
|
<span class="text-xl">
|
||||||
{{ option.value }}
|
{{ optionValue(option, index) }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,11 +55,22 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'MultiSelectStep',
|
name: 'MultiSelectStep',
|
||||||
|
inject: ['t', 'scrollIntoField'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
isLastStep: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
showFieldNames: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -53,6 +83,11 @@ export default {
|
|||||||
inputRefs: []
|
inputRefs: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
showOptions () {
|
||||||
|
return this.showFieldNames && (this.field.options.some((e) => e.value) || this.field.options.length < 5)
|
||||||
|
}
|
||||||
|
},
|
||||||
beforeUpdate () {
|
beforeUpdate () {
|
||||||
this.inputRefs = []
|
this.inputRefs = []
|
||||||
},
|
},
|
||||||
@@ -62,8 +97,15 @@ export default {
|
|||||||
this.inputRefs.push(el)
|
this.inputRefs.push(el)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
optionValue (option, index) {
|
||||||
|
if (option.value) {
|
||||||
|
return option.value
|
||||||
|
} else {
|
||||||
|
return `${this.t('option')} ${index + 1}`
|
||||||
|
}
|
||||||
|
},
|
||||||
onChange () {
|
onChange () {
|
||||||
this.$emit('update:model-value', this.inputRefs.filter(e => e.checked).map(e => e.value))
|
this.$emit('update:model-value', this.inputRefs.filter(e => e.checked).map((e, index) => this.optionValue(e, index)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
<template>
|
||||||
|
<label
|
||||||
|
v-if="!modelValue && !sessionId"
|
||||||
|
:for="field.uuid"
|
||||||
|
class="label text-2xl mb-2"
|
||||||
|
>{{ field.name || defaultName }}
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
:value="modelValue"
|
||||||
|
hidden
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
class="hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="modelValue && !sessionId"
|
||||||
|
class=" text-2xl mb-2"
|
||||||
|
>
|
||||||
|
Already paid
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<button
|
||||||
|
v-if="sessionId"
|
||||||
|
disabled
|
||||||
|
class="base-button w-full"
|
||||||
|
>
|
||||||
|
<IconLoader
|
||||||
|
width="22"
|
||||||
|
class="animate-spin"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ t('processing') }}...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
:id="field.uuid"
|
||||||
|
class="btn bg-[#7B73FF] text-white hover:bg-[#0A2540] text-lg w-full"
|
||||||
|
:class="{ disabled: isCreatingCheckout }"
|
||||||
|
:disabled="isCreatingCheckout"
|
||||||
|
@click.prevent="startCheckout"
|
||||||
|
>
|
||||||
|
<IconInnerShadowTop
|
||||||
|
v-if="isCreatingCheckout"
|
||||||
|
width="22"
|
||||||
|
class="animate-spin"
|
||||||
|
/>
|
||||||
|
<IconBrandStripe
|
||||||
|
v-else
|
||||||
|
width="22"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ t('pay_with_strip') }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconBrandStripe, IconInnerShadowTop, IconLoader } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PaymentStep',
|
||||||
|
components: {
|
||||||
|
IconBrandStripe,
|
||||||
|
IconInnerShadowTop,
|
||||||
|
IconLoader
|
||||||
|
},
|
||||||
|
inject: ['baseUrl', 't'],
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
submitterSlug: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['focus', 'submit', 'update:model-value', 'attached'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isCreatingCheckout: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
queryParams () {
|
||||||
|
return new URLSearchParams(window.location.search)
|
||||||
|
},
|
||||||
|
sessionId () {
|
||||||
|
return this.queryParams.get('stripe_session_id')
|
||||||
|
},
|
||||||
|
defaultName () {
|
||||||
|
const { price, currency } = this.field.preferences || {}
|
||||||
|
|
||||||
|
const formattedPrice = new Intl.NumberFormat([], {
|
||||||
|
style: 'currency',
|
||||||
|
currency
|
||||||
|
}).format(price)
|
||||||
|
|
||||||
|
return `Pay ${formattedPrice}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
if (this.sessionId) {
|
||||||
|
this.$emit('submit')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async submit () {
|
||||||
|
if (this.sessionId) {
|
||||||
|
return fetch((this.baseUrl || '/embed').replace('/embed', '/api/stripe_payments/' + this.sessionId), {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({
|
||||||
|
submitter_slug: this.submitterSlug
|
||||||
|
}),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}).then(async (resp) => {
|
||||||
|
if (resp.status === 422 || resp.status === 500) {
|
||||||
|
const data = await resp.json()
|
||||||
|
|
||||||
|
alert(data.error || 'Unexpected error')
|
||||||
|
|
||||||
|
return Promise.reject(new Error(data.error))
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachment = await resp.json()
|
||||||
|
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname)
|
||||||
|
|
||||||
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
this.$emit('attached', attachment)
|
||||||
|
|
||||||
|
return resp
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return Promise.resolve({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startCheckout () {
|
||||||
|
this.isCreatingCheckout = true
|
||||||
|
|
||||||
|
fetch((this.baseUrl || '/embed').replace('/embed', '/api/stripe_payments'), {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
submitter_slug: this.submitterSlug,
|
||||||
|
field_uuid: this.field.uuid
|
||||||
|
}),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}).then(async (resp) => {
|
||||||
|
if (resp.status === 422 || resp.status === 500) {
|
||||||
|
const data = await resp.json()
|
||||||
|
|
||||||
|
alert(data.message || 'Unexpected error')
|
||||||
|
|
||||||
|
return Promise.reject(new Error(data.message))
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url } = await resp.json()
|
||||||
|
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
|
link.href = url
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
link.click()
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.isCreatingCheckout = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<label
|
<label
|
||||||
:for="isCodeSent ? 'one_time_code' : field.uuid"
|
:for="isCodeSent ? 'one_time_code' : field.uuid"
|
||||||
class="label text-2xl mb-2"
|
class="label text-2xl mb-2"
|
||||||
>{{ field.name || t('verified_phone_number') }}
|
>{{ showFieldNames && field.name ? field.name : t('verified_phone_number') }}
|
||||||
<template v-if="!field.required">({{ t('optional') }})</template>
|
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||||
</label>
|
</label>
|
||||||
<div>
|
<div>
|
||||||
@@ -93,6 +93,11 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
showFieldNames: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -140,7 +145,11 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
if (!this.isCodeSent) {
|
if (!this.$refs.phone.value.toString().startsWith('+')) {
|
||||||
|
alert(this.t('use_international_format'))
|
||||||
|
|
||||||
|
return Promise.reject(new Error('phone invalid'))
|
||||||
|
} else if (!this.isCodeSent) {
|
||||||
this.sendVerificationCode()
|
this.sendVerificationCode()
|
||||||
|
|
||||||
this.$emit('update:model-value', this.$refs.phone.value)
|
this.$emit('update:model-value', this.$refs.phone.value)
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div dir="auto">
|
||||||
<div class="flex justify-between items-center w-full mb-2">
|
<div class="flex justify-between items-center w-full mb-2">
|
||||||
<label
|
<label
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
>{{ field.name || t('signature') }}</label>
|
>{{ showFieldNames && field.name ? field.name : t('signature') }}</label>
|
||||||
<div class="space-x-2 flex">
|
<div class="space-x-2 flex">
|
||||||
<span
|
<span
|
||||||
v-if="isTextSignature"
|
v-if="isTextSignature && field.preferences?.format !== 'typed'"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
:data-tip="t('draw_signature')"
|
:data-tip="t('draw_signature')"
|
||||||
>
|
>
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else-if="withTypedSignature && field.preferences?.format !== 'typed' && field.preferences?.format !== 'drawn'"
|
||||||
class="tooltip"
|
class="tooltip ml-2"
|
||||||
:data-tip="t('type_text')"
|
:data-tip="t('type_text')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -40,14 +40,16 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
v-if="field.preferences?.format !== 'typed'"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
data-tip="Take photo"
|
:data-tip="t('take_photo')"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="btn btn-outline btn-sm font-medium"
|
class="btn btn-outline btn-sm font-medium"
|
||||||
>
|
>
|
||||||
<IconCamera :width="16" />
|
<IconCamera :width="16" />
|
||||||
<input
|
<input
|
||||||
|
:key="uploadImageInputKey"
|
||||||
type="file"
|
type="file"
|
||||||
hidden
|
hidden
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -78,7 +80,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
title="Minimize"
|
:title="t('minimize')"
|
||||||
class="py-1.5 inline md:hidden"
|
class="py-1.5 inline md:hidden"
|
||||||
@click.prevent="$emit('minimize')"
|
@click.prevent="$emit('minimize')"
|
||||||
>
|
>
|
||||||
@@ -89,6 +91,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<AppearsOn :field="field" />
|
||||||
<input
|
<input
|
||||||
:value="modelValue || computedPreviousValue"
|
:value="modelValue || computedPreviousValue"
|
||||||
type="hidden"
|
type="hidden"
|
||||||
@@ -103,14 +106,14 @@
|
|||||||
v-show="!modelValue && !computedPreviousValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
style="padding: 1px; 0"
|
style="padding: 1px; 0"
|
||||||
class="bg-white border border-base-300 rounded-2xl"
|
class="bg-white border border-base-300 rounded-2xl w-full"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-if="isTextSignature"
|
v-if="isTextSignature"
|
||||||
id="signature_text_input"
|
id="signature_text_input"
|
||||||
ref="textInput"
|
ref="textInput"
|
||||||
class="base-input !text-2xl w-full mt-6"
|
class="base-input !text-2xl w-full mt-6"
|
||||||
:required="field.required"
|
:required="field.required && !isSignatureStarted"
|
||||||
:placeholder="`${t('type_signature_here')}...`"
|
:placeholder="`${t('type_signature_here')}...`"
|
||||||
type="text"
|
type="text"
|
||||||
@input="updateWrittenSignature"
|
@input="updateWrittenSignature"
|
||||||
@@ -122,12 +125,16 @@
|
|||||||
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2 } 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'
|
||||||
|
|
||||||
let isFontLoaded = false
|
let isFontLoaded = false
|
||||||
|
|
||||||
|
const scale = 3
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SignatureStep',
|
name: 'SignatureStep',
|
||||||
components: {
|
components: {
|
||||||
|
AppearsOn,
|
||||||
IconReload,
|
IconReload,
|
||||||
IconCamera,
|
IconCamera,
|
||||||
IconTextSize,
|
IconTextSize,
|
||||||
@@ -144,11 +151,21 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
showFieldNames: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
isDirectUpload: {
|
isDirectUpload: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withTypedSignature: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
attachmentsIndex: {
|
attachmentsIndex: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -170,7 +187,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
isSignatureStarted: !!this.previousValue,
|
isSignatureStarted: !!this.previousValue,
|
||||||
isUsePreviousValue: true,
|
isUsePreviousValue: true,
|
||||||
isTextSignature: false
|
isTextSignature: this.field.preferences?.format === 'typed',
|
||||||
|
uploadImageInputKey: Math.random().toString()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -185,8 +203,10 @@ export default {
|
|||||||
async mounted () {
|
async mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth
|
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
||||||
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth / 3
|
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth * scale / 3
|
||||||
|
|
||||||
|
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -202,8 +222,26 @@ export default {
|
|||||||
|
|
||||||
this.$emit('start')
|
this.$emit('start')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
||||||
|
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth * scale / 3
|
||||||
|
|
||||||
|
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||||
|
|
||||||
|
this.intersectionObserver?.disconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.intersectionObserver.observe(this.$refs.canvas)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
this.intersectionObserver?.disconnect()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
remove () {
|
remove () {
|
||||||
this.$emit('update:model-value', '')
|
this.$emit('update:model-value', '')
|
||||||
@@ -213,7 +251,7 @@ export default {
|
|||||||
},
|
},
|
||||||
loadFont () {
|
loadFont () {
|
||||||
if (!isFontLoaded) {
|
if (!isFontLoaded) {
|
||||||
const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript.otf) format("opentype")`)
|
const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript-Regular.otf) format("opentype")`)
|
||||||
|
|
||||||
font.load().then((loadedFont) => {
|
font.load().then((loadedFont) => {
|
||||||
document.fonts.add(loadedFont)
|
document.fonts.add(loadedFont)
|
||||||
@@ -234,7 +272,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateWrittenSignature (e) {
|
updateWrittenSignature (e) {
|
||||||
this.isSignatureStarted = true
|
this.isSignatureStarted = !!e.target.value
|
||||||
|
|
||||||
const canvas = this.$refs.canvas
|
const canvas = this.$refs.canvas
|
||||||
const context = canvas.getContext('2d')
|
const context = canvas.getContext('2d')
|
||||||
@@ -247,11 +285,12 @@ export default {
|
|||||||
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
|
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
|
||||||
context.textAlign = 'center'
|
context.textAlign = 'center'
|
||||||
|
|
||||||
context.clearRect(0, 0, canvas.width, canvas.height)
|
context.clearRect(0, 0, canvas.width / scale, canvas.height / scale)
|
||||||
context.fillText(e.target.value, canvas.width / 2, canvas.height / 2 + 11)
|
context.fillText(e.target.value, canvas.width / 2 / scale, canvas.height / 2 / scale + 11)
|
||||||
},
|
},
|
||||||
toggleTextInput () {
|
toggleTextInput () {
|
||||||
this.remove()
|
this.remove()
|
||||||
|
this.clear()
|
||||||
this.isTextSignature = !this.isTextSignature
|
this.isTextSignature = !this.isTextSignature
|
||||||
|
|
||||||
if (this.isTextSignature) {
|
if (this.isTextSignature) {
|
||||||
@@ -283,14 +322,16 @@ export default {
|
|||||||
const context = canvas.getContext('2d')
|
const context = canvas.getContext('2d')
|
||||||
|
|
||||||
const aspectRatio = img.width / img.height
|
const aspectRatio = img.width / img.height
|
||||||
|
const canvasWidth = canvas.width / scale
|
||||||
|
const canvasHeight = canvas.height / scale
|
||||||
|
|
||||||
let targetWidth = canvas.width
|
let targetWidth = canvasWidth
|
||||||
let targetHeight = canvas.height
|
let targetHeight = canvasHeight
|
||||||
|
|
||||||
if (canvas.width / canvas.height > aspectRatio) {
|
if (canvasWidth / canvasHeight > aspectRatio) {
|
||||||
targetWidth = canvas.height * aspectRatio
|
targetWidth = canvasHeight * aspectRatio
|
||||||
} else {
|
} else {
|
||||||
targetHeight = canvas.width / aspectRatio
|
targetHeight = canvasWidth / aspectRatio
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetHeight > targetWidth) {
|
if (targetHeight > targetWidth) {
|
||||||
@@ -299,17 +340,21 @@ export default {
|
|||||||
targetHeight = targetHeight * scale
|
targetHeight = targetHeight * scale
|
||||||
}
|
}
|
||||||
|
|
||||||
const x = (canvas.width - targetWidth) / 2
|
const x = (canvasWidth - targetWidth) / 2
|
||||||
const y = (canvas.height - targetHeight) / 2
|
const y = (canvasHeight - targetHeight) / 2
|
||||||
|
|
||||||
context.clearRect(0, 0, canvas.width, canvas.height)
|
setTimeout(() => {
|
||||||
context.drawImage(img, x, y, targetWidth, targetHeight)
|
context.clearRect(0, 0, canvasWidth, canvasHeight)
|
||||||
|
context.drawImage(img, x, y, targetWidth, targetHeight)
|
||||||
|
|
||||||
this.$emit('start')
|
this.$emit('start')
|
||||||
|
}, 50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
|
|
||||||
|
this.uploadImageInputKey = Math.random().toString()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
|
|||||||
@@ -1,27 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<label
|
<label
|
||||||
v-if="field.name"
|
v-if="showFieldNames && field.name"
|
||||||
:for="field.uuid"
|
:for="field.uuid"
|
||||||
class="label text-2xl mb-2"
|
dir="auto"
|
||||||
>{{ field.name }}
|
class="label text-2xl"
|
||||||
|
:class="{ 'mb-2': !field.description }"
|
||||||
|
><template v-if="field.title"><span v-html="field.title" /></template>
|
||||||
|
<template v-else>{{ field.name }}</template>
|
||||||
<template v-if="!field.required">({{ t('optional') }})</template>
|
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="py-1"
|
class="py-1"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
class="mb-3 px-1 text-lg"
|
||||||
|
v-html="field.description"
|
||||||
|
/>
|
||||||
|
<AppearsOn :field="field" />
|
||||||
<div class="items-center flex">
|
<div class="items-center flex">
|
||||||
<input
|
<input
|
||||||
v-if="!isTextArea"
|
v-if="!isTextArea"
|
||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
v-model="text"
|
v-model="text"
|
||||||
:maxlength="cellsMaxLegth"
|
:maxlength="cellsMaxLegth"
|
||||||
class="base-input !text-2xl w-full !pr-11 -mr-10"
|
dir="auto"
|
||||||
|
class="base-input !text-2xl w-full"
|
||||||
|
:class="{ '!pr-11 -mr-10': !field.validation?.pattern }"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
:pattern="field.validation?.pattern"
|
:pattern="field.validation?.pattern"
|
||||||
:oninvalid="field.validation?.message ? `this.setCustomValidity(${JSON.stringify(field.validation.message)})` : ''"
|
:oninvalid="field.validation?.message ? `this.setCustomValidity(${JSON.stringify(field.validation.message)})` : ''"
|
||||||
:oninput="field.validation?.message ? `this.setCustomValidity('')` : ''"
|
:oninput="field.validation?.message ? `this.setCustomValidity('')` : ''"
|
||||||
:placeholder="`${t('type_here')}...${field.required ? '' : ` (${t('optional')})`}`"
|
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
type="text"
|
type="text"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
@@ -31,15 +42,16 @@
|
|||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
ref="textarea"
|
ref="textarea"
|
||||||
v-model="text"
|
v-model="text"
|
||||||
|
dir="auto"
|
||||||
class="base-textarea !text-2xl w-full"
|
class="base-textarea !text-2xl w-full"
|
||||||
:placeholder="`${t('type_here')}...${field.required ? '' : ` (${t('optional')})`}`"
|
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
:required="field.required"
|
:required="field.required"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
@input="resizeTextarea"
|
@input="resizeTextarea"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="!isTextArea && field.type !== 'cells'"
|
v-if="!isTextArea && field.type !== 'cells' && !field.validation?.pattern"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
:data-tip="t('toggle_multiline_text')"
|
:data-tip="t('toggle_multiline_text')"
|
||||||
>
|
>
|
||||||
@@ -56,11 +68,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconAlignBoxLeftTop } from '@tabler/icons-vue'
|
import { IconAlignBoxLeftTop } from '@tabler/icons-vue'
|
||||||
|
import AppearsOn from './appears_on'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TextStep',
|
name: 'TextStep',
|
||||||
components: {
|
components: {
|
||||||
IconAlignBoxLeftTop
|
IconAlignBoxLeftTop,
|
||||||
|
AppearsOn
|
||||||
},
|
},
|
||||||
inject: ['t'],
|
inject: ['t'],
|
||||||
props: {
|
props: {
|
||||||
@@ -68,6 +82,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
showFieldNames: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -104,12 +123,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.isTextArea = this.modelValue?.includes('\n')
|
if (this.modelValue) {
|
||||||
|
this.isTextArea = this.modelValue.toString().includes('\n')
|
||||||
|
|
||||||
if (this.isTextArea) {
|
if (this.isTextArea) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.resizeTextarea()
|
this.resizeTextarea()
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -42,8 +42,9 @@
|
|||||||
v-model="field.submitter_uuid"
|
v-model="field.submitter_uuid"
|
||||||
class="border-r"
|
class="border-r"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
:editable="editable"
|
:editable="editable && !defaultField"
|
||||||
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px]'"
|
:allow-add-new="!defaultSubmitters.length"
|
||||||
|
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'"
|
||||||
:submitters="template.submitters"
|
:submitters="template.submitters"
|
||||||
@update:model-value="save"
|
@update:model-value="save"
|
||||||
@click="selectedAreaRef.value = area"
|
@click="selectedAreaRef.value = area"
|
||||||
@@ -51,7 +52,7 @@
|
|||||||
<FieldType
|
<FieldType
|
||||||
v-model="field.type"
|
v-model="field.type"
|
||||||
:button-width="27"
|
:button-width="27"
|
||||||
:editable="editable"
|
:editable="editable && !defaultField"
|
||||||
:button-classes="'px-1'"
|
:button-classes="'px-1'"
|
||||||
:menu-classes="'bg-white rounded-t-none'"
|
:menu-classes="'bg-white rounded-t-none'"
|
||||||
@update:model-value="[maybeUpdateOptions(), save()]"
|
@update:model-value="[maybeUpdateOptions(), save()]"
|
||||||
@@ -60,7 +61,8 @@
|
|||||||
<span
|
<span
|
||||||
v-if="field.type !== 'checkbox' || field.name"
|
v-if="field.type !== 'checkbox' || field.name"
|
||||||
ref="name"
|
ref="name"
|
||||||
:contenteditable="editable"
|
:contenteditable="editable && !defaultField"
|
||||||
|
dir="auto"
|
||||||
class="pr-1 cursor-text outline-none block"
|
class="pr-1 cursor-text outline-none block"
|
||||||
style="min-width: 2px"
|
style="min-width: 2px"
|
||||||
@keydown.enter.prevent="onNameEnter"
|
@keydown.enter.prevent="onNameEnter"
|
||||||
@@ -83,12 +85,12 @@
|
|||||||
class="label text-xs"
|
class="label text-xs"
|
||||||
@click.prevent="field.required = !field.required"
|
@click.prevent="field.required = !field.required"
|
||||||
@mousedown.prevent
|
@mousedown.prevent
|
||||||
>Required</label>
|
>{{ t('required') }}</label>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-else-if="editable"
|
v-else-if="editable"
|
||||||
class="pr-1"
|
class="pr-1"
|
||||||
title="Remove"
|
:title="t('remove')"
|
||||||
@click.prevent="$emit('remove')"
|
@click.prevent="$emit('remove')"
|
||||||
>
|
>
|
||||||
<IconX width="14" />
|
<IconX width="14" />
|
||||||
@@ -96,21 +98,32 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex items-center h-full w-full"
|
class="flex items-center h-full w-full"
|
||||||
|
dir="auto"
|
||||||
:class="[bgColors[submitterIndex], field?.default_value ? '' : 'justify-center']"
|
:class="[bgColors[submitterIndex], field?.default_value ? '' : 'justify-center']"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="field"
|
v-if="field"
|
||||||
class="flex justify-center items-center space-x-1 h-full"
|
class="flex justify-center items-center space-x-1 h-full"
|
||||||
|
:class="{'w-full h-full': field.type == 'checkbox'}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="field?.default_value"
|
v-if="field?.default_value"
|
||||||
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars }"
|
:class="{ 'w-full h-full': field.type == 'checkbox', 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="textContainer"
|
ref="textContainer"
|
||||||
class="flex items-center px-0.5"
|
class="flex items-center px-0.5"
|
||||||
|
:class="{'w-full h-full': field.type == 'checkbox'}"
|
||||||
>
|
>
|
||||||
<span class="whitespace-pre-wrap">{{ field.default_value }}</span>
|
<IconCheck
|
||||||
|
v-if="field.type == 'checkbox'"
|
||||||
|
class="aspect-square mx-auto"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="whitespace-pre-wrap"
|
||||||
|
>{{ field.default_value }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<component
|
<component
|
||||||
@@ -139,17 +152,18 @@
|
|||||||
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 } from '@tabler/icons-vue'
|
import { IconX, IconCheck } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
components: {
|
components: {
|
||||||
FieldType,
|
FieldType,
|
||||||
|
IconCheck,
|
||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
IconX
|
IconX
|
||||||
},
|
},
|
||||||
inject: ['template', 'selectedAreaRef', 'save'],
|
inject: ['template', 'selectedAreaRef', 'save', 't'],
|
||||||
props: {
|
props: {
|
||||||
area: {
|
area: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -160,6 +174,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
defaultField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
defaultSubmitters: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
editable: {
|
editable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -213,30 +237,50 @@ export default {
|
|||||||
},
|
},
|
||||||
borderColors () {
|
borderColors () {
|
||||||
return [
|
return [
|
||||||
'border-red-500/50',
|
'border-red-500/80',
|
||||||
'border-sky-500/50',
|
'border-sky-500/80',
|
||||||
'border-emerald-500/50',
|
'border-emerald-500/80',
|
||||||
'border-yellow-300/50',
|
'border-yellow-300/80',
|
||||||
'border-purple-600/50',
|
'border-purple-600/80',
|
||||||
'border-pink-500/50',
|
'border-pink-500/80',
|
||||||
'border-cyan-500/50',
|
'border-cyan-500/80',
|
||||||
'border-orange-500/50',
|
'border-orange-500/80',
|
||||||
'border-lime-500/50',
|
'border-lime-500/80',
|
||||||
'border-indigo-500/50'
|
'border-indigo-500/80',
|
||||||
|
'border-red-500/80',
|
||||||
|
'border-sky-500/80',
|
||||||
|
'border-emerald-500/80',
|
||||||
|
'border-yellow-300/80',
|
||||||
|
'border-purple-600/80',
|
||||||
|
'border-pink-500/80',
|
||||||
|
'border-cyan-500/80',
|
||||||
|
'border-orange-500/80',
|
||||||
|
'border-lime-500/80',
|
||||||
|
'border-indigo-500/80'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
bgColors () {
|
bgColors () {
|
||||||
return [
|
return [
|
||||||
'bg-red-100/50',
|
'bg-red-100/80',
|
||||||
'bg-sky-100/50',
|
'bg-sky-100/80',
|
||||||
'bg-emerald-100/50',
|
'bg-emerald-100/80',
|
||||||
'bg-yellow-100/50',
|
'bg-yellow-100/80',
|
||||||
'bg-purple-100/50',
|
'bg-purple-100/80',
|
||||||
'bg-pink-100/50',
|
'bg-pink-100/80',
|
||||||
'bg-cyan-100/50',
|
'bg-cyan-100/80',
|
||||||
'bg-orange-100/50',
|
'bg-orange-100/80',
|
||||||
'bg-lime-100/50',
|
'bg-lime-100/80',
|
||||||
'bg-indigo-100/50'
|
'bg-indigo-100/80',
|
||||||
|
'bg-red-100/80',
|
||||||
|
'bg-sky-100/80',
|
||||||
|
'bg-emerald-100/80',
|
||||||
|
'bg-yellow-100/80',
|
||||||
|
'bg-purple-100/80',
|
||||||
|
'bg-pink-100/80',
|
||||||
|
'bg-cyan-100/80',
|
||||||
|
'bg-orange-100/80',
|
||||||
|
'bg-lime-100/80',
|
||||||
|
'bg-indigo-100/80'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
isSelected () {
|
isSelected () {
|
||||||
|
|||||||
@@ -5,10 +5,12 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="$slots.buttons || withTitle"
|
v-if="$slots.buttons || withTitle"
|
||||||
class="flex justify-between py-1.5 items-center pr-4 sticky top-0 z-10"
|
id="title_container"
|
||||||
|
class="flex justify-between py-1.5 items-center pr-4 top-0 z-10"
|
||||||
|
:class="{ sticky: withStickySubmitters || isBreakpointLg }"
|
||||||
:style="{ backgroundColor }"
|
:style="{ backgroundColor }"
|
||||||
>
|
>
|
||||||
<div class="flex space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
<a
|
<a
|
||||||
v-if="withLogo"
|
v-if="withLogo"
|
||||||
href="/"
|
href="/"
|
||||||
@@ -19,7 +21,7 @@
|
|||||||
v-if="withTitle"
|
v-if="withTitle"
|
||||||
:model-value="template.name"
|
:model-value="template.name"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
class="text-3xl font-semibold focus:text-clip"
|
class="text-xl md:text-3xl font-semibold focus:text-clip"
|
||||||
:icon-stroke-width="2.3"
|
:icon-stroke-width="2.3"
|
||||||
@update:model-value="updateName"
|
@update:model-value="updateName"
|
||||||
/>
|
/>
|
||||||
@@ -31,9 +33,24 @@
|
|||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a
|
<a
|
||||||
:href="`/templates/${template.id}/submissions/new`"
|
:href="template.submitters.length > 1 ? `/templates/${template.id}/submissions/new?selfsign=true` : `/d/${template.slug}`"
|
||||||
|
class="btn btn-primary btn-ghost text-base hidden md:flex"
|
||||||
|
:target="template.submitters.length > 1 ? '' : '_blank'"
|
||||||
|
:data-turbo-frame="template.submitters.length > 1 ? 'modal' : ''"
|
||||||
|
@click="maybeShowEmptyTemplateAlert"
|
||||||
|
>
|
||||||
|
<IconWritingSign
|
||||||
|
width="20"
|
||||||
|
class="inline"
|
||||||
|
/>
|
||||||
|
<span class="hidden md:inline">
|
||||||
|
{{ t('sign_yourself') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
:href="`/templates/${template.id}/submissions/new?with_link=true`"
|
||||||
data-turbo-frame="modal"
|
data-turbo-frame="modal"
|
||||||
class="btn btn-primary text-base"
|
class="white-button md:!px-6"
|
||||||
@click="maybeShowEmptyTemplateAlert"
|
@click="maybeShowEmptyTemplateAlert"
|
||||||
>
|
>
|
||||||
<IconUsersPlus
|
<IconUsersPlus
|
||||||
@@ -41,10 +58,11 @@
|
|||||||
class="inline"
|
class="inline"
|
||||||
/>
|
/>
|
||||||
<span class="hidden md:inline">
|
<span class="hidden md:inline">
|
||||||
Recipients
|
{{ t('send') }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
|
v-if="editable"
|
||||||
class="base-button"
|
class="base-button"
|
||||||
:class="{ disabled: isSaving }"
|
:class="{ disabled: isSaving }"
|
||||||
v-bind="isSaving ? { disabled: true } : {}"
|
v-bind="isSaving ? { disabled: true } : {}"
|
||||||
@@ -60,9 +78,18 @@
|
|||||||
width="22"
|
width="22"
|
||||||
/>
|
/>
|
||||||
<span class="hidden md:inline">
|
<span class="hidden md:inline">
|
||||||
Save
|
{{ t('save') }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
<a
|
||||||
|
v-else
|
||||||
|
:href="`/templates/${template.id}`"
|
||||||
|
class="base-button"
|
||||||
|
>
|
||||||
|
<span class="hidden md:inline">
|
||||||
|
{{ t('back') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,6 +98,7 @@
|
|||||||
:class="$slots.buttons || withTitle ? 'md:max-h-[calc(100%_-_60px)]' : 'md:max-h-[100%]'"
|
:class="$slots.buttons || withTitle ? 'md:max-h-[calc(100%_-_60px)]' : 'md:max-h-[100%]'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
v-if="withDocumentsList"
|
||||||
ref="previews"
|
ref="previews"
|
||||||
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
|
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
|
||||||
class="overflow-y-auto overflow-x-hidden w-52 flex-none pr-3 mt-0.5 pt-0.5 hidden lg:block"
|
class="overflow-y-auto overflow-x-hidden w-52 flex-none pr-3 mt-0.5 pt-0.5 hidden lg:block"
|
||||||
@@ -95,7 +123,7 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="sticky bottom-0 py-2"
|
class="sticky bottom-0 py-2"
|
||||||
:style="withStickySubmitters ? { backgroundColor } : {}"
|
:style="{ backgroundColor }"
|
||||||
>
|
>
|
||||||
<Upload
|
<Upload
|
||||||
v-if="sortedDocuments.length && editable && withUploadButton"
|
v-if="sortedDocuments.length && editable && withUploadButton"
|
||||||
@@ -129,6 +157,9 @@
|
|||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
:document="document"
|
:document="document"
|
||||||
:is-drag="!!dragField"
|
:is-drag="!!dragField"
|
||||||
|
:default-fields="defaultFields"
|
||||||
|
:allow-draw="!onlyDefinedFields"
|
||||||
|
:default-submitters="defaultSubmitters"
|
||||||
:draw-field="drawField"
|
:draw-field="drawField"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:base-url="baseUrl"
|
:base-url="baseUrl"
|
||||||
@@ -160,66 +191,43 @@
|
|||||||
v-if="withUploadButton"
|
v-if="withUploadButton"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:is-direct-upload="isDirectUpload"
|
:is-direct-upload="isDirectUpload"
|
||||||
|
:accept-file-types="acceptFileTypes"
|
||||||
@success="updateFromUpload"
|
@success="updateFromUpload"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<MobileDrawField
|
|
||||||
v-if="drawField && isBreakpointLg"
|
|
||||||
:draw-field="drawField"
|
|
||||||
:fields="template.fields"
|
|
||||||
:submitters="template.submitters"
|
|
||||||
:selected-submitter="selectedSubmitter"
|
|
||||||
class="md:hidden"
|
|
||||||
:editable="editable"
|
|
||||||
@cancel="[drawField = null, drawOption = null]"
|
|
||||||
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
|
|
||||||
/>
|
|
||||||
<FieldType
|
|
||||||
v-if="sortedDocuments.length && !drawField && editable"
|
|
||||||
class="dropdown-top dropdown-end fixed bottom-4 right-4 z-10 md:hidden"
|
|
||||||
:model-value="''"
|
|
||||||
@update:model-value="startFieldDraw($event)"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="btn btn-neutral text-white btn-circle btn-lg group"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<IconPlus
|
|
||||||
class="group-focus:hidden"
|
|
||||||
width="28"
|
|
||||||
height="28"
|
|
||||||
/>
|
|
||||||
<IconX
|
|
||||||
class="hidden group-focus:inline"
|
|
||||||
width="28"
|
|
||||||
height="28"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</FieldType>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
v-if="withFieldsList"
|
||||||
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
|
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
|
||||||
:class="drawField ? 'overflow-hidden' : 'overflow-auto'"
|
:class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="drawField"
|
v-if="drawField"
|
||||||
class="sticky inset-0 h-full"
|
class="sticky inset-0 h-full z-20"
|
||||||
:style="{ backgroundColor }"
|
:style="{ backgroundColor }"
|
||||||
>
|
>
|
||||||
<div class="bg-base-300 rounded-lg p-5 text-center space-y-4">
|
<div class="bg-base-300 rounded-lg p-5 text-center space-y-4">
|
||||||
<p>
|
<p>
|
||||||
Draw {{ drawField.name }} field on the document
|
{{ t('draw_field_on_the_document').replace('{field}', drawField.name) }}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<div>
|
||||||
<button
|
<button
|
||||||
class="base-button"
|
class="base-button"
|
||||||
@click="[drawField = null, drawOption = null]"
|
@click="clearDrawField"
|
||||||
>
|
>
|
||||||
Cancel
|
{{ t('cancel') }}
|
||||||
</button>
|
</button>
|
||||||
</p>
|
<a
|
||||||
|
v-if="!drawOption && !drawField.areas.length && !['stamp', 'signature', 'initials'].includes(drawField.type)"
|
||||||
|
href="#"
|
||||||
|
class="link block mt-3 text-sm"
|
||||||
|
@click.prevent="[drawField = null, drawOption = null]"
|
||||||
|
>
|
||||||
|
{{ t('or_add_field_without_drawing') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -228,8 +236,12 @@
|
|||||||
:fields="template.fields"
|
:fields="template.fields"
|
||||||
:submitters="template.submitters"
|
:submitters="template.submitters"
|
||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
|
:with-help="withHelp"
|
||||||
|
:default-submitters="defaultSubmitters"
|
||||||
:default-fields="defaultFields"
|
:default-fields="defaultFields"
|
||||||
|
:field-types="fieldTypes"
|
||||||
:with-sticky-submitters="withStickySubmitters"
|
:with-sticky-submitters="withStickySubmitters"
|
||||||
|
:only-defined-fields="onlyDefinedFields"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
||||||
@set-drag="dragField = $event"
|
@set-drag="dragField = $event"
|
||||||
@@ -240,6 +252,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sticky bottom-0">
|
||||||
|
<MobileDrawField
|
||||||
|
v-if="drawField && isBreakpointLg"
|
||||||
|
:draw-field="drawField"
|
||||||
|
:fields="template.fields"
|
||||||
|
:submitters="template.submitters"
|
||||||
|
:selected-submitter="selectedSubmitter"
|
||||||
|
class="md:hidden"
|
||||||
|
:editable="editable"
|
||||||
|
@cancel="[drawField = null, drawOption = null]"
|
||||||
|
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
|
||||||
|
/>
|
||||||
|
<MobileFields
|
||||||
|
v-if="sortedDocuments.length && !drawField && editable"
|
||||||
|
:fields="template.fields"
|
||||||
|
:default-fields="defaultFields"
|
||||||
|
:field-types="fieldTypes"
|
||||||
|
:selected-submitter="selectedSubmitter"
|
||||||
|
@select="startFieldDraw($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -253,10 +286,11 @@ import Logo from './logo'
|
|||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import DocumentPreview from './preview'
|
import DocumentPreview from './preview'
|
||||||
import DocumentControls from './controls'
|
import DocumentControls from './controls'
|
||||||
import FieldType from './field_type'
|
import MobileFields from './mobile_fields'
|
||||||
import { IconUsersPlus, IconDeviceFloppy, IconInnerShadowTop, IconPlus, IconX } from '@tabler/icons-vue'
|
import { IconUsersPlus, IconDeviceFloppy, IconWritingSign, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
import { en as i18nEn } from './i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateBuilder',
|
name: 'TemplateBuilder',
|
||||||
@@ -265,9 +299,8 @@ export default {
|
|||||||
Document,
|
Document,
|
||||||
Fields,
|
Fields,
|
||||||
MobileDrawField,
|
MobileDrawField,
|
||||||
IconPlus,
|
IconWritingSign,
|
||||||
FieldType,
|
MobileFields,
|
||||||
IconX,
|
|
||||||
Logo,
|
Logo,
|
||||||
Dropzone,
|
Dropzone,
|
||||||
DocumentPreview,
|
DocumentPreview,
|
||||||
@@ -281,9 +314,13 @@ export default {
|
|||||||
return {
|
return {
|
||||||
template: this.template,
|
template: this.template,
|
||||||
save: this.save,
|
save: this.save,
|
||||||
|
t: this.t,
|
||||||
|
currencies: this.currencies,
|
||||||
baseFetch: this.baseFetch,
|
baseFetch: this.baseFetch,
|
||||||
|
fieldTypes: this.fieldTypes,
|
||||||
backgroundColor: this.backgroundColor,
|
backgroundColor: this.backgroundColor,
|
||||||
withPhone: this.withPhone,
|
withPhone: this.withPhone,
|
||||||
|
withPayment: this.withPayment,
|
||||||
selectedAreaRef: computed(() => this.selectedAreaRef)
|
selectedAreaRef: computed(() => this.selectedAreaRef)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -297,6 +334,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
i18n: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -307,11 +349,31 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withHelp: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
autosave: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
defaultFields: {
|
defaultFields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
currencies: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
fieldTypes: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
defaultSubmitters: {
|
defaultSubmitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -332,6 +394,20 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
onUpload: {
|
||||||
|
type: Function,
|
||||||
|
required: false,
|
||||||
|
default () {
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSave: {
|
||||||
|
type: Function,
|
||||||
|
required: false,
|
||||||
|
default () {
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
withStickySubmitters: {
|
withStickySubmitters: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -347,11 +423,31 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withFieldsList: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withDocumentsList: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withPhone: {
|
withPhone: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withPayment: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
onlyDefinedFields: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -370,8 +466,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
|
||||||
selectedAreaRef: () => ref(),
|
selectedAreaRef: () => ref(),
|
||||||
fieldAreasIndex () {
|
fieldAreasIndex () {
|
||||||
const areas = {}
|
const areas = {}
|
||||||
@@ -398,11 +492,20 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
const existingSubmittersUuids = this.defaultSubmitters.map((name) => {
|
||||||
|
return this.template.submitters.find(e => e.name === name)?.uuid
|
||||||
|
})
|
||||||
|
|
||||||
this.defaultSubmitters.forEach((name, index) => {
|
this.defaultSubmitters.forEach((name, index) => {
|
||||||
const submitter = (this.template.submitters[index] ||= {})
|
const submitter = (this.template.submitters[index] ||= {})
|
||||||
|
|
||||||
submitter.name = name
|
submitter.name = name
|
||||||
submitter.uuid ||= v4()
|
|
||||||
|
if (existingSubmittersUuids.filter(Boolean).length) {
|
||||||
|
submitter.uuid = existingSubmittersUuids[index] || v4()
|
||||||
|
} else {
|
||||||
|
submitter.uuid ||= v4()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.selectedSubmitter = this.template.submitters[0]
|
this.selectedSubmitter = this.template.submitters[0]
|
||||||
@@ -419,6 +522,12 @@ export default {
|
|||||||
window.addEventListener('keydown', this.onKeyDown)
|
window.addEventListener('keydown', this.onKeyDown)
|
||||||
|
|
||||||
window.addEventListener('resize', this.onWindowResize)
|
window.addEventListener('resize', this.onWindowResize)
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (document.location.search?.includes('stripe_connect_success')) {
|
||||||
|
document.querySelector('form[action="/auth/stripe_connect"]')?.closest('.dropdown')?.querySelector('label')?.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted () {
|
||||||
document.removeEventListener('keyup', this.onKeyUp)
|
document.removeEventListener('keyup', this.onKeyUp)
|
||||||
@@ -430,21 +539,41 @@ export default {
|
|||||||
this.documentRefs = []
|
this.documentRefs = []
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
startFieldDraw (type) {
|
t (key) {
|
||||||
const field = {
|
return this.i18n[key] || i18nEn[key] || key
|
||||||
name: '',
|
},
|
||||||
uuid: v4(),
|
startFieldDraw ({ name, type }) {
|
||||||
required: type !== 'checkbox',
|
const existingField = this.template.fields?.find((f) => f.submitter_uuid === this.selectedSubmitter.uuid && name && name === f.name)
|
||||||
areas: [],
|
|
||||||
submitter_uuid: this.selectedSubmitter.uuid,
|
if (existingField) {
|
||||||
type
|
this.drawField = existingField
|
||||||
|
} else {
|
||||||
|
const field = {
|
||||||
|
name: name || '',
|
||||||
|
uuid: v4(),
|
||||||
|
required: type !== 'checkbox',
|
||||||
|
areas: [],
|
||||||
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
|
type
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||||
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'stamp') {
|
||||||
|
field.readonly = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'date') {
|
||||||
|
field.preferences = {
|
||||||
|
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drawField = field
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['select', 'multiple', 'radio'].includes(type)) {
|
|
||||||
field.options = [{ value: '', uuid: v4() }]
|
|
||||||
}
|
|
||||||
|
|
||||||
this.drawField = field
|
|
||||||
this.drawOption = null
|
this.drawOption = null
|
||||||
},
|
},
|
||||||
undo () {
|
undo () {
|
||||||
@@ -492,14 +621,25 @@ export default {
|
|||||||
|
|
||||||
ref.$el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
ref.$el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
},
|
},
|
||||||
|
clearDrawField () {
|
||||||
|
if (this.drawField && !this.drawOption && this.drawField.areas.length === 0) {
|
||||||
|
const fieldIndex = this.template.fields.indexOf(this.drawField)
|
||||||
|
|
||||||
|
if (fieldIndex !== -1) {
|
||||||
|
this.template.fields.splice(fieldIndex, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.drawField = null
|
||||||
|
this.drawOption = null
|
||||||
|
},
|
||||||
onKeyUp (e) {
|
onKeyUp (e) {
|
||||||
if (e.code === 'Escape') {
|
if (e.code === 'Escape') {
|
||||||
this.drawField = null
|
this.clearDrawField()
|
||||||
this.drawOption = null
|
|
||||||
this.selectedAreaRef.value = null
|
this.selectedAreaRef.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['Backspace', 'Delete'].includes(e.key) && this.selectedAreaRef.value && document.activeElement === document.body) {
|
if (this.editable && ['Backspace', 'Delete'].includes(e.key) && this.selectedAreaRef.value && document.activeElement === document.body) {
|
||||||
this.removeArea(this.selectedAreaRef.value)
|
this.removeArea(this.selectedAreaRef.value)
|
||||||
|
|
||||||
this.selectedAreaRef.value = null
|
this.selectedAreaRef.value = null
|
||||||
@@ -552,8 +692,52 @@ export default {
|
|||||||
area.option_uuid = this.drawOption.uuid
|
area.option_uuid = this.drawOption.uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (area.w === 0 || area.h === 0) {
|
||||||
|
const previousArea = this.drawField.areas?.[this.drawField.areas.length - 1]
|
||||||
|
|
||||||
|
if (this.selectedField?.type === this.drawField.type) {
|
||||||
|
area.w = this.selectedAreaRef.value.w
|
||||||
|
area.h = this.selectedAreaRef.value.h
|
||||||
|
} else if (previousArea) {
|
||||||
|
area.w = previousArea.w
|
||||||
|
area.h = previousArea.h
|
||||||
|
} else {
|
||||||
|
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
|
||||||
|
const pageMask = documentRef.pageRefs[area.page].$refs.mask
|
||||||
|
|
||||||
|
if (this.drawField.type === 'checkbox' || this.drawOption) {
|
||||||
|
area.w = pageMask.clientWidth / 30 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 30 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
|
||||||
|
} else if (this.drawField.type === 'image') {
|
||||||
|
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
|
||||||
|
} else if (this.drawField.type === 'signature' || this.drawField.type === 'stamp') {
|
||||||
|
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2
|
||||||
|
} else if (this.drawField.type === 'initials') {
|
||||||
|
area.w = pageMask.clientWidth / 10 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
||||||
|
} else {
|
||||||
|
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
|
||||||
|
area.h = (pageMask.clientWidth / 35 / pageMask.clientWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
area.x -= area.w / 2
|
||||||
|
area.y -= area.h / 2
|
||||||
|
}
|
||||||
|
|
||||||
this.drawField.areas ||= []
|
this.drawField.areas ||= []
|
||||||
this.drawField.areas.push(area)
|
|
||||||
|
const insertBeforeAreaIndex = this.drawField.areas.findIndex((a) => {
|
||||||
|
return a.attachment_uuid === area.attachment_uuid && a.page > area.page
|
||||||
|
})
|
||||||
|
|
||||||
|
if (insertBeforeAreaIndex !== -1) {
|
||||||
|
this.drawField.areas.splice(insertBeforeAreaIndex, 0, area)
|
||||||
|
} else {
|
||||||
|
this.drawField.areas.push(area)
|
||||||
|
}
|
||||||
|
|
||||||
if (this.template.fields.indexOf(this.drawField) === -1) {
|
if (this.template.fields.indexOf(this.drawField) === -1) {
|
||||||
this.template.fields.push(this.drawField)
|
this.template.fields.push(this.drawField)
|
||||||
@@ -569,7 +753,11 @@ export default {
|
|||||||
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
|
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
|
||||||
const pageMask = documentRef.pageRefs[area.page].$refs.mask
|
const pageMask = documentRef.pageRefs[area.page].$refs.mask
|
||||||
|
|
||||||
const type = (pageMask.clientWidth * area.w) < 35 ? 'checkbox' : 'text'
|
let type = (pageMask.clientWidth * area.w) < 35 ? 'checkbox' : 'text'
|
||||||
|
|
||||||
|
if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes(type)) {
|
||||||
|
type = this.fieldTypes[0]
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'checkbox') {
|
if (type === 'checkbox') {
|
||||||
const previousField = [...this.template.fields].reverse().find((f) => f.type === type)
|
const previousField = [...this.template.fields].reverse().find((f) => f.type === type)
|
||||||
@@ -620,6 +808,16 @@ export default {
|
|||||||
field.options = [{ value: '', uuid: v4() }]
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field.type === 'stamp') {
|
||||||
|
field.readonly = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.type === 'date') {
|
||||||
|
field.preferences = {
|
||||||
|
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fieldArea = {
|
const fieldArea = {
|
||||||
x: (area.x - 6) / area.maskW,
|
x: (area.x - 6) / area.maskW,
|
||||||
y: area.y / area.maskH,
|
y: area.y / area.maskH,
|
||||||
@@ -646,7 +844,7 @@ export default {
|
|||||||
w: area.maskW / 5 / area.maskW,
|
w: area.maskW / 5 / area.maskW,
|
||||||
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
|
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
|
||||||
}
|
}
|
||||||
} else if (field.type === 'signature') {
|
} else if (field.type === 'signature' || field.type === 'stamp') {
|
||||||
baseArea = {
|
baseArea = {
|
||||||
w: area.maskW / 5 / area.maskW,
|
w: area.maskW / 5 / area.maskW,
|
||||||
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
|
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
|
||||||
@@ -690,6 +888,14 @@ export default {
|
|||||||
this.scrollIntoDocument(schema[0])
|
this.scrollIntoDocument(schema[0])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (this.template.name === 'New Document') {
|
||||||
|
this.template.name = this.template.schema[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.onUpload) {
|
||||||
|
this.onUpload(this.template)
|
||||||
|
}
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
updateName (value) {
|
updateName (value) {
|
||||||
@@ -698,7 +904,7 @@ export default {
|
|||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
onDocumentRemove (item) {
|
onDocumentRemove (item) {
|
||||||
if (window.confirm('Are you sure?')) {
|
if (window.confirm(this.t('are_you_sure'))) {
|
||||||
this.template.schema.splice(this.template.schema.indexOf(item), 1)
|
this.template.schema.splice(this.template.schema.indexOf(item), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -723,6 +929,10 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (this.onUpload) {
|
||||||
|
this.onUpload(this.template)
|
||||||
|
}
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
moveDocument (item, direction) {
|
moveDocument (item, direction) {
|
||||||
@@ -773,11 +983,17 @@ export default {
|
|||||||
headers: { ...this.fetchOptions.headers, ...options.headers }
|
headers: { ...this.fetchOptions.headers, ...options.headers }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
save () {
|
save ({ force } = { force: false }) {
|
||||||
if (this.$el.closest('template-builder')) {
|
if (!this.autosave && !force) {
|
||||||
this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template)
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$el.closest('template-builder')) {
|
||||||
|
this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.pushUndo()
|
this.pushUndo()
|
||||||
|
|
||||||
return this.baseFetch(`/api/templates/${this.template.id}`, {
|
return this.baseFetch(`/api/templates/${this.template.id}`, {
|
||||||
@@ -791,6 +1007,10 @@ export default {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}).then(() => {
|
||||||
|
if (this.onSave) {
|
||||||
|
this.onSave(this.template)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
ref="contenteditable"
|
ref="contenteditable"
|
||||||
|
dir="auto"
|
||||||
:contenteditable="editable"
|
:contenteditable="editable"
|
||||||
style="min-width: 2px"
|
style="min-width: 2px"
|
||||||
:class="iconInline ? 'inline' : 'block'"
|
:class="iconInline ? 'inline' : 'block'"
|
||||||
@@ -26,7 +27,7 @@
|
|||||||
<IconPencil
|
<IconPencil
|
||||||
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
||||||
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
||||||
title="Edit"
|
:title="t('edit')"
|
||||||
:class="{ invisible: !editable, 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
:class="{ invisible: !editable, 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
||||||
:width="iconWidth"
|
:width="iconWidth"
|
||||||
:stroke-width="iconStrokeWidth"
|
:stroke-width="iconStrokeWidth"
|
||||||
@@ -43,6 +44,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
IconPencil
|
IconPencil
|
||||||
},
|
},
|
||||||
|
inject: ['t'],
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
:number="index"
|
:number="index"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:areas="areasIndex[index]"
|
:areas="areasIndex[index]"
|
||||||
|
:allow-draw="allowDraw"
|
||||||
:is-drag="isDrag"
|
:is-drag="isDrag"
|
||||||
|
:default-fields="defaultFields"
|
||||||
|
:default-submitters="defaultSubmitters"
|
||||||
:draw-field="drawField"
|
:draw-field="drawField"
|
||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
:image="image"
|
:image="image"
|
||||||
@@ -35,6 +38,21 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
defaultFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
defaultSubmitters: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
allowDraw: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
selectedSubmitter: {
|
selectedSubmitter: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
@@ -84,7 +102,7 @@ export default {
|
|||||||
return this.previewImagesIndex[i] || {
|
return this.previewImagesIndex[i] || {
|
||||||
metadata: lazyloadMetadata,
|
metadata: lazyloadMetadata,
|
||||||
id: Math.random().toString(),
|
id: Math.random().toString(),
|
||||||
url: this.basePreviewUrl + `/preview/${this.document.uuid}/${i}.jpg`
|
url: this.basePreviewUrl + `/preview/${this.document.signed_uuid || this.document.uuid}/${i}.jpg`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<span class="font-medium">Click to upload</span> or drag and drop files
|
<span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,7 +61,7 @@ export default {
|
|||||||
IconCloudUpload,
|
IconCloudUpload,
|
||||||
IconInnerShadowTop
|
IconInnerShadowTop
|
||||||
},
|
},
|
||||||
inject: ['baseFetch'],
|
inject: ['baseFetch', 't'],
|
||||||
props: {
|
props: {
|
||||||
templateId: {
|
templateId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
@@ -91,13 +91,13 @@ export default {
|
|||||||
},
|
},
|
||||||
message () {
|
message () {
|
||||||
if (this.isLoading) {
|
if (this.isLoading) {
|
||||||
return 'Uploading...'
|
return this.t('uploading')
|
||||||
} else if (this.isProcessing) {
|
} else if (this.isProcessing) {
|
||||||
return 'Processing...'
|
return this.t('processing_')
|
||||||
} else if (this.acceptFileTypes === 'image/*, application/pdf') {
|
} else if (this.acceptFileTypes === 'image/*, application/pdf') {
|
||||||
return 'Add PDF documents or images'
|
return this.t('add_pdf_documents_or_images')
|
||||||
} else {
|
} else {
|
||||||
return 'Add documents or images'
|
return this.t('add_documents_or_images')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<div class="flex items-center p-1 space-x-1">
|
<div class="flex items-center p-1 space-x-1">
|
||||||
<FieldType
|
<FieldType
|
||||||
v-model="field.type"
|
v-model="field.type"
|
||||||
:editable="editable"
|
:editable="editable && !defaultField"
|
||||||
:button-width="20"
|
:button-width="20"
|
||||||
@update:model-value="[maybeUpdateOptions(), save()]"
|
@update:model-value="[maybeUpdateOptions(), save()]"
|
||||||
@click="scrollToFirstArea"
|
@click="scrollToFirstArea"
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<Contenteditable
|
<Contenteditable
|
||||||
ref="name"
|
ref="name"
|
||||||
:model-value="field.name || defaultName"
|
:model-value="field.name || defaultName"
|
||||||
:editable="editable"
|
:editable="editable && !defaultField"
|
||||||
:icon-inline="true"
|
:icon-inline="true"
|
||||||
:icon-width="18"
|
:icon-width="18"
|
||||||
:icon-stroke-width="1.6"
|
:icon-stroke-width="1.6"
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
class="label text-xs"
|
class="label text-xs"
|
||||||
@click.prevent="field.required = !field.required"
|
@click.prevent="field.required = !field.required"
|
||||||
@mousedown.prevent
|
@mousedown.prevent
|
||||||
>Required</label>
|
>{{ t('required') }}</label>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="field && !field.areas.length"
|
v-if="field && !field.areas.length"
|
||||||
title="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 })"
|
||||||
>
|
>
|
||||||
@@ -65,12 +65,19 @@
|
|||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
<PaymentSettings
|
||||||
|
v-if="field.type === 'payment'"
|
||||||
|
:field="field"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
|
v-else
|
||||||
class="dropdown dropdown-end"
|
class="dropdown dropdown-end"
|
||||||
|
@mouseenter="renderDropdown = true"
|
||||||
|
@touchstart="renderDropdown = true"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
title="Settings"
|
:title="t('settings')"
|
||||||
class="cursor-pointer text-transparent group-hover:text-base-content"
|
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
>
|
>
|
||||||
<IconSettings
|
<IconSettings
|
||||||
@@ -79,6 +86,7 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<ul
|
<ul
|
||||||
|
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 bg-base-100 rounded-box w-52 z-10"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@@ -86,14 +94,15 @@
|
|||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'text'"
|
v-if="field.type === 'text' && !defaultField"
|
||||||
class="py-1.5 px-1 relative"
|
class="py-1.5 px-1 relative"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-model="field.default_value"
|
v-model="field.default_value"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Default value"
|
:placeholder="t('default_value')"
|
||||||
|
dir="auto"
|
||||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
@@ -103,7 +112,71 @@
|
|||||||
class="absolute -top-1 left-2.5 px-1 h-4"
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
style="font-size: 8px"
|
style="font-size: 8px"
|
||||||
>
|
>
|
||||||
Default value
|
{{ 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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<li
|
<li
|
||||||
@@ -117,11 +190,39 @@
|
|||||||
class="toggle toggle-xs"
|
class="toggle toggle-xs"
|
||||||
@update:model-value="save"
|
@update:model-value="save"
|
||||||
>
|
>
|
||||||
<span class="label-text">Required</span>
|
<span class="label-text">{{ t('required') }}</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="field.type === 'text'"
|
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="field.type === 'text' && !defaultField"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -131,7 +232,7 @@
|
|||||||
class="toggle toggle-xs"
|
class="toggle toggle-xs"
|
||||||
@update:model-value="save"
|
@update:model-value="save"
|
||||||
>
|
>
|
||||||
<span class="label-text">Read-only</span>
|
<span class="label-text">{{ t('read_only') }}</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<hr class="pb-0.5 mt-0.5">
|
<hr class="pb-0.5 mt-0.5">
|
||||||
@@ -148,7 +249,7 @@
|
|||||||
:width="20"
|
:width="20"
|
||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
Page {{ area.page + 1 }}
|
{{ t('page') }} {{ area.page + 1 }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
|
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
|
||||||
@@ -161,7 +262,7 @@
|
|||||||
:width="20"
|
:width="20"
|
||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
Draw New Area
|
{{ t('draw_new_area') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells'].includes(field.type)">
|
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells'].includes(field.type)">
|
||||||
@@ -174,14 +275,14 @@
|
|||||||
:width="20"
|
:width="20"
|
||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
Copy to All Pages
|
{{ t('copy_to_all_pages') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
class="relative text-transparent group-hover:text-base-content pr-1"
|
class="relative text-transparent group-hover:text-base-content pr-1"
|
||||||
title="Remove"
|
:title="t('remove')"
|
||||||
@click="$emit('remove', field)"
|
@click="$emit('remove', field)"
|
||||||
>
|
>
|
||||||
<IconTrashX
|
<IconTrashX
|
||||||
@@ -214,11 +315,13 @@
|
|||||||
v-model="option.value"
|
v-model="option.value"
|
||||||
class="w-full input input-primary input-xs text-sm bg-transparent !pr-7 -mr-6"
|
class="w-full input input-primary input-xs text-sm bg-transparent !pr-7 -mr-6"
|
||||||
type="text"
|
type="text"
|
||||||
|
dir="auto"
|
||||||
required
|
required
|
||||||
|
:placeholder="`${t('option')} ${index + 1}`"
|
||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
title="Draw"
|
:title="t('draw')"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@click.prevent="$emit('set-draw', { field, option })"
|
@click.prevent="$emit('set-draw', { field, option })"
|
||||||
>
|
>
|
||||||
@@ -232,8 +335,10 @@
|
|||||||
v-else
|
v-else
|
||||||
v-model="option.value"
|
v-model="option.value"
|
||||||
class="w-full input input-primary input-xs text-sm bg-transparent"
|
class="w-full input input-primary input-xs text-sm bg-transparent"
|
||||||
|
:placeholder="`${t('option')} ${index + 1}`"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
|
dir="auto"
|
||||||
@focus="maybeFocusOnOptionArea(option)"
|
@focus="maybeFocusOnOptionArea(option)"
|
||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
@@ -250,7 +355,7 @@
|
|||||||
class="text-center text-sm w-full pb-1"
|
class="text-center text-sm w-full pb-1"
|
||||||
@click="addOption"
|
@click="addOption"
|
||||||
>
|
>
|
||||||
+ Add option
|
+ {{ t('add_option') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -260,6 +365,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
|
import PaymentSettings from './payment_settings'
|
||||||
import { IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
|
import { IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
@@ -269,17 +375,23 @@ export default {
|
|||||||
Contenteditable,
|
Contenteditable,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconShape,
|
IconShape,
|
||||||
|
PaymentSettings,
|
||||||
IconNewSection,
|
IconNewSection,
|
||||||
IconTrashX,
|
IconTrashX,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
FieldType
|
FieldType
|
||||||
},
|
},
|
||||||
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef'],
|
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
defaultField: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
editable: {
|
editable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -289,30 +401,93 @@ export default {
|
|||||||
emits: ['set-draw', 'remove', 'scroll-to'],
|
emits: ['set-draw', 'remove', 'scroll-to'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isNameFocus: false
|
isNameFocus: false,
|
||||||
|
showPaymentModal: false,
|
||||||
|
renderDropdown: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
|
dateFormats () {
|
||||||
|
return [
|
||||||
|
'MM/DD/YYYY',
|
||||||
|
'DD/MM/YYYY',
|
||||||
|
'YYYY-MM-DD',
|
||||||
|
'DD-MM-YYYY',
|
||||||
|
'DD.MM.YYYY',
|
||||||
|
'MMM D, YYYY',
|
||||||
|
'MMMM D, YYYY',
|
||||||
|
'D MMM YYYY',
|
||||||
|
'D MMMM YYYY'
|
||||||
|
]
|
||||||
|
},
|
||||||
defaultName () {
|
defaultName () {
|
||||||
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
|
if (this.field.type === 'payment' && this.field.preferences?.price) {
|
||||||
|
const { price, currency } = this.field.preferences || {}
|
||||||
|
|
||||||
const suffix = { multiple: 'Select', radio: 'Group' }[this.field.type] || 'Field'
|
const formattedPrice = new Intl.NumberFormat([], {
|
||||||
|
style: 'currency',
|
||||||
|
currency
|
||||||
|
}).format(price)
|
||||||
|
|
||||||
return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}`
|
return `${this.fieldNames[this.field.type]} ${formattedPrice}`
|
||||||
|
} else {
|
||||||
|
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
|
||||||
|
|
||||||
|
const suffix = { multiple: this.t('select'), radio: this.t('group') }[this.field.type] || this.t('field')
|
||||||
|
|
||||||
|
return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}`
|
||||||
|
}
|
||||||
},
|
},
|
||||||
areas () {
|
areas () {
|
||||||
return this.field.areas || []
|
return this.field.areas || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
this.field.preferences ||= {}
|
||||||
|
|
||||||
|
if (this.field.type === 'date') {
|
||||||
|
this.field.preferences.format ||=
|
||||||
|
(Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formatDate (date, format) {
|
||||||
|
const monthFormats = {
|
||||||
|
M: 'numeric',
|
||||||
|
MM: '2-digit',
|
||||||
|
MMM: 'short',
|
||||||
|
MMMM: 'long'
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayFormats = {
|
||||||
|
D: 'numeric',
|
||||||
|
DD: '2-digit'
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearFormats = {
|
||||||
|
YYYY: 'numeric',
|
||||||
|
YY: '2-digit'
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = new Intl.DateTimeFormat([], {
|
||||||
|
day: dayFormats[format.match(/D+/)],
|
||||||
|
month: monthFormats[format.match(/M+/)],
|
||||||
|
year: yearFormats[format.match(/Y+/)]
|
||||||
|
}).formatToParts(date)
|
||||||
|
|
||||||
|
return format
|
||||||
|
.replace(/D+/, parts.find((p) => p.type === 'day').value)
|
||||||
|
.replace(/M+/, parts.find((p) => p.type === 'month').value)
|
||||||
|
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
|
||||||
|
},
|
||||||
copyToAllPages (field) {
|
copyToAllPages (field) {
|
||||||
const areaString = JSON.stringify(field.areas[0])
|
const areaString = JSON.stringify(field.areas[0])
|
||||||
|
|
||||||
this.template.documents.forEach((attachment) => {
|
this.template.documents.forEach((attachment) => {
|
||||||
attachment.preview_images.forEach((page) => {
|
attachment.preview_images.forEach((page) => {
|
||||||
if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === parseInt(page.filename))) {
|
if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === parseInt(page.filename))) {
|
||||||
field.areas.push({ ...JSON.parse(areaString), page: parseInt(page.filename) })
|
field.areas.push({ ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page: parseInt(page.filename) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="mobileView">
|
<div
|
||||||
|
v-if="mobileView"
|
||||||
|
@mouseenter="renderDropdown = true"
|
||||||
|
@touchstart="renderDropdown = true"
|
||||||
|
>
|
||||||
<div class="flex space-x-2 items-end">
|
<div class="flex space-x-2 items-end">
|
||||||
<div class="group/contenteditable-container bg-base-100 rounded-md p-2 border border-base-300 w-full flex justify-between items-end">
|
<div class="group/contenteditable-container bg-base-100 rounded-md p-2 border border-base-300 w-full flex justify-between items-end">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
@@ -29,7 +33,7 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<ul
|
<ul
|
||||||
v-if="editable"
|
v-if="editable && renderDropdown"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="rounded-md min-w-max mb-2"
|
class="rounded-md min-w-max mb-2"
|
||||||
:class="menuClasses"
|
:class="menuClasses"
|
||||||
@@ -57,13 +61,13 @@
|
|||||||
<button
|
<button
|
||||||
v-if="submitters.length > 1 && editable"
|
v-if="submitters.length > 1 && editable"
|
||||||
class="px-2"
|
class="px-2"
|
||||||
@click.stop="remove(submitter)"
|
@click.prevent.stop="remove(submitter)"
|
||||||
>
|
>
|
||||||
<IconTrashX :width="18" />
|
<IconTrashX :width="18" />
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="submitters.length < 10 && editable">
|
<li v-if="submitters.length < names.length && editable">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="flex px-2"
|
class="flex px-2"
|
||||||
@@ -74,7 +78,7 @@
|
|||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
<span class="py-1">
|
<span class="py-1">
|
||||||
Add {{ names[submitters.length] }}
|
{{ t('add') }} {{ names[submitters.length] }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -85,6 +89,8 @@
|
|||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="dropdown"
|
class="dropdown"
|
||||||
|
@mouseenter="renderDropdown = true"
|
||||||
|
@touchstart="renderDropdown = true"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
v-if="compact"
|
v-if="compact"
|
||||||
@@ -99,6 +105,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label
|
<label
|
||||||
v-else
|
v-else
|
||||||
|
ref="label"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="group cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 hover:border-content w-full flex justify-between"
|
class="group cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 hover:border-content w-full flex justify-between"
|
||||||
>
|
>
|
||||||
@@ -118,14 +125,15 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="flex items-center transition-all duration-75 group-hover:border border-base-content/20 border-dashed w-6 h-6 flex justify-center items-center rounded">
|
<span class="flex items-center transition-all duration-75 group-hover:border border-base-content/20 border-dashed w-6 h-6 flex justify-center items-center rounded">
|
||||||
<IconPlus
|
<component
|
||||||
|
:is="editable ? 'IconPlus' : 'IconChevronDown'"
|
||||||
width="18"
|
width="18"
|
||||||
height="18"
|
height="18"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<ul
|
<ul
|
||||||
v-if="editable || !compact"
|
v-if="(editable || !compact) && renderDropdown"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
:class="menuClasses"
|
:class="menuClasses"
|
||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
@@ -137,7 +145,7 @@
|
|||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="flex px-2 group justify-between items-center"
|
class="flex px-2 group justify-between items-center"
|
||||||
:class="{ 'active': submitter === selectedSubmitter }"
|
:class="{ 'active': submitter === selectedSubmitter, 'py-0.5': submitters.length > 8 }"
|
||||||
@click.prevent="selectSubmitter(submitter)"
|
@click.prevent="selectSubmitter(submitter)"
|
||||||
>
|
>
|
||||||
<span class="py-1 flex items-center">
|
<span class="py-1 flex items-center">
|
||||||
@@ -149,16 +157,39 @@
|
|||||||
{{ submitter.name }}
|
{{ submitter.name }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<div
|
||||||
v-if="!compact && submitters.length > 1 && editable"
|
v-if="!compact && submitters.length > 1 && editable"
|
||||||
class="hidden group-hover:block px-2"
|
class="flex"
|
||||||
@click.stop="remove(submitter)"
|
|
||||||
>
|
>
|
||||||
<IconTrashX :width="18" />
|
<div class="flex-col pr-1 hidden group-hover:flex -mt-1 h-0">
|
||||||
</button>
|
<button
|
||||||
|
:title="t('up')"
|
||||||
|
class="relative w-2"
|
||||||
|
style="font-size: 10px; margin-bottom: -4px"
|
||||||
|
@click.prevent.stop="[move(submitter, -1), $refs.label.focus()] "
|
||||||
|
>
|
||||||
|
▲
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:title="t('down')"
|
||||||
|
class="relative w-2"
|
||||||
|
style="font-size: 10px; margin-top: -4px"
|
||||||
|
@click.prevent.stop="[move(submitter, 1), $refs.label.focus()] "
|
||||||
|
>
|
||||||
|
▼
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="!compact && submitters.length > 1 && editable"
|
||||||
|
class="hidden group-hover:block px-2"
|
||||||
|
@click.prevent.stop="remove(submitter)"
|
||||||
|
>
|
||||||
|
<IconTrashX :width="18" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="submitters.length < 10 && editable">
|
<li v-if="submitters.length < names.length && editable && allowAddNew">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="flex px-2"
|
class="flex px-2"
|
||||||
@@ -169,7 +200,7 @@
|
|||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
<span class="py-1">
|
<span class="py-1">
|
||||||
Add {{ names[submitters.length] }}
|
{{ t('add') }} {{ names[submitters.length] }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -178,7 +209,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp } from '@tabler/icons-vue'
|
import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp, IconChevronDown } from '@tabler/icons-vue'
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
@@ -186,11 +217,13 @@ export default {
|
|||||||
name: 'FieldSubmitter',
|
name: 'FieldSubmitter',
|
||||||
components: {
|
components: {
|
||||||
IconUserPlus,
|
IconUserPlus,
|
||||||
|
IconChevronDown,
|
||||||
Contenteditable,
|
Contenteditable,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconTrashX,
|
IconTrashX,
|
||||||
IconChevronUp
|
IconChevronUp
|
||||||
},
|
},
|
||||||
|
inject: ['t', 'save'],
|
||||||
props: {
|
props: {
|
||||||
submitters: {
|
submitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -211,6 +244,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
allowAddNew: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
@@ -222,9 +260,24 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:model-value', 'remove', 'new-submitter', 'name-change'],
|
emits: ['update:model-value', 'remove', 'new-submitter', 'name-change'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
renderDropdown: false
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
colors () {
|
colors () {
|
||||||
return [
|
return [
|
||||||
|
'bg-red-500',
|
||||||
|
'bg-sky-500',
|
||||||
|
'bg-emerald-500',
|
||||||
|
'bg-yellow-300',
|
||||||
|
'bg-purple-600',
|
||||||
|
'bg-pink-500',
|
||||||
|
'bg-cyan-500',
|
||||||
|
'bg-orange-500',
|
||||||
|
'bg-lime-500',
|
||||||
|
'bg-indigo-500',
|
||||||
'bg-red-500',
|
'bg-red-500',
|
||||||
'bg-sky-500',
|
'bg-sky-500',
|
||||||
'bg-emerald-500',
|
'bg-emerald-500',
|
||||||
@@ -239,16 +292,26 @@ export default {
|
|||||||
},
|
},
|
||||||
names () {
|
names () {
|
||||||
return [
|
return [
|
||||||
'First Party',
|
this.t('first_party'),
|
||||||
'Second Party',
|
this.t('second_party'),
|
||||||
'Third Party',
|
this.t('third_party'),
|
||||||
'Fourth Party',
|
this.t('fourth_party'),
|
||||||
'Fifth Party',
|
this.t('fifth_party'),
|
||||||
'Sixth Party',
|
this.t('sixth_party'),
|
||||||
'Seventh Party',
|
this.t('seventh_party'),
|
||||||
'Eighth Party',
|
this.t('eighth_party'),
|
||||||
'Ninth Party',
|
this.t('ninth_party'),
|
||||||
'Tenth Party'
|
this.t('tenth_party'),
|
||||||
|
this.t('eleventh_party'),
|
||||||
|
this.t('twelfth_party'),
|
||||||
|
this.t('thirteenth_party'),
|
||||||
|
this.t('fourteenth_party'),
|
||||||
|
this.t('fifteenth_party'),
|
||||||
|
this.t('sixteenth_party'),
|
||||||
|
this.t('seventeenth_party'),
|
||||||
|
this.t('eighteenth_party'),
|
||||||
|
this.t('nineteenth_party'),
|
||||||
|
this.t('twentieth_party')
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
selectedSubmitter () {
|
selectedSubmitter () {
|
||||||
@@ -260,10 +323,27 @@ export default {
|
|||||||
this.$emit('update:model-value', submitter.uuid)
|
this.$emit('update:model-value', submitter.uuid)
|
||||||
},
|
},
|
||||||
remove (submitter) {
|
remove (submitter) {
|
||||||
if (window.confirm('Are you sure?')) {
|
if (window.confirm(this.t('are_you_sure'))) {
|
||||||
this.$emit('remove', submitter)
|
this.$emit('remove', submitter)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
move (submitter, direction) {
|
||||||
|
const currentIndex = this.submitters.indexOf(submitter)
|
||||||
|
|
||||||
|
this.submitters.splice(currentIndex, 1)
|
||||||
|
|
||||||
|
if (currentIndex + direction > this.submitters.length) {
|
||||||
|
this.submitters.unshift(submitter)
|
||||||
|
} else if (currentIndex + direction < 0) {
|
||||||
|
this.submitters.push(submitter)
|
||||||
|
} else {
|
||||||
|
this.submitters.splice(currentIndex + direction, 0, submitter)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectSubmitter(submitter)
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
addSubmitter () {
|
addSubmitter () {
|
||||||
const newSubmitter = {
|
const newSubmitter = {
|
||||||
name: this.names[this.submitters.length],
|
name: this.names[this.submitters.length],
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<span class="dropdown">
|
<span
|
||||||
|
class="dropdown"
|
||||||
|
@mouseenter="renderDropdown = true"
|
||||||
|
@touchstart="renderDropdown = true"
|
||||||
|
>
|
||||||
<slot>
|
<slot>
|
||||||
<label
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -15,17 +19,17 @@
|
|||||||
</label>
|
</label>
|
||||||
</slot>
|
</slot>
|
||||||
<ul
|
<ul
|
||||||
v-if="editable"
|
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"
|
||||||
:class="menuClasses"
|
:class="menuClasses"
|
||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
v-for="(icon, type) in fieldIcons"
|
v-for="(icon, type) in fieldIconsSorted"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<li v-if="withPhone || type !== 'phone'">
|
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@@ -46,11 +50,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FiledTypeDropdown',
|
name: 'FiledTypeDropdown',
|
||||||
inject: ['withPhone'],
|
inject: ['withPhone', 'withPayment', 't', 'fieldTypes'],
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -78,21 +82,28 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:model-value'],
|
emits: ['update:model-value'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
renderDropdown: false
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames () {
|
fieldNames () {
|
||||||
return {
|
return {
|
||||||
text: 'Text',
|
text: this.t('text'),
|
||||||
signature: 'Signature',
|
signature: this.t('signature'),
|
||||||
initials: 'Initials',
|
initials: this.t('initials'),
|
||||||
date: 'Date',
|
date: this.t('date'),
|
||||||
image: 'Image',
|
image: this.t('image'),
|
||||||
file: 'File',
|
file: this.t('file'),
|
||||||
select: 'Select',
|
select: this.t('select'),
|
||||||
checkbox: 'Checkbox',
|
checkbox: this.t('checkbox'),
|
||||||
multiple: 'Multiple',
|
multiple: this.t('multiple'),
|
||||||
radio: 'Radio',
|
radio: this.t('radio'),
|
||||||
cells: 'Cells',
|
cells: this.t('cells'),
|
||||||
phone: 'Phone'
|
stamp: this.t('stamp'),
|
||||||
|
payment: this.t('payment'),
|
||||||
|
phone: this.t('phone')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fieldIcons () {
|
fieldIcons () {
|
||||||
@@ -103,13 +114,26 @@ export default {
|
|||||||
date: IconCalendarEvent,
|
date: IconCalendarEvent,
|
||||||
image: IconPhoto,
|
image: IconPhoto,
|
||||||
file: IconPaperclip,
|
file: IconPaperclip,
|
||||||
select: IconSelect,
|
|
||||||
checkbox: IconCheckbox,
|
checkbox: IconCheckbox,
|
||||||
radio: IconCircleDot,
|
radio: IconCircleDot,
|
||||||
|
select: IconSelect,
|
||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
|
stamp: IconRubberStamp,
|
||||||
|
payment: IconCreditCard,
|
||||||
phone: IconPhoneCheck
|
phone: IconPhoneCheck
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
fieldIconsSorted () {
|
||||||
|
if (this.fieldTypes.length) {
|
||||||
|
return this.fieldTypes.reduce((acc, type) => {
|
||||||
|
acc[type] = this.fieldIcons[type]
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
} else {
|
||||||
|
return this.fieldIcons
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
class="w-full rounded-lg"
|
class="w-full rounded-lg"
|
||||||
:class="{ 'bg-base-100': withStickySubmitters }"
|
:class="{ 'bg-base-100': withStickySubmitters }"
|
||||||
:submitters="submitters"
|
:submitters="submitters"
|
||||||
:editable="editable"
|
:editable="editable && !defaultSubmitters.length"
|
||||||
@new-submitter="save"
|
@new-submitter="save"
|
||||||
@remove="removeSubmitter"
|
@remove="removeSubmitter"
|
||||||
@name-change="save"
|
@name-change="save"
|
||||||
@@ -13,9 +13,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
ref="fields"
|
||||||
class="mb-1 mt-2"
|
class="mb-1 mt-2"
|
||||||
@dragover.prevent="onFieldDragover"
|
@dragover.prevent="onFieldDragover"
|
||||||
@drop="save"
|
@drop="reorderFields"
|
||||||
>
|
>
|
||||||
<Field
|
<Field
|
||||||
v-for="field in submitterFields"
|
v-for="field in submitterFields"
|
||||||
@@ -23,7 +24,8 @@
|
|||||||
:data-uuid="field.uuid"
|
:data-uuid="field.uuid"
|
||||||
:field="field"
|
:field="field"
|
||||||
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
||||||
:editable="editable && !dragField"
|
:editable="editable && (!dragField || dragField !== field)"
|
||||||
|
:default-field="defaultFields.find((f) => f.name === field.name)"
|
||||||
:draggable="editable"
|
:draggable="editable"
|
||||||
@dragstart="dragField = field"
|
@dragstart="dragField = field"
|
||||||
@dragend="dragField = null"
|
@dragend="dragField = null"
|
||||||
@@ -62,15 +64,15 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="editable"
|
v-if="editable && !onlyDefinedFields"
|
||||||
class="grid grid-cols-3 gap-1 pb-2"
|
class="grid grid-cols-3 gap-1 pb-2"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
v-for="(icon, type) in fieldIcons"
|
v-for="(icon, type) in fieldIconsSorted"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="withPhone || type != 'phone'"
|
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="group flex items-center justify-center border border-dashed border-base-300 hover:border-base-content/20 w-full rounded relative"
|
class="group flex items-center justify-center border border-dashed border-base-300 hover:border-base-content/20 w-full rounded relative"
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
@@ -89,8 +91,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else-if="type == 'phone' && (fieldTypes.length === 0 || fieldTypes.includes(type))"
|
||||||
class="tooltip tooltip-bottom-end flex"
|
class="tooltip tooltip-bottom flex"
|
||||||
|
:class="{'tooltip-bottom-start': !withPayment, 'tooltip-bottom': withPayment }"
|
||||||
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
|
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -117,7 +120,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="fields.length < 4 && editable"
|
v-if="fields.length < 4 && editable && withHelp"
|
||||||
class="text-xs p-2 border border-base-200 rounded"
|
class="text-xs p-2 border border-base-200 rounded"
|
||||||
>
|
>
|
||||||
<ul class="list-disc list-outside ml-3">
|
<ul class="list-disc list-outside ml-3">
|
||||||
@@ -128,7 +131,7 @@
|
|||||||
Drag & drop any other field type on the page
|
Drag & drop any other field type on the page
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Click on the field type above to add it to the form without drawing it on the document
|
Click on the field type above to start drawing it
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,12 +154,17 @@ export default {
|
|||||||
IconDrag,
|
IconDrag,
|
||||||
IconLock
|
IconLock
|
||||||
},
|
},
|
||||||
inject: ['save', 'backgroundColor', 'withPhone'],
|
inject: ['save', 'backgroundColor', 'withPhone', 'withPayment', 't'],
|
||||||
props: {
|
props: {
|
||||||
fields: {
|
fields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
withHelp: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
editable: {
|
editable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -167,11 +175,26 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
onlyDefinedFields: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
defaultSubmitters: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
withStickySubmitters: {
|
withStickySubmitters: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
fieldTypes: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
submitters: {
|
submitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
@@ -190,6 +213,17 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
fieldIconsSorted () {
|
||||||
|
if (this.fieldTypes.length) {
|
||||||
|
return this.fieldTypes.reduce((acc, type) => {
|
||||||
|
acc[type] = this.fieldIcons[type]
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
} else {
|
||||||
|
return this.fieldIcons
|
||||||
|
}
|
||||||
|
},
|
||||||
submitterFields () {
|
submitterFields () {
|
||||||
return this.fields.filter((f) => f.submitter_uuid === this.selectedSubmitter.uuid)
|
return this.fields.filter((f) => f.submitter_uuid === this.selectedSubmitter.uuid)
|
||||||
},
|
},
|
||||||
@@ -204,23 +238,33 @@ export default {
|
|||||||
this.$emit('set-drag', field)
|
this.$emit('set-drag', field)
|
||||||
},
|
},
|
||||||
onFieldDragover (e) {
|
onFieldDragover (e) {
|
||||||
const targetFieldUuid = e.target.closest('[data-uuid]')?.dataset?.uuid
|
const targetField = e.target.closest('[data-uuid]')
|
||||||
|
const dragField = this.$refs.fields.querySelector(`[data-uuid="${this.dragField.uuid}"]`)
|
||||||
|
|
||||||
if (this.dragField && targetFieldUuid && this.dragField.uuid !== targetFieldUuid) {
|
if (dragField && targetField && targetField !== dragField) {
|
||||||
const field = this.fields.find((f) => f.uuid === targetFieldUuid)
|
const fields = Array.from(this.$refs.fields.children)
|
||||||
|
const currentIndex = fields.indexOf(dragField)
|
||||||
const currentIndex = this.fields.indexOf(this.dragField)
|
const targetIndex = fields.indexOf(targetField)
|
||||||
const targetIndex = this.fields.indexOf(field)
|
|
||||||
|
|
||||||
if (currentIndex < targetIndex) {
|
if (currentIndex < targetIndex) {
|
||||||
this.fields.splice(targetIndex + 1, 0, this.dragField)
|
targetField.after(dragField)
|
||||||
this.fields.splice(currentIndex, 1)
|
|
||||||
} else {
|
} else {
|
||||||
this.fields.splice(targetIndex, 0, this.dragField)
|
targetField.before(dragField)
|
||||||
this.fields.splice(currentIndex + 1, 1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
reorderFields () {
|
||||||
|
Array.from(this.$refs.fields.children).forEach((el, index) => {
|
||||||
|
if (el.dataset.uuid !== this.fields[index].uuid) {
|
||||||
|
const field = this.fields.find((f) => f.uuid === el.dataset.uuid)
|
||||||
|
|
||||||
|
this.fields.splice(this.fields.indexOf(field), 1)
|
||||||
|
this.fields.splice(index, 0, field)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
removeSubmitter (submitter) {
|
removeSubmitter (submitter) {
|
||||||
[...this.fields].forEach((field) => {
|
[...this.fields].forEach((field) => {
|
||||||
if (field.submitter_uuid === submitter.uuid) {
|
if (field.submitter_uuid === submitter.uuid) {
|
||||||
@@ -255,9 +299,19 @@ export default {
|
|||||||
field.options = [{ value: '', uuid: v4() }]
|
field.options = [{ value: '', uuid: v4() }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'stamp') {
|
||||||
|
field.readonly = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'date') {
|
||||||
|
field.preferences = {
|
||||||
|
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.fields.push(field)
|
this.fields.push(field)
|
||||||
|
|
||||||
if (['signature', 'initials', 'cells'].includes(type)) {
|
if (!['payment', 'file'].includes(type)) {
|
||||||
this.$emit('set-draw', { field })
|
this.$emit('set-draw', { field })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
const en = {
|
||||||
|
are_you_sure: 'Are you sure?',
|
||||||
|
sign_yourself: 'Sign Yourself',
|
||||||
|
set_signing_date: 'Set signing date',
|
||||||
|
send: 'Send',
|
||||||
|
remove: 'Remove',
|
||||||
|
edit: 'Edit',
|
||||||
|
settings: 'Settings',
|
||||||
|
up: 'Up',
|
||||||
|
down: 'Down',
|
||||||
|
checked: 'Checked',
|
||||||
|
save: 'Save',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
any: 'Any',
|
||||||
|
drawn: 'Drawn',
|
||||||
|
typed: 'Typed',
|
||||||
|
draw_field_on_the_document: 'Draw {field} field on the document',
|
||||||
|
click_to_upload: 'Click to upload',
|
||||||
|
or_drag_and_drop_files: 'or drag and drop files',
|
||||||
|
uploading: 'Uploading',
|
||||||
|
processing_: 'Processing...',
|
||||||
|
add_pdf_documents_or_images: 'Add PDF documents or images',
|
||||||
|
add_documents_or_images: 'Add documents or images',
|
||||||
|
required: 'Required',
|
||||||
|
default_value: 'Default value',
|
||||||
|
format: 'Format',
|
||||||
|
read_only: 'Read-only',
|
||||||
|
page: 'Page',
|
||||||
|
draw_new_area: 'Draw New Area',
|
||||||
|
copy_to_all_pages: 'Copy to All Pages',
|
||||||
|
add_option: 'Add option',
|
||||||
|
option: 'Option',
|
||||||
|
first_party: 'First Party',
|
||||||
|
second_party: 'Second Party',
|
||||||
|
third_party: 'Third Party',
|
||||||
|
fourth_party: 'Fourth Party',
|
||||||
|
fifth_party: 'Fifth Party',
|
||||||
|
sixth_party: 'Sixth Party',
|
||||||
|
seventh_party: 'Seventh Party',
|
||||||
|
eighth_party: 'Eighth Party',
|
||||||
|
ninth_party: 'Ninth Party',
|
||||||
|
tenth_party: 'Tenth Party',
|
||||||
|
eleventh_party: 'Eleventh Party',
|
||||||
|
twelfth_party: 'Twelfth Party',
|
||||||
|
thirteenth_party: 'Thirteenth Party',
|
||||||
|
fourteenth_party: 'Fourteenth Party',
|
||||||
|
fifteenth_party: 'Fifteenth Party',
|
||||||
|
sixteenth_party: 'Sixteenth Party',
|
||||||
|
seventeenth_party: 'Seventeenth Party',
|
||||||
|
eighteenth_party: 'Eighteenth Party',
|
||||||
|
nineteenth_party: 'Nineteenth Party',
|
||||||
|
twentieth_party: 'Twentieth Party',
|
||||||
|
draw: 'Draw',
|
||||||
|
add: 'Add',
|
||||||
|
or_add_field_without_drawing: 'Or add field without drawing',
|
||||||
|
text: 'Text',
|
||||||
|
signature: 'Signature',
|
||||||
|
initials: 'Initials',
|
||||||
|
date: 'Date',
|
||||||
|
image: 'Image',
|
||||||
|
file: 'File',
|
||||||
|
select: 'Select',
|
||||||
|
checkbox: 'Checkbox',
|
||||||
|
multiple: 'Multiple',
|
||||||
|
radio: 'Radio',
|
||||||
|
cells: 'Cells',
|
||||||
|
stamp: 'Stamp',
|
||||||
|
payment: 'Payment',
|
||||||
|
phone: 'Phone',
|
||||||
|
field: 'Field',
|
||||||
|
group: 'Group',
|
||||||
|
draw_a_text_field_on_the_page_with_a_mouse: 'Draw a text field on the page with a mouse',
|
||||||
|
draw_field: 'Draw {field} Field',
|
||||||
|
replace: 'Replace',
|
||||||
|
uploading_: 'Uploading...',
|
||||||
|
add_document: 'Add Document'
|
||||||
|
}
|
||||||
|
|
||||||
|
export { en }
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="fixed text-center w-full bottom-0 pr-6 mb-4">
|
<div class="absolute text-center w-full bottom-0 pr-3 mb-4">
|
||||||
<span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20 md:hidden">
|
<span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20 md:hidden">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<component
|
<component
|
||||||
@@ -9,14 +9,14 @@
|
|||||||
class="inline"
|
class="inline"
|
||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
<span> Draw {{ fieldNames[drawField.type] }} Field </span>
|
<span>{{ t('draw_field').replace('{field}', fieldNames[drawField.type]) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="link block text-center"
|
class="link block text-center"
|
||||||
@click.prevent="$emit('cancel')"
|
@click.prevent="$emit('cancel')"
|
||||||
>
|
>
|
||||||
Cancel
|
{{ t('cancel') }}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<FieldSubmitter
|
<FieldSubmitter
|
||||||
@@ -43,7 +43,7 @@ export default {
|
|||||||
Field,
|
Field,
|
||||||
FieldSubmitter
|
FieldSubmitter
|
||||||
},
|
},
|
||||||
inject: ['save'],
|
inject: ['save', 't'],
|
||||||
props: {
|
props: {
|
||||||
drawField: {
|
drawField: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<span
|
||||||
|
class="dropdown dropdown-top dropdown-end absolute bottom-4 right-4 z-10 md:hidden"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="btn btn-neutral text-white btn-circle btn-lg group"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<IconPlus
|
||||||
|
class="group-focus:hidden"
|
||||||
|
width="28"
|
||||||
|
height="28"
|
||||||
|
/>
|
||||||
|
<IconX
|
||||||
|
class="hidden group-focus:inline"
|
||||||
|
width="28"
|
||||||
|
height="28"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
tabindex="0"
|
||||||
|
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3 mt-1.5 bg-base-100"
|
||||||
|
@click="closeDropdown"
|
||||||
|
>
|
||||||
|
<template v-if="submitterDefaultFields.length">
|
||||||
|
<template
|
||||||
|
v-for="field in submitterDefaultFields"
|
||||||
|
:key="field.name"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-sm py-1 px-2"
|
||||||
|
@click.prevent="$emit('select', { name: field.name || '', type: field.type || 'text' })"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="fieldIcons[field.type || 'text']"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
:width="20"
|
||||||
|
/>
|
||||||
|
{{ field.name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<template
|
||||||
|
v-for="(icon, type) in fieldIcons"
|
||||||
|
:key="type"
|
||||||
|
>
|
||||||
|
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-sm py-1 px-2"
|
||||||
|
:class="{ 'active': type === modelValue }"
|
||||||
|
@click.prevent="$emit('select', { type })"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="icon"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
:width="20"
|
||||||
|
/>
|
||||||
|
{{ fieldNames[type] }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { IconPlus, IconX } from '@tabler/icons-vue'
|
||||||
|
import FieldType from './field_type'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MobileFields',
|
||||||
|
components: {
|
||||||
|
IconPlus,
|
||||||
|
IconX
|
||||||
|
},
|
||||||
|
inject: ['withPhone', 'withPayment', 'backgroundColor', 't'],
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
selectedSubmitter: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
fieldTypes: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
defaultFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['select'],
|
||||||
|
computed: {
|
||||||
|
...FieldType.computed,
|
||||||
|
submitterFields () {
|
||||||
|
return this.fields.filter((f) => f.submitter_uuid === this.selectedSubmitter.uuid)
|
||||||
|
},
|
||||||
|
submitterDefaultFields () {
|
||||||
|
return this.defaultFields.filter((f) => {
|
||||||
|
return (!f.role || f.role === this.selectedSubmitter.name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeDropdown () {
|
||||||
|
document.activeElement.blur()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -23,6 +23,8 @@
|
|||||||
:area="item.area"
|
:area="item.area"
|
||||||
:field="item.field"
|
:field="item.field"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
|
:default-field="defaultFields.find((f) => f.name === item.field.name)"
|
||||||
|
:default-submitters="defaultSubmitters"
|
||||||
@start-resize="resizeDirection = $event"
|
@start-resize="resizeDirection = $event"
|
||||||
@stop-resize="resizeDirection = null"
|
@stop-resize="resizeDirection = null"
|
||||||
@start-drag="isMove = true"
|
@start-drag="isMove = true"
|
||||||
@@ -32,7 +34,7 @@
|
|||||||
<FieldArea
|
<FieldArea
|
||||||
v-if="newArea"
|
v-if="newArea"
|
||||||
:is-draw="true"
|
:is-draw="true"
|
||||||
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || 'text' }"
|
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || defaultFieldType }"
|
||||||
:area="newArea"
|
:area="newArea"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,6 +61,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
FieldArea
|
FieldArea
|
||||||
},
|
},
|
||||||
|
inject: ['fieldTypes'],
|
||||||
props: {
|
props: {
|
||||||
image: {
|
image: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -69,10 +72,25 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
defaultFields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
allowDraw: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
selectedSubmitter: {
|
selectedSubmitter: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
defaultSubmitters: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
drawField: {
|
drawField: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -104,6 +122,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
defaultFieldType () {
|
||||||
|
if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes('text')) {
|
||||||
|
return this.fieldTypes[0]
|
||||||
|
} else {
|
||||||
|
return 'text'
|
||||||
|
}
|
||||||
|
},
|
||||||
isMobile () {
|
isMobile () {
|
||||||
return /android|iphone|ipad/i.test(navigator.userAgent)
|
return /android|iphone|ipad/i.test(navigator.userAgent)
|
||||||
},
|
},
|
||||||
@@ -143,6 +168,10 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onStartDraw (e) {
|
onStartDraw (e) {
|
||||||
|
if (!this.allowDraw) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isMobile && !this.drawField) {
|
if (this.isMobile && !this.drawField) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
<template>
|
||||||
|
<span
|
||||||
|
class="dropdown dropdown-end"
|
||||||
|
:class="{ 'dropdown-open': (!field.preferences?.price || !isConnected) && !isLoading }"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
:title="t('settings')"
|
||||||
|
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
|
>
|
||||||
|
<IconSettings
|
||||||
|
:width="18"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
tabindex="0"
|
||||||
|
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow bg-base-100 rounded-box w-52 z-10"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart.prevent.stop
|
||||||
|
@click="closeDropdown"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
v-model="field.preferences.currency"
|
||||||
|
placeholder="Price"
|
||||||
|
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0"
|
||||||
|
@change="save"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="currency in currenciesList"
|
||||||
|
:key="currency"
|
||||||
|
:value="currency"
|
||||||
|
>
|
||||||
|
{{ currency }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
Currency
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="field.preferences.price"
|
||||||
|
type="number"
|
||||||
|
placeholder="Price"
|
||||||
|
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="field.preferences.price"
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||||
|
style="font-size: 8px"
|
||||||
|
>
|
||||||
|
Price
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!isConnected || isOauthSuccess"
|
||||||
|
class="py-1.5 px-1 relative"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isConnected && isOauthSuccess"
|
||||||
|
class="text-sm text-center"
|
||||||
|
>
|
||||||
|
<IconCircleCheck
|
||||||
|
class="inline text-green-600 w-4 h-4"
|
||||||
|
/>
|
||||||
|
Stripe Connected
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
v-if="!isConnected"
|
||||||
|
data-turbo="false"
|
||||||
|
action="/auth/stripe_connect"
|
||||||
|
accept-charset="UTF-8"
|
||||||
|
target="_blank"
|
||||||
|
method="post"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="state"
|
||||||
|
:value="oauthState"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="redirect_uri"
|
||||||
|
:value="redirectUri"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="scope"
|
||||||
|
value="read_write"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="authenticity_token"
|
||||||
|
:value="authenticityToken"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="btn bg-[#7B73FF] hover:bg-[#0A2540] btn-sm text-white w-full"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="isLoading"
|
||||||
|
class="flex items-center space-x-1"
|
||||||
|
>
|
||||||
|
<IconInnerShadowTop
|
||||||
|
class="w-4 h-4 animate-spin inline"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
Connect Stripe
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="flex items-center space-x-1"
|
||||||
|
>
|
||||||
|
<IconBrandStripe
|
||||||
|
class="w-4 h-4 inline"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
Connect Stripe
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<a
|
||||||
|
v-if="!isConnected"
|
||||||
|
class="block link text-center mt-1"
|
||||||
|
href="https://www.docuseal.co/blog/accept-payments-and-request-signatures-with-ease"
|
||||||
|
target="_blank"
|
||||||
|
data-turbo="false"
|
||||||
|
>Learn more</a>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconSettings, IconCircleCheck, IconBrandStripe, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const isConnected = ref(false)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PaymentSettings',
|
||||||
|
components: {
|
||||||
|
IconSettings,
|
||||||
|
IconCircleCheck,
|
||||||
|
IconInnerShadowTop,
|
||||||
|
IconBrandStripe
|
||||||
|
},
|
||||||
|
inject: ['backgroundColor', 'save', 'currencies', 't'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isConnected: () => isConnected.value,
|
||||||
|
isOauthSuccess () {
|
||||||
|
return document.location.search?.includes('stripe_connect_success')
|
||||||
|
},
|
||||||
|
redirectUri () {
|
||||||
|
return document.location.origin + '/auth/stripe_connect/callback'
|
||||||
|
},
|
||||||
|
defaultCurrencies () {
|
||||||
|
return ['USD', 'EUR', 'GBP']
|
||||||
|
},
|
||||||
|
currenciesList () {
|
||||||
|
return this.currencies.length ? this.currencies : this.defaultCurrencies
|
||||||
|
},
|
||||||
|
authenticityToken () {
|
||||||
|
return document.querySelector('meta[name="csrf-token"]')?.content
|
||||||
|
},
|
||||||
|
oauthState () {
|
||||||
|
const params = new URLSearchParams('')
|
||||||
|
|
||||||
|
params.set('redir', document.location.href)
|
||||||
|
|
||||||
|
return params.toString()
|
||||||
|
},
|
||||||
|
defaultCurrency () {
|
||||||
|
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
|
||||||
|
if (userTimezone.startsWith('Europe')) {
|
||||||
|
return 'EUR'
|
||||||
|
} else if (userTimezone.includes('London') || userTimezone.includes('Belfast')) {
|
||||||
|
return 'GBP'
|
||||||
|
} else {
|
||||||
|
return 'USD'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.field.preferences ||= {}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.field.preferences.currency ||= this.defaultCurrency
|
||||||
|
|
||||||
|
if (!this.isConnected) {
|
||||||
|
this.checkStatus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkStatus () {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
fetch('/api/stripe_connect').then(async (resp) => {
|
||||||
|
const { status } = await resp.json()
|
||||||
|
|
||||||
|
if (status === 'connected') {
|
||||||
|
isConnected.value = true
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
closeDropdown () {
|
||||||
|
document.activeElement.blur()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -67,6 +67,7 @@
|
|||||||
<Contenteditable
|
<Contenteditable
|
||||||
:model-value="item.name"
|
:model-value="item.name"
|
||||||
:icon-width="16"
|
:icon-width="16"
|
||||||
|
:editable="editable"
|
||||||
style="max-width: 95%"
|
style="max-width: 95%"
|
||||||
class="mx-auto"
|
class="mx-auto"
|
||||||
@update:model-value="onUpdateName"
|
@update:model-value="onUpdateName"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import Upload from './upload'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ReplaceDocument',
|
name: 'ReplaceDocument',
|
||||||
inject: ['baseFetch'],
|
inject: ['baseFetch', 't'],
|
||||||
props: {
|
props: {
|
||||||
templateId: {
|
templateId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
@@ -56,11 +56,11 @@ export default {
|
|||||||
},
|
},
|
||||||
message () {
|
message () {
|
||||||
if (this.isLoading) {
|
if (this.isLoading) {
|
||||||
return 'Uploading...'
|
return this.t('uploading_')
|
||||||
} else if (this.isProcessing) {
|
} else if (this.isProcessing) {
|
||||||
return 'Processing...'
|
return this.t('processing_')
|
||||||
} else {
|
} else {
|
||||||
return 'Replace'
|
return this.t('replace')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,13 +15,13 @@
|
|||||||
width="20"
|
width="20"
|
||||||
/>
|
/>
|
||||||
<span v-if="isLoading">
|
<span v-if="isLoading">
|
||||||
Uploading...
|
{{ t('uploading_') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="isProcessing">
|
<span v-else-if="isProcessing">
|
||||||
Processing...
|
{{ t('processing_') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
Add Document
|
{{ t('add_document') }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<form
|
<form
|
||||||
@@ -50,7 +50,7 @@ export default {
|
|||||||
IconUpload,
|
IconUpload,
|
||||||
IconInnerShadowTop
|
IconInnerShadowTop
|
||||||
},
|
},
|
||||||
inject: ['baseFetch'],
|
inject: ['baseFetch', 't'],
|
||||||
props: {
|
props: {
|
||||||
templateId: {
|
templateId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
@@ -123,9 +123,30 @@ export default {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ blobs }),
|
body: JSON.stringify({ blobs }),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
}).then(resp => resp.json()).then((data) => {
|
}).then((resp) => {
|
||||||
this.$emit('success', data)
|
if (resp.ok) {
|
||||||
this.$refs.input.value = ''
|
resp.json().then((data) => {
|
||||||
|
this.$emit('success', data)
|
||||||
|
this.$refs.input.value = ''
|
||||||
|
})
|
||||||
|
} else if (resp.status === 422) {
|
||||||
|
resp.json().then((data) => {
|
||||||
|
if (data.error === 'PDF encrypted') {
|
||||||
|
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ blobs, password: prompt('Enter PDF password') }),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}).then(async (resp) => {
|
||||||
|
if (resp.ok) {
|
||||||
|
this.$emit('success', await resp.json())
|
||||||
|
this.$refs.input.value = ''
|
||||||
|
} else {
|
||||||
|
alert('Wrong password')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
})
|
})
|
||||||
@@ -133,9 +154,33 @@ export default {
|
|||||||
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: new FormData(this.$refs.form)
|
body: new FormData(this.$refs.form)
|
||||||
}).then(resp => resp.json()).then((data) => {
|
}).then((resp) => {
|
||||||
this.$emit('success', data)
|
if (resp.ok) {
|
||||||
this.$refs.input.value = ''
|
resp.json().then((data) => {
|
||||||
|
this.$emit('success', data)
|
||||||
|
this.$refs.input.value = ''
|
||||||
|
})
|
||||||
|
} else if (resp.status === 422) {
|
||||||
|
resp.json().then((data) => {
|
||||||
|
if (data.error === 'PDF encrypted') {
|
||||||
|
const formData = new FormData(this.$refs.form)
|
||||||
|
|
||||||
|
formData.append('password', prompt('Enter PDF password'))
|
||||||
|
|
||||||
|
this.baseFetch(`/api/templates/${this.templateId}/documents`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
}).then(async (resp) => {
|
||||||
|
if (resp.ok) {
|
||||||
|
this.$emit('success', await resp.json())
|
||||||
|
this.$refs.input.value = ''
|
||||||
|
} else {
|
||||||
|
alert('Wrong password')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,21 +22,35 @@ class ProcessSubmitterCompletionJob < ApplicationJob
|
|||||||
end
|
end
|
||||||
|
|
||||||
def enqueue_completed_emails(submitter)
|
def enqueue_completed_emails(submitter)
|
||||||
user = submitter.submission.created_by_user || submitter.template.author
|
submission = submitter.submission
|
||||||
|
|
||||||
if submitter.template.account.users.exists?(id: user.id)
|
user = submission.created_by_user || submitter.template.author
|
||||||
bcc = submitter.submission.template.account.account_configs
|
|
||||||
.find_by(key: AccountConfig::BCC_EMAILS)&.value
|
|
||||||
|
|
||||||
SubmitterMailer.completed_email(submitter, user, bcc:).deliver_later!
|
if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false
|
||||||
|
if submitter.submission.submitters.map(&:email).exclude?(user.email) &&
|
||||||
|
user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false
|
||||||
|
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
||||||
|
end
|
||||||
|
|
||||||
|
bcc = submission.preferences['bcc_completed'].presence ||
|
||||||
|
submission.account.account_configs
|
||||||
|
.find_by(key: AccountConfig::BCC_EMAILS)&.value.presence
|
||||||
|
|
||||||
|
bcc.to_s.scan(User::EMAIL_REGEXP).each do |to|
|
||||||
|
SubmitterMailer.completed_email(submitter, user, to:).deliver_later!
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
|
to = build_to_addresses(submitter)
|
||||||
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
|
|
||||||
|
|
||||||
SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! if to.present?
|
SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! if to.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_to_addresses(submitter)
|
||||||
|
submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
|
||||||
|
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
|
||||||
|
end
|
||||||
|
|
||||||
def enqueue_next_submitter_request_notification(submitter)
|
def enqueue_next_submitter_request_notification(submitter)
|
||||||
next_submitter_item =
|
next_submitter_item =
|
||||||
submitter.submission.template_submitters.find do |e|
|
submitter.submission.template_submitters.find do |e|
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
class SendFormCompletedWebhookRequestJob < ApplicationJob
|
class SendFormCompletedWebhookRequestJob < ApplicationJob
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
def perform(submitter)
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
|
def perform(submitter, params = {})
|
||||||
|
attempt = params[:attempt].to_i
|
||||||
config = Accounts.load_webhook_configs(submitter.submission.account)
|
config = Accounts.load_webhook_configs(submitter.submission.account)
|
||||||
|
|
||||||
return if config.blank? || config.value.blank?
|
return if config.blank? || config.value.blank?
|
||||||
@@ -12,13 +15,22 @@ class SendFormCompletedWebhookRequestJob < ApplicationJob
|
|||||||
|
|
||||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
Faraday.post(config.value,
|
resp = Faraday.post(config.value,
|
||||||
{
|
{
|
||||||
event_type: 'form.completed',
|
event_type: 'form.completed',
|
||||||
timestamp: Time.current,
|
timestamp: Time.current,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'User-Agent' => USER_AGENT)
|
'User-Agent' => USER_AGENT)
|
||||||
|
|
||||||
|
if resp.status.to_i >= 400 && attempt <= MAX_ATTEMPTS &&
|
||||||
|
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||||
|
SendFormCompletedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
||||||
|
.perform_later(submitter, {
|
||||||
|
attempt: attempt + 1,
|
||||||
|
last_status: resp.status.to_i
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -3,20 +3,32 @@
|
|||||||
class SendFormStartedWebhookRequestJob < ApplicationJob
|
class SendFormStartedWebhookRequestJob < ApplicationJob
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
def perform(submitter)
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
|
def perform(submitter, params = {})
|
||||||
|
attempt = params[:attempt].to_i
|
||||||
config = Accounts.load_webhook_configs(submitter.submission.account)
|
config = Accounts.load_webhook_configs(submitter.submission.account)
|
||||||
|
|
||||||
return if config.blank? || config.value.blank?
|
return if config.blank? || config.value.blank?
|
||||||
|
|
||||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
Faraday.post(config.value,
|
resp = Faraday.post(config.value,
|
||||||
{
|
{
|
||||||
event_type: 'form.started',
|
event_type: 'form.started',
|
||||||
timestamp: Time.current,
|
timestamp: Time.current,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'User-Agent' => USER_AGENT)
|
'User-Agent' => USER_AGENT)
|
||||||
|
|
||||||
|
if resp.status.to_i >= 400 && attempt <= MAX_ATTEMPTS &&
|
||||||
|
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||||
|
SendFormStartedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
||||||
|
.perform_later(submitter, {
|
||||||
|
attempt: attempt + 1,
|
||||||
|
last_status: resp.status.to_i
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -3,20 +3,32 @@
|
|||||||
class SendFormViewedWebhookRequestJob < ApplicationJob
|
class SendFormViewedWebhookRequestJob < ApplicationJob
|
||||||
USER_AGENT = 'DocuSeal.co Webhook'
|
USER_AGENT = 'DocuSeal.co Webhook'
|
||||||
|
|
||||||
def perform(submitter)
|
MAX_ATTEMPTS = 10
|
||||||
|
|
||||||
|
def perform(submitter, params = {})
|
||||||
|
attempt = params[:attempt].to_i
|
||||||
config = Accounts.load_webhook_configs(submitter.submission.account)
|
config = Accounts.load_webhook_configs(submitter.submission.account)
|
||||||
|
|
||||||
return if config.blank? || config.value.blank?
|
return if config.blank? || config.value.blank?
|
||||||
|
|
||||||
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
|
||||||
Faraday.post(config.value,
|
resp = Faraday.post(config.value,
|
||||||
{
|
{
|
||||||
event_type: 'form.viewed',
|
event_type: 'form.viewed',
|
||||||
timestamp: Time.current,
|
timestamp: Time.current,
|
||||||
data: Submitters::SerializeForWebhook.call(submitter)
|
data: Submitters::SerializeForWebhook.call(submitter)
|
||||||
}.to_json,
|
}.to_json,
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
'User-Agent' => USER_AGENT)
|
'User-Agent' => USER_AGENT)
|
||||||
|
|
||||||
|
if resp.status.to_i >= 400 && attempt <= MAX_ATTEMPTS &&
|
||||||
|
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||||
|
SendFormViewedWebhookRequestJob.set(wait: (2**attempt).minutes)
|
||||||
|
.perform_later(submitter, {
|
||||||
|
attempt: attempt + 1,
|
||||||
|
last_status: resp.status.to_i
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -4,7 +4,11 @@ class SendSubmitterInvitationEmailJob < ApplicationJob
|
|||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
|
||||||
SubmitterMailer.invitation_email(submitter).deliver_now!
|
mail = SubmitterMailer.invitation_email(submitter)
|
||||||
|
|
||||||
|
Submitters::ValidateSending.call(submitter, mail)
|
||||||
|
|
||||||
|
mail.deliver_now!
|
||||||
|
|
||||||
SubmissionEvent.create!(submitter:, event_type: 'send_email')
|
SubmissionEvent.create!(submitter:, event_type: 'send_email')
|
||||||
|
|
||||||
|
|||||||
@@ -25,13 +25,15 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
DEFAULT_INVITATION_SUBJECT
|
DEFAULT_INVITATION_SUBJECT
|
||||||
end
|
end
|
||||||
|
|
||||||
mail(to: @submitter.friendly_name,
|
mail(
|
||||||
from: from_address_for_submitter(submitter),
|
to: @submitter.friendly_name,
|
||||||
subject:,
|
from: from_address_for_submitter(submitter),
|
||||||
reply_to: (submitter.submission.created_by_user || submitter.template.author)&.friendly_name)
|
subject:,
|
||||||
|
reply_to: (submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed_email(submitter, user, bcc: nil)
|
def completed_email(submitter, user, to: nil)
|
||||||
@current_account = submitter.submission.template.account
|
@current_account = submitter.submission.template.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
@submission = submitter.submission
|
@submission = submitter.submission
|
||||||
@@ -41,7 +43,9 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
|
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
|
||||||
|
|
||||||
add_completed_email_attachments!(submitter)
|
add_completed_email_attachments!(
|
||||||
|
submitter, with_audit_log: @email_config.nil? || @email_config.value['attach_audit_log'] != false
|
||||||
|
)
|
||||||
|
|
||||||
subject =
|
subject =
|
||||||
if @email_config
|
if @email_config
|
||||||
@@ -53,8 +57,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
|
|
||||||
mail(from: from_address_for_submitter(submitter),
|
mail(from: from_address_for_submitter(submitter),
|
||||||
to: user.friendly_name,
|
to: to || (user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name),
|
||||||
bcc:,
|
|
||||||
subject:)
|
subject:)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -64,10 +67,12 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(@submitter)
|
Submissions::EnsureResultGenerated.call(@submitter)
|
||||||
|
|
||||||
@documents = add_completed_email_attachments!(submitter)
|
|
||||||
|
|
||||||
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
||||||
|
|
||||||
|
@documents = add_completed_email_attachments!(
|
||||||
|
submitter, with_audit_log: @email_config.nil? || @email_config.value['attach_audit_log'] != false
|
||||||
|
)
|
||||||
|
|
||||||
subject =
|
subject =
|
||||||
if @email_config
|
if @email_config
|
||||||
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
|
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
|
||||||
@@ -82,19 +87,38 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def add_completed_email_attachments!(submitter)
|
def add_completed_email_attachments!(submitter, with_audit_log: true)
|
||||||
documents = Submitters.select_attachments_for_download(submitter)
|
documents = Submitters.select_attachments_for_download(submitter)
|
||||||
|
|
||||||
total_size = 0
|
total_size = 0
|
||||||
audit_trail_data = nil
|
audit_trail_data = nil
|
||||||
|
|
||||||
if submitter.submission.audit_trail.present?
|
if with_audit_log && submitter.submission.audit_trail.present?
|
||||||
audit_trail_data = submitter.submission.audit_trail.download
|
audit_trail_data = submitter.submission.audit_trail.download
|
||||||
|
|
||||||
total_size = audit_trail_data.size
|
total_size = audit_trail_data.size
|
||||||
end
|
end
|
||||||
|
|
||||||
documents.each do |attachment|
|
total_size = add_attachments_with_size_limit(documents, total_size)
|
||||||
|
|
||||||
|
attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data
|
||||||
|
|
||||||
|
file_fields = submitter.submission.template_fields.select { |e| e['type'].in?(%w[file payment]) }
|
||||||
|
|
||||||
|
if file_fields.pluck('submitter_uuid').uniq.size == 1
|
||||||
|
storage_attachments =
|
||||||
|
submitter.attachments.where(uuid: submitter.values.values_at(*file_fields.pluck('uuid')).flatten)
|
||||||
|
|
||||||
|
add_attachments_with_size_limit(storage_attachments, total_size)
|
||||||
|
end
|
||||||
|
|
||||||
|
documents
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_attachments_with_size_limit(storage_attachments, current_size)
|
||||||
|
total_size = current_size
|
||||||
|
|
||||||
|
storage_attachments.each do |attachment|
|
||||||
total_size += attachment.byte_size
|
total_size += attachment.byte_size
|
||||||
|
|
||||||
break if total_size >= MAX_ATTACHMENTS_SIZE
|
break if total_size >= MAX_ATTACHMENTS_SIZE
|
||||||
@@ -102,12 +126,15 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
attachments[attachment.filename.to_s] = attachment.download
|
attachments[attachment.filename.to_s] = attachment.download
|
||||||
end
|
end
|
||||||
|
|
||||||
attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data
|
total_size
|
||||||
|
|
||||||
documents
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def from_address_for_submitter(submitter)
|
def from_address_for_submitter(submitter)
|
||||||
submitter.submission.created_by_user&.friendly_name || submitter.submission.template.author.friendly_name
|
if submitter.submission.source.in?(%w[api embed]) &&
|
||||||
|
(from_email = AccountConfig.find_by(account: submitter.account, key: 'integration_from_email')&.value.presence)
|
||||||
|
from_email
|
||||||
|
else
|
||||||
|
(submitter.submission.created_by_user || submitter.submission.template.author).friendly_name
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -25,7 +25,7 @@ class AccessToken < ApplicationRecord
|
|||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|
||||||
before_validation :set_sha256, on: :create
|
before_validation :set_sha256
|
||||||
|
|
||||||
attribute :token, :string, default: -> { SecureRandom.base58(TOKEN_LENGTH) }
|
attribute :token, :string, default: -> { SecureRandom.base58(TOKEN_LENGTH) }
|
||||||
|
|
||||||
|
|||||||
@@ -22,12 +22,30 @@ class Account < ApplicationRecord
|
|||||||
class_name: 'TemplateFolder', dependent: :destroy, inverse_of: :account
|
class_name: 'TemplateFolder', dependent: :destroy, inverse_of: :account
|
||||||
has_many :submissions, through: :templates
|
has_many :submissions, through: :templates
|
||||||
has_many :submitters, through: :submissions
|
has_many :submitters, through: :submissions
|
||||||
|
has_many :account_linked_accounts, dependent: :destroy
|
||||||
|
has_many :account_testing_accounts, -> { testing }, dependent: :destroy,
|
||||||
|
class_name: 'AccountLinkedAccount',
|
||||||
|
inverse_of: :account
|
||||||
|
has_one :linked_account_account, dependent: :destroy,
|
||||||
|
foreign_key: :linked_account_id,
|
||||||
|
class_name: 'AccountLinkedAccount',
|
||||||
|
inverse_of: :linked_account
|
||||||
|
has_many :linked_account_accounts, dependent: :destroy,
|
||||||
|
foreign_key: :linked_account_id,
|
||||||
|
class_name: 'AccountLinkedAccount',
|
||||||
|
inverse_of: :linked_account
|
||||||
|
has_many :linked_accounts, through: :account_linked_accounts
|
||||||
|
has_many :testing_accounts, through: :account_testing_accounts, source: :linked_account
|
||||||
has_many :active_users, -> { active }, dependent: :destroy,
|
has_many :active_users, -> { active }, dependent: :destroy,
|
||||||
inverse_of: :account, class_name: 'User'
|
inverse_of: :account, class_name: 'User'
|
||||||
|
|
||||||
attribute :timezone, :string, default: 'UTC'
|
attribute :timezone, :string, default: 'UTC'
|
||||||
attribute :locale, :string, default: 'en-US'
|
attribute :locale, :string, default: 'en-US'
|
||||||
|
|
||||||
|
def testing?
|
||||||
|
linked_account_account&.testing?
|
||||||
|
end
|
||||||
|
|
||||||
def default_template_folder
|
def default_template_folder
|
||||||
super || build_default_template_folder(name: TemplateFolder::DEFAULT_NAME,
|
super || build_default_template_folder(name: TemplateFolder::DEFAULT_NAME,
|
||||||
author_id: users.minimum(:id)).tap(&:save!)
|
author_id: users.minimum(:id)).tap(&:save!)
|
||||||
|
|||||||
@@ -25,8 +25,13 @@ class AccountConfig < ApplicationRecord
|
|||||||
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
|
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
|
||||||
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
||||||
BCC_EMAILS = 'bcc_emails'
|
BCC_EMAILS = 'bcc_emails'
|
||||||
|
FORCE_MFA = 'force_mfa'
|
||||||
|
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
|
||||||
|
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
|
||||||
SUBMITTER_REMAILERS = 'submitter_reminders'
|
SUBMITTER_REMAILERS = 'submitter_reminders'
|
||||||
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
|
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
|
||||||
|
FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
|
||||||
|
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
|
||||||
|
|
||||||
DEFAULT_VALUES = {
|
DEFAULT_VALUES = {
|
||||||
SUBMITTER_INVITATION_EMAIL_KEY => {
|
SUBMITTER_INVITATION_EMAIL_KEY => {
|
||||||
@@ -57,5 +62,5 @@ class AccountConfig < ApplicationRecord
|
|||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
|
||||||
serialize :value, JSON
|
serialize :value, coder: JSON
|
||||||
end
|
end
|
||||||
Loaded 100 of 255 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user