Compare commits

..
159 Commits
Author SHA1 Message Date
Pete Matsyburka 3d870b2305 remove i18n duplicate keys 2024-01-29 21:08:57 +02:00
Pete Matsyburka 27d8cd57bd add dutch form translations 2024-01-29 19:53:27 +02:00
Pete Matsyburka 353b2e7a94 fix typed font 2024-01-29 17:44:55 +02:00
Pete Matsyburka d77bd87acb add account removal button 2024-01-29 01:30:06 +02:00
Pete Matsyburka bae7b924ae adjust testing audit log 2024-01-29 00:07:52 +02:00
Pete Matsyburka 89867fdde3 simplify submissions api 2024-01-28 14:43:13 +02:00
Pete Matsyburka c9a1f067ac make API fields match case insensitive 2024-01-28 13:47:17 +02:00
Pete Matsyburka eb26835d57 remove resubmit button from API submissions 2024-01-28 13:27:50 +02:00
Pete Matsyburka 7301661468 fix api params validator 2024-01-27 14:24:24 +02:00
Pete Matsyburka e4fe984a70 fix validator path 2024-01-26 23:03:39 +02:00
Pete Matsyburka 12ccf622c1 enable API errors 2024-01-26 22:26:55 +02:00
Pete Matsyburka 6c35ff6d1c make confetti configurable 2024-01-26 17:43:52 +02:00
Pete Matsyburka 1d7c84302e fix form area style 2024-01-26 17:24:01 +02:00
Pete Matsyburka f3e8889710 clear canvas on signature toggle 2024-01-26 17:12:08 +02:00
Pete Matsyburka 4b2f9ff306 improve form i18n 2024-01-26 16:45:32 +02:00
Pete Matsyburka b4ba642f8e add builder currencies prop 2024-01-26 10:43:37 +02:00
Pete Matsyburka 5c69b965c0 fix submission default values with 2+ submitters 2024-01-25 23:41:44 +02:00
Pete Matsyburka cba196385c do now show textarea toggle with validation pattern 2024-01-25 22:58:17 +02:00
Alex Turchyn 00abad56cb add the 'more_then' validator to the params validator 2024-01-25 01:36:50 +02:00
Pete Matsyburka d1070c4955 fix focus field border 2024-01-24 23:53:47 +02:00
Pete Matsyburka e84be523c6 add show field names prop 2024-01-24 23:20:53 +02:00
Pete Matsyburka 5d598e8268 improve radio checkboxes form 2024-01-24 22:28:45 +02:00
Pete Matsyburka 0e9d922204 fix normalize values when nil 2024-01-24 10:05:14 +02:00
Pete Matsyburka 80c1b04e83 fix mobile builder style 2024-01-24 01:11:04 +02:00
Pete Matsyburka b2fca3edf3 do not allow add new if parties predefined 2024-01-23 23:58:25 +02:00
Pete Matsyburka 2a292c8e80 add phone validation alert 2024-01-23 21:58:19 +02:00
Pete Matsyburka e6052ad0d8 validate email/phone submission format 2024-01-23 21:47:01 +02:00
Pete Matsyburka 8646e17d5a fix typo 2024-01-22 22:45:24 +02:00
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
193 changed files with 4012 additions and 1126 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.
+13 -4
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
@@ -31,11 +39,11 @@ 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
+12
View File
@@ -37,6 +37,18 @@ class AccountsController < ApplicationController
render :show, status: :unprocessable_entity render :show, status: :unprocessable_entity
end end
def destroy
authorize!(:manage, current_account)
true_user.update!(locked_at: Time.current)
render turbo_stream: turbo_stream.replace(
:account_delete_button,
html: helpers.tag.p('Your account removal request will be processed within 2 weeks. ' \
'Please contact us if you want to keep your account.')
)
end
private private
def load_account def load_account
+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
+55 -27
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: !Docuseal.multitenant?)
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
@@ -77,7 +96,8 @@ module Api
def create_submissions(template, params) def create_submissions(template, params)
is_send_email = !params[:send_email].in?(['false', false]) is_send_email = !params[:send_email].in?(['false', false])
if (emails = (params[:emails] || params[:email]).presence) && params[:submission].blank? if (emails = (params[:emails] || params[:email]).presence) &&
(params[:submission].blank? && params[:submitters].blank?)
Submissions.create_from_emails(template:, Submissions.create_from_emails(template:,
user: current_user, user: current_user,
source: :api, source: :api,
@@ -107,7 +127,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
@@ -121,19 +141,27 @@ module Api
end end
def submissions_params def submissions_params
key = params.key?(:submission) ? :submission : :submissions permitted_attrs = [
:send_email, :send_sms, :bcc_completed, :completed_redirect_url,
{
message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key,
{ values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value title description
readonly validation_pattern invalid_message]] }]]
}
]
params.permit( if params.key?(:submitters)
key => [ params.permit(*permitted_attrs)
[:send_email, :send_sms, { else
message: %i[subject body], key = params.key?(:submission) ? :submission : :submissions
submitters: [[:send_email, :send_sms, :uuid, :name, :email, :role,
:completed, :phone, :application_key, params.permit(
{ values: {}, readonly_fields: [], message: %i[subject body], key => [permitted_attrs]
fields: [%i[name default_value readonly validation_pattern invalid_message]] }]] ).fetch(key, [])
}] end
]
).fetch(key, [])
end end
end end
end end
+10 -2
View File
@@ -44,7 +44,7 @@ module Api
Submissions::CreateFromSubmitters.maybe_set_template_fields(@submitter.submission, Submissions::CreateFromSubmitters.maybe_set_template_fields(@submitter.submission,
[normalized_params], [normalized_params],
submitter_uuid: @submitter.uuid) default_submitter_uuid: @submitter.uuid)
assign_submitter_attrs(@submitter, normalized_params) assign_submitter_attrs(@submitter, normalized_params)
@@ -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
+5 -2
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,7 +43,7 @@ class SubmitFormController < ApplicationController
return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity
end end
if submitter.template.deleted_at? || submitter.submission.deleted_at? if submitter.template.archived_at? || submitter.submission.archived_at?
Rollbar.info("Archived template: #{submitter.template.id}") if defined?(Rollbar) Rollbar.info("Archived template: #{submitter.template.id}") if defined?(Rollbar)
return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity
@@ -58,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)
+4 -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,8 @@ 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',
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes, acceptFileTypes: this.dataset.acceptFileTypes,
isDirectUpload: this.dataset.isDirectUpload === 'true' isDirectUpload: this.dataset.isDirectUpload === 'true'
}) })
+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
+2 -1
View File
@@ -15,10 +15,11 @@ window.customElements.define('submission-form', class extends HTMLElement {
goToLast: this.dataset.goToLast === 'true', goToLast: this.dataset.goToLast === 'true',
isDemo: this.dataset.isDemo === 'true', isDemo: this.dataset.isDemo === 'true',
attribution: this.dataset.attribution !== 'false', attribution: this.dataset.attribution !== 'false',
withConfetti: true, withConfetti: this.dataset.withConfetti !== 'false',
withTypedSignature: this.dataset.withTypedSignature !== 'false', 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>
+74 -35
View File
@@ -1,8 +1,9 @@
<template> <template>
<div <div
class="flex absolute lg:text-base" class="flex absolute lg:text-base -outline-offset-1"
dir="auto"
:style="computedStyle" :style="computedStyle"
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-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, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
> >
<div <div
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid" v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
@@ -21,15 +22,20 @@
</span> </span>
</div> </div>
<div <div
v-if="isActive && withLabel && !area.option_uuid" v-if="isActive && withLabel && (!area.option_uuid || !option.value)"
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap" class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap pointer-events-none"
> >
{{ field.name || fieldNames[field.type] }} <template v-if="area.option_uuid && !option.value">
<template v-if="field.type === 'checkbox' && !field.name"> {{ optionValue(option) }}
{{ fieldIndex + 1 }}
</template> </template>
<template v-else-if="!field.required && field.type !== 'checkbox'"> <template v-else>
(optional) {{ field.name || fieldNames[field.type] }}
<template v-if="field.type === 'checkbox' && !field.name">
{{ fieldIndex + 1 }}
</template>
<template v-else-if="!field.required && field.type !== 'checkbox'">
(optional)
</template>
</template> </template>
</div> </div>
<div <div
@@ -42,6 +48,11 @@
class="object-contain mx-auto" class="object-contain mx-auto"
:src="image.url" :src="image.url"
> >
<img
v-else-if="field.type === 'stamp' && stamp"
class="object-contain mx-auto"
:src="stamp.url"
>
<img <img
v-else-if="field.type === 'signature' && signature" v-else-if="field.type === 'signature' && signature"
class="object-contain mx-auto" class="object-contain mx-auto"
@@ -70,7 +81,8 @@
</div> </div>
<div <div
v-else-if="field.type === 'checkbox'" v-else-if="field.type === 'checkbox'"
class="w-full p-[0.2vw] flex items-center justify-center" class="w-full p-[1px] flex items-center justify-center"
@click="$event.target.querySelector('input')?.click()"
> >
<input <input
v-if="submittable" v-if="submittable"
@@ -89,26 +101,28 @@
</div> </div>
<div <div
v-else-if="field.type === 'radio' && area.option_uuid" v-else-if="field.type === 'radio' && area.option_uuid"
class="w-full p-[0.2vw] flex items-center justify-center" class="w-full p-[1px] flex items-center justify-center"
@click="$event.target.querySelector('input')?.click()"
> >
<input <input
v-if="submittable" v-if="submittable"
type="radio" type="radio"
:value="false" :value="false"
class="aspect-square base-radio" class="aspect-square checked:checkbox checked:checkbox-xs"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :class="{ 'base-radio': !modelValue || modelValue !== optionValue(option), '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
:checked="!!modelValue && modelValue === field.options.find((o) => o.uuid === area.option_uuid)?.value" :checked="!!modelValue && modelValue === optionValue(option)"
@click="$emit('update:model-value', field.options.find((o) => o.uuid === area.option_uuid)?.value)" @click="$emit('update:model-value', optionValue(option))"
> >
<IconCheck <IconCheck
v-else-if="!!modelValue && modelValue === field.options.find((o) => o.uuid === area.option_uuid)?.value" v-else-if="!!modelValue && modelValue === optionValue(option)"
class="aspect-square" class="aspect-square"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/> />
</div> </div>
<div <div
v-else-if="field.type === 'multiple' && area.option_uuid" v-else-if="field.type === 'multiple' && area.option_uuid"
class="w-full p-[0.2vw] flex items-center justify-center" class="w-full p-[1px] flex items-center justify-center"
@click="$event.target.querySelector('input')?.click()"
> >
<input <input
v-if="submittable" v-if="submittable"
@@ -116,11 +130,11 @@
:value="false" :value="false"
class="aspect-square base-checkbox" class="aspect-square base-checkbox"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
:checked="!!modelValue && modelValue.includes(field.options.find((o) => o.uuid === area.option_uuid)?.value)" :checked="!!modelValue && modelValue.includes(optionValue(option))"
@change="updateMultipleSelectValue(field.options.find((o) => o.uuid === area.option_uuid)?.value)" @change="updateMultipleSelectValue(optionValue(option))"
> >
<IconCheck <IconCheck
v-else-if="!!modelValue && modelValue.includes(field.options.find((o) => o.uuid === area.option_uuid)?.value)" v-else-if="!!modelValue && modelValue.includes(optionValue(option))"
class="aspect-square" class="aspect-square"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/> />
@@ -141,7 +155,8 @@
<div <div
v-else v-else
ref="textContainer" ref="textContainer"
class="flex items-center px-0.5" dir="auto"
class="flex items-center px-0.5 w-full"
> >
<span v-if="Array.isArray(modelValue)"> <span v-if="Array.isArray(modelValue)">
{{ modelValue.join(', ') }} {{ modelValue.join(', ') }}
@@ -158,7 +173,7 @@
</template> </template>
<script> <script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, 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 +181,7 @@ export default {
IconPaperclip, IconPaperclip,
IconCheck IconCheck
}, },
inject: ['t'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -220,21 +236,25 @@ export default {
computed: { computed: {
fieldNames () { fieldNames () {
return { return {
text: 'Text', text: this.t('text'),
cells: 'Text', signature: this.t('signature'),
signature: 'Signature', initials: this.t('initials'),
date: 'Date', date: this.t('date'),
image: 'Image', image: this.t('image'),
initials: 'Initials', file: this.t('file'),
file: 'File', select: this.t('select'),
select: 'Select', checkbox: this.t('checkbox'),
checkbox: 'Checkbox', multiple: this.t('multiple'),
radio: 'Radio', radio: this.t('radio'),
multiple: 'Multiple Select', cells: this.t('cells'),
phone: 'Phone', stamp: this.t('stamp'),
payment: 'Payment' payment: this.t('payment'),
phone: this.t('phone')
} }
}, },
option () {
return this.field.options.find((o) => o.uuid === this.area.option_uuid)
},
fieldIcons () { fieldIcons () {
return { return {
text: IconTextSize, text: IconTextSize,
@@ -246,6 +266,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 +280,13 @@ export default {
return null return null
} }
}, },
stamp () {
if (this.field.type === 'stamp') {
return this.attachmentsIndex[this.modelValue]
} else {
return null
}
},
signature () { signature () {
if (this.field.type === 'signature') { if (this.field.type === 'signature') {
return this.attachmentsIndex[this.modelValue] return this.attachmentsIndex[this.modelValue]
@@ -321,6 +349,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()
@@ -42,7 +42,7 @@
> >
</template> </template>
<FileDropzone <FileDropzone
:message="`Upload ${field.name || 'Attachments'}${field.required ? '' : ' (optional)'}`" :message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:multiple="true" :multiple="true"
@@ -62,6 +62,7 @@ export default {
IconPaperclip, IconPaperclip,
IconTrashX IconTrashX
}, },
inject: ['t'],
props: { props: {
field: { field: {
type: Object, type: Object,
+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
}) })
} }
+11 -3
View File
@@ -1,10 +1,10 @@
<template> <template>
<div> <div dir="auto">
<div class="flex justify-between items-center w-full mb-2"> <div class="flex justify-between items-center w-full mb-2">
<label <label
:for="field.uuid" :for="field.uuid"
class="label text-2xl" class="label text-2xl"
>{{ field.name || t('date') }} >{{ field.name && showFieldNames ? field.name : t('date') }}
<template v-if="!field.required">({{ t('optional') }})</template> <template v-if="!field.required">({{ t('optional') }})</template>
</label> </label>
<button <button
@@ -15,6 +15,7 @@
{{ t('set_today') }} {{ t('set_today') }}
</button> </button>
</div> </div>
<AppearsOn :field="field" />
<div class="text-center"> <div class="text-center">
<input <input
ref="input" ref="input"
@@ -31,11 +32,13 @@
<script> <script>
import { IconCalendarCheck } from '@tabler/icons-vue' import { IconCalendarCheck } from '@tabler/icons-vue'
import AppearsOn from './appears_on'
export default { export default {
name: 'DateStep', name: 'DateStep',
components: { components: {
IconCalendarCheck IconCalendarCheck,
AppearsOn
}, },
inject: ['t'], inject: ['t'],
props: { props: {
@@ -43,6 +46,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
showFieldNames: {
type: Boolean,
required: false,
default: true
},
modelValue: { modelValue: {
type: String, type: String,
required: false, required: false,
+118 -35
View File
@@ -4,12 +4,13 @@
:steps="stepFields" :steps="stepFields"
:values="values" :values="values"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:with-label="!isAnonymousChecboxes" :with-label="!isAnonymousChecboxes && showFieldNames"
:current-step="currentStepFields" :current-step="currentStepFields"
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']" @focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
/> />
<button <button
v-if="!isFormVisible" v-if="!isFormVisible"
id="expand_form_button"
class="btn btn-neutral flex text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base" class="btn btn-neutral flex text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
@click.prevent="isFormVisible = true" @click.prevent="isFormVisible = true"
> >
@@ -23,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
@@ -61,21 +64,24 @@
<TextStep <TextStep
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:show-field-names="showFieldNames"
:field="currentField" :field="currentField"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
/> />
</div> </div>
<DateStep <DateStep
v-else-if="currentField.type === 'date'" v-else-if="currentField.type === 'date'"
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:show-field-names="showFieldNames"
:field="currentField" :field="currentField"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
/> />
<div v-else-if="currentField.type === 'select'"> <div v-else-if="currentField.type === 'select'">
<label <label
v-if="currentField.name" v-if="showFieldNames && currentField.name"
:for="currentField.uuid" :for="currentField.uuid"
dir="auto"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ currentField.name }} >{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template> <template v-if="!currentField.required">({{ t('optional') }})</template>
@@ -84,13 +90,15 @@
v-else v-else
class="py-1" class="py-1"
/> />
<AppearsOn :field="currentField" />
<select <select
:id="currentField.uuid" :id="currentField.uuid"
dir="auto"
:required="currentField.required" :required="currentField.required"
class="select base-input !text-2xl w-full text-center font-normal" class="select base-input !text-2xl w-full text-center font-normal"
:name="`values[${currentField.uuid}]`" :name="`values[${currentField.uuid}]`"
@change="values[currentField.uuid] = $event.target.value" @change="values[currentField.uuid] = $event.target.value"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
> >
<option <option
value="" value=""
@@ -110,16 +118,30 @@
</div> </div>
<div v-else-if="currentField.type === 'radio'"> <div v-else-if="currentField.type === 'radio'">
<label <label
v-if="currentField.name" v-if="showFieldNames && currentField.name"
:for="currentField.uuid" :for="currentField.uuid"
dir="auto"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ currentField.name }} >{{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template> <template v-if="!currentField.required">({{ t('optional') }})</template>
</label> </label>
<div class="flex w-full"> <div class="flex w-full max-h-44 overflow-y-auto">
<div class="space-y-3.5 mx-auto"> <div
v-if="!showFieldNames || (currentField.options.every((e) => !e.value) && currentField.options.length > 4)"
class="text-xl px-1"
>
<span
@click="scrollIntoField(currentField)"
>
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
</span>
</div>
<div
class="space-y-3.5 mx-auto"
:class="{ hidden: !showFieldNames || (currentField.options.every((e) => !e.value) && currentField.options.length > 4) }"
>
<div <div
v-for="option in currentField.options" v-for="(option, index) in currentField.options"
:key="option.uuid" :key="option.uuid"
> >
<label <label
@@ -132,11 +154,11 @@
type="radio" type="radio"
class="base-radio !h-7 !w-7" class="base-radio !h-7 !w-7"
:name="`values[${currentField.uuid}]`" :name="`values[${currentField.uuid}]`"
:value="option.value" :value="option.value || `${t('option')} ${index + 1}`"
:required="currentField.required" :required="currentField.required"
> >
<span class="text-xl"> <span class="text-xl">
{{ option.value }} {{ option.value || `${t('option')} ${index + 1}` }}
</span> </span>
</label> </label>
</div> </div>
@@ -147,6 +169,8 @@
v-else-if="currentField.type === 'multiple'" v-else-if="currentField.type === 'multiple'"
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:show-field-names="showFieldNames"
:is-last-step="stepFields.length === currentStep + 1"
:field="currentField" :field="currentField"
/> />
<div <div
@@ -161,7 +185,7 @@
<div <div
class="space-y-3.5 mx-auto" class="space-y-3.5 mx-auto"
> >
<template v-if="isAnonymousChecboxes"> <template v-if="isAnonymousChecboxes || !showFieldNames">
<span class="text-xl"> <span class="text-xl">
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>. {{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
</span> </span>
@@ -195,9 +219,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 +238,15 @@
</div> </div>
</div> </div>
<ImageStep <ImageStep
v-else-if="currentField.type === 'image'" v-else-if="currentField.type === 'image' || currentField.type === 'stamp'"
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]" :show-field-names="showFieldNames"
@attached="[attachments.push($event), scrollIntoField(currentField)]"
/> />
<SignatureStep <SignatureStep
v-else-if="currentField.type === 'signature'" v-else-if="currentField.type === 'signature'"
@@ -226,8 +259,9 @@
:with-typed-signature="withTypedSignature" :with-typed-signature="withTypedSignature"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:show-field-names="showFieldNames"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@start="$refs.areas.scrollIntoField(currentField)" @start="scrollIntoField(currentField)"
@minimize="isFormVisible = false" @minimize="isFormVisible = false"
/> />
<InitialsStep <InitialsStep
@@ -239,10 +273,11 @@
:previous-value="previousInitialsValue" :previous-value="previousInitialsValue"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:show-field-names="showFieldNames"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@start="$refs.areas.scrollIntoField(currentField)" @start="scrollIntoField(currentField)"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@minimize="isFormVisible = false" @minimize="isFormVisible = false"
/> />
<AttachmentStep <AttachmentStep
@@ -253,7 +288,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'"
@@ -261,9 +296,10 @@
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
:show-field-names="showFieldNames"
:default-value="submitter.phone" :default-value="submitter.phone"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@focus="$refs.areas.scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="submitStep" @submit="submitStep"
/> />
<PaymentStep <PaymentStep
@@ -274,7 +310,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 +319,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 +353,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,7 +396,8 @@ import TextStep from './text_step'
import DateStep from './date_step' import DateStep from './date_step'
import FormCompleted from './completed' import FormCompleted from './completed'
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import { t } from './i18n' import AppearsOn from './appears_on'
import i18n from './i18n'
export default { export default {
name: 'SubmissionForm', name: 'SubmissionForm',
@@ -360,6 +405,7 @@ export default {
FieldAreas, FieldAreas,
ImageStep, ImageStep,
SignatureStep, SignatureStep,
AppearsOn,
AttachmentStep, AttachmentStep,
InitialsStep, InitialsStep,
MultiSelectStep, MultiSelectStep,
@@ -375,6 +421,8 @@ export default {
provide () { provide () {
return { return {
baseUrl: this.baseUrl, baseUrl: this.baseUrl,
scrollIntoArea: this.scrollIntoArea,
scrollIntoField: this.scrollIntoField,
t: this.t t: this.t
} }
}, },
@@ -405,11 +453,21 @@ export default {
required: false, required: false,
default: false default: false
}, },
showFieldNames: {
type: Boolean,
required: false,
default: true
},
withTypedSignature: { withTypedSignature: {
type: Boolean, type: Boolean,
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,
@@ -450,6 +508,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
language: {
type: String,
required: false,
default: ''
},
values: { values: {
type: Object, type: Object,
required: false, required: false,
@@ -465,6 +528,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,
@@ -486,6 +554,9 @@ export default {
currentStepFields () { currentStepFields () {
return this.stepFields[this.currentStep] return this.stepFields[this.currentStep]
}, },
browserLanguage () {
return (navigator.language || navigator.userLanguage || 'en').split('-')[0]
},
queryParams () { queryParams () {
return new URLSearchParams(window.location.search) return new URLSearchParams(window.location.search)
}, },
@@ -511,7 +582,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 {
@@ -603,7 +674,9 @@ export default {
}) })
}, },
methods: { methods: {
t, t (key) {
return i18n[this.language?.toLowerCase()]?.[key] || i18n[this.browserLanguage]?.[key] || i18n.en[key] || key
},
maybeTrackEmailClick () { maybeTrackEmailClick () {
const { queryParams } = this const { queryParams } = this
@@ -671,7 +744,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 +772,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 +787,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,6 +826,10 @@ export default {
if (respData) { if (respData) {
this.onComplete(JSON.parse(respData)) this.onComplete(JSON.parse(respData))
} }
if (this.completedRedirectUrl) {
window.location.href = this.completedRedirectUrl
}
} }
}).catch(error => { }).catch(error => {
console.error(error) console.error(error)
+324 -49
View File
@@ -1,7 +1,25 @@
const en = { const en = {
text: 'Text',
signature: 'Signature',
initials: 'Initials',
date: 'Date',
image: 'Image',
take_photo: 'Take photo',
file: 'File',
select: 'Select',
checkbox: 'Checkbox',
multiple: 'Multiple',
radio: 'Radio',
cells: 'Cells',
stamp: 'Stamp',
payment: 'Payment',
phone: 'Phone',
submit_form: 'Submit Form', submit_form: 'Submit Form',
type_here: 'Type here', type_here_: 'Type here...',
optional: 'optional', optional: 'optional',
option: 'Option',
appears_on: 'Appears on',
page: 'Page',
select_your_option: 'Select your option', select_your_option: 'Select your option',
complete_hightlighted_checkboxes_and_click: 'Complete hightlighted checkboxes and click', complete_hightlighted_checkboxes_and_click: 'Complete hightlighted checkboxes and click',
submit: 'submit', submit: 'submit',
@@ -10,8 +28,6 @@ const en = {
or_drag_and_drop_files: 'or drag and drop files', or_drag_and_drop_files: 'or drag and drop files',
send_copy_via_email: 'Send copy via email', send_copy_via_email: 'Send copy via email',
download: 'Download', download: 'Download',
signature: 'Signature',
initials: 'Initials',
clear: 'Clear', clear: 'Clear',
redraw: 'Redraw', redraw: 'Redraw',
draw_initials: 'Draw initials', draw_initials: 'Draw initials',
@@ -37,16 +53,35 @@ const en = {
draw: 'Draw', draw: 'Draw',
type: 'Type', type: 'Type',
type_text: 'Type text', type_text: 'Type text',
date: 'Date',
email_has_been_sent: 'Email has been sent', email_has_been_sent: 'Email has been sent',
processing: 'Processing', processing: 'Processing',
pay_with_strip: 'Pay with Stripe' pay_with_strip: 'Pay with Stripe',
reupload: 'Reupload',
upload: 'Upload',
files: 'Files'
} }
const es = { const es = {
text: 'Texto',
signature: 'Firma',
initials: 'Iniciales',
date: 'Fecha',
image: 'Imagen',
file: 'Archivo',
select: 'Seleccionar',
checkbox: 'Casilla',
multiple: 'Múltiple',
radio: 'Radio',
cells: 'Celdas',
stamp: 'Sello',
payment: 'Pago',
phone: 'Teléfono',
take_photo: 'Tomar foto',
submit_form: 'Enviar Formulario', submit_form: 'Enviar Formulario',
type_here: 'Escribe aquí', type_here_: 'Escribe aquí...',
optional: 'opcional', optional: 'opcional',
appears_on: 'Aparece en',
page: 'Página',
select_your_option: 'Selecciona tu opción', select_your_option: 'Selecciona tu opción',
complete_hightlighted_checkboxes_and_click: 'Completa las casillas resaltadas y haz clic', complete_hightlighted_checkboxes_and_click: 'Completa las casillas resaltadas y haz clic',
submit: 'enviar', submit: 'enviar',
@@ -55,8 +90,6 @@ const es = {
or_drag_and_drop_files: 'o arrastra y suelta archivos', or_drag_and_drop_files: 'o arrastra y suelta archivos',
send_copy_via_email: 'Enviar copia por correo electrónico', send_copy_via_email: 'Enviar copia por correo electrónico',
download: 'Descargar', download: 'Descargar',
signature: 'Firma',
initials: 'Iniciales',
clear: 'Borrar', clear: 'Borrar',
redraw: 'Redibujar', redraw: 'Redibujar',
draw_initials: 'Dibujar iniciales', draw_initials: 'Dibujar iniciales',
@@ -76,7 +109,6 @@ const es = {
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS', verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS',
please_fill_all_required_fields: 'Por favor, complete todos los campos obligatorios', please_fill_all_required_fields: 'Por favor, complete todos los campos obligatorios',
set_today: 'Establecer Hoy', set_today: 'Establecer Hoy',
date: 'Fecha',
toggle_multiline_text: 'Alternar Texto Multilínea', toggle_multiline_text: 'Alternar Texto Multilínea',
draw_signature: 'Dibujar firma', draw_signature: 'Dibujar firma',
type_initial: 'Escribir iniciales', type_initial: 'Escribir iniciales',
@@ -85,13 +117,33 @@ const es = {
type_text: 'Escribir texto', type_text: 'Escribir texto',
email_has_been_sent: 'El correo electrónico ha sido enviado', email_has_been_sent: 'El correo electrónico ha sido enviado',
processing: 'Procesando', processing: 'Procesando',
pay_with_strip: 'Pagar con Stripe' pay_with_strip: 'Pagar con Stripe',
reupload: 'Volver a subir',
upload: 'Subir',
files: 'Archivos'
} }
const it = { const it = {
text: 'Testo',
signature: 'Firma',
initials: 'Iniziali',
date: 'Data',
image: 'Immagine',
file: 'File',
select: 'Seleziona',
checkbox: 'Casella di controllo',
multiple: 'Multiplo',
radio: 'Radio',
cells: 'Celle',
stamp: 'Timbro',
payment: 'Pagamento',
phone: 'Telefono',
submit_form: 'Invia Modulo', submit_form: 'Invia Modulo',
type_here: 'Digita qui', type_here_: 'Digita qui...',
optional: 'opzionale', optional: 'opzionale',
appears_on: 'Compare su',
page: 'Pagina',
take_photo: 'Scattare una foto',
select_your_option: 'Seleziona la tua opzione', select_your_option: 'Seleziona la tua opzione',
complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic', complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic',
submit: 'invia', submit: 'invia',
@@ -100,8 +152,6 @@ const it = {
or_drag_and_drop_files: 'oppure trascina e rilascia i file', or_drag_and_drop_files: 'oppure trascina e rilascia i file',
send_copy_via_email: 'Invia copia via email', send_copy_via_email: 'Invia copia via email',
download: 'Scarica', download: 'Scarica',
signature: 'Firma',
initials: 'Iniziali',
clear: 'Cancella', clear: 'Cancella',
redraw: 'Ridisegna', redraw: 'Ridisegna',
draw_initials: 'Disegna iniziali', draw_initials: 'Disegna iniziali',
@@ -121,7 +171,6 @@ const it = {
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS', verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS',
please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori', please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori',
set_today: 'Imposta Oggi', set_today: 'Imposta Oggi',
date: 'Data',
draw_signature: 'Disegna firma', draw_signature: 'Disegna firma',
type_initial: 'Inserisci iniziali', type_initial: 'Inserisci iniziali',
draw: 'Disegna', draw: 'Disegna',
@@ -130,13 +179,33 @@ const it = {
toggle_multiline_text: 'Attiva Testo Multilinea', toggle_multiline_text: 'Attiva Testo Multilinea',
email_has_been_sent: "L'email è stata inviata", email_has_been_sent: "L'email è stata inviata",
processing: 'Elaborazione', processing: 'Elaborazione',
pay_with_strip: 'Paga con Stripe' pay_with_strip: 'Paga con Stripe',
reupload: 'Ricarica',
upload: 'Carica',
files: 'File'
} }
const de = { const de = {
text: 'Text',
signature: 'Unterschrift',
initials: 'Initialen',
date: 'Datum',
image: 'Bild',
file: 'Datei',
select: 'Auswählen',
checkbox: 'Checkbox',
multiple: 'Mehrere',
radio: 'Radio',
cells: 'Zellen',
stamp: 'Stempel',
payment: 'Zahlung',
phone: 'Telefon',
submit_form: 'Formular absenden', submit_form: 'Formular absenden',
type_here: 'Hier eingeben', type_here_: 'Hier eingeben...',
optional: 'optional', optional: 'optional',
appears_on: 'Erscheint auf',
page: 'Seite',
take_photo: 'Foto aufnehmen',
select_your_option: 'Wähle deine Option', select_your_option: 'Wähle deine Option',
complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken', complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken',
submit: 'absenden', submit: 'absenden',
@@ -145,8 +214,6 @@ const de = {
or_drag_and_drop_files: 'oder Dateien hierher ziehen und ablegen', or_drag_and_drop_files: 'oder Dateien hierher ziehen und ablegen',
send_copy_via_email: 'Kopie per E-Mail senden', send_copy_via_email: 'Kopie per E-Mail senden',
download: 'Herunterladen', download: 'Herunterladen',
signature: 'Unterschrift',
initials: 'Initialen',
clear: 'Löschen', clear: 'Löschen',
redraw: 'Neu zeichnen', redraw: 'Neu zeichnen',
draw_initials: 'Initialen zeichnen', draw_initials: 'Initialen zeichnen',
@@ -166,7 +233,6 @@ const de = {
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet', verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet',
please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus', please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus',
set_today: 'Heute einstellen', set_today: 'Heute einstellen',
date: 'Datum',
draw_signature: 'Unterschrift zeichnen', draw_signature: 'Unterschrift zeichnen',
type_initial: 'Initialen eingeben', type_initial: 'Initialen eingeben',
draw: 'Zeichnen', draw: 'Zeichnen',
@@ -175,13 +241,33 @@ const de = {
toggle_multiline_text: 'Mehrzeiligen Text umschalten', toggle_multiline_text: 'Mehrzeiligen Text umschalten',
email_has_been_sent: 'Die E-Mail wurde gesendet', email_has_been_sent: 'Die E-Mail wurde gesendet',
processing: 'Verarbeitung', processing: 'Verarbeitung',
pay_with_strip: 'Mit Stripe bezahlen' pay_with_strip: 'Mit Stripe bezahlen',
reupload: 'Erneut hochladen',
upload: 'Hochladen',
files: 'Dateien'
} }
const fr = { const fr = {
text: 'Texte',
signature: 'Signature',
initials: 'Initiales',
date: 'Date',
image: 'Image',
file: 'Fichier',
select: 'Choisir',
checkbox: 'Coche',
multiple: 'Multiple',
radio: 'Radio',
cells: 'Cellules',
stamp: 'Tampon',
payment: 'Paiement',
phone: 'Téléphone',
submit_form: 'Envoyer le Formulaire', submit_form: 'Envoyer le Formulaire',
type_here: 'Tapez ici', type_here_: 'Tapez ici...',
optional: 'facultatif', optional: 'facultatif',
appears_on: 'Apparaît sur',
page: 'Page',
take_photo: 'Prendre une photo',
select_your_option: 'Sélectionnez votre option', select_your_option: 'Sélectionnez votre option',
complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez', complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez',
submit: 'envoyer', submit: 'envoyer',
@@ -190,8 +276,6 @@ const fr = {
or_drag_and_drop_files: 'ou faites glisser-déposer les fichiers', or_drag_and_drop_files: 'ou faites glisser-déposer les fichiers',
send_copy_via_email: 'Envoyer une copie par e-mail', send_copy_via_email: 'Envoyer une copie par e-mail',
download: 'Télécharger', download: 'Télécharger',
signature: 'Signature',
initials: 'Initiales',
clear: 'Effacer', clear: 'Effacer',
redraw: 'Redessiner', redraw: 'Redessiner',
draw_initials: 'Dessiner les initiales', draw_initials: 'Dessiner les initiales',
@@ -211,7 +295,6 @@ const fr = {
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS', verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires', please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
set_today: "Définir Aujourd'hui", set_today: "Définir Aujourd'hui",
date: 'Date',
draw_signature: 'Dessiner une signature', draw_signature: 'Dessiner une signature',
type_initial: 'Saisir les initiales', type_initial: 'Saisir les initiales',
draw: 'Dessiner', draw: 'Dessiner',
@@ -220,23 +303,41 @@ const fr = {
toggle_multiline_text: 'Basculer le Texte Multiligne', toggle_multiline_text: 'Basculer le Texte Multiligne',
email_has_been_sent: "L'email a été envoyé", email_has_been_sent: "L'email a été envoyé",
processing: 'Traitement', processing: 'Traitement',
pay_with_strip: 'Paiement avec Stripe' pay_with_strip: 'Paiement avec Stripe',
reupload: 'Recharger',
upload: 'Télécharger',
files: 'Fichiers'
} }
const pl = { const pl = {
text: 'Tekst',
signature: 'Podpis',
initials: 'Inicjały',
date: 'Data',
image: 'Obraz',
file: 'Plik',
select: 'Wybierz',
checkbox: 'Pole wyboru',
multiple: 'Wielokrotne',
radio: 'Radio',
cells: 'Komórki',
stamp: 'Pieczątka',
payment: 'Płatność',
phone: 'Telefon',
submit_form: 'Wyślij Formularz', submit_form: 'Wyślij Formularz',
type_here: 'Wpisz tutaj', type_here_: 'Wpisz tutaj...',
optional: 'opcjonalny', optional: 'opcjonalny',
appears_on: 'Pojawia się na',
page: 'Strona',
select_your_option: 'Wybierz swoją opcję', select_your_option: 'Wybierz swoją opcję',
complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij', complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij',
submit: 'wyślij', submit: 'wyślij',
next: 'dalej', next: 'dalej',
take_photo: 'Zrób zdjęcie',
click_to_upload: 'Kliknij, aby przesłać', click_to_upload: 'Kliknij, aby przesłać',
or_drag_and_drop_files: 'lub przeciągnij i upuść pliki', or_drag_and_drop_files: 'lub przeciągnij i upuść pliki',
send_copy_via_email: 'Wyślij kopię drogą mailową', send_copy_via_email: 'Wyślij kopię drogą mailową',
download: 'Pobierz', download: 'Pobierz',
signature: 'Podpis',
initials: 'Inicjały',
clear: 'Wyczyść', clear: 'Wyczyść',
redraw: 'Przerysuj', redraw: 'Przerysuj',
draw_initials: 'Narysuj inicjały', draw_initials: 'Narysuj inicjały',
@@ -256,7 +357,6 @@ const pl = {
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany', verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany',
please_fill_all_required_fields: 'Proszę wypełnić wszystkie wymagane pola', please_fill_all_required_fields: 'Proszę wypełnić wszystkie wymagane pola',
set_today: 'Ustaw Dziś', set_today: 'Ustaw Dziś',
date: 'Data',
draw_signature: 'Rysuj podpis', draw_signature: 'Rysuj podpis',
type_initial: 'Wprowadź inicjały', type_initial: 'Wprowadź inicjały',
draw: 'Rysuj', draw: 'Rysuj',
@@ -265,13 +365,33 @@ const pl = {
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy', toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
email_has_been_sent: 'E-mail został wysłany', email_has_been_sent: 'E-mail został wysłany',
processing: 'Przetwarzanie', processing: 'Przetwarzanie',
pay_with_strip: 'Płatność za pomocą Stripe' pay_with_strip: 'Płatność za pomocą Stripe',
reupload: 'Ponowne przesłanie',
upload: 'Przesyłanie',
files: 'Pliki'
} }
const uk = { const uk = {
text: 'Текст',
signature: 'Підпис',
initials: 'Ініціали',
date: 'Дата',
image: 'Зображення',
file: 'Файл',
select: 'Вибрати',
checkbox: 'Прапорець',
multiple: 'Множинний',
radio: 'Радіо',
cells: 'Клітинки',
stamp: 'Штамп',
payment: 'Платіж',
phone: 'Телефон',
submit_form: 'Надіслати Форму', submit_form: 'Надіслати Форму',
type_here: 'Введіть тут', type_here_: 'Введіть тут',
optional: 'необов’язково', optional: 'необов’язково',
appears_on: "З'являється на",
page: 'Сторінка',
take_photo: 'Зробити фото',
select_your_option: 'Виберіть варіант', select_your_option: 'Виберіть варіант',
complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть', complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть',
submit: 'надіслати', submit: 'надіслати',
@@ -280,8 +400,6 @@ const uk = {
or_drag_and_drop_files: 'або перетягніть файли сюди', or_drag_and_drop_files: 'або перетягніть файли сюди',
send_copy_via_email: 'Надіслати копію електронною поштою', send_copy_via_email: 'Надіслати копію електронною поштою',
download: 'Завантажити', download: 'Завантажити',
signature: 'Підпис',
initials: 'Ініціали',
clear: 'Очистити', clear: 'Очистити',
redraw: 'Перемалювати', redraw: 'Перемалювати',
draw_initials: 'Намалювати ініціали', draw_initials: 'Намалювати ініціали',
@@ -301,7 +419,6 @@ const uk = {
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий', verification_code_has_been_resent: 'Код підтвердження був повторно надісланий',
please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля", please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля",
set_today: 'Задати Сьогодні', set_today: 'Задати Сьогодні',
date: 'Дата',
draw_signature: 'Намалюйте підпис', draw_signature: 'Намалюйте підпис',
type_initial: 'Введіть ініціали', type_initial: 'Введіть ініціали',
draw: 'Підпис', draw: 'Підпис',
@@ -310,14 +427,34 @@ const uk = {
toggle_multiline_text: 'Перемкнути Багаторядковий Текст', toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
email_has_been_sent: 'Електронний лист був відправлений', email_has_been_sent: 'Електронний лист був відправлений',
processing: 'Обробка', processing: 'Обробка',
pay_with_strip: 'Сплатити за допомогою Stripe' pay_with_strip: 'Сплатити за допомогою Stripe',
reupload: 'Перезавантажити',
upload: 'Завантажити',
files: 'Файли'
} }
const cs = { const cs = {
text: 'Text',
signature: 'Podpis',
initials: 'Iniciály',
date: 'Datum',
image: 'Obrázek',
file: 'Soubor',
select: 'Vybrat',
checkbox: 'Zaškrtávací políčko',
multiple: 'Více možností',
radio: 'Rádio',
cells: 'Buňky',
stamp: 'Razítko',
payment: 'Platba',
phone: 'Telefon',
submit_form: 'Odeslat formulář', submit_form: 'Odeslat formulář',
type_here: 'Zadejte zde', type_here_: 'Zadejte zde',
optional: 'volitelné', optional: 'volitelné',
appears_on: 'Zobrazuje se na',
page: 'Stránka',
select_your_option: 'Vyberte svou volbu', select_your_option: 'Vyberte svou volbu',
take_photo: 'Fotit',
complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na', complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na',
submit: 'odeslat', submit: 'odeslat',
next: 'další', next: 'další',
@@ -325,8 +462,6 @@ const cs = {
or_drag_and_drop_files: 'nebo přetáhněte soubory sem', or_drag_and_drop_files: 'nebo přetáhněte soubory sem',
send_copy_via_email: 'Odeslat kopii e-mailem', send_copy_via_email: 'Odeslat kopii e-mailem',
download: 'Stáhnout', download: 'Stáhnout',
signature: 'Podpis',
initials: 'Iniciály',
clear: 'Smazat', clear: 'Smazat',
redraw: 'Překreslit', redraw: 'Překreslit',
draw_initials: 'Nakreslit iniciály', draw_initials: 'Nakreslit iniciály',
@@ -346,7 +481,6 @@ const cs = {
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán', verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán',
please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky', please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky',
set_today: 'Nastavit Dnes', set_today: 'Nastavit Dnes',
date: 'Datum',
draw_signature: 'Nakreslit podpis', draw_signature: 'Nakreslit podpis',
type_initial: 'Zadat iniciály', type_initial: 'Zadat iniciály',
draw: 'Kreslit', draw: 'Kreslit',
@@ -355,13 +489,33 @@ const cs = {
toggle_multiline_text: 'Přepnout Víceřádkový Text', toggle_multiline_text: 'Přepnout Víceřádkový Text',
email_has_been_sent: 'E-mail byl odeslán', email_has_been_sent: 'E-mail byl odeslán',
processing: 'Zpracování', processing: 'Zpracování',
pay_with_strip: 'Zaplacení přes Stripe' pay_with_strip: 'Zaplacení přes Stripe',
reupload: 'Znovu nahrát',
upload: 'Nahrát',
files: 'Soubory'
} }
const pt = { const pt = {
text: 'Texto',
signature: 'Assinatura',
initials: 'Iniciais',
date: 'Data',
image: 'Imagem',
file: 'Arquivo',
select: 'Selecionar',
checkbox: 'Caixa',
multiple: 'Múltiplo',
radio: 'Rádio',
cells: 'Células',
stamp: 'Carimbo',
payment: 'Pagamento',
phone: 'Telefone',
submit_form: 'Enviar Formulário', submit_form: 'Enviar Formulário',
type_here: 'Digite aqui', type_here_: 'Digite aqui',
optional: 'opcional', optional: 'opcional',
appears_on: 'Aparece em',
page: 'Página',
take_photo: 'Tirar foto',
select_your_option: 'Selecione sua opção', select_your_option: 'Selecione sua opção',
complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique', complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique',
submit: 'enviar', submit: 'enviar',
@@ -370,8 +524,6 @@ const pt = {
or_drag_and_drop_files: 'ou arraste e solte arquivos', or_drag_and_drop_files: 'ou arraste e solte arquivos',
send_copy_via_email: 'Enviar cópia por e-mail', send_copy_via_email: 'Enviar cópia por e-mail',
download: 'Baixar', download: 'Baixar',
signature: 'Assinatura',
initials: 'Iniciais',
clear: 'Limpar', clear: 'Limpar',
redraw: 'Redesenhar', redraw: 'Redesenhar',
draw_initials: 'Desenhar iniciais', draw_initials: 'Desenhar iniciais',
@@ -391,7 +543,6 @@ const pt = {
verification_code_has_been_resent: 'O código de verificação foi reenviado via SMS', verification_code_has_been_resent: 'O código de verificação foi reenviado via SMS',
please_fill_all_required_fields: 'Por favor, preencha todos os campos obrigatórios', please_fill_all_required_fields: 'Por favor, preencha todos os campos obrigatórios',
set_today: 'Definir Hoje', set_today: 'Definir Hoje',
date: 'Data',
draw_signature: 'Desenhar assinatura', draw_signature: 'Desenhar assinatura',
type_initial: 'Inserir iniciais', type_initial: 'Inserir iniciais',
draw: 'Desenhar', draw: 'Desenhar',
@@ -400,14 +551,138 @@ const pt = {
toggle_multiline_text: 'Alternar Texto Multilinha', toggle_multiline_text: 'Alternar Texto Multilinha',
email_has_been_sent: 'Email enviado', email_has_been_sent: 'Email enviado',
processing: 'Processamento', processing: 'Processamento',
pay_with_strip: 'Pagar com Stripe' pay_with_strip: 'Pagar com Stripe',
reupload: 'Reenviar',
upload: 'Carregar',
files: 'Arquivos'
} }
const i18n = { en, es, it, de, fr, pl, uk, cs, pt } const he = {
text: 'טקסט',
signature: 'חתימה',
initials: 'ראשי תיקיות',
date: 'תאריך',
image: 'תמונה',
file: 'קובץ',
select: 'בחר',
checkbox: 'תיק בחירה',
multiple: 'רב ערכים',
radio: 'רדיו',
cells: 'תאים',
stamp: 'חותמת',
payment: 'תשלום',
phone: 'טלפון',
submit_form: 'שלח טופס',
type_here_: 'הקלד כאן',
optional: 'אופציונלי',
option: 'אפשרות',
appears_on: 'מופיע ב',
page: 'עמוד',
take_photo: 'צלם תמונה',
select_your_option: 'בחר את האפשרות שלך',
complete_hightlighted_checkboxes_and_click: 'השלם את תיבות הסימון המודגשות ולחץ',
submit: 'שליחה',
next: 'הבא',
click_to_upload: 'לחץ להעלאה',
or_drag_and_drop_files: 'או גרור ושחרר קבצים',
send_copy_via_email: 'שלח עותק באמצעות אימייל',
download: 'הורדה',
clear: 'נקה',
redraw: 'צייר מחדש',
draw_initials: 'צייר ציוני ראשי תיבות',
type_signature_here: 'הקלד חתימה כאן',
type_initial_here: 'הקלד ציוני ראשי תיבות כאן',
form_has_been_completed: 'הטופס הושלם',
create_a_free_account: 'צור חשבון חינם',
signed_with: 'חתום באמצעות',
please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך',
open_source_documents_software: 'תוכנה פתוחה למסמכים',
verified_phone_number: 'אימות מספר טלפון',
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
six_digits_code: 'קוד משתמש שש ספרות',
change_phone_number: 'שינוי מספר טלפון',
sending: 'שולח',
resend_code: 'שלח מחדש קוד',
verification_code_has_been_resent: 'קוד האימות נשלח מחדש בSMS',
please_fill_all_required_fields: 'אנא מלא את כל השדות הדרושים',
set_today: 'קבע היום',
toggle_multiline_text: 'שנה בין טקסט במספר שורות לטקסט בשורה אחת',
draw_signature: 'צייר חתימה',
type_initial: 'הקלד ציוני ראשי תיבות',
draw: 'צייר',
type: 'הקלד',
type_text: 'הקלד טקסט',
email_has_been_sent: 'האימייל נשלח',
processing: 'מעבד',
pay_with_strip: 'שלם עם סטרייפ',
reupload: 'העלה שוב',
upload: 'העלאה',
files: 'קבצים'
}
const browserLanguage = (navigator.language || navigator.userLanguage || 'en').split('-')[0] const nl = {
text: 'Tekst',
signature: 'Handtekening',
initials: 'Initialen',
date: 'Datum',
image: 'Afbeelding',
take_photo: 'Maak een foto',
file: 'Bestand',
select: 'Selecteer',
checkbox: 'Aankruisvakje',
multiple: 'Meerdere',
radio: 'Radio',
cells: 'Cellen',
stamp: 'Stempel',
payment: 'Betaling',
phone: 'Telefoon',
submit_form: 'Formulier verzenden',
type_here_: 'Typ hier...',
optional: 'Optioneel',
option: 'Optie',
appears_on: 'Verschijnt op',
page: 'Pagina',
select_your_option: 'Selecteer uw optie',
complete_hightlighted_checkboxes_and_click: 'Voltooi de gemarkeerde selectievakjes en klik',
submit: 'Verzenden',
next: 'Volgende',
click_to_upload: 'Klik om te uploaden',
or_drag_and_drop_files: 'of sleep en plaats bestanden',
send_copy_via_email: 'Stuur kopie per e-mail',
download: 'Downloaden',
clear: 'Wissen',
redraw: 'Opnieuw tekenen',
draw_initials: 'Initialen tekenen',
type_signature_here: 'Typ hier uw handtekening',
type_initial_here: 'Typ hier uw initialen',
form_has_been_completed: 'Formulier is voltooid!',
create_a_free_account: 'Maak een gratis account aan',
signed_with: 'Ondertekend met',
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan',
open_source_documents_software: 'Open source documenten software',
verified_phone_number: 'Geverifieerd telefoonnummer',
use_international_format: 'Gebruik internationaal formaat: +1xxx',
six_digits_code: '6-cijferige code',
change_phone_number: 'Wijzig telefoonnummer',
sending: 'Verzenden...',
resend_code: 'Code opnieuw verzenden',
verification_code_has_been_resent: 'Verificatiecode is opnieuw verzonden via SMS',
please_fill_all_required_fields: 'Vul alle verplichte velden in',
set_today: 'Stel vandaag in',
toggle_multiline_text: 'Schakel meerregelige tekst in/uit',
draw_signature: 'Handtekening tekenen',
type_initial: 'Typ initialen',
draw: 'Tekenen',
type: 'Typen',
type_text: 'Typ tekst',
email_has_been_sent: 'E-mail is verzonden',
processing: 'Verwerken',
pay_with_strip: 'Betalen met Stripe',
reupload: 'Opnieuw uploaden',
upload: 'Uploaden',
files: 'Bestanden'
}
const t = (key) => i18n[browserLanguage]?.[key] || i18n.en[key] || key const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl }
export default i18n export default i18n
export { t }
@@ -3,13 +3,13 @@
<div class="flex justify-between items-center w-full mb-2"> <div class="flex justify-between items-center w-full mb-2">
<label <label
class="label text-2xl" class="label text-2xl"
>{{ field.name || 'Image' }}</label> >{{ showFieldNames && field.name ? field.name : t('image') }}</label>
<button <button
class="btn btn-outline btn-sm" class="btn btn-outline btn-sm"
@click.prevent="remove" @click.prevent="remove"
> >
<IconReload :width="16" /> <IconReload :width="16" />
Reupload {{ t('reupload') }}
</button> </button>
</div> </div>
<div> <div>
@@ -27,7 +27,7 @@
<div> <div>
<FileDropzone <FileDropzone
v-if="!modelValue" v-if="!modelValue"
:message="`Upload ${field.name || 'Image'}${field.required ? '' : ' (optional)'}`" :message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:accept="'image/*'" :accept="'image/*'"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
@@ -46,6 +46,7 @@ export default {
FileDropzone, FileDropzone,
IconReload IconReload
}, },
inject: ['t'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -56,6 +57,11 @@ export default {
required: true, required: true,
default: false default: false
}, },
showFieldNames: {
type: Boolean,
required: false,
default: true
},
submitterSlug: { submitterSlug: {
type: String, type: String,
required: true required: true
@@ -1,9 +1,9 @@
<template> <template>
<div> <div dir="auto">
<div class="flex justify-between items-center w-full mb-2"> <div class="flex justify-between items-center w-full mb-2">
<label <label
class="label text-2xl" class="label text-2xl"
>{{ field.name || t('initials') }}</label> >{{ showFieldNames && field.name ? field.name : t('initials') }}</label>
<div class="space-x-2 flex"> <div class="space-x-2 flex">
<span <span
v-if="isDrawInitials" v-if="isDrawInitials"
@@ -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,
@@ -122,6 +125,11 @@ export default {
type: String, type: String,
required: true required: true
}, },
showFieldNames: {
type: Boolean,
required: false,
default: true
},
isDirectUpload: { isDirectUpload: {
type: Boolean, type: Boolean,
required: true, required: true,
@@ -1,13 +1,32 @@
<template> <template>
<label <label
v-if="field.name" v-if="showFieldNames && field.name"
:for="field.uuid" :for="field.uuid"
dir="auto"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ field.name }}</label> >{{ field.name }}</label>
<div class="flex w-full"> <div class="flex w-full max-h-44 overflow-y-auto">
<div class="space-y-3.5 mx-auto"> <input
v-if="modelValue.length === 0"
type="text"
:name="`values[${field.uuid}][]`"
:value="''"
class="hidden"
>
<div
v-if="!showOptions"
class="text-xl px-1"
>
<span @click="scrollIntoField(field)">
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ isLastStep ? t('submit') : t('next') }}</span>.
</span>
</div>
<div
class="space-y-3.5 mx-auto"
:class="{ hidden: !showOptions }"
>
<div <div
v-for="option in field.options" v-for="(option, index) in field.options"
:key="option.uuid" :key="option.uuid"
> >
<label <label
@@ -19,13 +38,13 @@
:ref="setInputRef" :ref="setInputRef"
type="checkbox" type="checkbox"
:name="`values[${field.uuid}][]`" :name="`values[${field.uuid}][]`"
:value="option.value" :value="optionValue(option, index)"
class="base-checkbox !h-7 !w-7" class="base-checkbox !h-7 !w-7"
:checked="(modelValue || []).includes(option.value)" :checked="(modelValue || []).includes(optionValue(option, index))"
@change="onChange" @change="onChange"
> >
<span class="text-xl"> <span class="text-xl">
{{ option.value }} {{ optionValue(option, index) }}
</span> </span>
</label> </label>
</div> </div>
@@ -36,11 +55,22 @@
<script> <script>
export default { export default {
name: 'MultiSelectStep', name: 'MultiSelectStep',
inject: ['t', 'scrollIntoField'],
props: { props: {
field: { field: {
type: Object, type: Object,
required: true required: true
}, },
isLastStep: {
type: Boolean,
required: true,
default: false
},
showFieldNames: {
type: Boolean,
required: false,
default: true
},
modelValue: { modelValue: {
type: Array, type: Array,
required: false, required: false,
@@ -53,6 +83,11 @@ export default {
inputRefs: [] inputRefs: []
} }
}, },
computed: {
showOptions () {
return this.showFieldNames && (this.field.options.some((e) => e.value) || this.field.options.length < 5)
}
},
beforeUpdate () { beforeUpdate () {
this.inputRefs = [] this.inputRefs = []
}, },
@@ -62,8 +97,15 @@ export default {
this.inputRefs.push(el) this.inputRefs.push(el)
} }
}, },
optionValue (option, index) {
if (option.value) {
return option.value
} else {
return `${this.t('option')} ${index + 1}`
}
},
onChange () { onChange () {
this.$emit('update:model-value', this.inputRefs.filter(e => e.checked).map(e => e.value)) this.$emit('update:model-value', this.inputRefs.filter(e => e.checked).map((e, index) => this.optionValue(e, index)))
} }
} }
} }
+11 -2
View File
@@ -3,7 +3,7 @@
<label <label
:for="isCodeSent ? 'one_time_code' : field.uuid" :for="isCodeSent ? 'one_time_code' : field.uuid"
class="label text-2xl mb-2" class="label text-2xl mb-2"
>{{ field.name || t('verified_phone_number') }} >{{ showFieldNames && field.name ? field.name : t('verified_phone_number') }}
<template v-if="!field.required">({{ t('optional') }})</template> <template v-if="!field.required">({{ t('optional') }})</template>
</label> </label>
<div> <div>
@@ -93,6 +93,11 @@ export default {
type: String, type: String,
required: true required: true
}, },
showFieldNames: {
type: Boolean,
required: false,
default: true
},
modelValue: { modelValue: {
type: String, type: String,
required: false, required: false,
@@ -140,7 +145,11 @@ export default {
}) })
}, },
async submit () { async submit () {
if (!this.isCodeSent) { if (!this.$refs.phone.value.toString().startsWith('+')) {
alert(this.t('use_international_format'))
return Promise.reject(new Error('phone invalid'))
} else if (!this.isCodeSent) {
this.sendVerificationCode() this.sendVerificationCode()
this.$emit('update:model-value', this.$refs.phone.value) this.$emit('update:model-value', this.$refs.phone.value)
@@ -1,9 +1,9 @@
<template> <template>
<div> <div dir="auto">
<div class="flex justify-between items-center w-full mb-2"> <div class="flex justify-between items-center w-full mb-2">
<label <label
class="label text-2xl" class="label text-2xl"
>{{ field.name || t('signature') }}</label> >{{ showFieldNames && field.name ? field.name : t('signature') }}</label>
<div class="space-x-2 flex"> <div class="space-x-2 flex">
<span <span
v-if="isTextSignature" v-if="isTextSignature"
@@ -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
@@ -41,7 +41,7 @@
</span> </span>
<span <span
class="tooltip" class="tooltip"
data-tip="Take photo" :data-tip="t('take_photo')"
> >
<label <label
class="btn btn-outline btn-sm font-medium" class="btn btn-outline btn-sm font-medium"
@@ -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,
@@ -144,6 +147,11 @@ export default {
type: String, type: String,
required: true required: true
}, },
showFieldNames: {
type: Boolean,
required: false,
default: true
},
isDirectUpload: { isDirectUpload: {
type: Boolean, type: Boolean,
required: true, required: true,
@@ -218,7 +226,7 @@ export default {
}, },
loadFont () { loadFont () {
if (!isFontLoaded) { if (!isFontLoaded) {
const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript.otf) format("opentype")`) const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript-Regular.otf) format("opentype")`)
font.load().then((loadedFont) => { font.load().then((loadedFont) => {
document.fonts.add(loadedFont) document.fonts.add(loadedFont)
@@ -257,6 +265,7 @@ export default {
}, },
toggleTextInput () { toggleTextInput () {
this.remove() this.remove()
this.clear()
this.isTextSignature = !this.isTextSignature this.isTextSignature = !this.isTextSignature
if (this.isTextSignature) { if (this.isTextSignature) {
+27 -8
View File
@@ -1,27 +1,38 @@
<template> <template>
<label <label
v-if="field.name" v-if="showFieldNames && field.name"
:for="field.uuid" :for="field.uuid"
class="label text-2xl mb-2" dir="auto"
>{{ field.name }} class="label text-2xl"
:class="{ 'mb-2': !field.description }"
><template v-if="field.title"><span v-html="field.title" /></template>
<template v-else>{{ field.name }}</template>
<template v-if="!field.required">({{ t('optional') }})</template> <template v-if="!field.required">({{ t('optional') }})</template>
</label> </label>
<div <div
v-else v-else
class="py-1" class="py-1"
/> />
<div
v-if="field.description"
class="mb-3 px-1 text-lg"
v-html="field.description"
/>
<AppearsOn :field="field" />
<div class="items-center flex"> <div class="items-center flex">
<input <input
v-if="!isTextArea" v-if="!isTextArea"
:id="field.uuid" :id="field.uuid"
v-model="text" v-model="text"
:maxlength="cellsMaxLegth" :maxlength="cellsMaxLegth"
class="base-input !text-2xl w-full !pr-11 -mr-10" dir="auto"
class="base-input !text-2xl w-full"
:class="{ '!pr-11 -mr-10': !field.validation?.pattern }"
:required="field.required" :required="field.required"
:pattern="field.validation?.pattern" :pattern="field.validation?.pattern"
:oninvalid="field.validation?.message ? `this.setCustomValidity(${JSON.stringify(field.validation.message)})` : ''" :oninvalid="field.validation?.message ? `this.setCustomValidity(${JSON.stringify(field.validation.message)})` : ''"
:oninput="field.validation?.message ? `this.setCustomValidity('')` : ''" :oninput="field.validation?.message ? `this.setCustomValidity('')` : ''"
:placeholder="`${t('type_here')}...${field.required ? '' : ` (${t('optional')})`}`" :placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
type="text" type="text"
:name="`values[${field.uuid}]`" :name="`values[${field.uuid}]`"
@focus="$emit('focus')" @focus="$emit('focus')"
@@ -31,15 +42,16 @@
:id="field.uuid" :id="field.uuid"
ref="textarea" ref="textarea"
v-model="text" v-model="text"
dir="auto"
class="base-textarea !text-2xl w-full" class="base-textarea !text-2xl w-full"
:placeholder="`${t('type_here')}...${field.required ? '' : ` (${t('optional')})`}`" :placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
:required="field.required" :required="field.required"
:name="`values[${field.uuid}]`" :name="`values[${field.uuid}]`"
@input="resizeTextarea" @input="resizeTextarea"
@focus="$emit('focus')" @focus="$emit('focus')"
/> />
<div <div
v-if="!isTextArea && field.type !== 'cells'" v-if="!isTextArea && field.type !== 'cells' && !field.validation?.pattern"
class="tooltip" class="tooltip"
:data-tip="t('toggle_multiline_text')" :data-tip="t('toggle_multiline_text')"
> >
@@ -56,11 +68,13 @@
<script> <script>
import { IconAlignBoxLeftTop } from '@tabler/icons-vue' import { IconAlignBoxLeftTop } from '@tabler/icons-vue'
import AppearsOn from './appears_on'
export default { export default {
name: 'TextStep', name: 'TextStep',
components: { components: {
IconAlignBoxLeftTop IconAlignBoxLeftTop,
AppearsOn
}, },
inject: ['t'], inject: ['t'],
props: { props: {
@@ -68,6 +82,11 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
showFieldNames: {
type: Boolean,
required: false,
default: true
},
modelValue: { modelValue: {
type: String, type: String,
required: false, required: false,
+10 -3
View File
@@ -43,7 +43,8 @@
class="border-r" class="border-r"
:compact="true" :compact="true"
:editable="editable && !defaultField" :editable="editable && !defaultField"
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px]'" :allow-add-new="!defaultSubmitters.length"
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'"
:submitters="template.submitters" :submitters="template.submitters"
@update:model-value="save" @update:model-value="save"
@click="selectedAreaRef.value = area" @click="selectedAreaRef.value = area"
@@ -61,6 +62,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 +85,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 +151,7 @@ export default {
FieldSubmitter, FieldSubmitter,
IconX IconX
}, },
inject: ['template', 'selectedAreaRef', 'save'], inject: ['template', 'selectedAreaRef', 'save', 't'],
props: { props: {
area: { area: {
type: Object, type: Object,
@@ -165,6 +167,11 @@ export default {
required: false, required: false,
default: null default: null
}, },
defaultSubmitters: {
type: Array,
required: false,
default: () => []
},
editable: { editable: {
type: Boolean, type: Boolean,
required: false, required: false,
+232 -80
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,8 @@
:document="document" :document="document"
:is-drag="!!dragField" :is-drag="!!dragField"
:default-fields="defaultFields" :default-fields="defaultFields"
:allow-draw="!onlyDefinedFields"
:default-submitters="defaultSubmitters"
:draw-field="drawField" :draw-field="drawField"
:editable="editable" :editable="editable"
:base-url="baseUrl" :base-url="baseUrl"
@@ -176,66 +180,43 @@
v-if="withUploadButton" v-if="withUploadButton"
:template-id="template.id" :template-id="template.id"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:accept-file-types="acceptFileTypes"
@success="updateFromUpload" @success="updateFromUpload"
/> />
</div> </div>
</template> </template>
</div> </div>
<MobileDrawField
v-if="drawField && isBreakpointLg"
:draw-field="drawField"
:fields="template.fields"
:submitters="template.submitters"
:selected-submitter="selectedSubmitter"
class="md:hidden"
:editable="editable"
@cancel="[drawField = null, drawOption = null]"
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
/>
<FieldType
v-if="sortedDocuments.length && !drawField && editable"
class="dropdown-top dropdown-end fixed bottom-4 right-4 z-10 md:hidden"
:model-value="''"
@update:model-value="startFieldDraw($event)"
>
<label
class="btn btn-neutral text-white btn-circle btn-lg group"
tabindex="0"
>
<IconPlus
class="group-focus:hidden"
width="28"
height="28"
/>
<IconX
class="hidden group-focus:inline"
width="28"
height="28"
/>
</label>
</FieldType>
</div> </div>
<div <div
v-if="withFieldsList"
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block" class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
:class="drawField ? 'overflow-hidden' : 'overflow-auto'" :class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
> >
<div <div
v-if="drawField" v-if="drawField"
class="sticky inset-0 h-full" class="sticky inset-0 h-full z-20"
:style="{ backgroundColor }" :style="{ backgroundColor }"
> >
<div class="bg-base-300 rounded-lg p-5 text-center space-y-4"> <div class="bg-base-300 rounded-lg p-5 text-center space-y-4">
<p> <p>
Draw {{ drawField.name }} field on the document {{ t('draw_field_on_the_document').replace('{field}', drawField.name) }}
</p> </p>
<p> <div>
<button <button
class="base-button" class="base-button"
@click="[drawField = null, drawOption = null]" @click="clearDrawField"
> >
Cancel {{ t('cancel') }}
</button> </button>
</p> <a
v-if="!drawOption && !drawField.areas.length && !['stamp', 'signature', 'initials'].includes(drawField.type)"
href="#"
class="link block mt-3 text-sm"
@click.prevent="[drawField = null, drawOption = null]"
>
{{ t('or_add_field_without_drawing') }}
</a>
</div>
</div> </div>
</div> </div>
<div> <div>
@@ -244,9 +225,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"
@@ -257,6 +241,27 @@
</div> </div>
</div> </div>
</div> </div>
<div class="sticky bottom-0">
<MobileDrawField
v-if="drawField && isBreakpointLg"
:draw-field="drawField"
:fields="template.fields"
:submitters="template.submitters"
:selected-submitter="selectedSubmitter"
class="md:hidden"
:editable="editable"
@cancel="[drawField = null, drawOption = null]"
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
/>
<MobileFields
v-if="sortedDocuments.length && !drawField && editable"
:fields="template.fields"
:default-fields="defaultFields"
:field-types="fieldTypes"
:selected-submitter="selectedSubmitter"
@select="startFieldDraw($event)"
/>
</div>
</div> </div>
</template> </template>
@@ -270,10 +275,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 +288,8 @@ export default {
Document, Document,
Fields, Fields,
MobileDrawField, MobileDrawField,
IconPlus,
FieldType,
IconX,
IconWritingSign, IconWritingSign,
MobileFields,
Logo, Logo,
Dropzone, Dropzone,
DocumentPreview, DocumentPreview,
@@ -299,7 +303,10 @@ export default {
return { return {
template: this.template, template: this.template,
save: this.save, save: this.save,
t: this.t,
currencies: this.currencies,
baseFetch: this.baseFetch, baseFetch: this.baseFetch,
fieldTypes: this.fieldTypes,
backgroundColor: this.backgroundColor, backgroundColor: this.backgroundColor,
withPhone: this.withPhone, withPhone: this.withPhone,
withPayment: this.withPayment, withPayment: this.withPayment,
@@ -316,6 +323,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 +338,31 @@ export default {
required: false, required: false,
default: true default: true
}, },
withHelp: {
type: Boolean,
required: false,
default: true
},
autosave: {
type: Boolean,
required: false,
default: true
},
defaultFields: { defaultFields: {
type: Array, type: Array,
required: false, required: false,
default: () => [] default: () => []
}, },
currencies: {
type: Array,
required: false,
default: () => []
},
fieldTypes: {
type: Array,
required: false,
default: () => []
},
defaultSubmitters: { defaultSubmitters: {
type: Array, type: Array,
required: false, required: false,
@@ -358,6 +390,13 @@ export default {
return () => {} return () => {}
} }
}, },
onSave: {
type: Function,
required: false,
default () {
return () => {}
}
},
withStickySubmitters: { withStickySubmitters: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -373,6 +412,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,
@@ -383,6 +432,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,
@@ -401,8 +455,6 @@ export default {
} }
}, },
computed: { computed: {
fieldIcons: FieldType.computed.fieldIcons,
fieldNames: FieldType.computed.fieldNames,
selectedAreaRef: () => ref(), selectedAreaRef: () => ref(),
fieldAreasIndex () { fieldAreasIndex () {
const areas = {} const areas = {}
@@ -429,11 +481,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]
@@ -467,27 +528,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 () {
@@ -535,10 +610,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
} }
@@ -595,8 +681,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)
@@ -612,7 +742,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)
@@ -663,6 +797,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'
@@ -695,7 +833,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
@@ -739,6 +877,10 @@ 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) { if (this.onUpload) {
this.onUpload(this.template) this.onUpload(this.template)
} }
@@ -751,7 +893,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)
} }
@@ -830,11 +972,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}`, {
@@ -848,6 +996,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,8 +7,10 @@
:number="index" :number="index"
:editable="editable" :editable="editable"
:areas="areasIndex[index]" :areas="areasIndex[index]"
:allow-draw="allowDraw"
:is-drag="isDrag" :is-drag="isDrag"
:default-fields="defaultFields" :default-fields="defaultFields"
:default-submitters="defaultSubmitters"
:draw-field="drawField" :draw-field="drawField"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
:image="image" :image="image"
@@ -41,6 +43,16 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
defaultSubmitters: {
type: Array,
required: false,
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"
> >
@@ -118,14 +125,15 @@
/> />
</div> </div>
<span class="flex items-center transition-all duration-75 group-hover:border border-base-content/20 border-dashed w-6 h-6 flex justify-center items-center rounded"> <span class="flex items-center transition-all duration-75 group-hover:border border-base-content/20 border-dashed w-6 h-6 flex justify-center items-center rounded">
<IconPlus <component
:is="editable ? 'IconPlus' : 'IconChevronDown'"
width="18" width="18"
height="18" height="18"
/> />
</span> </span>
</label> </label>
<ul <ul
v-if="editable || !compact" v-if="(editable || !compact) && renderDropdown"
tabindex="0" tabindex="0"
:class="menuClasses" :class="menuClasses"
@click="closeDropdown" @click="closeDropdown"
@@ -149,16 +157,39 @@
{{ submitter.name }} {{ submitter.name }}
</span> </span>
</span> </span>
<button <div
v-if="!compact && submitters.length > 1 && editable" v-if="!compact && submitters.length > 1 && editable"
class="hidden group-hover:block px-2" class="flex"
@click.stop="remove(submitter)"
> >
<IconTrashX :width="18" /> <div class="flex-col pr-1 hidden group-hover:flex -mt-1 h-0">
</button> <button
title="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 && allowAddNew">
<a <a
href="#" href="#"
class="flex px-2" class="flex px-2"
@@ -169,7 +200,7 @@
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span class="py-1"> <span class="py-1">
Add {{ names[submitters.length] }} {{ t('add') }} {{ names[submitters.length] }}
</span> </span>
</a> </a>
</li> </li>
@@ -178,7 +209,7 @@
</template> </template>
<script> <script>
import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp } from '@tabler/icons-vue' import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp, IconChevronDown } from '@tabler/icons-vue'
import Contenteditable from './contenteditable' import Contenteditable from './contenteditable'
import { v4 } from 'uuid' import { v4 } from 'uuid'
@@ -186,11 +217,13 @@ export default {
name: 'FieldSubmitter', name: 'FieldSubmitter',
components: { components: {
IconUserPlus, IconUserPlus,
IconChevronDown,
Contenteditable, Contenteditable,
IconPlus, IconPlus,
IconTrashX, IconTrashX,
IconChevronUp IconChevronUp
}, },
inject: ['t', 'save'],
props: { props: {
submitters: { submitters: {
type: Array, type: Array,
@@ -211,6 +244,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
allowAddNew: {
type: Boolean,
required: false,
default: true
},
modelValue: { modelValue: {
type: String, type: String,
required: true required: true
@@ -222,6 +260,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 +282,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 +303,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 }
@@ -1,5 +1,5 @@
<template> <template>
<div class="fixed text-center w-full bottom-0 pr-6 mb-4"> <div class="absolute text-center w-full bottom-0 pr-3 mb-4">
<span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20 md:hidden"> <span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20 md:hidden">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<component <component
@@ -9,14 +9,14 @@
class="inline" class="inline"
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span> Draw {{ fieldNames[drawField.type] }} Field </span> <span>{{ t('draw_field').replace('{field}', fieldNames[drawField.type]) }}</span>
</div> </div>
<a <a
href="#" href="#"
class="link block text-center" class="link block text-center"
@click.prevent="$emit('cancel')" @click.prevent="$emit('cancel')"
> >
Cancel {{ t('cancel') }}
</a> </a>
</span> </span>
<FieldSubmitter <FieldSubmitter
@@ -43,7 +43,7 @@ export default {
Field, Field,
FieldSubmitter FieldSubmitter
}, },
inject: ['save'], inject: ['save', 't'],
props: { props: {
drawField: { drawField: {
type: Object, type: Object,
@@ -0,0 +1,126 @@
<template>
<span
class="dropdown dropdown-top dropdown-end absolute bottom-4 right-4 z-10 md:hidden"
>
<label
class="btn btn-neutral text-white btn-circle btn-lg group"
tabindex="0"
>
<IconPlus
class="group-focus:hidden"
width="28"
height="28"
/>
<IconX
class="hidden group-focus:inline"
width="28"
height="28"
/>
</label>
<ul
tabindex="0"
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3 mt-1.5 bg-base-100"
@click="closeDropdown"
>
<template v-if="submitterDefaultFields.length">
<template
v-for="field in submitterDefaultFields"
:key="field.name"
>
<li>
<a
href="#"
class="text-sm py-1 px-2"
@click.prevent="$emit('select', { name: field.name || '', type: field.type || 'text' })"
>
<component
:is="fieldIcons[field.type || 'text']"
:stroke-width="1.6"
:width="20"
/>
{{ field.name }}
</a>
</li>
</template>
</template>
<template v-else>
<template
v-for="(icon, type) in fieldIcons"
:key="type"
>
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')">
<a
href="#"
class="text-sm py-1 px-2"
:class="{ 'active': type === modelValue }"
@click.prevent="$emit('select', { type })"
>
<component
:is="icon"
:stroke-width="1.6"
:width="20"
/>
{{ fieldNames[type] }}
</a>
</li>
</template>
</template>
</ul>
</span>
</template>
<script>
import { IconPlus, IconX } from '@tabler/icons-vue'
import FieldType from './field_type'
export default {
name: 'MobileFields',
components: {
IconPlus,
IconX
},
inject: ['withPhone', 'withPayment', 'backgroundColor', 't'],
props: {
modelValue: {
type: String,
required: false,
default: ''
},
fields: {
type: Array,
required: false,
default: () => []
},
selectedSubmitter: {
type: Object,
required: true
},
fieldTypes: {
type: Array,
required: false,
default: () => []
},
defaultFields: {
type: Array,
required: false,
default: () => []
}
},
emits: ['select'],
computed: {
...FieldType.computed,
submitterFields () {
return this.fields.filter((f) => f.submitter_uuid === this.selectedSubmitter.uuid)
},
submitterDefaultFields () {
return this.defaultFields.filter((f) => {
return (!f.role || f.role === this.selectedSubmitter.name)
})
}
},
methods: {
closeDropdown () {
document.activeElement.blur()
}
}
}
</script>
+24 -1
View File
@@ -24,6 +24,7 @@
:field="item.field" :field="item.field"
:editable="editable" :editable="editable"
:default-field="defaultFields.find((f) => f.name === item.field.name)" :default-field="defaultFields.find((f) => f.name === item.field.name)"
:default-submitters="defaultSubmitters"
@start-resize="resizeDirection = $event" @start-resize="resizeDirection = $event"
@stop-resize="resizeDirection = null" @stop-resize="resizeDirection = null"
@start-drag="isMove = true" @start-drag="isMove = true"
@@ -33,7 +34,7 @@
<FieldArea <FieldArea
v-if="newArea" v-if="newArea"
:is-draw="true" :is-draw="true"
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || 'text' }" :field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || defaultFieldType }"
:area="newArea" :area="newArea"
/> />
</div> </div>
@@ -60,6 +61,7 @@ export default {
components: { components: {
FieldArea FieldArea
}, },
inject: ['fieldTypes'],
props: { props: {
image: { image: {
type: Object, type: Object,
@@ -75,10 +77,20 @@ 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
}, },
defaultSubmitters: {
type: Array,
required: false,
default: () => []
},
drawField: { drawField: {
type: Object, type: Object,
required: false, required: false,
@@ -110,6 +122,13 @@ export default {
} }
}, },
computed: { computed: {
defaultFieldType () {
if (this.fieldTypes.length !== 0 && !this.fieldTypes.includes('text')) {
return this.fieldTypes[0]
} else {
return 'text'
}
},
isMobile () { isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent) return /android|iphone|ipad/i.test(navigator.userAgent)
}, },
@@ -149,6 +168,10 @@ export default {
}) })
}, },
onStartDraw (e) { onStartDraw (e) {
if (!this.allowDraw) {
return
}
if (this.isMobile && !this.drawField) { if (this.isMobile && !this.drawField) {
return return
} }
@@ -31,7 +31,7 @@
@change="save" @change="save"
> >
<option <option
v-for="currency in currencies" v-for="currency in currenciesList"
:key="currency" :key="currency"
:value="currency" :value="currency"
> >
@@ -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>
@@ -160,7 +167,7 @@ export default {
IconInnerShadowTop, IconInnerShadowTop,
IconBrandStripe IconBrandStripe
}, },
inject: ['backgroundColor', 'save'], inject: ['backgroundColor', 'save', 'currencies'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -180,9 +187,12 @@ export default {
redirectUri () { redirectUri () {
return document.location.origin + '/auth/stripe_connect/callback' return document.location.origin + '/auth/stripe_connect/callback'
}, },
currencies () { defaultCurrencies () {
return ['USD', 'EUR', 'GBP'] return ['USD', 'EUR', 'GBP']
}, },
currenciesList () {
return this.currencies.length ? this.currencies : this.defaultCurrencies
},
authenticityToken () { authenticityToken () {
return document.querySelector('meta[name="csrf-token"]')?.content return document.querySelector('meta[name="csrf-token"]')?.content
}, },
@@ -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!)
+3 -1
View File
@@ -29,6 +29,8 @@ 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'
FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
ESIGNING_PREFERENCE_KEY = 'esigning_preference'
DEFAULT_VALUES = { DEFAULT_VALUES = {
SUBMITTER_INVITATION_EMAIL_KEY => { SUBMITTER_INVITATION_EMAIL_KEY => {
@@ -59,5 +61,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
+8 -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' %>
@@ -65,6 +65,12 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% if can?(:manage, current_account) && Docuseal.multitenant? && true_user == current_user %>
<div class="px-1 mt-8">
<h2 class="text-2xl font-bold mb-2">Danger Zone</h2>
<%= button_to button_title(title: 'Delete my account'), settings_account_path, class: 'btn btn-outline btn-error block', data: { turbo_confirm: 'Schedule account for deletion?' }, method: :delete, id: :account_delete_button %>
</div>
<% end %>
</div> </div>
<div class="w-0 md:w-52"></div> <div class="w-0 md:w-52"></div>
</div> </div>
+19 -17
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>
@@ -29,20 +35,16 @@
--header 'X-Auth-Token: <%= current_user.access_token.token %>' \ --header 'X-Auth-Token: <%= current_user.access_token.token %>' \
--data-raw '{ --data-raw '{
"template_id": <%= current_account.templates.last&.id || 1 %>, "template_id": <%= current_account.templates.last&.id || 1 %>,
"submission": [ "submitters": [
{ {
"submitters": [ "name": "John Doe",
{ "role": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Party' %>",
"name": "John Doe", "email": "<%= current_user.email.sub('@', '+test@') %>",
"role": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Party' %>", "values": {
"email": "<%= current_user.email.sub('@', '+test@') %>", "Form Text Field Name": "Default Value"
"values": { }
"Form Text Field Name": "Default Value" },
} { "role": "Second Submitter", "email": "<%= current_user.email.sub('@', '+test2@') %>" }
},
{ "role": "Second Submitter", "email": "<%= current_user.email.sub('@', '+test2@') %>" }
]
}
] ]
}'<% end.to_str %> }'<% end.to_str %>
<span class="top-0 right-0 absolute"> <span class="top-0 right-0 absolute">
+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

Loaded 100 of 193 files, more files were not shown because too many files have changed in this diff. Show more