Compare commits

...
134 Commits
Author SHA1 Message Date
Pete Matsyburka e6785b0afd fix sign in form without oauth 2024-01-22 09:32:19 +02:00
Pete Matsyburka 009033a411 fix params validation 2024-01-22 09:32:18 +02:00
Alex Turchyn fdc3d7742a fix recipients switch 2024-01-21 18:07:29 +02:00
Alex Turchyn f9ccca1376 add base API error messages 2024-01-21 15:19:16 +02:00
Alex Turchyn 0a00fe6ab8 fix minor mobile UI issues 2024-01-21 13:36:58 +02:00
Pete Matsyburka 6253e80a70 add testing environment 2024-01-21 11:20:34 +02:00
Pete Matsyburka 3dcb7c813f fix rspec 2024-01-19 12:45:43 +02:00
Pete Matsyburka 39cdc41a67 fix erblint 2024-01-19 12:41:36 +02:00
Pete Matsyburka d3000487f4 prevent builder submitter click # nav 2024-01-19 10:15:21 +02:00
Pete Matsyburka 80254da1c1 adjust consider json middleware 2024-01-19 09:54:49 +02:00
Pete Matsyburka 73fc2d92f7 fix annotations 2024-01-19 01:15:13 +02:00
Pete Matsyburka f69aa33f3d i18n builder confirm 2024-01-18 21:12:08 +02:00
Pete Matsyburka e16202c9f7 ajust 2fa message 2024-01-18 10:33:25 +02:00
Pete Matsyburka 7453d0d274 reduce image size 2024-01-17 22:22:50 +02:00
Pete Matsyburka c102828c93 fix manual send button 2024-01-17 13:52:38 +02:00
Alex Turchyn afeb6dd318 optimize submissions show page for mobile devices 2024-01-17 01:45:44 +02:00
Pete Matsyburka 6156623f0e remove welcome message 2024-01-17 01:45:44 +02:00
Pete Matsyburka ccc06fcc3d allow to remove template permanently 2024-01-17 01:45:40 +02:00
Pete Matsyburka bbe3942056 validate sender email 2024-01-16 23:46:41 +02:00
Pete Matsyburka 49cea59b94 do not authenticate archived users 2024-01-16 00:18:58 +02:00
Pete Matsyburka ce1323e849 sign in-person button 2024-01-15 17:43:27 +02:00
Pete Matsyburka fc3c1e65e3 reduce image size 2024-01-15 02:30:10 +02:00
Pete Matsyburka f1ebd8f912 text utils for RTL 2024-01-15 02:22:53 +02:00
Pete Matsyburka 98f5bba832 update pdf processor 2024-01-15 02:22:53 +02:00
Alex Turchyn e864e8a47f process password protected pdf files 2024-01-15 02:22:40 +02:00
Alex Turchyn a573545ead fix right-to-left text direction for legacy Outlook clients 2024-01-15 00:02:44 +02:00
Pete Matsyburka 15ff4386e5 remove via linkk tab if 2+ parties 2024-01-14 14:09:23 +02:00
Pete Matsyburka 7a4aa3bf87 fix success page 2024-01-14 12:49:54 +02:00
Pete Matsyburka c1ba6288c8 audit log toggle in documents email 2024-01-14 12:38:08 +02:00
Pete Matsyburka d0ba180729 add submitter completed redirect url 2024-01-14 12:11:13 +02:00
Pete Matsyburka 2edc76414b do not show archived submission 2024-01-14 11:11:02 +02:00
Pete Matsyburka 452db5dc39 allow to disable audit log PDF attachment 2024-01-14 01:59:13 +02:00
Pete Matsyburka 21c4c53feb update readme docker compose command 2024-01-14 01:11:16 +02:00
Pete Matsyburka 6292789cac return error if template is blank 2024-01-14 00:49:07 +02:00
Pete Matsyburka d445bbc02d fix accepted builder field types 2024-01-14 00:43:14 +02:00
Pete Matsyburka 8627f58cbc add via link recipients tab 2024-01-14 00:43:13 +02:00
Pete Matsyburka 66ab0059bb accept field title and description 2024-01-13 13:52:58 +02:00
Pete Matsyburka 3f06932277 send bcc_completed as multiple to 2024-01-12 23:53:42 +02:00
Pete Matsyburka 59cf1b435e enable_starttls_auto true when using ENV 2024-01-12 22:24:03 +02:00
Pete Matsyburka c9a5b0e66e add submitter slug variable 2024-01-12 18:22:09 +02:00
Pete Matsyburka bcd5e1da98 fix auth token rotate 2024-01-12 09:45:40 +02:00
Pete Matsyburka f74ad050b4 add field types prop 2024-01-10 23:18:30 +02:00
Pete Matsyburka b6f43c9b23 add onSave builder event prop 2024-01-10 17:54:35 +02:00
Pete Matsyburka 298c64a119 fix RTL reverse 2024-01-10 10:28:06 +02:00
Pete Matsyburka 8b80679b3a add bcc completed submission param 2024-01-10 02:06:31 +02:00
Pete Matsyburka 82a2bc561a remove talk to sales checkbox 2024-01-09 11:31:13 +02:00
Pete Matsyburka e019542d3f fix template 2024-01-09 10:49:51 +02:00
Pete Matsyburka 9356247ef0 add signer user forms translations 2024-01-09 01:32:39 +02:00
Pete Matsyburka 4db77c18e3 add Hebrew form translations 2024-01-08 20:18:57 +02:00
Pete Matsyburka 0d0b248988 use arabic letter connector 2024-01-08 19:32:20 +02:00
Pete Matsyburka 44f3910266 fix build 2024-01-07 18:18:24 +02:00
Pete Matsyburka 35c6798a80 fix RTL form area 2024-01-07 15:09:36 +02:00
Pete Matsyburka 0995c7ee63 adjust RTL regexp 2024-01-07 14:52:10 +02:00
Pete Matsyburka 0a25f78329 touch .version 2024-01-07 14:31:10 +02:00
Pete Matsyburka bb8fffc491 link to releases page 2024-01-07 14:25:38 +02:00
Alex Turchyn 6f6c51fb32 add tooltips to personalize emails 2024-01-07 14:22:43 +02:00
Alex Turchyn 39d14c15d6 add display of app version in UI 2024-01-07 14:20:34 +02:00
Pete Matsyburka 17de28965b add LTR script support 2024-01-07 14:19:21 +02:00
Pete Matsyburka ae7865e508 add reorder parties button 2024-01-06 22:25:13 +02:00
Pete Matsyburka dc8e8cc5c9 draw field on field type click 2024-01-06 21:16:27 +02:00
Pete Matsyburka 5208470c38 make current field area more visible 2024-01-06 19:55:20 +02:00
Pete Matsyburka e7ef7b552f show field area pages list inthe form 2024-01-06 17:21:15 +02:00
Pete Matsyburka a478112b07 fix multiple area fields draw 2024-01-06 16:19:40 +02:00
Pete Matsyburka 7d0f32e1f9 handle multiple checkboxes required 2024-01-06 15:38:03 +02:00
Pete Matsyburka d35d8bead3 support checkbox option without name 2024-01-06 15:05:15 +02:00
Pete Matsyburka ab9cc52bcc add rotate api token button 2024-01-06 12:34:20 +02:00
Pete Matsyburka 7dc342a6f9 make it possible to remove default signature 2024-01-05 22:48:55 +02:00
Pete Matsyburka 80a3ef9193 add submission URL to webhooks serializer 2024-01-05 20:27:38 +02:00
Pete Matsyburka bb723d20cb add builder props 2024-01-05 18:40:45 +02:00
Pete Matsyburka 0c24a2016c use Pro instead of Enterprise 2024-01-04 15:28:46 +02:00
Pete Matsyburka 2124965996 add builder i18n 2024-01-04 01:44:16 +02:00
Pete Matsyburka 6f1e104067 add builder autosave prop 2024-01-04 01:44:11 +02:00
Pete Matsyburka b20f40b5a3 fix webhook data fields order 2024-01-03 20:43:47 +02:00
Pete Matsyburka 1093ca5fc4 add md breakpoint prop to form 2024-01-03 18:19:44 +02:00
Pete Matsyburka 0d99c55d3f disable pdf validate on write 2024-01-02 22:59:34 +02:00
Pete Matsyburka 86dc1a88ed Revert "do not use puma pidfile"
This reverts commit 8961294a29.
2024-01-02 22:39:23 +02:00
Pete Matsyburka 968c20d8ca completed email bcc sending via to address 2024-01-02 11:41:28 +02:00
Pete Matsyburka 1b2d978449 download multiple files on safari ios 2024-01-01 20:44:52 +02:00
Pete Matsyburka 30582b2d75 fix phone/payment fields visibility 2024-01-01 16:32:35 +02:00
Pete Matsyburka 38a8037a4d fix draw area message 2024-01-01 13:18:09 +02:00
Pete Matsyburka 67de45f787 optimize fields reorder 2024-01-01 13:07:25 +02:00
Pete Matsyburka f358f24abb optimize form render with many fields 2023-12-31 17:23:27 +02:00
Pete Matsyburka fd4e15003a adjust create button style 2023-12-31 00:17:18 +02:00
Pete Matsyburka 81df249d5c refactor user roles validate 2023-12-30 22:51:26 +02:00
Pete Matsyburka ba60997992 fix signature update 2023-12-30 15:12:00 +02:00
Pete Matsyburka 8961294a29 do not use puma pidfile 2023-12-30 13:34:22 +02:00
Pete Matsyburka bc2e0a000a adjust phone tooltip position 2023-12-30 11:46:04 +02:00
Pete Matsyburka 1a693fba2d remove payment field type from selfhosted 2023-12-30 11:42:46 +02:00
Pete Matsyburka 05b7acf6aa fix AR encryption 2023-12-30 01:44:05 +02:00
Pete Matsyburka de13d8d397 refactor pdf info creator 2023-12-30 01:19:46 +02:00
Pete Matsyburka 6d7d8ae87c rename deleted_at to archived_at 2023-12-30 00:37:03 +02:00
Pete Matsyburka cfb1bdcd52 back to ruby 3.2.2 2023-12-29 21:52:23 +02:00
Pete Matsyburka 48d41833f1 use Rails.application.secret_key_base 2023-12-29 21:08:10 +02:00
Alex Turchyn 01535f1afc add health-check endpoint 2023-12-29 20:51:19 +02:00
Alex Turchyn fe845bfa36 rails v7.1.2 and ruby to v3.3.0 2023-12-29 20:50:36 +02:00
Pete Matsyburka b94dbc75b7 preserve default values on resubmit 2023-12-29 19:09:43 +02:00
Pete Matsyburka bdf40640d0 add payment learn more button 2023-12-29 01:55:51 +02:00
Pete Matsyburka 496f6c706b render cell only if width specified 2023-12-29 00:05:55 +02:00
Pete Matsyburka 79f97d5555 fix update submitter with API completion 2023-12-28 21:18:23 +02:00
Pete Matsyburka 82f679c84c add error message when template not found 2023-12-28 17:39:09 +02:00
Pete Matsyburka 9b789889fa add stamp translations 2023-12-28 17:38:21 +02:00
Pete Matsyburka dd8dd118da add button ids 2023-12-28 16:59:50 +02:00
Pete Matsyburka 8222a4b6a4 fix template show page 2023-12-28 01:13:47 +02:00
Pete Matsyburka 31f8057bfa fix api pagination 2023-12-27 17:17:35 +02:00
Pete Matsyburka 1d8e0417bd adjust default field area size 2023-12-27 16:44:20 +02:00
Pete Matsyburka 8100ca46c2 show builder default fields on mobile dropdown 2023-12-27 16:36:00 +02:00
Pete Matsyburka 94d745cf1c rescue json parse error 2023-12-27 11:28:41 +02:00
Pete Matsyburka a92bd0ad0f error message on undefined submitter role 2023-12-27 11:05:07 +02:00
Pete Matsyburka 94a13ffca2 do not use devise strategy for api auth 2023-12-26 01:56:00 +02:00
Pete Matsyburka 64c8707957 fix multiple signatures config 2023-12-25 13:16:59 +02:00
Pete Matsyburka d421a73988 fix default submitters order 2023-12-25 13:03:41 +02:00
Pete Matsyburka f7eef1f8d6 clone only active template schema docs 2023-12-24 20:44:01 +02:00
Pete Matsyburka 08c780d309 add status and documents to submission get api 2023-12-23 22:24:57 +02:00
Alex Turchyn baaf1f510f generate unique uuids for cloned templates 2023-12-23 21:30:46 +02:00
Pete Matsyburka 8898dcc054 fix archive submission api 2023-12-23 17:47:58 +02:00
Pete Matsyburka 33521c8aeb draw default area size on click 2023-12-23 12:08:46 +02:00
Pete Matsyburka 9db03bd7b9 fix sso page header 2023-12-23 12:08:46 +02:00
Pete Matsyburka bd600c1fba adjust builder header 2023-12-23 12:08:46 +02:00
Pete Matsyburka 82b18b08f1 set template name from first document 2023-12-23 12:08:46 +02:00
Pete Matsyburka 0aefe616b3 add archived add to submissions 2023-12-23 12:08:45 +02:00
Pete Matsyburka 59a66bbd16 draw stamp when added 2023-12-22 16:39:43 +02:00
Pete Matsyburka 155c708e4f update errors controller 2023-12-22 16:36:15 +02:00
Pete Matsyburka 5502c2ab1b fix phone tooltip placement 2023-12-22 16:32:48 +02:00
Pete Matsyburka e89b42de40 add stamp field type 2023-12-22 13:53:51 +02:00
Pete Matsyburka 2562c4f432 add option to toggle single digital sign 2023-12-21 23:29:06 +02:00
Pete Matsyburka 17323074ca fix images document sign 2023-12-21 23:13:32 +02:00
Pete Matsyburka 8a4c5c5e09 remove users utils module 2023-12-21 17:54:39 +02:00
Pete Matsyburka 3033faff0d fix time format 2023-12-21 10:02:57 +02:00
Pete Matsyburka 0e6aab0e8d add builder section props 2023-12-20 22:13:19 +02:00
Pete Matsyburka 75e3dd839a fix document name editable prop 2023-12-20 21:58:47 +02:00
Pete Matsyburka 830e6eb88d hardcode signature size when using tsa 2023-12-20 21:25:27 +02:00
Pete Matsyburka 3c8fae78d1 use alpine3.18 2023-12-20 00:45:31 +02:00
Pete Matsyburka 441c1d7044 add builder on upload callback 2023-12-19 19:27:07 +02:00
Pete Matsyburka 203f4633f7 return 422 when submit archived submission 2023-12-19 15:51:56 +02:00
187 changed files with 3456 additions and 988 deletions

No files matched your search

+2 -1
View File
@@ -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,
+3
View File
@@ -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:
View File
Whitespace-only changes.
+14 -5
View File
@@ -1,4 +1,12 @@
FROM ruby:3.2.2-alpine as webpack FROM ruby:3.2.2-alpine3.18 as fonts
WORKDIR /fonts
RUN apk --no-cache add fontforge wget ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf . && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansArabic/hinted/ttf/NotoSansArabic-Regular.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
RUN fontforge -lang=py -c 'font1 = fontforge.open("LiberationSans-Regular.ttf"); font2 = fontforge.open("NotoSansArabic-Regular.ttf"); font1.mergeFonts(font2); font1.generate("LiberationSans-Regular.ttf")'
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 +32,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 +53,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/LiberationSans-Regular.ttf /fonts/LiberationSans-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
View File
@@ -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
@@ -25,7 +26,9 @@ 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'
@@ -39,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
+198 -160
View File
@@ -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,30 +236,30 @@ 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)
hexapdf (0.34.1) hexapdf (0.34.1)
cmdparse (~> 3.0, >= 3.0.3) cmdparse (~> 3.0, >= 3.0.3)
@@ -256,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)
@@ -272,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)
@@ -287,49 +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)
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)
oj (3.16.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)
@@ -338,34 +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-proxy (0.7.6) rack-proxy (0.7.7)
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
@@ -380,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)
@@ -400,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)
@@ -419,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)
@@ -428,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)
@@ -436,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)
@@ -445,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)
@@ -466,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)
@@ -488,34 +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)
sqlite3 (1.6.3) sqlite3 (1.7.0)
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)
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)
@@ -525,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
@@ -533,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
@@ -563,8 +598,10 @@ DEPENDENCIES
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
@@ -584,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
@@ -592,4 +630,4 @@ RUBY VERSION
ruby 3.2.2p53 ruby 3.2.2p53
BUNDLED WITH BUNDLED WITH
2.4.10 2.5.3
+1 -1
View File
@@ -73,7 +73,7 @@ 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
@@ -6,7 +6,8 @@ class AccountConfigsController < ApplicationController
ALLOWED_KEYS = [ ALLOWED_KEYS = [
AccountConfig::ALLOW_TYPED_SIGNATURE, AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::FORCE_MFA AccountConfig::FORCE_MFA,
AccountConfig::ESIGNING_PREFERENCE_KEY
].freeze ].freeze
def create def create
+25 -2
View File
@@ -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.error(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.error(e) if defined?(Rollbar)
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_entity
end
end end
private private
@@ -27,12 +39,23 @@ 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
+37 -17
View File
@@ -31,29 +31,48 @@ 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) serialized_submitters = submitters.map do |submitter|
end Submissions::EnsureResultGenerated.call(submitter) if submitter.completed_at?
Submitters::SerializeForApi.call(submitter)
end
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: true)
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
if @template.fields.blank?
Rollbar.error("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)
@@ -67,9 +86,9 @@ module Api
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
@@ -107,7 +126,7 @@ 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
@@ -125,12 +144,13 @@ module Api
params.permit( params.permit(
key => [ key => [
[:send_email, :send_sms, { [:send_email, :send_sms, :bcc_completed, :completed_redirect_url, {
message: %i[subject body], message: %i[subject body],
submitters: [[:send_email, :send_sms, :uuid, :name, :email, :role, submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :completed, :phone, :application_key,
{ values: {}, readonly_fields: [], message: %i[subject body], { values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value readonly validation_pattern invalid_message]] }]] fields: [%i[name default_value title description
readonly validation_pattern invalid_message]] }]]
}] }]
] ]
).fetch(key, []) ).fetch(key, [])
+9 -1
View File
@@ -80,18 +80,26 @@ module Api
def assign_submitter_attrs(submitter, attrs) def assign_submitter_attrs(submitter, attrs)
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email) 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.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
submitter.values = submitter.values.merge(attrs[:values].to_unsafe_h) if attrs[:values] submitter.values = submitter.values.merge(attrs[:values].to_unsafe_h) if attrs[:values].present?
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
submitter.application_key = attrs[:application_key] if attrs.key?(:application_key) submitter.application_key = attrs[:application_key] if attrs.key?(:application_key)
assign_submission_fields(submitter.submission)
assign_preferences(submitter, attrs) assign_preferences(submitter, attrs)
submitter submitter
end end
def assign_submission_fields(submission)
submission.template_fields ||= submission.template.fields
submission.template_schema ||= submission.template.schema
end
def assign_preferences(submitter, attrs) def assign_preferences(submitter, attrs)
submitter_preferences = Submitters.normalize_preferences(submitter.account, current_user, 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') if submitter_preferences.key?('send_email')
submitter.preferences['send_email'] = submitter_preferences['send_email'] submitter.preferences['send_email'] = submitter_preferences['send_email']
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
@@ -7,23 +7,18 @@ module Api
def create def create
authorize!(:manage, @template) authorize!(:manage, @template)
template = current_account.templates.new(source: :api) cloned_template = Templates::Clone.call(@template,
author: current_user,
name: params[:name],
application_key: params[:application_key],
folder_name: params[:folder_name])
template.application_key = params[:application_key] cloned_template.source = :api
template.name = params[:name] || "#{@template.name} (Clone)" cloned_template.save!
template.account = @template.account
template.author = current_user
template.assign_attributes(@template.slice(:folder_id, :fields, :schema, :submitters))
if params[:folder_name].present? Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
end
template.save! render json: cloned_template.as_json(serialize_params)
Templates::CloneAttachments.call(template:, original_template: @template)
render json: template.as_json(serialize_params)
end end
private private
+6 -2
View File
@@ -38,9 +38,13 @@ module Api
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
@@ -22,6 +22,8 @@ module Api
} }
) )
} }
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
+29 -1
View File
@@ -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,6 +16,8 @@ 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
@@ -22,7 +26,7 @@ class ApplicationController < ActionController::Base
rescue_from CanCan::AccessDenied do |e| rescue_from CanCan::AccessDenied do |e|
Rollbar.error(e) if defined?(Rollbar) Rollbar.error(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
@@ -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)
+6 -2
View File
@@ -2,13 +2,17 @@
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
@@ -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
+15 -2
View File
@@ -6,6 +6,7 @@ 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
@@ -13,7 +14,7 @@ class StartFormController < ApplicationController
end end
def update def update
@submitter = Submitter.where(submission: @template.submissions.where(deleted_at: nil)) @submitter = Submitter.where(submission: @template.submissions.where(archived_at: nil))
.order(id: :desc) .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)
@@ -46,13 +47,25 @@ class StartFormController < ApplicationController
private private
def assign_submission_attributes(submitter, template) 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( submitter.assign_attributes(
uuid: template.submitters.first['uuid'], uuid: template.submitters.first['uuid'],
ip: request.remote_ip, ip: request.remote_ip,
ua: request.user_agent, ua: request.user_agent,
preferences: { 'send_email' => true } values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true }
) )
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:, submitter.submission ||= Submission.new(template:,
template_submitters: template.submitters, template_submitters: template.submitters,
source: :link) source: :link)
+1 -1
View File
@@ -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
+10 -1
View File
@@ -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,7 +33,7 @@ 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
@@ -42,6 +43,12 @@ class SubmitFormController < ApplicationController
return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity
end end
if submitter.template.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
@@ -52,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!
@@ -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))
+11 -6
View File
@@ -7,7 +7,7 @@ class TemplatesController < ApplicationController
def show def show
submissions = @template.submissions submissions = @template.submissions
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
@@ -42,10 +42,15 @@ 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.account = current_account
@template.author = current_user
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
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
@@ -65,7 +70,7 @@ class TemplatesController < ApplicationController
'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
@@ -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(current_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
@@ -30,6 +30,7 @@ class TimestampServerController < ApplicationController
reason: 'Test', reason: 'Test',
certificate: pkcs.certificate, certificate: pkcs.certificate,
key: pkcs.key, key: pkcs.key,
signature_size: 10_000,
certificate_chain: pkcs.ca_certs || [], certificate_chain: pkcs.ca_certs || [],
timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url)) timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url))
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
+8 -4
View File
@@ -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)
+3 -1
View File
@@ -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,7 +88,7 @@ window.customElements.define('template-builder', class extends HTMLElement {
backgroundColor: '#faf7f5', backgroundColor: '#faf7f5',
withPhone: this.dataset.withPhone === 'true', withPhone: this.dataset.withPhone === 'true',
withLogo: this.dataset.withLogo !== 'false', withLogo: this.dataset.withLogo !== 'false',
withPayment: this.dataset.withPayment !== 'false', withPayment: this.dataset.withPayment === 'true',
acceptFileTypes: this.dataset.acceptFileTypes, acceptFileTypes: this.dataset.acceptFileTypes,
isDirectUpload: this.dataset.isDirectUpload === 'true' isDirectUpload: this.dataset.isDirectUpload === 'true'
}) })
+4 -4
View File
@@ -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%;
+52 -15
View File
@@ -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
+1
View File
@@ -19,6 +19,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
withTypedSignature: this.dataset.withTypedSignature !== '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>
+52 -16
View File
@@ -1,8 +1,9 @@
<template> <template>
<div <div
class="flex absolute lg:text-base" class="flex absolute lg:text-base"
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-80': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }" :class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed border-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"
@@ -97,11 +108,11 @@
:value="false" :value="false"
class="aspect-square base-radio" class="aspect-square base-radio"
: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 === 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 }"
/> />
@@ -116,11 +127,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 +152,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 +170,7 @@
</template> </template>
<script> <script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard } 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 +178,7 @@ export default {
IconPaperclip, IconPaperclip,
IconCheck IconCheck
}, },
inject: ['t'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -225,6 +238,7 @@ export default {
signature: 'Signature', signature: 'Signature',
date: 'Date', date: 'Date',
image: 'Image', image: 'Image',
stamp: 'Stamp',
initials: 'Initials', initials: 'Initials',
file: 'File', file: 'File',
select: 'Select', select: 'Select',
@@ -235,6 +249,9 @@ export default {
payment: 'Payment' payment: 'Payment'
} }
}, },
option () {
return this.field.options.find((o) => o.uuid === this.area.option_uuid)
},
fieldIcons () { fieldIcons () {
return { return {
text: IconTextSize, text: IconTextSize,
@@ -246,6 +263,7 @@ 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,
@@ -259,6 +277,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]
@@ -321,6 +346,17 @@ 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) { formatDate (date, format) {
const monthFormats = { const monthFormats = {
M: 'numeric', M: 'numeric',
+15 -12
View File
@@ -83,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()
+54 -16
View File
@@ -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"
@@ -179,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
}) })
} }
+5 -2
View File
@@ -1,5 +1,5 @@
<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"
@@ -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: {
+74 -28
View File
@@ -10,6 +10,7 @@
/> />
<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,11 +24,13 @@
<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="Minimize"
@click.prevent="isFormVisible = false" @click.prevent="isFormVisible = false"
@@ -43,7 +46,7 @@
ref="form" ref="form"
:action="submitPath" :action="submitPath"
method="post" method="post"
class="md:mx-16" :class="{ 'md:mx-16': isBreakpointMd }"
@submit.prevent="submitStep" @submit.prevent="submitStep"
> >
<input <input
@@ -62,7 +65,7 @@
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
/> />
</div> </div>
<DateStep <DateStep
@@ -70,12 +73,13 @@
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
: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="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 +88,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=""
@@ -112,6 +118,7 @@
<label <label
v-if="currentField.name" v-if="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>
@@ -119,7 +126,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="space-y-3.5 mx-auto"> <div class="space-y-3.5 mx-auto">
<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 +139,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>
@@ -195,9 +202,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 +221,14 @@
</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)]" @attached="[attachments.push($event), scrollIntoField(currentField)]"
/> />
<SignatureStep <SignatureStep
v-else-if="currentField.type === 'signature'" v-else-if="currentField.type === 'signature'"
@@ -227,7 +242,7 @@
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@start="$refs.areas.scrollIntoField(currentField)" @start="scrollIntoField(currentField)"
@minimize="isFormVisible = false" @minimize="isFormVisible = false"
/> />
<InitialsStep <InitialsStep
@@ -241,8 +256,8 @@
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
: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
@@ -253,7 +268,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'"
@@ -263,7 +278,7 @@
:field="currentField" :field="currentField"
:default-value="submitter.phone" :default-value="submitter.phone"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="submitStep" @submit="submitStep"
/> />
<PaymentStep <PaymentStep
@@ -274,7 +289,7 @@
:field="currentField" :field="currentField"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="submitStep" @submit="submitStep"
/> />
</div> </div>
@@ -283,6 +298,7 @@
class="mt-6 md:mt-8" 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"
@@ -316,25 +332,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" :with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
:with-download-button="withDownloadButton" :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>
@@ -352,6 +375,7 @@ 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 AppearsOn from './appears_on'
import { t } from './i18n' import { t } from './i18n'
export default { export default {
@@ -360,6 +384,7 @@ export default {
FieldAreas, FieldAreas,
ImageStep, ImageStep,
SignatureStep, SignatureStep,
AppearsOn,
AttachmentStep, AttachmentStep,
InitialsStep, InitialsStep,
MultiSelectStep, MultiSelectStep,
@@ -375,6 +400,7 @@ export default {
provide () { provide () {
return { return {
baseUrl: this.baseUrl, baseUrl: this.baseUrl,
scrollIntoArea: this.scrollIntoArea,
t: this.t t: this.t
} }
}, },
@@ -410,6 +436,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
isBreakpointMd: {
type: Boolean,
required: false,
default: true
},
baseUrl: { baseUrl: {
type: String, type: String,
required: false, required: false,
@@ -465,6 +496,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
completedRedirectUrl: {
type: String,
required: false,
default: ''
},
completedButton: { completedButton: {
type: Object, type: Object,
required: false, required: false,
@@ -511,7 +547,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 {
@@ -671,7 +707,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()
@@ -699,6 +735,12 @@ 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
@@ -708,7 +750,7 @@ export default {
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)
@@ -747,9 +789,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 => {
alert(error) console.error(error)
}).finally(() => { }).finally(() => {
this.isSubmitting = false this.isSubmitting = false
}) })
+77 -10
View File
@@ -1,7 +1,10 @@
const en = { const en = {
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',
@@ -45,8 +48,10 @@ const en = {
const es = { const es = {
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',
@@ -90,8 +95,10 @@ const es = {
const it = { const it = {
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',
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',
@@ -135,8 +142,10 @@ const it = {
const de = { const de = {
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',
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',
@@ -180,8 +189,10 @@ const de = {
const fr = { const fr = {
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',
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',
@@ -225,8 +236,10 @@ const fr = {
const pl = { const pl = {
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',
@@ -270,8 +283,10 @@ const pl = {
const uk = { const uk = {
submit_form: 'Надіслати Форму', submit_form: 'Надіслати Форму',
type_here: 'Введіть тут', type_here_: 'Введіть тут',
optional: 'необов’язково', optional: 'необов’язково',
appears_on: "З'являється на",
page: 'Сторінка',
select_your_option: 'Виберіть варіант', select_your_option: 'Виберіть варіант',
complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть', complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть',
submit: 'надіслати', submit: 'надіслати',
@@ -315,8 +330,10 @@ const uk = {
const cs = { const cs = {
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',
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',
@@ -360,8 +377,10 @@ const cs = {
const pt = { const pt = {
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',
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',
@@ -403,7 +422,55 @@ const pt = {
pay_with_strip: 'Pagar com Stripe' pay_with_strip: 'Pagar com Stripe'
} }
const i18n = { en, es, it, de, fr, pl, uk, cs, pt } const he = {
submit_form: 'שלח טופס',
type_here_: 'הקלד כאן',
optional: 'אופציונלי',
option: 'אפשרות',
appears_on: 'מופיע ב',
page: 'עמוד',
select_your_option: 'בחר את האפשרות שלך',
complete_hightlighted_checkboxes_and_click: 'השלם את תיבות הסימון המודגשות ולחץ',
submit: 'שליחה',
next: 'הבא',
click_to_upload: 'לחץ להעלאה',
or_drag_and_drop_files: 'או גרור ושחרר קבצים',
send_copy_via_email: 'שלח עותק באמצעות אימייל',
download: 'הורדה',
signature: 'חתימה',
initials: 'ציוני ראשי תיבות',
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: 'הקלד טקסט',
date: 'תאריך',
email_has_been_sent: 'האימייל נשלח',
processing: 'מעבד',
pay_with_strip: 'שלם עם סטרייפ'
}
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he }
const browserLanguage = (navigator.language || navigator.userLanguage || 'en').split('-')[0] const browserLanguage = (navigator.language || navigator.userLanguage || 'en').split('-')[0]
@@ -1,5 +1,5 @@
<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"
@@ -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
@@ -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"
@@ -103,10 +104,12 @@
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'
export default { export default {
name: 'InitialsStep', name: 'InitialsStep',
components: { components: {
AppearsOn,
IconReload, IconReload,
IconTextSize, IconTextSize,
IconSignature, IconSignature,
@@ -2,12 +2,20 @@
<label <label
v-if="field.name" v-if="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">
<input
v-if="modelValue.length === 0"
type="text"
:name="`values[${field.uuid}][]`"
:value="''"
class="hidden"
>
<div class="space-y-3.5 mx-auto"> <div class="space-y-3.5 mx-auto">
<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 +27,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,6 +44,7 @@
<script> <script>
export default { export default {
name: 'MultiSelectStep', name: 'MultiSelectStep',
inject: ['t'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -62,8 +71,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)))
} }
} }
} }
@@ -1,5 +1,5 @@
<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"
@@ -24,7 +24,7 @@
</span> </span>
<span <span
v-else-if="withTypedSignature" v-else-if="withTypedSignature"
class="tooltip" class="tooltip ml-2"
:data-tip="t('type_text')" :data-tip="t('type_text')"
> >
<a <a
@@ -89,6 +89,7 @@
</a> </a>
</div> </div>
</div> </div>
<AppearsOn :field="field" />
<input <input
:value="modelValue || computedPreviousValue" :value="modelValue || computedPreviousValue"
type="hidden" type="hidden"
@@ -122,12 +123,14 @@
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
export default { export default {
name: 'SignatureStep', name: 'SignatureStep',
components: { components: {
AppearsOn,
IconReload, IconReload,
IconCamera, IconCamera,
IconTextSize, IconTextSize,
+18 -5
View File
@@ -2,26 +2,36 @@
<label <label
v-if="field.name" v-if="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"
dir="auto"
class="base-input !text-2xl w-full !pr-11 -mr-10" class="base-input !text-2xl w-full !pr-11 -mr-10"
: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,8 +41,9 @@
: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"
@@ -56,11 +67,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: {
+3 -2
View File
@@ -61,6 +61,7 @@
v-if="field.type !== 'checkbox' || field.name" v-if="field.type !== 'checkbox' || field.name"
ref="name" ref="name"
:contenteditable="editable && !defaultField" :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,7 +84,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>
</div> </div>
<button <button
v-else-if="editable" v-else-if="editable"
@@ -149,7 +150,7 @@ export default {
FieldSubmitter, FieldSubmitter,
IconX IconX
}, },
inject: ['template', 'selectedAreaRef', 'save'], inject: ['template', 'selectedAreaRef', 'save', 't'],
props: { props: {
area: { area: {
type: Object, type: Object,
+226 -68
View File
@@ -5,7 +5,8 @@
> >
<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" 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 items-center space-x-3"> <div class="flex items-center space-x-3">
@@ -42,11 +43,11 @@
class="inline" class="inline"
/> />
<span class="hidden md:inline"> <span class="hidden md:inline">
Sign Yourself {{ t('sign_yourself') }}
</span> </span>
</a> </a>
<a <a
:href="`/templates/${template.id}/submissions/new`" :href="`/templates/${template.id}/submissions/new?with_link=true`"
data-turbo-frame="modal" data-turbo-frame="modal"
class="white-button md:!px-6" class="white-button md:!px-6"
@click="maybeShowEmptyTemplateAlert" @click="maybeShowEmptyTemplateAlert"
@@ -56,7 +57,7 @@
class="inline" class="inline"
/> />
<span class="hidden md:inline"> <span class="hidden md:inline">
Send {{ t('send') }}
</span> </span>
</a> </a>
<button <button
@@ -75,7 +76,7 @@
width="22" width="22"
/> />
<span class="hidden md:inline"> <span class="hidden md:inline">
Save {{ t('save') }}
</span> </span>
</button> </button>
</template> </template>
@@ -86,6 +87,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"
@@ -110,7 +112,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"
@@ -145,6 +147,7 @@
:document="document" :document="document"
:is-drag="!!dragField" :is-drag="!!dragField"
:default-fields="defaultFields" :default-fields="defaultFields"
:allow-draw="!onlyDefinedFields"
:draw-field="drawField" :draw-field="drawField"
:editable="editable" :editable="editable"
:base-url="baseUrl" :base-url="baseUrl"
@@ -176,6 +179,7 @@
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>
@@ -192,50 +196,45 @@
@cancel="[drawField = null, drawOption = null]" @cancel="[drawField = null, drawOption = null]"
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]" @change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
/> />
<FieldType <MobileFields
v-if="sortedDocuments.length && !drawField && editable" v-if="sortedDocuments.length && !drawField && editable"
class="dropdown-top dropdown-end fixed bottom-4 right-4 z-10 md:hidden" :fields="template.fields"
:model-value="''" :default-fields="defaultFields"
@update:model-value="startFieldDraw($event)" :field-types="fieldTypes"
> :selected-submitter="selectedSubmitter"
<label @select="startFieldDraw($event)"
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>
@@ -244,9 +243,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-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"
@@ -270,10 +272,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, IconWritingSign, 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',
@@ -282,10 +285,8 @@ export default {
Document, Document,
Fields, Fields,
MobileDrawField, MobileDrawField,
IconPlus,
FieldType,
IconX,
IconWritingSign, IconWritingSign,
MobileFields,
Logo, Logo,
Dropzone, Dropzone,
DocumentPreview, DocumentPreview,
@@ -299,7 +300,9 @@ export default {
return { return {
template: this.template, template: this.template,
save: this.save, save: this.save,
t: this.t,
baseFetch: this.baseFetch, baseFetch: this.baseFetch,
fieldTypes: this.fieldTypes,
backgroundColor: this.backgroundColor, backgroundColor: this.backgroundColor,
withPhone: this.withPhone, withPhone: this.withPhone,
withPayment: this.withPayment, withPayment: this.withPayment,
@@ -316,6 +319,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,
@@ -326,11 +334,26 @@ 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: () => []
}, },
fieldTypes: {
type: Array,
required: false,
default: () => []
},
defaultSubmitters: { defaultSubmitters: {
type: Array, type: Array,
required: false, required: false,
@@ -351,6 +374,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,
@@ -366,6 +403,16 @@ 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,
@@ -376,6 +423,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
onlyDefinedFields: {
type: Boolean,
required: false,
default: false
},
fetchOptions: { fetchOptions: {
type: Object, type: Object,
required: false, required: false,
@@ -394,8 +446,6 @@ export default {
} }
}, },
computed: { computed: {
fieldIcons: FieldType.computed.fieldIcons,
fieldNames: FieldType.computed.fieldNames,
selectedAreaRef: () => ref(), selectedAreaRef: () => ref(),
fieldAreasIndex () { fieldAreasIndex () {
const areas = {} const areas = {}
@@ -422,11 +472,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]
@@ -460,27 +519,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,
type
}
if (['select', 'multiple', 'radio'].includes(type)) { if (existingField) {
field.options = [{ value: '', uuid: v4() }] this.drawField = existingField
} } else {
const field = {
if (type === 'date') { name: name || '',
field.preferences = { uuid: v4(),
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY' 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
} }
this.drawField = field
this.drawOption = null this.drawOption = null
}, },
undo () { undo () {
@@ -528,10 +601,21 @@ 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
} }
@@ -588,8 +672,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)
@@ -605,7 +733,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)
@@ -656,6 +788,10 @@ export default {
field.options = [{ value: '', uuid: v4() }] field.options = [{ value: '', uuid: v4() }]
} }
if (field.type === 'stamp') {
field.readonly = true
}
if (field.type === 'date') { if (field.type === 'date') {
field.preferences = { field.preferences = {
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY' format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
@@ -688,7 +824,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
@@ -732,6 +868,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) {
@@ -740,7 +884,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)
} }
@@ -765,6 +909,10 @@ export default {
}) })
}) })
if (this.onUpload) {
this.onUpload(this.template)
}
this.save() this.save()
}, },
moveDocument (item, direction) { moveDocument (item, direction) {
@@ -815,11 +963,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}`, {
@@ -833,6 +987,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'"
@@ -7,6 +7,7 @@
: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-fields="defaultFields"
:draw-field="drawField" :draw-field="drawField"
@@ -41,6 +42,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
allowDraw: {
type: Boolean,
required: false,
default: true
},
selectedSubmitter: { selectedSubmitter: {
type: Object, type: Object,
required: true required: true
+6 -6
View File
@@ -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')
} }
} }
}, },
+27 -18
View File
@@ -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
@@ -70,8 +70,10 @@
:field="field" :field="field"
/> />
<span <span
v-else-if="!defaultField" v-else
class="dropdown dropdown-end" class="dropdown dropdown-end"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
> >
<label <label
tabindex="0" tabindex="0"
@@ -84,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"
@@ -91,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"
> >
@@ -108,7 +112,7 @@
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> </label>
</div> </div>
<div <div
@@ -118,7 +122,7 @@
> >
<select <select
v-model="field.preferences.format" v-model="field.preferences.format"
placeholder="Format" :placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0" class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0"
@change="save" @change="save"
> >
@@ -135,7 +139,7 @@
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"
> >
Format {{ t('format') }}
</label> </label>
</div> </div>
<li <li
@@ -149,11 +153,11 @@
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 === 'text' && !defaultField"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
@@ -163,7 +167,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">
@@ -180,7 +184,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)">
@@ -193,7 +197,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)">
@@ -206,7 +210,7 @@
: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>
@@ -246,7 +250,9 @@
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
@@ -264,8 +270,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"
> >
@@ -282,7 +290,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>
@@ -308,7 +316,7 @@ export default {
IconCopy, IconCopy,
FieldType FieldType
}, },
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef'], inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -329,7 +337,8 @@ export default {
data () { data () {
return { return {
isNameFocus: false, isNameFocus: false,
showPaymentModal: false showPaymentModal: false,
renderDropdown: false
} }
}, },
computed: { computed: {
@@ -360,7 +369,7 @@ export default {
} else { } else {
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field) const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
const suffix = { multiple: 'Select', radio: 'Group' }[this.field.type] || 'Field' 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}` return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}`
} }
@@ -413,7 +422,7 @@ export default {
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,7 +61,7 @@
<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>
@@ -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"
> >
@@ -125,7 +132,7 @@
</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"
@@ -149,13 +156,36 @@
{{ 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="Up"
class="relative w-2"
style="font-size: 10px; margin-bottom: -4px"
@click.prevent.stop="[move(submitter, -1), $refs.label.focus()] "
>
▲
</button>
<button
title="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 < 10 && editable">
@@ -169,7 +199,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>
@@ -191,6 +221,7 @@ export default {
IconTrashX, IconTrashX,
IconChevronUp IconChevronUp
}, },
inject: ['t', 'save'],
props: { props: {
submitters: { submitters: {
type: Array, type: Array,
@@ -222,6 +253,11 @@ 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 [
@@ -239,16 +275,16 @@ 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')
] ]
}, },
selectedSubmitter () { selectedSubmitter () {
@@ -260,10 +296,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],
+32 -21
View File
@@ -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,7 +19,7 @@
</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"
@@ -25,7 +29,7 @@
v-for="(icon, type) in fieldIcons" v-for="(icon, type) in fieldIcons"
:key="type" :key="type"
> >
<li v-if="withPhone || withPayment || !['phone', 'payment'].includes(type)"> <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, IconCreditCard } 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', 'withPayment'], inject: ['withPhone', 'withPayment', 't', 'fieldTypes'],
props: { props: {
modelValue: { modelValue: {
type: String, type: String,
@@ -78,22 +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: 'Payment' payment: this.t('payment'),
phone: this.t('phone')
} }
}, },
fieldIcons () { fieldIcons () {
@@ -104,13 +114,14 @@ 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,
phone: IconPhoneCheck, stamp: IconRubberStamp,
payment: IconCreditCard payment: IconCreditCard,
phone: IconPhoneCheck
} }
} }
}, },
+51 -20
View File
@@ -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,7 @@
: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)" :default-field="defaultFields.find((f) => f.name === field.name)"
:draggable="editable" :draggable="editable"
@dragstart="dragField = field" @dragstart="dragField = field"
@@ -63,7 +64,7 @@
</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
@@ -71,7 +72,7 @@
:key="type" :key="type"
> >
<button <button
v-if="(withPhone || type != 'phone') && (withPayment || type != 'payment')" v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')"
draggable="true" draggable="true"
class="group flex items-center justify-center border border-dashed border-base-300 hover:border-base-content/20 w-full rounded relative" class="group flex items-center justify-center border border-dashed border-base-300 hover:border-base-content/20 w-full rounded relative"
:style="{ backgroundColor: backgroundColor }" :style="{ backgroundColor: backgroundColor }"
@@ -90,8 +91,9 @@
</div> </div>
</button> </button>
<div <div
v-else-if="type == 'phone'" 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
@@ -118,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">
@@ -129,7 +131,7 @@
Drag &amp; drop any other field type on the page Drag &amp; 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>
@@ -152,12 +154,17 @@ export default {
IconDrag, IconDrag,
IconLock IconLock
}, },
inject: ['save', 'backgroundColor', 'withPhone', 'withPayment'], 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,
@@ -168,6 +175,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
onlyDefinedFields: {
type: Boolean,
required: false,
default: true
},
defaultSubmitters: { defaultSubmitters: {
type: Array, type: Array,
required: false, required: false,
@@ -178,6 +190,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
fieldTypes: {
type: Array,
required: false,
default: () => []
},
submitters: { submitters: {
type: Array, type: Array,
required: true required: true
@@ -210,23 +227,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) {
@@ -261,6 +288,10 @@ export default {
field.options = [{ value: '', uuid: v4() }] field.options = [{ value: '', uuid: v4() }]
} }
if (type === 'stamp') {
field.readonly = true
}
if (type === 'date') { if (type === 'date') {
field.preferences = { field.preferences = {
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY' format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
@@ -269,7 +300,7 @@ export default {
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 })
} }
+58
View File
@@ -0,0 +1,58 @@
const en = {
are_you_sure: 'Are you sure?',
sign_yourself: 'Sign Yourself',
send: 'Send',
save: 'Save',
cancel: 'Cancel',
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',
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 }
@@ -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 fixed 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>
+18 -1
View File
@@ -33,7 +33,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>
@@ -60,6 +60,7 @@ export default {
components: { components: {
FieldArea FieldArea
}, },
inject: ['fieldTypes'],
props: { props: {
image: { image: {
type: Object, type: Object,
@@ -75,6 +76,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
allowDraw: {
type: Boolean,
required: false,
default: true
},
selectedSubmitter: { selectedSubmitter: {
type: Object, type: Object,
required: true required: true
@@ -110,6 +116,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)
}, },
@@ -149,6 +162,10 @@ export default {
}) })
}, },
onStartDraw (e) { onStartDraw (e) {
if (!this.allowDraw) {
return
}
if (this.isMobile && !this.drawField) { if (this.isMobile && !this.drawField) {
return return
} }
@@ -141,6 +141,13 @@
</span> </span>
</button> </button>
</form> </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> </div>
</ul> </ul>
</span> </span>
@@ -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"
+4 -4
View File
@@ -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')
} }
} }
}, },
+55 -10
View File
@@ -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
}) })
+18 -7
View File
@@ -22,22 +22,33 @@ 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
user = submission.created_by_user || submitter.template.author
if submitter.template.account.users.exists?(id: user.id) && if submitter.template.account.users.exists?(id: user.id) &&
submitter.submission.preferences['send_email'] != false submission.preferences['send_email'] != false
bcc = submitter.submission.template.account.account_configs SubmitterMailer.completed_email(submitter, user).deliver_later!
.find_by(key: AccountConfig::BCC_EMAILS)&.value
SubmitterMailer.completed_email(submitter, user, bcc:).deliver_later! bcc = submission.preferences['bcc_completed'].presence ||
submission.template.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|
@@ -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')
+11 -8
View File
@@ -33,7 +33,7 @@ class SubmitterMailer < ApplicationMailer
) )
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
@@ -43,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
@@ -55,8 +57,7 @@ class SubmitterMailer < ApplicationMailer
end end
mail(from: from_address_for_submitter(submitter), mail(from: from_address_for_submitter(submitter),
to: user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name, to: to || (user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name),
bcc:,
subject:) subject:)
end end
@@ -66,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:)
@@ -84,13 +87,13 @@ 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
+1 -1
View File
@@ -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) }
+14
View File
@@ -22,12 +22,26 @@ 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_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!)
+2 -1
View File
@@ -29,6 +29,7 @@ class AccountConfig < ApplicationRecord
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature' ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
SUBMITTER_REMAILERS = 'submitter_reminders' SUBMITTER_REMAILERS = 'submitter_reminders'
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button' FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
DEFAULT_VALUES = { DEFAULT_VALUES = {
SUBMITTER_INVITATION_EMAIL_KEY => { SUBMITTER_INVITATION_EMAIL_KEY => {
@@ -59,5 +60,5 @@ class AccountConfig < ApplicationRecord
belongs_to :account belongs_to :account
serialize :value, JSON serialize :value, coder: JSON
end end
+36
View File
@@ -0,0 +1,36 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: account_linked_accounts
#
# id :bigint not null, primary key
# account_type :text not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# linked_account_id :bigint not null
#
# Indexes
#
# idx_on_account_id_linked_account_id_48ab9f79d2 (account_id,linked_account_id) UNIQUE
# index_account_linked_accounts_on_account_id (account_id)
# index_account_linked_accounts_on_linked_account_id (linked_account_id)
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# fk_rails_... (linked_account_id => accounts.id)
#
class AccountLinkedAccount < ApplicationRecord
belongs_to :account
belongs_to :linked_account, class_name: 'Account'
attribute :account_type, :string, default: 'testing'
scope :testing, -> { where(account_type: :testing) }
def testing?
account_type == 'testing'
end
end
+1 -1
View File
@@ -34,5 +34,5 @@ class EncryptedConfig < ApplicationRecord
encrypts :value encrypts :value
serialize :value, JSON serialize :value, coder: JSON
end end
+1 -1
View File
@@ -25,5 +25,5 @@ class EncryptedUserConfig < ApplicationRecord
encrypts :value encrypts :value
serialize :value, JSON serialize :value, coder: JSON
end end
+6 -6
View File
@@ -5,7 +5,7 @@
# Table name: submissions # Table name: submissions
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# deleted_at :datetime # archived_at :datetime
# preferences :text not null # preferences :text not null
# slug :string not null # slug :string not null
# source :text not null # source :text not null
@@ -39,10 +39,10 @@ class Submission < ApplicationRecord
attribute :preferences, :string, default: -> { {} } attribute :preferences, :string, default: -> { {} }
serialize :template_fields, JSON serialize :template_fields, coder: JSON
serialize :template_schema, JSON serialize :template_schema, coder: JSON
serialize :template_submitters, JSON serialize :template_submitters, coder: JSON
serialize :preferences, JSON serialize :preferences, coder: JSON
attribute :source, :string, default: 'link' attribute :source, :string, default: 'link'
attribute :submitters_order, :string, default: 'random' attribute :submitters_order, :string, default: 'random'
@@ -55,7 +55,7 @@ class Submission < ApplicationRecord
->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) }, ->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) },
through: :template, source: :documents_attachments through: :template, source: :documents_attachments
scope :active, -> { where(deleted_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).distinct } scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).distinct }
scope :completed, -> { where.not(id: pending.select(:submission_id)) } scope :completed, -> { where.not(id: pending.select(:submission_id)) }
+1 -1
View File
@@ -31,7 +31,7 @@ class SubmissionEvent < ApplicationRecord
attribute :data, :string, default: -> { {} } attribute :data, :string, default: -> { {} }
attribute :event_timestamp, :datetime, default: -> { Time.current } attribute :event_timestamp, :datetime, default: -> { Time.current }
serialize :data, JSON serialize :data, coder: JSON
before_validation :set_submission_id, on: :create before_validation :set_submission_id, on: :create
+2 -2
View File
@@ -41,8 +41,8 @@ class Submitter < ApplicationRecord
attribute :preferences, :string, default: -> { {} } attribute :preferences, :string, default: -> { {} }
attribute :slug, :string, default: -> { SecureRandom.base58(14) } attribute :slug, :string, default: -> { SecureRandom.base58(14) }
serialize :values, JSON serialize :values, coder: JSON
serialize :preferences, JSON serialize :preferences, coder: JSON
has_many_attached :documents has_many_attached :documents
has_many_attached :attachments has_many_attached :attachments
+6 -6
View File
@@ -6,7 +6,7 @@
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# application_key :string # application_key :string
# deleted_at :datetime # archived_at :datetime
# fields :text not null # fields :text not null
# name :string not null # name :string not null
# schema :text not null # schema :text not null
@@ -47,9 +47,9 @@ class Template < ApplicationRecord
attribute :slug, :string, default: -> { SecureRandom.base58(14) } attribute :slug, :string, default: -> { SecureRandom.base58(14) }
attribute :source, :string, default: 'native' attribute :source, :string, default: 'native'
serialize :fields, JSON serialize :fields, coder: JSON
serialize :schema, JSON serialize :schema, coder: JSON
serialize :submitters, JSON serialize :submitters, coder: JSON
has_many_attached :documents has_many_attached :documents
@@ -58,8 +58,8 @@ class Template < ApplicationRecord
has_many :submissions, dependent: :destroy has_many :submissions, dependent: :destroy
scope :active, -> { where(deleted_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(deleted_at: nil) } scope :archived, -> { where.not(archived_at: nil) }
private private
+9 -9
View File
@@ -4,13 +4,13 @@
# #
# Table name: template_folders # Table name: template_folders
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# deleted_at :datetime # archived_at :datetime
# name :string not null # name :string not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# account_id :bigint not null # account_id :bigint not null
# author_id :bigint not null # author_id :bigint not null
# #
# Indexes # Indexes
# #
@@ -29,10 +29,10 @@ class TemplateFolder < ApplicationRecord
belongs_to :account belongs_to :account
has_many :templates, dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder has_many :templates, dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder
has_many :active_templates, -> { where(deleted_at: nil) }, has_many :active_templates, -> { where(archived_at: nil) },
class_name: 'Template', dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder class_name: 'Template', dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder
scope :active, -> { where(deleted_at: nil) } scope :active, -> { where(archived_at: nil) }
def default? def default?
name == DEFAULT_NAME name == DEFAULT_NAME
+6 -4
View File
@@ -5,10 +5,10 @@
# Table name: users # Table name: users
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# archived_at :datetime
# consumed_timestep :integer # consumed_timestep :integer
# current_sign_in_at :datetime # current_sign_in_at :datetime
# current_sign_in_ip :string # current_sign_in_ip :string
# deleted_at :datetime
# email :string not null # email :string not null
# encrypted_password :string not null # encrypted_password :string not null
# failed_attempts :integer default(0), not null # failed_attempts :integer default(0), not null
@@ -49,6 +49,8 @@ class User < ApplicationRecord
EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/ EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/
has_one_attached :signature
belongs_to :account belongs_to :account
has_one :access_token, dependent: :destroy has_one :access_token, dependent: :destroy
has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
@@ -57,12 +59,12 @@ class User < ApplicationRecord
has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig' has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig'
has_many :email_messages, dependent: :destroy, foreign_key: :author_id, inverse_of: :author has_many :email_messages, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable, :lockable
attribute :role, :string, default: ADMIN_ROLE attribute :role, :string, default: ADMIN_ROLE
attribute :uuid, :string, default: -> { SecureRandom.uuid } attribute :uuid, :string, default: -> { SecureRandom.uuid }
scope :active, -> { where(deleted_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :admins, -> { where(role: ADMIN_ROLE) } scope :admins, -> { where(role: ADMIN_ROLE) }
def access_token def access_token
@@ -70,7 +72,7 @@ class User < ApplicationRecord
end end
def active_for_authentication? def active_for_authentication?
!deleted_at? super && !archived_at?
end end
def remember_me def remember_me
+1 -1
View File
@@ -25,5 +25,5 @@ class UserConfig < ApplicationRecord
belongs_to :user belongs_to :user
serialize :value, JSON serialize :value, coder: JSON
end end
+2 -2
View File
@@ -6,7 +6,7 @@
<%= f.fields_for current_account do |ff| %> <%= f.fields_for current_account do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :name, 'Company Name', class: 'label' %> <%= ff.label :name, 'Company Name', class: 'label' %>
<%= ff.text_field :name, required: true, class: 'base-input' %> <%= ff.text_field :name, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<div class="grid md:grid-cols-2 gap-4"> <div class="grid md:grid-cols-2 gap-4">
<div class="form-control"> <div class="form-control">
@@ -22,7 +22,7 @@
</div> </div>
<% end %> <% end %>
<% encrypted_config = @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) %> <% encrypted_config = @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) %>
<% if !Docuseal.multitenant? && can?(:manage, encrypted_config) %> <% if !Docuseal.multitenant? && can?(:manage, encrypted_config) && !current_account.testing? %>
<%= f.fields_for encrypted_config do |ff| %> <%= f.fields_for encrypted_config do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :value, 'App URL', class: 'label' %> <%= ff.label :value, 'App URL', class: 'label' %>
+10 -4
View File
@@ -1,13 +1,19 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow"> <div class="flex-grow">
<h1 class="text-4xl font-bold mb-4">API</h1> <div class="flex justify-between">
<h1 class="text-4xl font-bold mb-4">API</h1>
<%= render 'shared/test_mode_toggle' %>
</div>
<div class="card bg-base-200"> <div class="card bg-base-200">
<div class="card-body p-6"> <div class="card-body p-6">
<label for="api_key" class="text-sm font-semibold">X-Auth-Token</label> <label for="api_key" class="text-sm font-semibold">X-Auth-Token</label>
<div class="flex flex-row space-x-4"> <div class="flex flex-col md:flex-row gap-4">
<input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly> <div class="flex w-full space-x-4">
<%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %> <input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
<%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</div>
<%= button_to button_title(title: 'Rotate', disabled_with: 'Rotate', icon: svg_icon('reload', class: 'w-6 h-6')), settings_api_index_path, class: 'white-button w-full', data: { turbo_confirm: 'Remove existing API token and generated a new one. Are you sure?' } %>
</div> </div>
</div> </div>
</div> </div>
+69 -89
View File
@@ -1,100 +1,80 @@
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %> <% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
<% if @pagy.count > 0 || params[:q].present? || @template_folders.present? %> <div class="flex justify-between mb-4 items-center">
<div class="flex justify-between mb-4 items-center"> <h1 class="text-4xl font-bold"><span class="hidden md:inline">Document</span> Templates</h1>
<h1 class="text-4xl font-bold"><span class="hidden md:inline">Document</span> Templates</h1> <div class="flex space-x-2">
<div class="flex space-x-2"> <% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %>
<% if params[:q].present? || @pagy.pages > 1 || @template_folders.present? %> <%= render 'shared/search_input' %>
<%= render 'shared/search_input' %> <% end %>
<% if can?(:create, ::Template) %>
<%= render 'templates/upload_button' %>
<%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="hidden md:block">Create</span>
<% end %> <% end %>
<% if can?(:create, ::Template) %> <% end %>
<%= render 'templates/upload_button' %>
<%= link_to new_template_path, class: 'white-button !border gap-2', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="hidden md:block">Create</span>
<% end %>
<% end %>
</div>
</div> </div>
<% view_archived_html = capture do %> </div>
<% if current_account.templates.where.not(deleted_at: nil).exists? %> <% view_archived_html = capture do %>
<div> <% if current_account.templates.where.not(archived_at: nil).exists? %>
<a href="<%= templates_archived_index_path %>" class="link text-sm">View Archived</a> <div>
</div> <a href="<%= templates_archived_index_path %>" class="link text-sm">View Archived</a>
<% end %>
<% end %>
<% if @template_folders.present? %>
<div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
<%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
</div> </div>
<% end %> <% end %>
<% if @templates.present? %> <% end %>
<div class="grid gap-4 md:grid-cols-3"> <% if @template_folders.present? %>
<%= render partial: 'templates/template', collection: @templates %> <div class="grid gap-4 md:grid-cols-3 <%= 'mb-6' if @templates.present? %>">
</div> <%= render partial: 'template_folders/folder', collection: @template_folders, as: :folder %>
<% end %> </div>
<% if params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %> <% end %>
<%= form_for '', url: templates_upload_path, method: :post, class: 'mt-8 block', html: { enctype: 'multipart/form-data' } do %> <% if @templates.present? %>
<button type="submit" class="hidden"></button> <div class="grid gap-4 md:grid-cols-3">
<file-dropzone data-submit-on-upload="true" class="w-full"> <%= render partial: 'templates/template', collection: @templates %>
<label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border border-2 border-base-300 border-dashed"> </div>
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center"> <% end %>
<div class="flex flex-col items-center"> <% if params[:q].blank? && @pagy.pages == 1 && ((@template_folders.size < 10 && @templates.size.zero?) || (@template_folders.size < 7 && @templates.size < 4) || (@template_folders.size < 4 && @templates.size < 7)) %>
<span data-target="file-dropzone.icon" class="flex flex-col items-center"> <%= form_for '', url: templates_upload_path, id: form_id = SecureRandom.uuid, method: :post, class: 'mt-8 block', html: { enctype: 'multipart/form-data' } do %>
<span> <input type="hidden" name="form_id" value="<%= form_id %>">
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %> <button type="submit" class="hidden"></button>
</span> <file-dropzone data-submit-on-upload="true" class="w-full">
<div class="font-medium mb-1"> <label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border border-2 border-base-300 border-dashed">
Upload New Document <div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
</div> <div class="flex flex-col items-center">
<div class="text-xs"> <span data-target="file-dropzone.icon" class="flex flex-col items-center">
<span class="font-medium">Click to upload</span> or drag and drop <span>
</div> <%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
</span> </span>
<span data-target="file-dropzone.loading" class="flex flex-col items-center hidden"> <div class="font-medium mb-1">
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %> Upload New Document
<div class="font-medium mb-1"> </div>
Uploading... <div class="text-xs">
</div> <span class="font-medium">Click to upload</span> or drag and drop
</span> </div>
</div> </span>
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls' if Docuseal.multitenant? %>" multiple> <span data-target="file-dropzone.loading" class="flex flex-col items-center hidden">
</div> <%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
</label> <div class="font-medium mb-1">
</file-dropzone> Uploading...
<% end %> </div>
<% end %> </span>
<% if @templates.present? || params[:q].blank? %>
<% if @pagy.pages > 1 %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html %>
<% else %>
<div class="mt-2">
<%= view_archived_html %>
</div>
<% end %>
<% elsif params[:q].present? %>
<div class="text-center">
<div class="mt-16 text-3xl font-semibold">
Templates not Found
</div>
</div>
<% end %>
<% else %>
<div class="card bg-base-200 h-96 mb-2">
<div class="card-body text-center">
<div class="flex items-center h-full">
<div class="mx-auto">
<div class="max-w-xl mx-auto">
<h1 class="text-5xl font-bold text-base-content">👋 Welcome to <%= Docuseal.product_name %></h1>
</div>
<div class="max-w-lg mx-auto">
<p class="py-6 text-gray-600">Streamline document workflows, from creating customizable templates to filling and signing document forms</p>
<%= link_to new_template_path, class: 'base-button', data: { turbo_frame: :modal } do %>
<%= svg_icon('plus', class: 'w-6 h-6 stroke-2') %>
<span class="mr-1">Create Template</span>
<% end %>
</div> </div>
<input id="file_dropzone_input" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="image/*, application/pdf<%= ', .docx, .doc, .xlsx, .xls' if Docuseal.multitenant? %>" multiple>
</div> </div>
</div> </label>
</file-dropzone>
<% end %>
<% end %>
<% if @templates.present? || params[:q].blank? %>
<% if @pagy.pages > 1 %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html %>
<% else %>
<div class="mt-2">
<%= view_archived_html %>
</div>
<% end %>
<% elsif params[:q].present? %>
<div class="text-center">
<div class="mt-16 text-3xl font-semibold">
Templates not Found
</div> </div>
</div> </div>
<% end %> <% end %>
@@ -41,13 +41,6 @@
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.button button_title(title: 'Sign up', disabled_with: 'Signing up'), class: 'base-button' %> <%= f.button button_title(title: 'Sign up', disabled_with: 'Signing up'), class: 'base-button' %>
<% if Docuseal.multitenant? %>
<button id="talk_to_sales_button" class="hidden" type="submit" formaction="<%= enquiries_path %>" formnovalidate="true" formmethod="post"></button>
<label class="flex items-center cursor-pointer">
<input type="checkbox" name="talk_to_sales" class="base-checkbox" onchange="window.talk_to_sales_button.click()">
<span class="label">I would like to talk to sales</span>
</label>
<% end %>
</div> </div>
<% end %> <% end %>
<%= render 'devise/shared/links' %> <%= render 'devise/shared/links' %>
+12 -8
View File
@@ -17,15 +17,19 @@
<% end %> <% end %>
<% if devise_mapping.omniauthable? %> <% if devise_mapping.omniauthable? %>
<div class="space-y-4"> <div class="space-y-4">
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %> <% if User.omniauth_providers.include?(:google_oauth2) %>
<set-timezone data-input-id="state" data-params="true"></set-timezone> <%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %> <set-timezone data-input-id="state" data-params="true"></set-timezone>
<%= f.button button_title(title: 'Sign up with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %> <%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %>
<%= f.button button_title(title: 'Sign up with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %>
<% end %>
<% end %> <% end %>
<%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %> <% if User.omniauth_providers.include?(:microsoft_office365) %>
<set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone> <%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %> <set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone>
<%= f.button button_title(title: 'Sign up with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %> <%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %>
<%= f.button button_title(title: 'Sign up with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %>
<% end %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
+12 -8
View File
@@ -21,15 +21,19 @@
<% end %> <% end %>
<% if devise_mapping.omniauthable? %> <% if devise_mapping.omniauthable? %>
<div class="space-y-4"> <div class="space-y-4">
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %> <% if User.omniauth_providers.include?(:google_oauth2) %>
<set-timezone data-input-id="state" data-params="true"></set-timezone> <%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %> <set-timezone data-input-id="state" data-params="true"></set-timezone>
<%= f.button button_title(title: 'Sign in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %> <%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %>
<%= f.button button_title(title: 'Sign in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %>
<% end %>
<% end %> <% end %>
<%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %> <% if User.omniauth_providers.include?(:microsoft_office365) %>
<set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone> <%= form_for '', url: omniauth_authorize_path(resource_name, :microsoft_office365), data: { turbo: false }, method: :post do |f| %>
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %> <set-timezone data-input-id="state_microsoft" data-params="true"></set-timezone>
<%= f.button button_title(title: 'Sign in with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %> <%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query, id: 'state_microsoft' %>
<%= f.button button_title(title: 'Sign in with Microsoft', icon: svg_icon('brand_microsoft', class: 'w-6 h-6')), class: 'white-button w-full' %>
<% end %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
+1 -1
View File
@@ -8,7 +8,7 @@
<% end %> <% end %>
<div class="space-y-2"> <div class="space-y-2">
<div class="form-control"> <div class="form-control">
<%= f.label :otp_attempt, 'Two-Factor Code', class: 'label' %> <%= f.label :otp_attempt, 'Two-Factor Code from Authenticator App', class: 'label' %>
<%= f.text_field :otp_attempt, autofocus: true, placeholder: 'XXX-XXX', required: true, class: 'base-input' %> <%= f.text_field :otp_attempt, autofocus: true, placeholder: 'XXX-XXX', required: true, class: 'base-input' %>
</div> </div>
</div> </div>
+22 -1
View File
@@ -121,7 +121,28 @@
<%= f.button button_title(title: 'Save', disabled_with: 'Updating'), class: 'base-button' %> <%= f.button button_title(title: 'Save', disabled_with: 'Updating'), class: 'base-button' %>
</div> </div>
<% end %> <% end %>
</div>
<% end %>
<% if !Docuseal.multitenant? %>
<% account_config = AccountConfig.where(account: current_account, key: AccountConfig::ESIGNING_PREFERENCE_KEY).first_or_initialize(value: 'multiple') %>
<% if can?(:manage, account_config) %>
<div class="px-1 mt-8 max-w-xl">
<div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-3xl font-bold">Preferences</h2>
</div>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
Apply multiple PDF digital signatures in the document per each signer
</span>
<%= f.check_box :value, { class: 'toggle', checked: account_config.value == 'multiple', onchange: 'this.form.requestSubmit()' }, 'multiple', 'single' %>
</div>
<% end %>
<% end %>
</div>
<% end %> <% end %>
</div> <% end %>
</div> </div>
</div> </div>
+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M4 13h5" />
<path d="M12 16v-8h3a2 2 0 0 1 2 2v1a2 2 0 0 1 -2 2h-3" />
<path d="M20 8v8" />
<path d="M9 16v-5.5a2.5 2.5 0 0 0 -5 0v5.5" />
</svg>

After

Width:  |  Height:  |  Size: 439 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M15 6l-6 6l6 6" />
</svg>

After

Width:  |  Height:  |  Size: 313 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M8.5 13.5l-1.5 -1.5l1.5 -1.5" />
<path d="M15.5 10.5l1.5 1.5l-1.5 1.5" />
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M13 9.5l-2 5.5" />
</svg>

After

Width:  |  Height:  |  Size: 458 B

+1 -1
View File
@@ -6,7 +6,7 @@
<style> <style>
</style> </style>
</head> </head>
<body> <body dir="<%= yield&.match?(TextUtils::RTL_REGEXP) ? 'rtl' : 'auto' %>">
<%= yield %> <%= yield %>
<%= render partial: 'shared/mailer_attribution' %> <%= render partial: 'shared/mailer_attribution' %>
</body> </body>
+1 -1
View File
@@ -3,7 +3,7 @@
Use an authenticator mobile app like Google Authenticator or 1Password to scan the QR code below. Use an authenticator mobile app like Google Authenticator or 1Password to scan the QR code below.
</p> </p>
<div> <div>
<%== RQRCode::QRCode.new(@provision_url).as_svg(viewbox: true, svg_attributes: { class: 'w-80 h-80 my-4 mx-auto' }) %> <%== RQRCode::QRCode.new(@provision_url).as_svg(viewbox: true, svg_attributes: { class: 'md:w-80 md:h-80 my-4 mx-auto' }) %>
</div> </div>
<div class="form-control my-6 space-y-2"> <div class="form-control my-6 space-y-2">
<%= f.text_field :otp_attempt, required: true, placeholder: 'XXX-XXX', class: 'base-input text-center' %> <%= f.text_field :otp_attempt, required: true, placeholder: 'XXX-XXX', class: 'base-input text-center' %>
@@ -2,7 +2,7 @@
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<div class="form-control"> <div class="form-control">
<% record = Struct.new(:first_duration, :second_duration, :third_duration).new(*(f.object.value || {}).values_at('first_duration', 'second_duration', 'third_duration')) %> <% record = Struct.new(:first_duration, :second_duration, :third_duration).new(*(f.object.value || {}).values_at('first_duration', 'second_duration', 'third_duration')) %>
<div class="flex space-x-4"> <div class="flex flex-col md:flex-row gap-4">
<div class="w-full"> <div class="w-full">
<%= f.fields_for :value, record do |ff| %> <%= f.fields_for :value, record do |ff| %>
<%= ff.label :first_duration, 'First reminder in', class: 'label' %> <%= ff.label :first_duration, 'First reminder in', class: 'label' %>
+9 -3
View File
@@ -4,11 +4,16 @@
<div class="text-center"> <div class="text-center">
<div class="relative flex flex-col items-center"> <div class="relative flex flex-col items-center">
<%= render 'shared/logo', width: '100', height: '100' %> <%= render 'shared/logo', width: '100', height: '100' %>
<h1 class="text-6xl font-bold mt-4 mb-8"> <h1 class="text-6xl font-bold mt-4 mb-4">
DocuSeal DocuSeal
</h1> </h1>
<% if Docuseal.version.present? %>
<a href="https://github.com/docusealco/docuseal/releases" target="_blank" class="badge badge-outline badge-lg block mx-auto">
v<%= Docuseal.version %>
</a>
<% end %>
</div> </div>
<h2 class="mb-16 text-lg text-center text-gray-600"> <h2 class="mt-4 mb-16 text-lg text-center text-gray-600">
A self-hosted and open-source web platform that provides secure and efficient digital document signing and processing. A self-hosted and open-source web platform that provides secure and efficient digital document signing and processing.
</h2> </h2>
</div> </div>
@@ -62,7 +67,8 @@
</div> </div>
<h3 class="mb-4 text-2xl font-semibold">Open Source</h3> <h3 class="mb-4 text-2xl font-semibold">Open Source</h3>
<p class="text-base text-gray-500"> <p class="text-base text-gray-500">
Source code is available under <a href="<%= Docuseal::GITHUB_URL %>" class="link link-neutral font-bold" target="_blank">github.com/docusealco</a>.<br>Open-source contributors are always ready to help! Source code is available under <a href="<%= Docuseal::GITHUB_URL %>" class="link link-neutral font-bold" target="_blank">github.com/docusealco</a>.<br>
Open-source contributors are always ready to help!
</p> </p>
</div> </div>
</div> </div>
Loaded 100 of 187 files, more files were not shown because too many files have changed in this diff. Show more