Compare commits

...
154 Commits
Author SHA1 Message Date
Alex Turchyn d4e94ac2bc Merge from docusealco/wip 2026-03-02 17:26:04 +02:00
Pete Matsyburka 75bc789bb1 add reply to 2026-03-02 14:47:37 +02:00
Pete Matsyburka bc231d5bf1 add reply to 2026-03-02 14:34:16 +02:00
Alex Turchyn ab9008b3be fix brakeman SQL injection warning 2026-03-01 16:14:15 +02:00
Pete Matsyburka 65491936c3 fix archived export 2026-02-26 17:20:05 +02:00
Pete Matsyburka e589998840 add field type class 2026-02-26 11:08:26 +02:00
Pete Matsyburka 0afba8d11e adjust with prefillable 2026-02-26 11:03:08 +02:00
Pete Matsyburka f622ed168f fix autocomplete 2026-02-25 13:01:03 +02:00
Pete Matsyburka 5a31df40d8 add field settings classes 2026-02-25 11:02:00 +02:00
Pete Matsyburka db3b80c96c fix clone 2026-02-25 11:02:00 +02:00
Pete Matsyburka bd28a761d0 adjust bmp loader 2026-02-25 11:02:00 +02:00
Pete Matsyburka 4f2830a34d increase image resolution 2026-02-25 11:02:00 +02:00
Pete Matsyburka a5716cd518 show html textarea 2026-02-25 11:02:00 +02:00
Pete Matsyburka d035525c7a additional terms 2026-02-25 11:02:00 +02:00
Alex Turchyn 13fa87c449 Merge from docusealco/wip 2026-02-23 13:56:04 +02:00
Pete Matsyburka 246ccfb049 fix highlight 2026-02-23 08:31:38 +02:00
Pete Matsyburka cbffb1e5b8 cloudflare r2 fix 2026-02-22 20:13:19 +02:00
Pete Matsyburka c4b007411d radio select in formula 2026-02-22 19:54:06 +02:00
Pete Matsyburka 04264fe937 fix reason field 2026-02-22 12:15:44 +02:00
Pete Matsyburka 0c37ad12b7 add variables key 2026-02-22 09:46:04 +02:00
Pete Matsyburka 8c5298ed2e refactor link tooltip 2026-02-22 09:28:30 +02:00
Pete Matsyburka 262118e047 update deps 2026-02-22 07:37:05 +02:00
Pete Matsyburka e5b63ea2b0 fix multi party email form 2026-02-21 17:31:04 +02:00
Pete Matsyburka 746757d402 rtl in editor 2026-02-21 16:38:25 +02:00
Pete Matsyburka e6e640328b use br for email content 2026-02-21 16:20:48 +02:00
Pete Matsyburka 845782a69c link input width 2026-02-21 15:48:59 +02:00
Pete Matsyburka fc6baa1b3b adjust autolink 2026-02-21 15:48:58 +02:00
Pete Matsyburka 47822ecc15 remove autolink 2026-02-21 14:55:11 +02:00
Pete Matsyburka 3d0c7f1118 markdown in preferences 2026-02-21 14:44:19 +02:00
Pete Matsyburka e9c8e4d325 adjust editor 2026-02-21 14:32:10 +02:00
Alex Turchyn 39cc82ce0c add markdown editor 2026-02-21 11:15:09 +02:00
Pete Matsyburka 451f83421a fix cell render 2026-02-21 10:49:05 +02:00
Pete Matsyburka 5073f2eefb adjsut sign yourself 2026-02-21 10:17:58 +02:00
Pete Matsyburka 231fff5508 canvas blocked message 2026-02-21 09:53:03 +02:00
Pete Matsyburka f1d146eca3 fix typos 2026-02-20 22:39:03 +02:00
Pete Matsyburka b6635fcc4f change user only when root 2026-02-20 22:25:12 +02:00
Pete Matsyburka fb5e13ee4c adjust recipients form 2026-02-20 18:26:10 +02:00
Pete Matsyburka ba84741a64 invite party via field 2026-02-20 12:41:45 +02:00
Pete Matsyburka 65c275ac17 rename 2026-02-20 10:16:06 +02:00
Pete Matsyburka 825322d489 adjust for custom domain 2026-02-20 09:14:36 +02:00
Pete Matsyburka 739e2abdf8 vips size limit 2026-02-19 14:24:05 +02:00
Pete Matsyburka 1d2394e31e size limit 2026-02-19 14:17:59 +02:00
Pete Matsyburka 1b41af798d update 2026-02-17 19:42:47 +02:00
Pete Matsyburka 118f4a231b detailed time format 2026-02-17 18:09:34 +02:00
Pete Matsyburka e48652f425 fix rubocop 2026-02-17 10:29:00 +02:00
Pete Matsyburka 848f01edf8 add spec 2026-02-17 10:19:29 +02:00
Pete Matsyburka 43fbc42770 fix reason field 2026-02-17 10:04:11 +02:00
Pete Matsyburka 2c736a0eed validate readonly field 2026-02-17 08:53:13 +02:00
Alex Turchyn 33ca930055 Merge from docusealco/wip
Wip
2026-02-16 15:08:07 +02:00
Pete Matsyburka 3b396f8421 fix redirect 2026-02-16 10:00:51 +02:00
Pete Matsyburka aa77e3a8d3 adjust port check 2026-02-16 08:08:41 +02:00
Pete Matsyburka 75316d8d87 fix draw custom field 2026-02-15 22:02:00 +02:00
Pete Matsyburka a999109a5c add port check 2026-02-15 12:18:53 +02:00
Pete Matsyburka bdd33c7d6b fix spec 2026-02-15 09:53:31 +02:00
Pete Matsyburka 61c5ee22a0 hide form credentials 2026-02-15 09:11:40 +02:00
Pete Matsyburka 12c5b909e0 use cancan 2026-02-14 19:13:05 +02:00
Pete Matsyburka 871ef6dda6 add fetch options 2026-02-14 18:50:48 +02:00
Pete Matsyburka 40052a2d7c use query_params 2026-02-14 17:42:08 +02:00
Pete Matsyburka 347be0137d refactor 2fa 2026-02-14 17:35:10 +02:00
Pete Matsyburka fe6baba8bf fix erb lint 2026-02-14 12:10:21 +02:00
Pete Matsyburka ed8c313bd4 timestamp controller multitenant 2026-02-14 12:02:37 +02:00
Pete Matsyburka ca0acb34d6 use url for open modal 2026-02-14 12:02:31 +02:00
Pete Matsyburka 34ea639c25 escape wildcard query 2026-02-14 12:02:25 +02:00
Pete Matsyburka 680ab9dbed raise invalid param 2026-02-14 10:18:58 +02:00
Pete Matsyburka 9377766e52 fix url download 2026-02-14 09:08:52 +02:00
Pete Matsyburka 7a2c37454e update favicon 2026-02-12 15:56:54 +02:00
Pete Matsyburka 8f6f418f54 fix font size 2026-02-12 14:27:15 +02:00
Pete Matsyburka 9b7745c565 hide resend on archived 2026-02-11 17:32:04 +02:00
Alex Turchyn a389e8cc38 Merge from docusealco/wip 2026-02-11 16:49:32 +02:00
Pete Matsyburka 2056301f70 remember send tab selection 2026-02-11 13:14:09 +02:00
Pete Matsyburka 93babe3660 update logo 2026-02-11 10:01:12 +02:00
Pete Matsyburka 4aef2d8eb0 update deps 2026-02-10 09:02:37 +02:00
Pete Matsyburka c5ec2ed230 fix document condition 2026-02-05 11:56:22 +02:00
Pete Matsyburka a156d5f6cc increase webhook timeout 2026-02-05 08:42:53 +02:00
Pete Matsyburka f84674cddd adjust field focus 2026-02-04 17:05:12 +02:00
Pete Matsyburka b7b62c197a fix date field 2026-02-04 16:56:35 +02:00
Pete Matsyburka f5350eac36 prefill typed signature and initials from name 2026-02-04 16:48:40 +02:00
Pete Matsyburka ee6228db15 only required field prop 2026-02-04 10:51:11 +02:00
Pete Matsyburka 3e434c0878 fix 2026-02-04 00:47:34 +02:00
Pete Matsyburka 7c5372a8e2 fix pdf sign 2026-02-02 22:04:43 +02:00
Alex Turchyn 6ab68cc36b Merge from docusealco/wip 2026-02-02 14:13:54 +02:00
Pete Matsyburka ee2400a5c5 touch signature attachment 2026-02-02 12:46:34 +02:00
Pete Matsyburka 80be5d2cf5 sandbox 2026-02-01 08:10:02 +02:00
Pete Matsyburka 76cf44a31b fix 2026-01-31 22:17:11 +02:00
Pete Matsyburka eb63113227 use trilogy mysql 2026-01-31 22:15:17 +02:00
Pete Matsyburka b7045fb3d9 fix azure proxy 2026-01-31 13:07:42 +02:00
Pete Matsyburka 11a5b5842d starttls label 2026-01-31 11:41:52 +02:00
Pete Matsyburka 362994afd5 adjust i18n 2026-01-31 10:26:09 +02:00
Pete Matsyburka d7682f4ebd add number conditions 2026-01-31 09:00:24 +02:00
Pete Matsyburka 56d4fe7fa9 add white color 2026-01-31 07:58:12 +02:00
Pete Matsyburka 93a852ac14 fix save modal 2026-01-30 21:45:55 +02:00
Pete Matsyburka 964db0ab08 fix menu 2026-01-30 21:03:56 +02:00
Pete Matsyburka 8547295da7 border color 2026-01-30 20:53:47 +02:00
Pete Matsyburka c75050fa46 adjust kba 2026-01-30 18:17:47 +02:00
Pete Matsyburka 65372c52aa fix order 2026-01-30 17:42:50 +02:00
Alex Turchyn 12c767b7d3 add more options to context menu 2026-01-30 16:36:50 +02:00
Pete Matsyburka 2fc5e9cfb3 format cells 2026-01-30 11:21:47 +02:00
Pete Matsyburka bccc78ee65 optimize builder 2026-01-30 10:23:51 +02:00
Pete Matsyburka c0793f3ce7 cell font settings 2026-01-29 21:43:01 +02:00
Pete Matsyburka a8dd8699a3 adjust kba 2026-01-29 15:55:15 +02:00
Pete Matsyburka 91bb25e8b6 fix reload 2026-01-29 15:33:24 +02:00
Pete Matsyburka c3f4480cdc add submitter index 2026-01-29 13:01:44 +02:00
Pete Matsyburka 33e226861e add SMTP_SSL_VERIFY env 2026-01-29 11:34:37 +02:00
Pete Matsyburka 4f5acbec9d readonly radio 2026-01-28 21:36:48 +02:00
Pete Matsyburka d2c9bbf6ab optimize conditions 2026-01-28 13:24:05 +02:00
Pete Matsyburka b5e57e1f2f add include fields 2026-01-28 11:49:35 +02:00
Pete Matsyburka c82c8b2272 fix race condition 2026-01-28 08:50:44 +02:00
Pete Matsyburka 43f1b598a7 adjust smtp noverify 2026-01-28 08:27:25 +02:00
Pete Matsyburka 5081e387d0 fix undefined 2026-01-27 20:13:26 +02:00
Pete Matsyburka 1e5f55c1d6 fix Point is invalid 2026-01-27 20:07:57 +02:00
Alex Turchyn aa38773be3 Merge from docusealco/wip 2026-01-27 18:39:04 +02:00
Pete Matsyburka 96530bd809 fix race condition 2026-01-27 17:06:30 +02:00
Pete Matsyburka 127f95a9d0 fix typo 2026-01-27 12:31:06 +02:00
Pete Matsyburka cf31e12348 adjust smtp configs 2026-01-27 10:57:04 +02:00
Pete Matsyburka 4adaa531b7 fix pagination 2026-01-27 10:34:18 +02:00
Pete Matsyburka c7659563b8 fix puma warnings 2026-01-26 20:23:11 +02:00
Alex Turchyn 7b5494468f update rails to 8.1.2 2026-01-26 18:22:47 +02:00
Pete Matsyburka d12c67fd54 with force open 2026-01-26 18:00:46 +02:00
Pete Matsyburka e20eacb5ee center on click 2026-01-26 12:35:53 +02:00
Pete Matsyburka 98055f0762 add can parse polyfill 2026-01-26 12:17:57 +02:00
Pete Matsyburka a46c42c3a0 fix resize warning 2026-01-26 12:06:41 +02:00
Pete Matsyburka c3d1503361 cleanup 2026-01-26 11:44:12 +02:00
Pete Matsyburka 65b47d27cd adjust form 2026-01-26 11:38:33 +02:00
Pete Matsyburka 968c07f8be include idcode 2026-01-26 11:13:01 +02:00
Pete Matsyburka 91814ca105 custom fields 2026-01-26 11:12:55 +02:00
Pete Matsyburka 9128f24270 fix synology acl 2026-01-24 18:01:49 +02:00
Pete Matsyburka 264897bbd5 audit with sender 2026-01-23 12:12:37 +02:00
Pete Matsyburka 67d59ff1ba fix invite by order 2026-01-22 21:20:34 +02:00
Pete Matsyburka 81eee56e1e i18n 2026-01-21 20:14:53 +02:00
Pete Matsyburka 92ea8af786 fix options drag 2026-01-21 16:02:20 +02:00
Pete Matsyburka df0616f77e paste options 2026-01-21 15:42:38 +02:00
Pete Matsyburka a337dff85c change model 2026-01-21 10:23:26 +02:00
Pete Matsyburka 48e349c1d2 fix signature pad when resize 2026-01-20 17:24:05 +02:00
Pete Matsyburka df9fa0fec7 fix scroll into field 2026-01-20 15:27:35 +02:00
Alex Turchyn b3e72f0726 Merge from docusealco/wip 2026-01-19 15:40:56 +02:00
Pete Matsyburka 700ebc4727 render reason 2026-01-19 13:56:17 +02:00
Pete Matsyburka 745a18efca fix select mode draw 2026-01-19 11:57:09 +02:00
Pete Matsyburka b4a8147ff0 fix radio group paste 2026-01-19 11:51:00 +02:00
Pete Matsyburka 0543c25054 detect fields in context menu 2026-01-19 09:23:04 +02:00
Pete Matsyburka b1a0afea66 fix menu 2026-01-18 10:29:15 +02:00
Pete Matsyburka 43c41e2557 add select fields mode 2026-01-18 09:52:43 +02:00
Pete Matsyburka bc8eb33f05 Revert "hide area name menu on desktop"
This reverts commit 5212773d0a.
2026-01-17 19:42:27 +02:00
Pete Matsyburka d1cebc92f7 fix eslint 2026-01-16 12:38:29 +02:00
Pete Matsyburka bf02cf4058 fix paste 2026-01-16 12:25:50 +02:00
Pete Matsyburka 5212773d0a hide area name menu on desktop 2026-01-16 12:18:17 +02:00
Alex Turchyn bf53204a41 add context menu to template builder 2026-01-16 12:07:59 +02:00
Pete Matsyburka 092a19966c adjust condition 2026-01-15 17:34:05 +02:00
Pete Matsyburka 6060a2b798 fix icon 2026-01-14 21:58:30 +02:00
Pete Matsyburka ce12af68b2 fix required 2026-01-14 14:32:00 +02:00
Pete Matsyburka c05f8d47aa require 2fa 2026-01-14 12:44:27 +02:00
Pete Matsyburka f8be19c20e pg 18 2026-01-13 20:44:52 +02:00
Pete Matsyburka 2fe32a2145 show version 2026-01-13 12:25:08 +02:00
Pete Matsyburka 794ea4d0e8 remove expired from pending filter 2026-01-13 12:07:39 +02:00
Pete Matsyburka 307b0f5afd fix build 2026-01-13 09:44:58 +02:00
202 changed files with 8529 additions and 2017 deletions

No files matched your search

+6 -6
View File
@@ -12,7 +12,7 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.4.2 ruby-version: 4.0.1
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -37,7 +37,7 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.4.2 ruby-version: 4.0.1
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -62,7 +62,7 @@ jobs:
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 20.9.0 node-version: 20.19.0
- name: Cache directory path - name: Cache directory path
id: yarn-cache-dir-path id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)" run: echo "::set-output name=dir::$(yarn cache dir)"
@@ -89,7 +89,7 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.4.2 ruby-version: 4.0.1
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -132,11 +132,11 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.4.2 ruby-version: 4.0.1
- name: Set up Node - name: Set up Node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 20.9.0 node-version: 20.19.0
- name: Install Chrome - name: Install Chrome
uses: browser-actions/setup-chrome@latest uses: browser-actions/setup-chrome@latest
with: with:
+16 -3
View File
@@ -1,4 +1,4 @@
require: plugins:
- rubocop-performance - rubocop-performance
- rubocop-rails - rubocop-rails
- rubocop-rspec - rubocop-rspec
@@ -10,7 +10,7 @@ AllCops:
- node_modules/**/* - node_modules/**/*
- bin/* - bin/*
- vendor/**/* - vendor/**/*
TargetRubyVersion: '3.3' TargetRubyVersion: '4.0'
SuggestExtensions: false SuggestExtensions: false
Metrics/BlockLength: Metrics/BlockLength:
@@ -84,7 +84,7 @@ RSpec/AnyInstance:
Enabled: false Enabled: false
Metrics/BlockNesting: Metrics/BlockNesting:
Max: 5 Max: 6
Rails/I18nLocaleTexts: Rails/I18nLocaleTexts:
Enabled: false Enabled: false
@@ -100,3 +100,16 @@ Rails/ApplicationController:
Rails/Output: Rails/Output:
Enabled: false Enabled: false
Rails/StrongParametersExpect:
Enabled: false
Rails/RedirectBackOrTo:
Enabled: false
Rails/UnknownEnv:
Environments:
- development
- test
- production
- local
+26 -28
View File
@@ -1,4 +1,4 @@
FROM ruby:3.4.2-alpine AS download FROM ruby:4.0.1-alpine AS download
WORKDIR /fonts WORKDIR /fonts
@@ -9,14 +9,14 @@ RUN apk --no-cache add fontforge wget && \
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \ wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \ wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \ wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.1.0/model_704_int8.onnx" && \ wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.0.0/model_704_int8.onnx" && \
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \ wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
mkdir -p /pdfium-linux && \ mkdir -p /pdfium-linux && \
tar -xzf pdfium-linux.tgz -C /pdfium-linux tar -xzf pdfium-linux.tgz -C /pdfium-linux
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")' RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
FROM ruby:3.4.2-alpine AS webpack FROM ruby:4.0.1-alpine AS webpack
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV NODE_ENV=production ENV NODE_ENV=production
@@ -42,16 +42,17 @@ COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:3.4.2-alpine AS app FROM ruby:4.0.1-alpine AS app
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test" ENV BUNDLE_WITHOUT="development:test"
ENV LD_PRELOAD=/lib/libgcompat.so.0 ENV LD_PRELOAD=/lib/libgcompat.so.0
ENV OPENSSL_CONF=/app/openssl_legacy.cnf ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
ENV VIPS_MAX_COORD=15000
WORKDIR /app WORKDIR /app
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf RUN apk add --no-cache sqlite-dev libpq-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont onnxruntime && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
@@ -65,36 +66,33 @@ legacy = legacy_sect\n\
activate = 1\n\ activate = 1\n\
\n\ \n\
[legacy_sect]\n\ [legacy_sect]\n\
activate = 1' >> /app/openssl_legacy.cnf activate = 1' >> /etc/openssl_legacy.cnf
COPY ./Gemfile ./Gemfile.lock ./ COPY --chown=docuseal:docuseal ./Gemfile ./Gemfile.lock ./
RUN apk add --no-cache build-base && bundle install && apk del --no-cache build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first") RUN apk add --no-cache build-base git && bundle install && apk del --no-cache build-base git && 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 && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
RUN echo 'https://dl-cdn.alpinelinux.org/alpine/edge/main' >> /etc/apk/repositories && echo 'https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache onnxruntime COPY --chown=docuseal:docuseal ./bin ./bin
COPY --chown=docuseal:docuseal ./app ./app
COPY --chown=docuseal:docuseal ./config ./config
COPY --chown=docuseal:docuseal ./db/migrate ./db/migrate
COPY --chown=docuseal:docuseal ./log ./log
COPY --chown=docuseal:docuseal ./lib ./lib
COPY --chown=docuseal:docuseal ./public ./public
COPY --chown=docuseal:docuseal ./tmp ./tmp
COPY --chown=docuseal:docuseal LICENSE LICENSE_ADDITIONAL_TERMS README.md Rakefile config.ru .version ./
COPY --chown=docuseal:docuseal .version ./public/version
COPY ./bin ./bin COPY --chown=docuseal:docuseal --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
COPY ./app ./app
COPY ./config ./config
COPY ./db/migrate ./db/migrate
COPY ./log ./log
COPY ./lib ./lib
COPY ./public ./public
COPY ./tmp ./tmp
COPY LICENSE README.md Rakefile config.ru .version ./
COPY .version ./public/version
COPY --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
COPY --from=download /model.onnx /app/tmp/model.onnx COPY --chown=docuseal:docuseal --from=download /model.onnx /app/tmp/model.onnx
COPY --from=webpack /app/public/packs ./public/packs COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs
RUN ln -s /fonts /app/public/fonts RUN ln -s /fonts /app/public/fonts && \
RUN bundle exec bootsnap precompile -j 1 --gemfile app/ lib/ bundle exec bootsnap precompile -j 1 --gemfile app/ lib/ && \
chown -R docuseal:docuseal /app/tmp/cache
RUN chown -R docuseal:docuseal /app
WORKDIR /data/docuseal WORKDIR /data/docuseal
ENV HOME=/home/docuseal ENV HOME=/home/docuseal
+11 -12
View File
@@ -2,16 +2,16 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '3.4.2' ruby '4.0.1'
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic' gem 'arabic-letter-connector', require: false
gem 'aws-sdk-s3', require: false gem 'aws-sdk-s3', require: false
gem 'aws-sdk-secretsmanager', require: false gem 'aws-sdk-secretsmanager', require: false
gem 'azure-storage-blob', require: false gem 'azure-blob', require: false
gem 'bootsnap', require: false gem 'bootsnap', require: false
gem 'cancancan' gem 'cancancan'
gem 'csv' gem 'csv', require: false
gem 'csv-safe' gem 'csv-safe', require: false
gem 'devise' gem 'devise'
gem 'devise-two-factor' gem 'devise-two-factor'
gem 'dotenv', require: false gem 'dotenv', require: false
@@ -21,12 +21,11 @@ gem 'faraday-follow_redirects'
gem 'google-cloud-storage', require: false gem 'google-cloud-storage', require: false
gem 'hexapdf' gem 'hexapdf'
gem 'image_processing' gem 'image_processing'
gem 'jwt' gem 'jwt', require: false
gem 'lograge' gem 'lograge'
gem 'mysql2', require: false gem 'numo-narray-alt', require: false
gem 'numo-narray'
gem 'oj' gem 'oj'
gem 'onnxruntime' gem 'onnxruntime', require: false
gem 'pagy' gem 'pagy'
gem 'pg', require: false gem 'pg', require: false
gem 'premailer-rails' gem 'premailer-rails'
@@ -34,17 +33,17 @@ gem 'pretender'
gem 'puma', require: false gem 'puma', require: false
gem 'rack' gem 'rack'
gem 'rails' gem 'rails'
gem 'rails_autolink'
gem 'rails-i18n' gem 'rails-i18n'
gem 'rotp' gem 'rotp'
gem 'rouge', require: false gem 'rouge', require: false
gem 'rqrcode' gem 'rqrcode', require: false
gem 'ruby-vips' gem 'ruby-vips'
gem 'rubyXL' gem 'rubyXL', require: false
gem 'shakapacker' gem 'shakapacker'
gem 'sidekiq' gem 'sidekiq'
gem 'sqlite3', require: false gem 'sqlite3', require: false
gem 'strip_attributes' gem 'strip_attributes'
gem 'trilogy', github: 'trilogy-libraries/trilogy', glob: 'contrib/ruby/*.gemspec', require: false
gem 'turbo-rails' gem 'turbo-rails'
gem 'twitter_cldr', require: false gem 'twitter_cldr', require: false
gem 'tzinfo-data' gem 'tzinfo-data'
+284 -292
View File
@@ -1,29 +1,39 @@
GIT
remote: https://github.com/trilogy-libraries/trilogy.git
revision: 3963d490459df7a2b5bedb42424c3285f25eab22
glob: contrib/ruby/*.gemspec
specs:
trilogy (2.10.0)
bigdecimal
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (8.0.2.1) action_text-trix (2.1.16)
actionpack (= 8.0.2.1) railties
activesupport (= 8.0.2.1) actioncable (8.1.2)
actionpack (= 8.1.2)
activesupport (= 8.1.2)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (8.0.2.1) actionmailbox (8.1.2)
actionpack (= 8.0.2.1) actionpack (= 8.1.2)
activejob (= 8.0.2.1) activejob (= 8.1.2)
activerecord (= 8.0.2.1) activerecord (= 8.1.2)
activestorage (= 8.0.2.1) activestorage (= 8.1.2)
activesupport (= 8.0.2.1) activesupport (= 8.1.2)
mail (>= 2.8.0) mail (>= 2.8.0)
actionmailer (8.0.2.1) actionmailer (8.1.2)
actionpack (= 8.0.2.1) actionpack (= 8.1.2)
actionview (= 8.0.2.1) actionview (= 8.1.2)
activejob (= 8.0.2.1) activejob (= 8.1.2)
activesupport (= 8.0.2.1) activesupport (= 8.1.2)
mail (>= 2.8.0) mail (>= 2.8.0)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (8.0.2.1) actionpack (8.1.2)
actionview (= 8.0.2.1) actionview (= 8.1.2)
activesupport (= 8.0.2.1) activesupport (= 8.1.2)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
rack (>= 2.2.4) rack (>= 2.2.4)
rack-session (>= 1.0.1) rack-session (>= 1.0.1)
@@ -31,55 +41,58 @@ GEM
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) useragent (~> 0.16)
actiontext (8.0.2.1) actiontext (8.1.2)
actionpack (= 8.0.2.1) action_text-trix (~> 2.1.15)
activerecord (= 8.0.2.1) actionpack (= 8.1.2)
activestorage (= 8.0.2.1) activerecord (= 8.1.2)
activesupport (= 8.0.2.1) activestorage (= 8.1.2)
activesupport (= 8.1.2)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (8.0.2.1) actionview (8.1.2)
activesupport (= 8.0.2.1) activesupport (= 8.1.2)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activejob (8.0.2.1) activejob (8.1.2)
activesupport (= 8.0.2.1) activesupport (= 8.1.2)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (8.0.2.1) activemodel (8.1.2)
activesupport (= 8.0.2.1) activesupport (= 8.1.2)
activerecord (8.0.2.1) activerecord (8.1.2)
activemodel (= 8.0.2.1) activemodel (= 8.1.2)
activesupport (= 8.0.2.1) activesupport (= 8.1.2)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (8.0.2.1) activestorage (8.1.2)
actionpack (= 8.0.2.1) actionpack (= 8.1.2)
activejob (= 8.0.2.1) activejob (= 8.1.2)
activerecord (= 8.0.2.1) activerecord (= 8.1.2)
activesupport (= 8.0.2.1) activesupport (= 8.1.2)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (8.0.2.1) activesupport (8.1.2)
base64 base64
benchmark (>= 0.3)
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1) concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5) connection_pool (>= 2.2.5)
drb drb
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
json
logger (>= 1.4.2) logger (>= 1.4.2)
minitest (>= 5.1) minitest (>= 5.1)
securerandom (>= 0.3) securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5) tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1) uri (>= 0.13.1)
addressable (2.8.7) addressable (2.8.8)
public_suffix (>= 2.0.2, < 7.0) public_suffix (>= 2.0.2, < 8.0)
annotaterb (4.14.0) annotaterb (4.20.0)
activerecord (>= 6.0.0)
activesupport (>= 6.0.0)
arabic-letter-connector (0.1.1) arabic-letter-connector (0.1.1)
ast (2.4.3) ast (2.4.3)
aws-eventstream (1.4.0) aws-eventstream (1.4.0)
aws-partitions (1.1197.0) aws-partitions (1.1209.0)
aws-sdk-core (3.240.0) aws-sdk-core (3.241.4)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0) aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9) aws-sigv4 (~> 1.9)
@@ -87,44 +100,38 @@ GEM
bigdecimal bigdecimal
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
logger logger
aws-sdk-kms (1.118.0) aws-sdk-kms (1.121.0)
aws-sdk-core (~> 3, >= 3.239.1) aws-sdk-core (~> 3, >= 3.241.4)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.208.0) aws-sdk-s3 (1.212.0)
aws-sdk-core (~> 3, >= 3.234.0) aws-sdk-core (~> 3, >= 3.241.4)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sdk-secretsmanager (1.110.0) aws-sdk-secretsmanager (1.128.0)
aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-core (~> 3, >= 3.241.4)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1) aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3) azure-blob (0.8.0)
azure-storage-common (~> 2.0) cgi
nokogiri (~> 1, >= 1.10.8) rexml
azure-storage-common (2.0.4)
faraday (~> 1.0)
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
net-http-persistent (~> 4.0)
nokogiri (~> 1, >= 1.10.8)
base64 (0.3.0) base64 (0.3.0)
bcrypt (3.1.20) bcrypt (3.1.21)
benchmark (0.4.1) better_html (2.2.0)
better_html (2.1.1) actionview (>= 7.0)
actionview (>= 6.0) activesupport (>= 7.0)
activesupport (>= 6.0)
ast (~> 2.0) ast (~> 2.0)
erubi (~> 1.4) erubi (~> 1.4)
parser (>= 2.4) parser (>= 2.4)
smart_properties smart_properties
bigdecimal (3.2.2) bigdecimal (4.0.1)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.18.4) bootsnap (1.21.1)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (7.0.0) brakeman (7.1.2)
racc racc
builder (3.3.0) builder (3.3.0)
bullet (8.0.0) bullet (8.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
camertron-eprun (1.1.1) camertron-eprun (1.1.1)
@@ -138,28 +145,29 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
cgi (0.5.1)
childprocess (5.1.0) childprocess (5.1.0)
logger (~> 1.5) logger (~> 1.5)
chunky_png (1.4.0) chunky_png (1.4.0)
cldr-plurals-runtime-rb (1.1.0) cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7) cmdparse (3.0.7)
coderay (1.1.3) coderay (1.1.3)
concurrent-ruby (1.3.5) concurrent-ruby (1.3.6)
connection_pool (2.5.3) connection_pool (3.0.2)
crack (1.0.0) crack (1.0.1)
bigdecimal bigdecimal
rexml rexml
crass (1.0.6) crass (1.0.6)
css_parser (1.21.0) css_parser (1.21.1)
addressable addressable
csv (3.3.2) csv (3.3.5)
csv-safe (3.3.1) csv-safe (3.3.1)
csv (~> 3.0) csv (~> 3.0)
cuprite (0.15.1) cuprite (0.17)
capybara (~> 3.0) capybara (~> 3.0)
ferrum (~> 0.15.0) ferrum (~> 0.17.0)
date (3.4.1) date (3.5.1)
debug (1.10.0) debug (1.11.1)
irb (~> 1.10) irb (~> 1.10)
reline (>= 0.3.8) reline (>= 0.3.8)
declarative (0.0.20) declarative (0.0.20)
@@ -169,20 +177,20 @@ GEM
railties (>= 4.1.0) railties (>= 4.1.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise-two-factor (6.1.0) devise-two-factor (6.3.1)
activesupport (>= 7.0, < 8.1) activesupport (>= 7.0, < 8.2)
devise (~> 4.0) devise (>= 4.0, < 5.0)
railties (>= 7.0, < 8.1) railties (>= 7.0, < 8.2)
rotp (~> 6.0) rotp (~> 6.0)
diff-lcs (1.5.1) diff-lcs (1.6.2)
digest-crc (0.6.5) digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
docile (1.4.1) docile (1.4.1)
dotenv (3.1.7) dotenv (3.2.0)
drb (2.2.3) drb (2.2.3)
email_typo (0.2.3) email_typo (0.2.3)
erb (5.0.2) erb (6.0.1)
erb_lint (0.7.0) erb_lint (0.9.0)
activesupport activesupport
better_html (>= 2.0.1) better_html (>= 2.0.1)
parser (>= 2.7.1.4) parser (>= 2.7.1.4)
@@ -190,117 +198,100 @@ GEM
rubocop (>= 1) rubocop (>= 1)
smart_properties smart_properties
erubi (1.13.1) erubi (1.13.1)
factory_bot (6.5.0) factory_bot (6.5.6)
activesupport (>= 5.0.0) activesupport (>= 6.1.0)
factory_bot_rails (6.4.4) factory_bot_rails (6.5.1)
factory_bot (~> 6.5) factory_bot (~> 6.5)
railties (>= 5.0.0) railties (>= 6.1.0)
faker (3.5.1) faker (3.6.0)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (1.10.4) faraday (2.14.1)
faraday-em_http (~> 1.0) faraday-net_http (>= 2.0, < 3.5)
faraday-em_synchrony (~> 1.0) json
faraday-excon (~> 1.1) logger
faraday-httpclient (~> 1.0) faraday-follow_redirects (0.5.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3) faraday (>= 1, < 3)
faraday-httpclient (1.0.1) faraday-net_http (3.4.2)
faraday-multipart (1.1.0) net-http (~> 0.5)
multipart-post (~> 2.0) ferrum (0.17.1)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday (~> 1.0)
ferrum (0.15)
addressable (~> 2.5) addressable (~> 2.5)
base64 (~> 0.2)
concurrent-ruby (~> 1.1) concurrent-ruby (~> 1.1)
webrick (~> 1.7) webrick (~> 1.7)
websocket-driver (~> 0.7) websocket-driver (~> 0.7)
ffi (1.17.1) ffi (1.17.3-aarch64-linux-gnu)
ffi (1.17.1-aarch64-linux-gnu) ffi (1.17.3-aarch64-linux-musl)
ffi (1.17.1-aarch64-linux-musl) ffi (1.17.3-arm64-darwin)
ffi (1.17.1-arm64-darwin) ffi (1.17.3-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-gnu) ffi (1.17.3-x86_64-linux-musl)
ffi (1.17.1-x86_64-linux-musl) foreman (0.90.0)
foreman (0.88.1) thor (~> 1.4)
geom2d (0.4.1) geom2d (0.4.1)
globalid (1.2.1) globalid (1.3.0)
activesupport (>= 6.1) activesupport (>= 6.1)
google-apis-core (0.15.1) google-apis-core (1.0.2)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.8, >= 2.8.7)
googleauth (~> 1.9) faraday (~> 2.13)
httpclient (>= 2.8.3, < 3.a) faraday-follow_redirects (~> 0.3)
mini_mime (~> 1.0) googleauth (~> 1.14)
mutex_m mini_mime (~> 1.1)
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.a) retriable (~> 3.1)
google-apis-iamcredentials_v1 (0.22.0) google-apis-iamcredentials_v1 (0.26.0)
google-apis-core (>= 0.15.0, < 2.a) google-apis-core (>= 0.15.0, < 2.a)
google-apis-storage_v1 (0.49.0) google-apis-storage_v1 (0.59.0)
google-apis-core (>= 0.15.0, < 2.a) google-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.7.1) google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a) google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (2.2.1) google-cloud-env (2.3.1)
base64 (~> 0.2)
faraday (>= 1.0, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-errors (1.4.0) google-cloud-errors (1.5.0)
google-cloud-storage (1.54.0) google-cloud-storage (1.58.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-core (~> 0.13) google-apis-core (>= 0.18, < 2)
google-apis-iamcredentials_v1 (~> 0.18) google-apis-iamcredentials_v1 (~> 0.18)
google-apis-storage_v1 (~> 0.38) google-apis-storage_v1 (>= 0.42)
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (~> 1.9) googleauth (~> 1.9)
mini_mime (~> 1.0) mini_mime (~> 1.0)
google-logging-utils (0.1.0) google-logging-utils (0.2.0)
googleauth (1.12.2) googleauth (1.16.1)
faraday (>= 1.0, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.2) google-cloud-env (~> 2.2)
google-logging-utils (~> 0.1) google-logging-utils (~> 0.1)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 4.0)
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.1.2) hashdiff (1.2.1)
hexapdf (1.4.0) hexapdf (1.5.0)
cmdparse (~> 3.0, >= 3.0.3) cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.4, >= 0.4.1) geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1) openssl (>= 2.2.1)
strscan (>= 3.1.2) strscan (>= 3.1.2)
htmlentities (4.3.4) htmlentities (4.4.2)
httpclient (2.8.3) i18n (1.14.8)
i18n (1.14.7)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.13.0) image_processing (1.14.0)
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 6)
ruby-vips (>= 2.0.17, < 3) ruby-vips (>= 2.0.17, < 3)
io-console (0.8.1) io-console (0.8.2)
irb (1.15.2) irb (1.16.0)
pp (>= 0.6.0) pp (>= 0.6.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
json (2.15.0) json (2.18.1)
jwt (2.9.3) jwt (3.1.2)
base64 base64
language_server-protocol (3.17.0.5) language_server-protocol (3.17.0.5)
launchy (3.0.1) launchy (3.1.1)
addressable (~> 2.8) addressable (~> 2.8)
childprocess (~> 5.0) childprocess (~> 5.0)
logger (~> 1.6)
letter_opener (1.10.0) letter_opener (1.10.0)
launchy (>= 2.2, < 4) launchy (>= 2.2, < 4)
letter_opener_web (3.0.0) letter_opener_web (3.0.0)
@@ -315,29 +306,28 @@ GEM
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.24.1) loofah (2.25.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.8.1) mail (2.9.0)
logger
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
marcel (1.0.4) marcel (1.1.0)
matrix (0.4.2) matrix (0.4.3)
method_source (1.1.0) method_source (1.1.0)
mini_magick (4.13.2) mini_magick (5.3.1)
logger
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.9) minitest (6.0.1)
minitest (5.25.5) prism (~> 1.5)
msgpack (1.7.5) msgpack (1.8.0)
multi_json (1.15.0) multi_json (1.19.1)
multipart-post (2.4.1) net-http (0.9.1)
mutex_m (0.3.0) uri (>= 0.11.1)
mysql2 (0.5.6) net-imap (0.6.2)
net-http-persistent (4.0.5)
connection_pool (~> 2.2)
net-imap (0.5.9)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
@@ -346,44 +336,45 @@ GEM
timeout timeout
net-smtp (0.5.1) net-smtp (0.5.1)
net-protocol net-protocol
nio4r (2.7.4) nio4r (2.7.5)
nokogiri (1.18.9) nokogiri (1.19.1-aarch64-linux-gnu)
mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.9-aarch64-linux-gnu) nokogiri (1.19.1-aarch64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.9-aarch64-linux-musl) nokogiri (1.19.1-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.9-arm64-darwin) nokogiri (1.19.1-x86_64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.9-x86_64-linux-gnu) nokogiri (1.19.1-x86_64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.9-x86_64-linux-musl) numo-narray-alt (0.9.13)
racc (~> 1.4) oj (3.16.13)
numo-narray (0.9.2.1)
oj (3.16.11)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
ostruct (>= 0.2) ostruct (>= 0.2)
onnxruntime (0.10.1)
ffi
onnxruntime (0.10.1-aarch64-linux) onnxruntime (0.10.1-aarch64-linux)
ffi ffi
onnxruntime (0.10.1-arm64-darwin) onnxruntime (0.10.1-arm64-darwin)
ffi ffi
onnxruntime (0.10.1-x86_64-linux) onnxruntime (0.10.1-x86_64-linux)
ffi ffi
openssl (3.3.0) openssl (4.0.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.1.4) os (1.1.4)
ostruct (0.6.3) ostruct (0.6.3)
package_json (0.1.0) package_json (0.2.0)
pagy (9.3.3) pagy (43.2.8)
json
yaml
parallel (1.27.0) parallel (1.27.0)
parser (3.3.9.0) parser (3.3.10.1)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pg (1.5.9) pg (1.6.3-aarch64-linux)
pp (0.6.2) pg (1.6.3-aarch64-linux-musl)
pg (1.6.3-arm64-darwin)
pg (1.6.3-x86_64-linux)
pg (1.6.3-x86_64-linux-musl)
pp (0.6.3)
prettyprint prettyprint
premailer (1.27.0) premailer (1.27.0)
addressable addressable
@@ -393,23 +384,24 @@ 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) pretender (0.6.0)
actionpack (>= 6.1) actionpack (>= 7.1)
prettyprint (0.2.0) prettyprint (0.2.0)
prism (1.5.1) prism (1.8.0)
pry (0.15.0) pry (0.16.0)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
reline (>= 0.6.0)
pry-rails (0.3.11) pry-rails (0.3.11)
pry (>= 0.13.0) pry (>= 0.13.0)
psych (5.2.6) psych (5.3.1)
date date
stringio stringio
public_suffix (6.0.1) public_suffix (7.0.2)
puma (6.5.0) puma (7.2.0)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.8.1) racc (1.8.1)
rack (3.2.3) rack (3.2.5)
rack-proxy (0.7.7) rack-proxy (0.7.7)
rack rack
rack-session (2.1.1) rack-session (2.1.1)
@@ -417,22 +409,22 @@ GEM
rack (>= 3.0.0) rack (>= 3.0.0)
rack-test (2.2.0) rack-test (2.2.0)
rack (>= 1.3) rack (>= 1.3)
rackup (2.2.1) rackup (2.3.1)
rack (>= 3) rack (>= 3)
rails (8.0.2.1) rails (8.1.2)
actioncable (= 8.0.2.1) actioncable (= 8.1.2)
actionmailbox (= 8.0.2.1) actionmailbox (= 8.1.2)
actionmailer (= 8.0.2.1) actionmailer (= 8.1.2)
actionpack (= 8.0.2.1) actionpack (= 8.1.2)
actiontext (= 8.0.2.1) actiontext (= 8.1.2)
actionview (= 8.0.2.1) actionview (= 8.1.2)
activejob (= 8.0.2.1) activejob (= 8.1.2)
activemodel (= 8.0.2.1) activemodel (= 8.1.2)
activerecord (= 8.0.2.1) activerecord (= 8.1.2)
activestorage (= 8.0.2.1) activestorage (= 8.1.2)
activesupport (= 8.0.2.1) activesupport (= 8.1.2)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 8.0.2.1) railties (= 8.1.2)
rails-dom-testing (2.3.0) rails-dom-testing (2.3.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
@@ -440,30 +432,28 @@ GEM
rails-html-sanitizer (1.6.2) rails-html-sanitizer (1.6.2)
loofah (~> 2.21) loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-i18n (8.0.1) rails-i18n (8.1.0)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9) railties (>= 8.0.0, < 9)
rails_autolink (1.1.8) railties (8.1.2)
actionview (> 3.1) actionpack (= 8.1.2)
activesupport (> 3.1) activesupport (= 8.1.2)
railties (> 3.1)
railties (8.0.2.1)
actionpack (= 8.0.2.1)
activesupport (= 8.0.2.1)
irb (~> 1.13) irb (~> 1.13)
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0, >= 1.2.2) thor (~> 1.0, >= 1.2.2)
tsort (>= 0.2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.3.0) rake (13.3.1)
rdoc (6.14.2) rdoc (7.1.0)
erb erb
psych (>= 4.0.0) psych (>= 4.0.0)
redis-client (0.23.0) tsort
redis-client (0.26.4)
connection_pool connection_pool
regexp_parser (2.11.3) regexp_parser (2.11.3)
reline (0.6.2) reline (0.6.3)
io-console (~> 0.5) io-console (~> 0.5)
representable (3.2.0) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
@@ -471,35 +461,35 @@ GEM
uber (< 0.2.0) uber (< 0.2.0)
request_store (1.7.0) request_store (1.7.0)
rack (>= 1.4) rack (>= 1.4)
responders (3.1.1) responders (3.2.0)
actionpack (>= 5.2) actionpack (>= 7.0)
railties (>= 5.2) railties (>= 7.0)
retriable (3.1.2) retriable (3.1.2)
rexml (3.4.4) rexml (3.4.4)
rotp (6.3.0) rotp (6.3.0)
rouge (4.5.2) rouge (4.7.0)
rqrcode (2.2.0) rqrcode (3.2.0)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 1.0) rqrcode_core (~> 2.0)
rqrcode_core (1.2.0) rqrcode_core (2.1.0)
rspec-core (3.13.2) rspec-core (3.13.6)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-expectations (3.13.3) rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-mocks (3.13.2) rspec-mocks (3.13.7)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-rails (7.1.0) rspec-rails (8.0.2)
actionpack (>= 7.0) actionpack (>= 7.2)
activesupport (>= 7.0) activesupport (>= 7.2)
railties (>= 7.0) railties (>= 7.2)
rspec-core (~> 3.13) rspec-core (~> 3.13)
rspec-expectations (~> 3.13) rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13) rspec-mocks (~> 3.13)
rspec-support (~> 3.13) rspec-support (~> 3.13)
rspec-support (3.13.2) rspec-support (3.13.6)
rubocop (1.81.1) rubocop (1.82.1)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0) lint_roller (~> 1.1.0)
@@ -507,86 +497,89 @@ GEM
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0) regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.47.1, < 2.0) rubocop-ast (>= 1.48.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0) unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.47.1) rubocop-ast (1.49.0)
parser (>= 3.3.7.2) parser (>= 3.3.7.2)
prism (~> 1.4) prism (~> 1.7)
rubocop-performance (1.23.0) rubocop-performance (1.26.1)
rubocop (>= 1.48.1, < 2.0) lint_roller (~> 1.1)
rubocop-ast (>= 1.31.1, < 2.0) rubocop (>= 1.75.0, < 2.0)
rubocop-rails (2.27.0) rubocop-ast (>= 1.47.1, < 2.0)
rubocop-rails (2.34.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.52.0, < 2.0) rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.44.0, < 2.0)
rubocop-rspec (3.3.0) rubocop-rspec (3.9.0)
rubocop (~> 1.61) lint_roller (~> 1.1)
rubocop (~> 1.81)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-vips (2.2.2) ruby-vips (2.3.0)
ffi (~> 1.12) ffi (~> 1.12)
logger logger
ruby2_keywords (0.0.5) rubyXL (3.4.35)
rubyXL (3.4.33)
nokogiri (>= 1.10.8) nokogiri (>= 1.10.8)
rubyzip (>= 1.3.0) rubyzip (>= 3.2.2)
rubyzip (2.3.2) rubyzip (3.2.2)
securerandom (0.4.1) securerandom (0.4.1)
semantic_range (3.1.0) semantic_range (3.1.0)
shakapacker (8.0.2) shakapacker (9.5.0)
activesupport (>= 5.2) activesupport (>= 5.2)
package_json package_json
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) semantic_range (>= 2.3.0)
sidekiq (7.3.7) sidekiq (8.1.0)
connection_pool (>= 2.3.0) connection_pool (>= 3.0.0)
logger json (>= 2.16.0)
rack (>= 2.2.4) logger (>= 1.7.0)
redis-client (>= 0.22.2) rack (>= 3.2.0)
signet (0.19.0) redis-client (>= 0.26.0)
signet (0.21.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, < 4.0)
multi_json (~> 1.10) multi_json (~> 1.10)
simplecov (0.22.0) simplecov (0.22.0)
docile (~> 1.1) docile (~> 1.1)
simplecov-html (~> 0.11) simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1) simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.1) simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4) simplecov_json_formatter (0.1.4)
smart_properties (1.17.0) smart_properties (1.17.0)
sqlite3 (2.5.0) sqlite3 (2.9.0-aarch64-linux-gnu)
mini_portile2 (~> 2.8.0) sqlite3 (2.9.0-aarch64-linux-musl)
sqlite3 (2.5.0-aarch64-linux-gnu) sqlite3 (2.9.0-arm64-darwin)
sqlite3 (2.5.0-aarch64-linux-musl) sqlite3 (2.9.0-x86_64-linux-gnu)
sqlite3 (2.5.0-arm64-darwin) sqlite3 (2.9.0-x86_64-linux-musl)
sqlite3 (2.5.0-x86_64-linux-gnu) stringio (3.2.0)
sqlite3 (2.5.0-x86_64-linux-musl) strip_attributes (2.0.1)
stringio (3.1.7)
strip_attributes (1.14.1)
activemodel (>= 3.0, < 9.0) activemodel (>= 3.0, < 9.0)
strscan (3.1.5) strscan (3.1.7)
thor (1.4.0) thor (1.5.0)
timeout (0.4.3) timeout (0.6.0)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
turbo-rails (2.0.11) tsort (0.2.0)
actionpack (>= 6.0.0) turbo-rails (2.0.21)
railties (>= 6.0.0) actionpack (>= 7.1.0)
twitter_cldr (6.12.1) railties (>= 7.1.0)
twitter_cldr (6.14.0)
base64
camertron-eprun camertron-eprun
cldr-plurals-runtime-rb (~> 1.1) cldr-plurals-runtime-rb (~> 1.1)
tzinfo tzinfo
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
tzinfo-data (1.2024.2) tzinfo-data (1.2025.3)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
uber (0.1.0) uber (0.1.0)
unicode-display_width (3.2.0) unicode-display_width (3.2.0)
unicode-emoji (~> 4.1) unicode-emoji (~> 4.1)
unicode-emoji (4.1.0) unicode-emoji (4.2.0)
uniform_notifier (1.16.0) uniform_notifier (1.18.0)
uri (1.1.1) uri (1.1.1)
useragent (0.16.11) useragent (0.16.11)
warden (1.2.9) warden (1.2.9)
@@ -596,24 +589,24 @@ GEM
activemodel (>= 6.0.0) activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
webmock (3.24.0) webmock (3.26.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)
webrick (1.9.1) webrick (1.9.2)
websocket-driver (0.8.0) websocket-driver (0.8.0)
base64 base64
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
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.7.3) yaml (0.4.0)
zeitwerk (2.7.4)
PLATFORMS PLATFORMS
aarch64-linux aarch64-linux
aarch64-linux-musl aarch64-linux-musl
arm64-darwin arm64-darwin
ruby
x86_64-linux x86_64-linux
x86_64-linux-musl x86_64-linux-musl
@@ -622,7 +615,7 @@ DEPENDENCIES
arabic-letter-connector arabic-letter-connector
aws-sdk-s3 aws-sdk-s3
aws-sdk-secretsmanager aws-sdk-secretsmanager
azure-storage-blob azure-blob
better_html better_html
bootsnap bootsnap
brakeman brakeman
@@ -649,8 +642,7 @@ DEPENDENCIES
jwt jwt
letter_opener_web letter_opener_web
lograge lograge
mysql2 numo-narray-alt
numo-narray
oj oj
onnxruntime onnxruntime
pagy pagy
@@ -662,7 +654,6 @@ DEPENDENCIES
rack rack
rails rails
rails-i18n rails-i18n
rails_autolink
rotp rotp
rouge rouge
rqrcode rqrcode
@@ -678,6 +669,7 @@ DEPENDENCIES
simplecov simplecov
sqlite3 sqlite3
strip_attributes strip_attributes
trilogy!
turbo-rails turbo-rails
twitter_cldr twitter_cldr
tzinfo-data tzinfo-data
@@ -685,7 +677,7 @@ DEPENDENCIES
webmock webmock
RUBY VERSION RUBY VERSION
ruby 3.4.2p28 ruby 4.0.1
BUNDLED WITH BUNDLED WITH
2.5.3 4.0.3
+5
View File
@@ -0,0 +1,5 @@
Additional Terms
In accordance with Section 7(b) of the GNU Affero General Public License,
a covered work must retain the original DocuSeal attribution in interactive
user interfaces.
+3 -3
View File
@@ -1,7 +1,7 @@
<h1 align="center" style="border-bottom: none"> <h1 align="center" style="border-bottom: none">
<div> <div>
<a href="https://www.docuseal.com"> <a href="https://www.docuseal.com">
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" /> <img alt="DocuSeal" src="https://github.com/user-attachments/assets/38b45682-ffa4-4919-abde-d2d422325c44" width="80" />
<br> <br>
</a> </a>
DocuSeal DocuSeal
@@ -97,8 +97,8 @@ At DocuSeal we have expertise and technologies to make documents creation, filli
## License ## License
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information. Distributed under the AGPLv3 License with Section 7(b) Additional Terms. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) and [LICENSE_ADDITIONAL_TERMS](https://github.com/docusealco/docuseal/blob/master/LICENSE_ADDITIONAL_TERMS) for more information.
Unless otherwise noted, all files © 2023 DocuSeal LLC. Unless otherwise noted, all files © 2023-2026 DocuSeal LLC.
## Tools ## Tools
@@ -0,0 +1,33 @@
# frozen_string_literal: true
class AccountCustomFieldsController < ApplicationController
before_action :load_account_config, only: :create
def create
authorize!(:create, Template)
@account_config.update!(account_config_params)
render json: @account_config.value
end
private
def load_account_config
@account_config =
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY)
end
def account_config_params
params.permit(
value: [[:uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
{ preferences: {},
default_value: [],
options: [%i[value uuid]],
validation: %i[message pattern min max step],
areas: [%i[x y w h cell_w option_uuid]] }]]
)
end
end
+1 -1
View File
@@ -3,7 +3,7 @@
module Api module Api
class ApiBaseController < ActionController::API class ApiBaseController < ActionController::API
include ActiveStorage::SetCurrent include ActiveStorage::SetCurrent
include Pagy::Backend include Pagy::Method
DEFAULT_LIMIT = 10 DEFAULT_LIMIT = 10
MAX_LIMIT = 100 MAX_LIMIT = 100
@@ -172,7 +172,10 @@ module Api
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters) Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters)
submitters.each do |submitter| submitters.each do |submitter|
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) if submitter.completed_at? if submitter.completed_at?
Submitters::SubmitValues.maybe_invite_via_field(submitter, request)
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request)
end
end end
submissions submissions
@@ -188,7 +191,7 @@ module Api
message: %i[subject body], message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role, submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last, :completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
:require_phone_2fa, :require_email_2fa, :order, :invite_by, :require_phone_2fa, :require_email_2fa, :order, :index, :invite_by,
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body], { metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
fields: [:name, :uuid, :default_value, :value, :title, :description, fields: [:name, :uuid, :default_value, :value, :title, :description,
:readonly, :required, :validation_pattern, :invalid_message, :readonly, :required, :validation_pattern, :invalid_message,
+6 -1
View File
@@ -34,6 +34,7 @@ module Api
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:) render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:)
end end
# rubocop:disable Metrics/MethodLength
def update def update
if @submitter.completed_at? if @submitter.completed_at?
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
@@ -60,7 +61,10 @@ module Api
@submitter.submission.save! @submitter.submission.save!
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) if @submitter.completed_at? if @submitter.completed_at?
Submitters::SubmitValues.maybe_invite_via_field(@submitter, request)
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request)
end
end end
if @submitter.completed_at? if @submitter.completed_at?
@@ -78,6 +82,7 @@ module Api
render json: { error: e.message }, status: :unprocessable_content render json: { error: e.message }, status: :unprocessable_content
end end
# rubocop:enable Metrics/MethodLength
def submitter_params def submitter_params
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
+2 -1
View File
@@ -107,7 +107,8 @@ module Api
:external_id, :external_id,
:shared_link, :shared_link,
{ {
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email order]], submitters: [%i[name uuid is_requester invite_by_uuid invite_via_field_uuid
optional_invite_by_uuid linked_to_uuid email order]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :prefillable, :title, :description, :prefillable,
+3 -7
View File
@@ -4,7 +4,7 @@ class ApplicationController < ActionController::Base
BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/ BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/
include ActiveStorage::SetCurrent include ActiveStorage::SetCurrent
include Pagy::Backend include Pagy::Method
check_authorization unless: :devise_controller? check_authorization unless: :devise_controller?
@@ -23,7 +23,7 @@ class ApplicationController < ActionController::Base
impersonates :user, with: ->(uuid) { User.find_by(uuid:) } impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
rescue_from Pagy::OverflowError do rescue_from Pagy::RangeError do
redirect_to request.path redirect_to request.path
end end
@@ -42,10 +42,6 @@ class ApplicationController < ActionController::Base
end end
def default_url_options def default_url_options
if request.domain == 'docuseal.com'
return { host: 'docuseal.com', protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
end
Docuseal.default_url_options Docuseal.default_url_options
end end
@@ -60,7 +56,7 @@ class ApplicationController < ActionController::Base
def pagy_auto(collection, **keyword_args) def pagy_auto(collection, **keyword_args)
if current_ability.can?(:manage, :countless) if current_ability.can?(:manage, :countless)
pagy_countless(collection, **keyword_args) pagy(:countless, collection, **keyword_args)
else else
pagy(collection, **keyword_args) pagy(collection, **keyword_args)
end end
@@ -27,20 +27,18 @@ class SubmissionsDownloadController < ApplicationController
Submissions::EnsureResultGenerated.call(last_submitter) Submissions::EnsureResultGenerated.call(last_submitter)
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter) if !signature_valid && !current_user_submitter?(last_submitter)
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar) return head :not_found unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
return head :not_found if last_submitter.completed_at < TTL.ago
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
return head :not_found
end
end end
if params[:combined] == 'true' if params[:combined] == 'true'
url = build_combined_url(@submitter) respond_with_combined(last_submitter)
if url
render json: [url]
else
head :not_found
end
else else
render json: build_urls(last_submitter) render json: build_urls(last_submitter)
end end
@@ -48,8 +46,18 @@ class SubmissionsDownloadController < ApplicationController
private private
def respond_with_combined(submitter)
url = build_combined_url(submitter)
if url
render json: [url]
else
head :not_found
end
end
def current_user_submitter?(submitter) def current_user_submitter?(submitter)
current_user && current_user.account.submitters.exists?(id: submitter.id) current_user && current_ability.can?(:read, submitter)
end end
def build_urls(submitter) def build_urls(submitter)
@@ -57,7 +65,7 @@ class SubmissionsDownloadController < ApplicationController
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
Submitters.select_attachments_for_download(submitter).map do |attachment| Submitters.select_attachments_for_download(submitter).map do |attachment|
ActiveStorage::Blob.proxy_url( ActiveStorage::Blob.proxy_path(
attachment.blob, attachment.blob,
expires_at: FILES_TTL.from_now.to_i, expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format) filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
@@ -75,7 +83,7 @@ class SubmissionsDownloadController < ApplicationController
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id, filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
ActiveStorage::Blob.proxy_url( ActiveStorage::Blob.proxy_path(
attachment.blob, attachment.blob,
expires_at: FILES_TTL.from_now.to_i, expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format) filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
@@ -5,10 +5,11 @@ class SubmissionsExportController < ApplicationController
load_and_authorize_resource :submission, through: :template, parent: false, only: :index load_and_authorize_resource :submission, through: :template, parent: false, only: :index
def index def index
submissions = @submissions.active submissions = params[:archived] == 'true' ? @submissions.archived : @submissions.active
.preload(submitters: { documents_attachments: :blob,
attachments_attachments: :blob }) submissions = submissions.preload(submitters: { documents_attachments: :blob,
.order(id: :asc) attachments_attachments: :blob })
.order(id: :asc)
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true) submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
submissions = Submissions::Filter.call(submissions, current_user, params) submissions = Submissions::Filter.call(submissions, current_user, params)
+7 -16
View File
@@ -9,15 +9,15 @@ class SubmitFormController < ApplicationController
before_action :load_submitter, only: %i[show update completed] before_action :load_submitter, only: %i[show update completed]
before_action :maybe_render_locked_page, only: :show before_action :maybe_render_locked_page, only: :show
before_action :maybe_require_link_2fa, only: %i[show update] before_action :maybe_require_link_2fa, only: %i[show]
CONFIG_KEYS = [].freeze CONFIG_KEYS = [].freeze
def show def show
submission = @submitter.submission submission = @submitter.submission
return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request)
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
return render :email_2fa if require_email_2fa?(@submitter)
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS) @form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
@@ -48,7 +48,7 @@ class SubmitFormController < ApplicationController
end end
def update def update
if require_email_2fa?(@submitter) unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') }, return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
status: :unprocessable_content status: :unprocessable_content
end end
@@ -84,7 +84,9 @@ class SubmitFormController < ApplicationController
def completed def completed
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at? raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
redirect_to submit_form_path(params[:submit_form_slug]) if require_email_2fa?(@submitter) return if Submitters::AuthorizedForForm.call(@submitter, current_user, request)
redirect_to submit_form_path(params[:submit_form_slug])
end end
def success; end def success; end
@@ -92,10 +94,7 @@ class SubmitFormController < ApplicationController
private private
def maybe_require_link_2fa def maybe_require_link_2fa
return if @submitter.submission.source != 'link' return if Submitters::AuthorizedForForm.pass_link_2fa?(@submitter, current_user, request)
return unless @submitter.submission.template&.preferences&.dig('shared_link_2fa') == true
return if cookies.encrypted[:email_2fa_slug] == @submitter.slug
return if @submitter.email == current_user&.email && current_user&.account_id == @submitter.account_id
redirect_to start_form_path(@submitter.submission.template.slug) redirect_to start_form_path(@submitter.submission.template.slug)
end end
@@ -117,12 +116,4 @@ class SubmitFormController < ApplicationController
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments) ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid) .preload(:blob).index_by(&:uuid)
end end
def require_email_2fa?(submitter)
return false if submitter.submission.template&.preferences&.dig('require_email_2fa') != true &&
submitter.preferences['require_email_2fa'] != true
return false if cookies.encrypted[:email_2fa_slug] == submitter.slug
true
end
end end
@@ -11,7 +11,9 @@ class SubmitFormDeclineController < ApplicationController
submitter.completed_at? || submitter.completed_at? ||
submitter.submission.archived_at? || submitter.submission.archived_at? ||
submitter.submission.expired? || submitter.submission.expired? ||
submitter.submission.template&.archived_at? submitter.submission.template&.archived_at? ||
!Submitters::AuthorizedForForm.call(submitter, current_user,
request)
ApplicationRecord.transaction do ApplicationRecord.transaction do
submitter.update!(declined_at: Time.current) submitter.update!(declined_at: Time.current)
@@ -17,7 +17,8 @@ class SubmitFormDownloadController < ApplicationController
@submitter.submission.template&.archived_at? || @submitter.submission.template&.archived_at? ||
AccountConfig.exists?(account_id: @submitter.account_id, AccountConfig.exists?(account_id: @submitter.account_id,
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY, key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
value: false) value: false) ||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
last_completed_submitter = @submitter.submission.submitters last_completed_submitter = @submitter.submission.submitters
.where.not(id: @submitter.id) .where.not(id: @submitter.id)
@@ -32,7 +33,7 @@ class SubmitFormDownloadController < ApplicationController
end end
urls = attachments.map do |attachment| urls = attachments.map do |attachment|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i) ActiveStorage::Blob.proxy_path(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
end end
render json: urls render json: urls
@@ -12,7 +12,8 @@ class SubmitFormDrawSignatureController < ApplicationController
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? ||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
return redirect_to submit_form_path(@submitter.slug) return redirect_to submit_form_path(@submitter.slug)
end end
@@ -19,7 +19,9 @@ class SubmitFormInviteController < ApplicationController
next unless attrs next unless attrs
next if attrs[:email].blank? next if attrs[:email].blank?
submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id) email = Submissions.normalize_email(attrs[:email])
submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: submitter.account_id)
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid }) SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
end end
@@ -45,7 +47,8 @@ class SubmitFormInviteController < ApplicationController
!submitter.completed_at? && !submitter.completed_at? &&
!submitter.submission.archived_at? && !submitter.submission.archived_at? &&
!submitter.submission.expired? && !submitter.submission.expired? &&
!submitter.submission.template&.archived_at? !submitter.submission.template&.archived_at? &&
Submitters::AuthorizedForForm.call(submitter, current_user, request)
end end
def filter_invite_submitters(submitter, key = 'invite_by_uuid') def filter_invite_submitters(submitter, key = 'invite_by_uuid')
@@ -7,10 +7,12 @@ class SubmitFormValuesController < ApplicationController
def index def index
submitter = Submitter.find_by!(slug: params[:submit_form_slug]) submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return render json: {} if submitter.completed_at? || submitter.declined_at? return render json: {} if submitter.completed_at? ||
return render json: {} if submitter.submission.template&.archived_at? || submitter.declined_at? ||
submitter.submission.template&.archived_at? ||
submitter.submission.archived_at? || submitter.submission.archived_at? ||
submitter.submission.expired? submitter.submission.expired? ||
!Submitters::AuthorizedForForm.call(submitter, current_user, request)
value = submitter.values[params['field_uuid']] value = submitter.values[params['field_uuid']]
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present? attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
@@ -7,25 +7,34 @@ class SubmittersAutocompleteController < ApplicationController
LIMIT = 100 LIMIT = 100
def index def index
submitters = search_submitters(@submitters) field = SELECT_COLUMNS.find { |c| c == params[:field] }
submitters = search_submitters(@submitters, field)
arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] } arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] }
values = submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
values =
if field
max_ids = submitters.group(field).limit(LIMIT).select(Submitter.arel_table[:id].maximum)
submitters.where(id: max_ids).order(id: :desc).pluck(arel_columns)
else
submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
end
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h } attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
render json: attrs render json: attrs
end end
private private
def search_submitters(submitters) def search_submitters(submitters, field)
if SELECT_COLUMNS.include?(params[:field]) if field
if Docuseal.fulltext_search? if Docuseal.fulltext_search?
Submitters.fulltext_search_field(current_user, submitters, params[:q], params[:field]) Submitters.fulltext_search_field(current_user, submitters, params[:q], field)
else else
column = Submitter.arel_table[params[:field].to_sym] column = Submitter.arel_table[field.to_sym]
term = "#{params[:q].downcase}%" term = "#{params[:q].downcase}%"
@@ -6,7 +6,7 @@ class TemplateDocumentsController < ApplicationController
FILES_TTL = 5.minutes FILES_TTL = 5.minutes
def index def index
render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_url(d.blob, expires_at: FILES_TTL.from_now.to_i) } render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_path(d.blob, expires_at: FILES_TTL.from_now.to_i) }
end end
def create def create
@@ -21,7 +21,7 @@ class TemplatesCloneController < ApplicationController
authorize!(:create, @template) authorize!(:create, @template)
if params[:account_id].present? && true_ability.authorize!(:manage, Account.find(params[:account_id])) if params[:account_id].present? && true_ability.can?(:manage, Account.find(params[:account_id]))
@template.account_id = params[:account_id] @template.account_id = params[:account_id]
@template.author = true_user if true_user.account_id == @template.account_id @template.author = true_user if true_user.account_id == @template.account_id
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id @template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
+2 -1
View File
@@ -97,7 +97,8 @@ class TemplatesController < ApplicationController
:name, :name,
{ schema: [[:attachment_uuid, :google_drive_file_id, :name, { schema: [[:attachment_uuid, :google_drive_file_id, :name,
{ conditions: [%i[field_uuid value action operation]] }]], { conditions: [%i[field_uuid value action operation]] }]],
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email order]], submitters: [%i[name uuid is_requester linked_to_uuid invite_via_field_uuid
invite_by_uuid optional_invite_by_uuid email order]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :prefillable, :title, :description, :prefillable,
@@ -11,11 +11,14 @@ class TemplatesDetectFieldsController < ApplicationController
sse = SSE.new(response.stream) sse = SSE.new(response.stream)
documents = @template.schema_documents.preload(:blob) documents = @template.schema_documents.preload(:blob)
documents = documents.where(uuid: params[:attachment_uuid]) if params[:attachment_uuid].present?
page_number = params[:page].presence&.to_i
documents.each do |document| documents.each do |document|
io = StringIO.new(document.download) io = StringIO.new(document.download)
Templates::DetectFields.call(io, attachment: document) do |(attachment_uuid, page, fields)| Templates::DetectFields.call(io, attachment: document, page_number:) do |(attachment_uuid, page, fields)|
sse.write({ attachment_uuid:, page:, fields: }) sse.write({ attachment_uuid:, page:, fields: })
end end
end end
@@ -22,7 +22,7 @@ class TemplatesRecipientsController < ApplicationController
def submitters_params def submitters_params
permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid
invite_by_uuid linked_to_uuid email option order]] } invite_by_uuid invite_via_field_uuid linked_to_uuid email option order]] }
params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s| params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
next if s[:uuid].blank? next if s[:uuid].blank?
@@ -36,6 +36,7 @@ class TemplatesRecipientsController < ApplicationController
s[:order] = s[:order].to_i if s[:order].present? s[:order] = s[:order].to_i if s[:order].present?
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank? s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank? s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank?
s.delete(:invite_via_field_uuid) if s[:invite_via_field_uuid].blank?
normalize_option_value(s) normalize_option_value(s)
end end
@@ -53,6 +54,7 @@ class TemplatesRecipientsController < ApplicationController
attrs.delete(:email) attrs.delete(:email)
attrs.delete(:linked_to_uuid) attrs.delete(:linked_to_uuid)
attrs.delete(:invite_by_uuid) attrs.delete(:invite_by_uuid)
attrs.delete(:invite_via_field_uuid)
attrs.delete(:optional_invite_by_uuid) attrs.delete(:optional_invite_by_uuid)
when /\Alinked_to_(.*)\z/ when /\Alinked_to_(.*)\z/
attrs[:linked_to_uuid] = ::Regexp.last_match(-1) attrs[:linked_to_uuid] = ::Regexp.last_match(-1)
@@ -56,7 +56,7 @@ class TemplatesUploadsController < ApplicationController
def create_file_params_from_url def create_file_params_from_url
tempfile = Tempfile.new tempfile = Tempfile.new
tempfile.binmode tempfile.binmode
tempfile.write(DownloadUtils.call(params[:url]).body) tempfile.write(DownloadUtils.call(params[:url], validate: true).body)
tempfile.rewind tempfile.rewind
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present? filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?
@@ -32,7 +32,7 @@ class TimestampServerController < ApplicationController
uri = Addressable::URI.parse(url) uri = Addressable::URI.parse(url)
conn = Faraday.new(uri.origin) do |c| conn = Faraday.new(uri.origin) do |c|
c.basic_auth(uri.user, uri.password) if uri.password.present? c.request :authorization, :basic, uri.user, uri.password if uri.password.present?
end end
response = conn.post(uri.path, req.to_der, response = conn.post(uri.path, req.to_der,
@@ -15,7 +15,7 @@ class WebhookSettingsController < ApplicationController
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status]) @webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
@pagy, @webhook_events = pagy_countless(@webhook_events.order(id: :desc)) @pagy, @webhook_events = pagy(:countless, @webhook_events.order(id: :desc))
render :show render :show
end end
@@ -26,7 +26,7 @@ class WebhookSettingsController < ApplicationController
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status]) @webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
@pagy, @webhook_events = pagy_countless(@webhook_events.order(id: :desc)) @pagy, @webhook_events = pagy(:countless, @webhook_events.order(id: :desc))
end end
def new; end def new; end
+8 -1
View File
@@ -40,6 +40,7 @@ import DashboardDropzone from './elements/dashboard_dropzone'
import RequiredCheckboxGroup from './elements/required_checkbox_group' import RequiredCheckboxGroup from './elements/required_checkbox_group'
import PageContainer from './elements/page_container' import PageContainer from './elements/page_container'
import EmailEditor from './elements/email_editor' import EmailEditor from './elements/email_editor'
import MarkdownEditor from './elements/markdown_editor'
import MountOnClick from './elements/mount_on_click' import MountOnClick from './elements/mount_on_click'
import RemoveOnEvent from './elements/remove_on_event' import RemoveOnEvent from './elements/remove_on_event'
import ScrollTo from './elements/scroll_to' import ScrollTo from './elements/scroll_to'
@@ -131,6 +132,7 @@ safeRegisterElement('check-on-click', CheckOnClick)
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup) safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
safeRegisterElement('page-container', PageContainer) safeRegisterElement('page-container', PageContainer)
safeRegisterElement('email-editor', EmailEditor) safeRegisterElement('email-editor', EmailEditor)
safeRegisterElement('markdown-editor', MarkdownEditor)
safeRegisterElement('mount-on-click', MountOnClick) safeRegisterElement('mount-on-click', MountOnClick)
safeRegisterElement('remove-on-event', RemoveOnEvent) safeRegisterElement('remove-on-event', RemoveOnEvent)
safeRegisterElement('scroll-to', ScrollTo) safeRegisterElement('scroll-to', ScrollTo)
@@ -153,17 +155,22 @@ safeRegisterElement('template-builder', class extends HTMLElement {
this.appElem.classList.add('md:h-screen') this.appElem.classList.add('md:h-screen')
const template = reactive(JSON.parse(this.dataset.template))
this.app = createApp(TemplateBuilder, { this.app = createApp(TemplateBuilder, {
template: reactive(JSON.parse(this.dataset.template)), template,
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
backgroundColor: '#faf7f5', backgroundColor: '#faf7f5',
locale: this.dataset.locale, locale: this.dataset.locale,
withPhone: this.dataset.withPhone === 'true', withPhone: this.dataset.withPhone === 'true',
withPrefillable: template.fields?.some((f) => f.prefillable),
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null, withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
withKba: ['true', 'false'].includes(this.dataset.withKba) ? this.dataset.withKba === 'true' : null, withKba: ['true', 'false'].includes(this.dataset.withKba) ? this.dataset.withKba === 'true' : null,
withLogo: this.dataset.withLogo !== 'false', withLogo: this.dataset.withLogo !== 'false',
withFieldsDetection: this.dataset.withFieldsDetection === 'true', withFieldsDetection: this.dataset.withFieldsDetection === 'true',
editable: this.dataset.editable !== 'false', editable: this.dataset.editable !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content, authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
withCustomFields: true,
withPayment: this.dataset.withPayment === 'true', withPayment: this.dataset.withPayment === 'true',
isPaymentConnected: this.dataset.isPaymentConnected === 'true', isPaymentConnected: this.dataset.isPaymentConnected === 'true',
withFormula: this.dataset.withFormula === 'true', withFormula: this.dataset.withFormula === 'true',
+4
View File
@@ -155,3 +155,7 @@ button[disabled] .enabled, button.btn-disabled .enabled {
.font-courier { .font-courier {
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco; font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
} }
markdown-editor [contenteditable] p {
margin-bottom: 18px;
}
+51 -6
View File
@@ -4,15 +4,26 @@ import { isValidSignatureCanvas } from './submission_form/validate_signature'
window.customElements.define('draw-signature', class extends HTMLElement { window.customElements.define('draw-signature', class extends HTMLElement {
connectedCallback () { connectedCallback () {
const scale = 3 this.setCanvasSize()
this.canvas.width = this.canvas.parentNode.clientWidth * scale
this.canvas.height = this.canvas.parentNode.clientHeight * scale
this.canvas.getContext('2d').scale(scale, scale)
this.pad = new SignaturePad(this.canvas) this.pad = new SignaturePad(this.canvas)
this.resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(() => {
if (!this.canvas) return
const { width, height } = this.canvas
this.setCanvasSize()
if (this.canvas.width !== width || this.canvas.height !== height) {
this.redrawCanvas(width, height)
}
})
})
this.resizeObserver.observe(this.canvas.parentNode)
if (this.dataset.color) { if (this.dataset.color) {
this.pad.penColor = this.dataset.color this.pad.penColor = this.dataset.color
} }
@@ -57,6 +68,40 @@ window.customElements.define('draw-signature', class extends HTMLElement {
this.updateSubmitButtonVisibility() this.updateSubmitButtonVisibility()
} }
disconnectedCallback () {
if (this.resizeObserver) {
this.resizeObserver.disconnect()
}
}
setCanvasSize () {
const scale = 3
const width = this.canvas.parentNode.clientWidth
const height = this.canvas.parentNode.clientHeight
if (this.canvas.width !== width * scale || this.canvas.height !== height * scale) {
this.canvas.width = width * scale
this.canvas.height = height * scale
this.canvas.getContext('2d').scale(scale, scale)
}
}
redrawCanvas (oldWidth, oldHeight) {
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
const sx = this.canvas.width / oldWidth
const sy = this.canvas.height / oldHeight
const scaledData = this.pad.toData().map((stroke) => ({
...stroke,
points: stroke.points.map((p) => ({ ...p, x: p.x * sx, y: p.y * sy }))
}))
this.pad.fromData(scaledData)
}
}
updateSubmitButtonVisibility () { updateSubmitButtonVisibility () {
if (this.pad.isEmpty()) { if (this.pad.isEmpty()) {
this.submitButton.style.display = 'none' this.submitButton.style.display = 'none'
@@ -64,6 +64,22 @@ export default class extends HTMLElement {
if (action === 'empty' || action === 'unchecked') return this.isEmpty(actual) if (action === 'empty' || action === 'unchecked') return this.isEmpty(actual)
if (action === 'not_empty' || action === 'checked') return !this.isEmpty(actual) if (action === 'not_empty' || action === 'checked') return !this.isEmpty(actual)
if (['equal', 'not_equal', 'greater_than', 'less_than'].includes(action) && this.sourceEl?.getAttribute('type') === 'number') {
if (this.isEmpty(actual) || this.isEmpty(expected)) return false
const actualNumber = parseFloat(actual)
const expectedNumber = parseFloat(expected)
if (Number.isNaN(actualNumber) || Number.isNaN(expectedNumber)) return false
if (action === 'equal') return Math.abs(actualNumber - expectedNumber) < Number.EPSILON
if (action === 'not_equal') return Math.abs(actualNumber - expectedNumber) > Number.EPSILON
if (action === 'greater_than') return actualNumber > expectedNumber
if (action === 'less_than') return actualNumber < expectedNumber
return false
}
if (action === 'equal') { if (action === 'equal') {
const list = Array.isArray(actual) ? actual : [actual] const list = Array.isArray(actual) ? actual : [actual]
return list.filter((v) => v !== null && v !== undefined).map(String).includes(String(expected)) return list.filter((v) => v !== null && v !== undefined).map(String).includes(String(expected))
+382
View File
@@ -0,0 +1,382 @@
import { target, targetable } from '@github/catalyst/lib/targetable'
import { actionable } from '@github/catalyst/lib/actionable'
function loadTiptap () {
return Promise.all([
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/core'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-bold'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-italic'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-paragraph'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-text'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-hard-break'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-document'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-link'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-underline'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extensions'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/markdown'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/state'),
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/view')
]).then(([core, bold, italic, paragraph, text, hardBreak, document, link, underline, extensions, markdown, pmState, pmView]) => ({
Editor: core.Editor,
Extension: core.Extension,
Bold: bold.default || bold,
Italic: italic.default || italic,
Paragraph: paragraph.default || paragraph,
Text: text.default || text,
HardBreak: hardBreak.default || hardBreak,
Document: document.default || document,
Link: link.default || link,
Underline: underline.default || underline,
UndoRedo: extensions.UndoRedo,
Markdown: markdown.Markdown,
Plugin: pmState.Plugin,
Decoration: pmView.Decoration,
DecorationSet: pmView.DecorationSet
}))
}
class LinkTooltip {
constructor (container, editor, templateEl) {
this.container = container
this.editor = editor
this.tooltip = templateEl.content.firstElementChild.cloneNode(true)
this.input = this.tooltip.querySelector('input')
this.saveButton = this.tooltip.querySelector('[data-role="link-save"]')
this.removeButton = this.tooltip.querySelector('[data-role="link-remove"]')
container.style.position = 'relative'
container.appendChild(this.tooltip)
}
isVisible () {
return !this.tooltip.classList.contains('hidden')
}
normalizeUrl (url) {
if (!url) return url
if (/^{/i.test(url)) return url
if (/^https?:\/\//i.test(url)) return url
if (/^mailto:/i.test(url)) return url
return `https://${url}`
}
show (url, pos, { focus = false } = {}) {
this.input.value = url || ''
this.removeButton.classList.toggle('hidden', !url)
this.tooltip.classList.remove('hidden')
const coords = this.editor.view.coordsAtPos(pos)
const containerRect = this.container.getBoundingClientRect()
this.tooltip.style.left = `${coords.left - containerRect.left}px`
this.tooltip.style.top = `${coords.bottom - containerRect.top + 4}px`
if (focus) this.input.focus()
this.saveHandler = () => {
const inputUrl = this.input.value.trim()
if (inputUrl) {
this.editor.chain().focus().extendMarkRange('link').setLink({ href: this.normalizeUrl(inputUrl) }).run()
}
this.hide()
}
this.removeHandler = () => {
this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
this.hide()
}
this.keyHandler = (e) => {
if (e.key === 'Enter') {
e.preventDefault()
this.saveHandler()
} else if (e.key === 'Escape') {
e.preventDefault()
this.hide()
}
}
this.saveButton.addEventListener('click', this.saveHandler, { once: true })
this.removeButton.addEventListener('click', this.removeHandler, { once: true })
this.input.addEventListener('keydown', this.keyHandler)
}
hide () {
if (this.saveHandler) {
this.saveButton.removeEventListener('click', this.saveHandler)
this.saveHandler = null
}
if (this.removeHandler) {
this.removeButton.removeEventListener('click', this.removeHandler)
this.removeHandler = null
}
if (this.keyHandler) {
this.input.removeEventListener('keydown', this.keyHandler)
this.keyHandler = null
}
this.tooltip.classList.add('hidden')
this.currentMark = null
}
}
export default actionable(targetable(class extends HTMLElement {
static [target.static] = [
'textarea',
'editorElement',
'boldButton',
'italicButton',
'underlineButton',
'linkButton',
'linkTooltipTemplate'
]
async connectedCallback () {
if (!this.textarea || !this.editorElement) return
this.textarea.style.display = 'none'
this.adjustShortcutsForPlatform()
const { Editor, Extension, Bold, Italic, Paragraph, Text, HardBreak, UndoRedo, Document, Link, Underline, Markdown, Plugin, Decoration, DecorationSet } = await loadTiptap()
const buildDecorations = (doc) => {
const decorations = []
const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g
doc.descendants((node, pos) => {
if (!node.isText) return
let match
while ((match = regex.exec(node.text)) !== null) {
decorations.push(
Decoration.inline(pos + match.index, pos + match.index + match[0].length, {
class: 'bg-amber-100 py-0.5 px-1 rounded'
})
)
}
})
return DecorationSet.create(doc, decorations)
}
const VariableHighlight = Extension.create({
name: 'variableHighlight',
addProseMirrorPlugins () {
return [new Plugin({
state: {
init (_, { doc }) {
return buildDecorations(doc)
},
apply (tr, oldSet) {
return tr.docChanged ? buildDecorations(tr.doc) : oldSet
}
},
props: {
decorations (state) {
return this.getState(state)
}
}
})]
}
})
this.editor = new Editor({
element: this.editorElement,
extensions: [
Markdown,
Document,
Paragraph,
Text,
Bold,
Italic,
HardBreak.extend({
addKeyboardShortcuts () {
return {
Enter: () => this.editor.commands.setHardBreak()
}
}
}),
UndoRedo,
Link.extend({
inclusive: true,
addKeyboardShortcuts: () => ({
'Mod-k': () => {
this.toggleLink()
return true
}
})
}).configure({
openOnClick: false,
HTMLAttributes: {
class: 'link',
'data-turbo': 'false',
style: 'color: #2563eb; text-decoration: underline; cursor: text;'
}
}),
Underline,
VariableHighlight
],
content: (this.textarea.value || '').trim().replace(/ *\n/g, '<br>'),
contentType: 'markdown',
editorProps: {
attributes: {
style: 'min-height: 220px',
dir: 'auto',
class: 'p-3 outline-none focus:outline-none'
}
},
onUpdate: ({ editor }) => {
this.textarea.value = editor.getMarkdown()
this.textarea.dispatchEvent(new Event('input', { bubbles: true }))
},
onSelectionUpdate: ({ editor }) => {
this.updateToolbarState()
this.handleLinkTooltip(editor)
},
onBlur: () => {
setTimeout(() => {
if (!this.linkTooltip.tooltip.contains(document.activeElement)) {
this.linkTooltip.hide()
}
}, 0)
}
})
this.linkTooltip = new LinkTooltip(this, this.editor, this.linkTooltipTemplate)
}
adjustShortcutsForPlatform () {
if ((navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')) {
this.querySelectorAll('.tooltip[data-tip]').forEach(tooltip => {
const tip = tooltip.getAttribute('data-tip')
if (tip && tip.includes('Ctrl')) {
tooltip.setAttribute('data-tip', tip.replace(/Ctrl/g, '⌘'))
}
})
}
}
bold (e) {
e.preventDefault()
this.editor.chain().focus().toggleBold().run()
this.updateToolbarState()
}
italic (e) {
e.preventDefault()
this.editor.chain().focus().toggleItalic().run()
this.updateToolbarState()
}
underline (e) {
e.preventDefault()
this.editor.chain().focus().toggleUnderline().run()
this.updateToolbarState()
}
linkSelection (e) {
e.preventDefault()
this.toggleLink()
this.updateToolbarState()
}
undo (e) {
e.preventDefault()
this.editor.chain().focus().undo().run()
this.updateToolbarState()
}
redo (e) {
e.preventDefault()
this.editor.chain().focus().redo().run()
this.updateToolbarState()
}
updateToolbarState () {
this.boldButton.classList.toggle('bg-base-200', this.editor.isActive('bold'))
this.italicButton.classList.toggle('bg-base-200', this.editor.isActive('italic'))
this.underlineButton.classList.toggle('bg-base-200', this.editor.isActive('underline'))
this.linkButton.classList.toggle('bg-base-200', this.editor.isActive('link'))
}
handleLinkTooltip (editor) {
const { from } = editor.state.selection
const mark = editor.state.doc.resolve(from).marks().find(m => m.type.name === 'link')
if (!mark) {
if (this.linkTooltip.isVisible()) this.linkTooltip.hide()
return
}
if (this.linkTooltip.isVisible() && this.linkTooltip.currentMark === mark) return
let linkStart = from
const start = editor.state.doc.resolve(from).start()
for (let i = from - 1; i >= start; i--) {
if (editor.state.doc.resolve(i).marks().some(m => m.eq(mark))) {
linkStart = i
} else {
break
}
}
this.linkTooltip.hide()
this.linkTooltip.show(mark.attrs.href, linkStart > start ? linkStart - 1 : linkStart)
this.linkTooltip.currentMark = mark
}
toggleLink () {
if (this.editor.isActive('link')) {
this.linkTooltip.hide()
this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
this.updateToolbarState()
} else {
const { from } = this.editor.state.selection
this.linkTooltip.hide()
this.linkTooltip.show(this.editor.getAttributes('link').href, from, { focus: true })
}
}
insertVariable (e) {
const variable = e.target.closest('[data-variable]')?.dataset.variable
if (variable) {
const { from, to } = this.editor.state.selection
if (variable.includes('link') && from !== to) {
this.editor.chain().focus().setLink({ href: `{${variable}}` }).run()
} else {
this.editor.chain().focus().insertContent(`{${variable}}`).run()
}
}
}
disconnectedCallback () {
this.linkTooltip.hide()
if (this.editor) {
this.editor.destroy()
}
}
}))
+52 -7
View File
@@ -5,17 +5,28 @@ export default targetable(class extends HTMLElement {
static [target.static] = ['canvas', 'input', 'clear', 'button'] static [target.static] = ['canvas', 'input', 'clear', 'button']
async connectedCallback () { async connectedCallback () {
const scale = 3
this.canvas.width = this.canvas.parentNode.clientWidth * scale
this.canvas.height = this.canvas.parentNode.clientHeight * scale
this.canvas.getContext('2d').scale(scale, scale)
const { default: SignaturePad } = await import('signature_pad') const { default: SignaturePad } = await import('signature_pad')
this.setCanvasSize()
this.pad = new SignaturePad(this.canvas) this.pad = new SignaturePad(this.canvas)
this.resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(() => {
if (!this.canvas) return
const { width, height } = this.canvas
this.setCanvasSize()
if (this.canvas.width !== width || this.canvas.height !== height) {
this.redrawCanvas(width, height)
}
})
})
this.resizeObserver.observe(this.canvas.parentNode)
this.clear.addEventListener('click', (e) => { this.clear.addEventListener('click', (e) => {
e.preventDefault() e.preventDefault()
@@ -47,4 +58,38 @@ export default targetable(class extends HTMLElement {
this.closest('form').requestSubmit() this.closest('form').requestSubmit()
} }
disconnectedCallback () {
if (this.resizeObserver) {
this.resizeObserver.disconnect()
}
}
setCanvasSize () {
const scale = 3
const width = this.canvas.parentNode.clientWidth
const height = this.canvas.parentNode.clientWidth / 2.5
if (this.canvas.width !== width * scale || this.canvas.height !== height * scale) {
this.canvas.width = width * scale
this.canvas.height = height * scale
this.canvas.getContext('2d').scale(scale, scale)
}
}
redrawCanvas (oldWidth, oldHeight) {
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
const sx = this.canvas.width / oldWidth
const sy = this.canvas.height / oldHeight
const scaledData = this.pad.toData().map((stroke) => ({
...stroke,
points: stroke.points.map((p) => ({ ...p, x: p.x * sx, y: p.y * sy }))
}))
this.pad.fromData(scaledData)
}
}
}) })
+8 -2
View File
@@ -1,12 +1,18 @@
export default class extends HTMLElement { export default class extends HTMLElement {
connectedCallback () { connectedCallback () {
this.input.addEventListener('change', (event) => { this.input.addEventListener('change', (event) => {
if (!this.target) return
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value
const dataValue = this.dataset.value === 'false' ? false : this.dataset.value || true
if (this.dataset.attribute) { if (this.dataset.attribute) {
this.target[this.dataset.attribute] = event.target.checked this.target[this.dataset.attribute] = value === dataValue
} }
if (this.dataset.className) { if (this.dataset.className) {
this.target.classList.toggle(this.dataset.className, event.target.value !== this.dataset.value) this.target.classList.toggle(this.dataset.className, value !== dataValue)
if (this.dataset.className === 'hidden' && this.target.tagName === 'INPUT') { if (this.dataset.className === 'hidden' && this.target.tagName === 'INPUT') {
this.target.disabled = event.target.value !== this.dataset.value this.target.disabled = event.target.value !== this.dataset.value
} }
+10 -2
View File
@@ -1,10 +1,18 @@
export default class extends HTMLElement { export default class extends HTMLElement {
connectedCallback () { connectedCallback () {
const button = this.querySelector('a, button') const button = this.querySelector('a, button, label')
const target = this.dataset.targetId ? document.getElementById(this.dataset.targetId) : button
button.addEventListener('click', () => { button.addEventListener('click', () => {
this.dataset.classes.split(' ').forEach((cls) => { this.dataset.classes.split(' ').forEach((cls) => {
button.classList.toggle(cls) if (this.dataset.action === 'remove') {
target.classList.remove(cls)
} else if (this.dataset.action === 'add') {
target.classList.add(cls)
} else {
target.classList.toggle(cls)
}
}) })
}) })
} }
+1 -1
View File
@@ -12,6 +12,6 @@ export default class extends HTMLElement {
} }
get button () { get button () {
return this.querySelector('button') return this.querySelector('button, label')
} }
} }
+2 -2
View File
@@ -182,8 +182,8 @@
</div> </div>
<div <div
v-else-if="field.type === 'cells'" v-else-if="field.type === 'cells'"
class="w-full flex items-center" class="w-full flex"
:class="{ 'justify-end': field.preferences?.align === 'right', ...fontClasses }" :class="{ 'justify-end': field.preferences?.align === 'right', ...alignClasses, ...fontClasses }"
> >
<div <div
v-for="(char, index) in modelValue" v-for="(char, index) in modelValue"
+9 -1
View File
@@ -161,6 +161,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
fetchOptions: {
type: Object,
required: false,
default: () => ({})
},
completedButton: { completedButton: {
type: Object, type: Object,
required: false, required: false,
@@ -214,7 +219,10 @@ export default {
download () { download () {
this.isDownloading = true this.isDownloading = true
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => { fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`, {
method: 'GET',
...this.fetchOptions
}).then(async (response) => {
if (response.ok) { if (response.ok) {
const urls = await response.json() const urls = await response.json()
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent) const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
+1 -1
View File
@@ -42,7 +42,7 @@
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
<AppearsOn :field="field" /> <AppearsOn :field="field" />
<div class="text-center"> <div class="text-center flex">
<input <input
:id="field.uuid" :id="field.uuid"
ref="input" ref="input"
+132 -36
View File
@@ -69,7 +69,7 @@
id="expand_form_button" id="expand_form_button"
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button text-base" class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button text-base"
style="width: 96%; margin-left: 2%" style="width: 96%; margin-left: 2%"
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]" @click.prevent="[isFormVisible = true, $nextTick(() => scrollIntoField(currentField))]"
> >
<template v-if="['initials', 'signature'].includes(currentField.type)"> <template v-if="['initials', 'signature'].includes(currentField.type)">
<IconWritingSign stroke-width="1.5" /> <IconWritingSign stroke-width="1.5" />
@@ -395,7 +395,9 @@
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:reason="values[currentField.preferences?.reason_field_uuid]" :reason="values[currentField.preferences?.reason_field_uuid]"
:field="currentField" :field="currentField"
:values="values"
:previous-value="previousSignatureValueFor(currentField) || previousSignatureValue" :previous-value="previousSignatureValueFor(currentField) || previousSignatureValue"
:touch-attachment-uuid="previousSignatureValue"
:with-typed-signature="withTypedSignature" :with-typed-signature="withTypedSignature"
:remember-signature="rememberSignature" :remember-signature="rememberSignature"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
@@ -407,6 +409,7 @@
:submitter="submitter" :submitter="submitter"
:show-field-names="showFieldNames" :show-field-names="showFieldNames"
@update:reason="values[currentField.preferences?.reason_field_uuid] = $event" @update:reason="values[currentField.preferences?.reason_field_uuid] = $event"
@touch-attachment="attachmentsIndex[previousSignatureValue] ? attachmentsIndex[previousSignatureValue].created_at = new Date() : null"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@start="scrollIntoField(currentField)" @start="scrollIntoField(currentField)"
@minimize="minimizeForm" @minimize="minimizeForm"
@@ -418,6 +421,7 @@
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
:dry-run="dryRun" :dry-run="dryRun"
:submitter="submitter"
:previous-value="previousInitialsValue" :previous-value="previousInitialsValue"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:show-field-names="showFieldNames" :show-field-names="showFieldNames"
@@ -526,6 +530,7 @@
v-else-if="isInvite" v-else-if="isInvite"
:submitters="inviteSubmitters" :submitters="inviteSubmitters"
:optional-submitters="optionalInviteSubmitters" :optional-submitters="optionalInviteSubmitters"
:fetch-options="fetchOptions"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:authenticity-token="authenticityToken" :authenticity-token="authenticityToken"
:url="baseUrl + submitPath + '/invite'" :url="baseUrl + submitPath + '/invite'"
@@ -539,6 +544,7 @@
:has-signature-fields="stepFields.some((fields) => fields.some((f) => ['signature', 'initials'].includes(f.type)))" :has-signature-fields="stepFields.some((fields) => fields.some((f) => ['signature', 'initials'].includes(f.type)))"
:has-multiple-documents="hasMultipleDocuments" :has-multiple-documents="hasMultipleDocuments"
:completed-button="completedRedirectUrl ? {} : completedButton" :completed-button="completedRedirectUrl ? {} : completedButton"
:fetch-options="fetchOptions"
:completed-message="completedRedirectUrl ? {} : completedMessage" :completed-message="completedRedirectUrl ? {} : completedMessage"
:with-send-copy-button="withSendCopyButton && !completedRedirectUrl" :with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
:with-download-button="withDownloadButton && !completedRedirectUrl && !dryRun" :with-download-button="withDownloadButton && !completedRedirectUrl && !dryRun"
@@ -551,14 +557,18 @@
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1 select-none" class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1 select-none"
> >
<div class="flex items-center flex-wrap steps-progress"> <div class="flex items-center flex-wrap steps-progress">
<a <template
v-for="(step, index) in stepFields" v-for="(step, index) in stepFields"
:key="step[0].uuid" :key="step[0].uuid"
href="#" >
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1" <a
:class="{ 'bg-base-300 steps-progress-current': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }" v-if="!onlyRequiredFields || step.some((f) => f.required)"
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(index, true)]" href="#"
/> class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1"
:class="{ 'bg-base-300 steps-progress-current': 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(index, true)]"
/>
</template>
</div> </div>
</div> </div>
<div <div
@@ -592,6 +602,18 @@ import AppearsOn from './appears_on'
import i18n from './i18n' import i18n from './i18n'
import { sanitizeUrl } from '@braintree/sanitize-url' import { sanitizeUrl } from '@braintree/sanitize-url'
if (typeof URL.canParse !== 'function') {
URL.canParse = function (url, base) {
try {
const parsed = new URL(url, base)
return !!parsed
} catch {
return false
}
}
}
const isEmpty = (obj) => { const isEmpty = (obj) => {
if (obj == null) return true if (obj == null) return true
@@ -658,6 +680,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
fetchOptions: {
type: Object,
required: false,
default: () => ({})
},
optionalInviteSubmitters: { optionalInviteSubmitters: {
type: Array, type: Array,
required: false, required: false,
@@ -678,6 +705,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
onlyRequiredFields: {
type: Boolean,
required: false,
default: false
},
requireSigningReason: { requireSigningReason: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -914,10 +946,12 @@ export default {
}, {}) }, {})
}, },
attachmentConditionsIndex () { attachmentConditionsIndex () {
const cache = {}
return this.schema.reduce((acc, item) => { return this.schema.reduce((acc, item) => {
if (item.conditions?.length) { if (item.conditions?.length) {
if (item.conditions.every((c) => this.fieldsUuidIndex[c.field_uuid])) { if (item.conditions.every((c) => this.fieldsUuidIndex[c.field_uuid])) {
acc[item.attachment_uuid] = this.checkFieldConditions(item) acc[item.attachment_uuid] = this.checkFieldConditions(item, cache)
} else { } else {
acc[item.attachment_uuid] = true acc[item.attachment_uuid] = true
} }
@@ -938,7 +972,13 @@ export default {
submitButtonText () { submitButtonText () {
if (this.alwaysMinimize) { if (this.alwaysMinimize) {
return this.t('submit') return this.t('submit')
} else if (this.stepFields.length === this.currentStep + 1) { } else if (!this.onlyRequiredFields && this.stepFields.length === this.currentStep + 1) {
if (this.currentField.type === 'signature') {
return this.t('sign_and_complete')
} else {
return this.t('complete')
}
} else if (this.onlyRequiredFields && !this.findNextStep(this.currentStep)) {
if (this.currentField.type === 'signature') { if (this.currentField.type === 'signature') {
return this.t('sign_and_complete') return this.t('sign_and_complete')
} else { } else {
@@ -984,7 +1024,9 @@ export default {
}, },
previousInitialsValue () { previousInitialsValue () {
if (this.reuseSignature !== false) { if (this.reuseSignature !== false) {
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid]) const initialsField = this.fields.findLast
? this.fields.findLast((field) => field.type === 'initials' && !!this.values[field.uuid])
: [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
return this.values[initialsField?.uuid] return this.values[initialsField?.uuid]
} else { } else {
@@ -1011,7 +1053,9 @@ export default {
return this.readonlyFields.filter((f) => f.conditions?.length) return this.readonlyFields.filter((f) => f.conditions?.length)
}, },
readonlyFields () { readonlyFields () {
return this.fields.filter((f) => f.readonly && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f)) const cache = {}
return this.fields.filter((f) => f.readonly && this.checkFieldConditions(f, cache) && this.checkFieldDocumentsConditions(f))
}, },
stepFields () { stepFields () {
const verificationFields = [] const verificationFields = []
@@ -1066,10 +1110,12 @@ export default {
sortedFields.push(verificationFields.pop()) sortedFields.push(verificationFields.pop())
} }
const cache = {}
return sortedFields.reduce((acc, f) => { return sortedFields.reduce((acc, f) => {
const prevStep = acc[acc.length - 1] const prevStep = acc[acc.length - 1]
if (this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f)) { if (this.checkFieldConditions(f, cache) && this.checkFieldDocumentsConditions(f)) {
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) { if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) {
prevStep.push(f) prevStep.push(f)
} else { } else {
@@ -1081,7 +1127,9 @@ export default {
}, []) }, [])
}, },
formulaFields () { formulaFields () {
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment' && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f)) const cache = {}
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment' && this.checkFieldConditions(f, cache) && this.checkFieldDocumentsConditions(f))
}, },
attachmentsIndex () { attachmentsIndex () {
return this.attachments.reduce((acc, a) => { return this.attachments.reduce((acc, a) => {
@@ -1143,15 +1191,17 @@ export default {
this.currentStep = Math.max(stepIndex, 0) this.currentStep = Math.max(stepIndex, 0)
} else if (this.goToLast) { } else if (this.goToLast) {
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.submittedValues[f.uuid]))) const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.submittedValues[f.uuid])))
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1 const lastFilledStepIndex = this.stepFields.indexOf(this.stepFields.findLast
? this.stepFields.findLast((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))
: [...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1
const indexesList = [this.stepFields.length - 1] const indexesList = [this.stepFields.length - 1]
if (requiredEmptyStepIndex !== -1) { if (requiredEmptyStepIndex !== -1 && (!this.onlyRequiredFields || this.stepFields[requiredEmptyStepIndex].some((f) => f.required))) {
indexesList.push(requiredEmptyStepIndex) indexesList.push(requiredEmptyStepIndex)
} }
if (lastFilledStepIndex !== -1) { if (lastFilledStepIndex !== -1 && (!this.onlyRequiredFields || this.stepFields[lastFilledStepIndex].some((f) => f.required))) {
indexesList.push(lastFilledStepIndex) indexesList.push(lastFilledStepIndex)
} }
@@ -1211,27 +1261,35 @@ export default {
return true return true
} }
}, },
checkFieldConditions (field) { checkFieldConditions (field, cache = {}) {
const cacheKey = field.uuid || field.attachment_uuid
if (cache[cacheKey] !== undefined) {
return cache[cacheKey]
}
if (field.conditions?.length) { if (field.conditions?.length) {
const result = field.conditions.reduce((acc, cond) => { const result = field.conditions.reduce((acc, cond) => {
if (cond.operation === 'or') { if (cond.operation === 'or') {
acc.push(acc.pop() || this.checkFieldCondition(cond)) acc.push(acc.pop() || this.checkFieldCondition(cond, cache))
} else { } else {
acc.push(this.checkFieldCondition(cond)) acc.push(this.checkFieldCondition(cond, cache))
} }
return acc return acc
}, []) }, [])
return !result.includes(false) cache[cacheKey] = !result.includes(false)
} else { } else {
return true cache[cacheKey] = true
} }
return cache[cacheKey]
}, },
checkFieldCondition (condition) { checkFieldCondition (condition, cache = {}) {
const field = this.fieldsUuidIndex[condition.field_uuid] const field = this.fieldsUuidIndex[condition.field_uuid]
if (['not_empty', 'checked', 'equal', 'contains'].includes(condition.action) && field && !this.checkFieldConditions(field)) { if (['not_empty', 'checked', 'equal', 'contains', 'greater_than', 'less_than'].includes(condition.action) && field && !this.checkFieldConditions(field, cache)) {
return false return false
} }
@@ -1241,20 +1299,50 @@ export default {
return isEmpty(this.values[condition.field_uuid] ?? defaultValue) return isEmpty(this.values[condition.field_uuid] ?? defaultValue)
} else if (['not_empty', 'checked'].includes(condition.action)) { } else if (['not_empty', 'checked'].includes(condition.action)) {
return !isEmpty(this.values[condition.field_uuid] ?? defaultValue) return !isEmpty(this.values[condition.field_uuid] ?? defaultValue)
} else if (field?.type === 'number' && ['equal', 'not_equal', 'greater_than', 'less_than'].includes(condition.action)) {
const value = this.values[condition.field_uuid] ?? defaultValue
if (isEmpty(value) || isEmpty(condition.value)) return false
const actual = parseFloat(value)
const expected = parseFloat(condition.value)
if (Number.isNaN(actual) || Number.isNaN(expected)) return false
if (condition.action === 'equal') return Math.abs(actual - expected) < Number.EPSILON
if (condition.action === 'not_equal') return Math.abs(actual - expected) > Number.EPSILON
if (condition.action === 'greater_than') return actual > expected
if (condition.action === 'less_than') return actual < expected
return false
} else if (['equal', 'contains'].includes(condition.action) && field) { } else if (['equal', 'contains'].includes(condition.action) && field) {
if (field.options) { if (field.options) {
const option = field.options.find((o) => o.uuid === condition.value) const option = field.options.find((o) => o.uuid === condition.value)
const values = [this.values[condition.field_uuid] ?? defaultValue].flat()
return values.includes(this.optionValue(option, field.options.indexOf(option))) if (option) {
const values = [this.values[condition.field_uuid] ?? defaultValue].flat()
return values.includes(this.optionValue(option, field.options.indexOf(option)))
} else {
return false
}
} else { } else {
return [this.values[condition.field_uuid] ?? defaultValue].flat().includes(condition.value) return [this.values[condition.field_uuid] ?? defaultValue].flat().includes(condition.value)
} }
} else if (['not_equal', 'does_not_contain'].includes(condition.action) && field) { } else if (['not_equal', 'does_not_contain'].includes(condition.action) && field) {
const option = field.options.find((o) => o.uuid === condition.value) if (field.options) {
const values = [this.values[condition.field_uuid] ?? defaultValue].flat() const option = field.options.find((o) => o.uuid === condition.value)
return !values.includes(this.optionValue(option, field.options.indexOf(option))) if (option) {
const values = [this.values[condition.field_uuid] ?? defaultValue].flat()
return !values.includes(this.optionValue(option, field.options.indexOf(option)))
} else {
return false
}
} else {
return false
}
} else { } else {
return true return true
} }
@@ -1266,6 +1354,13 @@ export default {
return `${this.t('option')} ${index + 1}` return `${this.t('option')} ${index + 1}`
} }
}, },
findNextStep (currentStepIndex) {
if (this.onlyRequiredFields) {
return this.stepFields.find((step, index) => index > currentStepIndex && step.some((f) => f.required))
} else {
return this.stepFields[currentStepIndex + 1]
}
},
maybeTrackEmailClick () { maybeTrackEmailClick () {
const { queryParams } = this const { queryParams } = this
@@ -1327,9 +1422,9 @@ export default {
}, },
previousSignatureValueFor (field) { previousSignatureValueFor (field) {
if (this.reuseSignature !== false) { if (this.reuseSignature !== false) {
const signatureField = [...this.fields].reverse().find((f) => const signatureField = this.fields.findLast
f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid] ? this.fields.findLast((f) => f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid])
) : [...this.fields].reverse().find((f) => f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid])
return this.values[signatureField?.uuid] return this.values[signatureField?.uuid]
} else { } else {
@@ -1379,7 +1474,8 @@ export default {
} else { } else {
return fetch(this.baseUrl + this.submitPath, { return fetch(this.baseUrl + this.submitPath, {
method: 'POST', method: 'POST',
body: formData || new FormData(this.$refs.form) body: formData || new FormData(this.$refs.form),
...this.fetchOptions
}).then((response) => { }).then((response) => {
if (response.status === 200) { if (response.status === 200) {
currentFieldUuids.forEach((fieldUuid) => { currentFieldUuids.forEach((fieldUuid) => {
@@ -1412,7 +1508,7 @@ export default {
this.isSubmittingComplete = true this.isSubmittingComplete = true
} }
const submitStep = this.currentStep const submitStepIndex = this.currentStep
const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification', 'kba'].includes(this.currentField.type) const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification', 'kba'].includes(this.currentField.type)
? this.$refs.currentStep.submit ? this.$refs.currentStep.submit
@@ -1420,7 +1516,7 @@ export default {
stepPromise().then(async () => { stepPromise().then(async () => {
const emptyRequiredField = this.stepFields.find((fields, index) => { const emptyRequiredField = this.stepFields.find((fields, index) => {
if (forceComplete ? index === submitStep : index >= submitStep) { if (forceComplete ? index === submitStepIndex : index >= submitStepIndex) {
return false return false
} }
@@ -1430,7 +1526,7 @@ export default {
}) })
const formData = new FormData(this.$refs.form) const formData = new FormData(this.$refs.form)
const isLastStep = (submitStep === this.stepFields.length - 1) || forceComplete const isLastStep = (this.onlyRequiredFields ? !this.findNextStep(submitStepIndex) : (submitStepIndex === this.stepFields.length - 1)) || forceComplete
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) { if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) {
formData.append('completed', 'true') formData.append('completed', 'true')
@@ -1474,7 +1570,7 @@ export default {
return Promise.reject(new Error(data.error)) return Promise.reject(new Error(data.error))
} }
const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.stepFields[submitStep + 1]) const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.findNextStep(submitStepIndex))
if (nextStep) { if (nextStep) {
if (this.alwaysMinimize) { if (this.alwaysMinimize) {
+15 -1
View File
@@ -1,7 +1,7 @@
const en = { const en = {
kba: 'KBA', kba: 'KBA',
please_upload_an_image_file: 'Please upload an image file', please_upload_an_image_file: 'Please upload an image file',
must_be_characters_length: 'Must be {number} characters length', must_be_characters_length: 'Must be {number} characters long',
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.', complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
verify_id: 'Verify ID', verify_id: 'Verify ID',
identity_verification: 'Identity verification', identity_verification: 'Identity verification',
@@ -97,6 +97,7 @@ const en = {
upload: 'Upload', upload: 'Upload',
files: 'Files', files: 'Files',
signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.', signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.',
browser_privacy_settings_block_canvas: 'Your browser privacy settings restrict use of the drawing canvas. Please use a different browser or device, or disable privacy settings that block canvas in order to sign.',
wait_countdown_seconds: 'Wait {countdown} seconds' wait_countdown_seconds: 'Wait {countdown} seconds'
} }
@@ -199,6 +200,7 @@ const es = {
upload: 'Subir', upload: 'Subir',
files: 'Archivos', files: 'Archivos',
signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.', signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.',
browser_privacy_settings_block_canvas: 'La configuración de privacidad de su navegador restringe el uso del lienzo de dibujo. Utilice un navegador o dispositivo diferente, o desactive la configuración de privacidad que bloquea el lienzo para firmar.',
wait_countdown_seconds: 'Espera {countdown} segundos' wait_countdown_seconds: 'Espera {countdown} segundos'
} }
@@ -301,6 +303,7 @@ const it = {
upload: 'Carica', upload: 'Carica',
files: 'File', files: 'File',
signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.', signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.',
browser_privacy_settings_block_canvas: 'Le impostazioni sulla privacy del browser limitano l\'uso dell\'area di disegno. Utilizza un browser o dispositivo diverso oppure disattiva le impostazioni sulla privacy che bloccano il canvas per firmare.',
wait_countdown_seconds: 'Attendi {countdown} secondi' wait_countdown_seconds: 'Attendi {countdown} secondi'
} }
@@ -403,6 +406,7 @@ const de = {
upload: 'Hochladen', upload: 'Hochladen',
files: 'Dateien', files: 'Dateien',
signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte neu zeichnen.', signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte neu zeichnen.',
browser_privacy_settings_block_canvas: 'Die Datenschutzeinstellungen Ihres Browsers schränken die Nutzung der Zeichenfläche ein. Bitte verwenden Sie einen anderen Browser oder ein anderes Gerät oder deaktivieren Sie die Datenschutzeinstellungen, die Canvas blockieren, um zu unterschreiben.',
wait_countdown_seconds: 'Bitte {countdown} Sekunden warten' wait_countdown_seconds: 'Bitte {countdown} Sekunden warten'
} }
@@ -505,6 +509,7 @@ const fr = {
upload: 'Téléverser', upload: 'Téléverser',
files: 'Fichiers', files: 'Fichiers',
signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.', signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.',
browser_privacy_settings_block_canvas: 'Les paramètres de confidentialité de votre navigateur empêchent l\'utilisation du canevas de dessin. Veuillez utiliser un autre navigateur ou appareil, ou désactiver les paramètres de confidentialité qui bloquent le canevas pour signer.',
wait_countdown_seconds: 'Veuillez patienter {countdown} secondes' wait_countdown_seconds: 'Veuillez patienter {countdown} secondes'
} }
@@ -607,6 +612,7 @@ const pl = {
upload: 'Przesyłanie', upload: 'Przesyłanie',
files: 'Pliki', files: 'Pliki',
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.', signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.',
browser_privacy_settings_block_canvas: 'Ustawienia prywatności przeglądarki blokują użycie obszaru rysowania. Użyj innej przeglądarki lub urządzenia albo wyłącz ustawienia prywatności blokujące canvas, aby podpisać.',
wait_countdown_seconds: 'Poczekaj {countdown} sekund' wait_countdown_seconds: 'Poczekaj {countdown} sekund'
} }
@@ -709,6 +715,7 @@ const uk = {
upload: 'Завантажити', upload: 'Завантажити',
files: 'Файли', files: 'Файли',
signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.', signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.',
browser_privacy_settings_block_canvas: 'Налаштування конфіденційності вашого браузера блокують використання полотна для малювання. Будь ласка, скористайтеся іншим браузером або пристроєм, або вимкніть налаштування конфіденційності, що блокують canvas, щоб підписати.',
wait_countdown_seconds: 'Зачекайте {countdown} секунд' wait_countdown_seconds: 'Зачекайте {countdown} секунд'
} }
@@ -811,6 +818,7 @@ const cs = {
upload: 'Nahrát', upload: 'Nahrát',
files: 'Soubory', files: 'Soubory',
signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.', signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.',
browser_privacy_settings_block_canvas: 'Nastavení soukromí vašeho prohlížeče omezuje použití kreslicího plátna. Použijte prosím jiný prohlížeč nebo zařízení, nebo vypněte nastavení soukromí blokující canvas pro podepsání.',
wait_countdown_seconds: 'Počkejte {countdown} sekund' wait_countdown_seconds: 'Počkejte {countdown} sekund'
} }
@@ -913,6 +921,7 @@ const pt = {
upload: 'Carregar', upload: 'Carregar',
files: 'Arquivos', files: 'Arquivos',
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.', signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
browser_privacy_settings_block_canvas: 'As configurações de privacidade do seu navegador restringem o uso da área de desenho. Use um navegador ou dispositivo diferente, ou desative as configurações de privacidade que bloqueiam o canvas para assinar.',
wait_countdown_seconds: 'Aguarde {countdown} segundos' wait_countdown_seconds: 'Aguarde {countdown} segundos'
} }
@@ -1015,6 +1024,7 @@ const he = {
upload: 'העלאה', upload: 'העלאה',
files: 'קבצים', files: 'קבצים',
signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.', signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.',
browser_privacy_settings_block_canvas: 'הגדרות הפרטיות של הדפדפן שלך מגבילות את השימוש באזור הציור. אנא השתמש בדפדפן או מכשיר אחר, או בטל את הגדרות הפרטיות החוסמות canvas כדי לחתום.',
wait_countdown_seconds: 'המתן {countdown} שניות' wait_countdown_seconds: 'המתן {countdown} שניות'
} }
@@ -1117,6 +1127,7 @@ const nl = {
upload: 'Uploaden', upload: 'Uploaden',
files: 'Bestanden', files: 'Bestanden',
signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.', signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.',
browser_privacy_settings_block_canvas: 'De privacyinstellingen van uw browser beperken het gebruik van het tekenveld. Gebruik een andere browser of ander apparaat, of schakel de privacyinstellingen uit die canvas blokkeren om te ondertekenen.',
wait_countdown_seconds: 'Wacht {countdown} seconden' wait_countdown_seconds: 'Wacht {countdown} seconden'
} }
@@ -1219,6 +1230,7 @@ const ar = {
upload: 'تحميل', upload: 'تحميل',
files: 'الملفات', files: 'الملفات',
signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.', signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.',
browser_privacy_settings_block_canvas: 'إعدادات الخصوصية في متصفحك تمنع استخدام لوحة الرسم. يرجى استخدام متصفح أو جهاز مختلف، أو تعطيل إعدادات الخصوصية التي تحظر canvas للتوقيع.',
wait_countdown_seconds: 'انتظر {countdown} ثانية' wait_countdown_seconds: 'انتظر {countdown} ثانية'
} }
@@ -1321,6 +1333,7 @@ const ko = {
upload: '업로드', upload: '업로드',
files: '파일', files: '파일',
signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.', signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.',
browser_privacy_settings_block_canvas: '브라우저 개인정보 보호 설정으로 인해 그리기 캔버스를 사용할 수 없습니다. 다른 브라우저나 기기를 사용하거나, 서명을 위해 캔버스를 차단하는 개인정보 보호 설정을 비활성화해 주세요.',
wait_countdown_seconds: '{countdown}초 기다리세요' wait_countdown_seconds: '{countdown}초 기다리세요'
} }
@@ -1423,6 +1436,7 @@ const ja = {
upload: 'アップロード', upload: 'アップロード',
files: 'ファイル', files: 'ファイル',
signature_is_too_small_or_simple_please_redraw: '署名が小さすぎるか単純すぎます。もう一度描いてください。', signature_is_too_small_or_simple_please_redraw: '署名が小さすぎるか単純すぎます。もう一度描いてください。',
browser_privacy_settings_block_canvas: 'ブラウザのプライバシー設定により、描画キャンバスの使用が制限されています。別のブラウザまたはデバイスを使用するか、署名するためにキャンバスをブロックするプライバシー設定を無効にしてください。',
wait_countdown_seconds: '{countdown} 秒お待ちください' wait_countdown_seconds: '{countdown} 秒お待ちください'
} }
@@ -150,6 +150,7 @@
<script> <script>
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import { isCanvasBlocked } from './validate_signature'
import { IconReload, IconTextSize, IconUpload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconReload, IconTextSize, IconUpload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on' import AppearsOn from './appears_on'
@@ -175,6 +176,10 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
submitter: {
type: Object,
required: true
},
dryRun: { dryRun: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -257,6 +262,14 @@ export default {
this.$refs.canvas.getContext('2d').scale(scale, scale) this.$refs.canvas.getContext('2d').scale(scale, scale)
if (!this.isDrawInitials) {
this.$nextTick(() => {
if (this.$refs.textInput) {
this.initTextInitial()
}
})
}
this.intersectionObserver?.disconnect() this.intersectionObserver?.disconnect()
} }
}) })
@@ -332,12 +345,27 @@ export default {
if (!this.isDrawInitials) { if (!this.isDrawInitials) {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.textInput.focus() if (this.$refs.textInput) {
if (!this.submitter.name) {
this.$refs.textInput.focus()
}
this.$emit('start') this.initTextInitial()
this.$emit('start')
}
}) })
} }
}, },
initTextInitial () {
if (this.submitter.name) {
this.$refs.textInput.value = this.submitter.name.trim().split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase() || '').join('')
}
if (this.$refs.textInput.value) {
this.updateWrittenInitials({ target: this.$refs.textInput })
}
},
async submit () { async submit () {
if (this.modelValue || this.computedPreviousValue) { if (this.modelValue || this.computedPreviousValue) {
if (this.computedPreviousValue) { if (this.computedPreviousValue) {
@@ -392,7 +420,15 @@ export default {
} }
}).catch((error) => { }).catch((error) => {
if (this.field.required === true) { if (this.field.required === true) {
alert(this.t('signature_is_too_small_or_simple_please_redraw')) if (isCanvasBlocked()) {
alert(this.t('browser_privacy_settings_block_canvas'))
if (window.Rollbar) {
window.Rollbar.info('Canvas blocked')
}
} else {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
}
return reject(error) return reject(error)
} else { } else {
@@ -78,6 +78,11 @@ export default {
type: Array, type: Array,
required: true required: true
}, },
fetchOptions: {
type: Object,
required: false,
default: () => ({})
},
optionalSubmitters: { optionalSubmitters: {
type: Array, type: Array,
required: false, required: false,
@@ -108,7 +113,8 @@ export default {
return fetch(this.url, { return fetch(this.url, {
method: 'POST', method: 'POST',
body: new FormData(this.$refs.form) body: new FormData(this.$refs.form),
...this.fetchOptions
}).then((response) => { }).then((response) => {
if (response.status === 200) { if (response.status === 200) {
this.$emit('success') this.$emit('success')
+7 -3
View File
@@ -497,10 +497,10 @@ export default {
body: JSON.stringify(payload) body: JSON.stringify(payload)
}) })
if (!resp.ok) throw new Error('Failed to start KBA')
const data = await resp.json() const data = await resp.json()
if (!resp.ok) throw new Error(data.error || 'Failed to start KBA')
if (data.result && data.result.action === 'FAIL') { if (data.result && data.result.action === 'FAIL') {
if (data.result.detail === 'NO MATCH') { if (data.result.detail === 'NO MATCH') {
throw new Error('Unfortunately, we were unable to start Knowledge Based Authentication with the details provided. Please review and confirm that all your personal details are correct.') throw new Error('Unfortunately, we were unable to start Knowledge Based Authentication with the details provided. Please review and confirm that all your personal details are correct.')
@@ -555,7 +555,11 @@ export default {
const data = await resp.json() const data = await resp.json()
if (data.result?.action !== 'PASS') { if (data.result?.action !== 'PASS') {
this.error = 'Knowledge Based Authentication Failed - make sure you provide correct answers for the Knowledge Based authentication.' if (data.result?.issues?.length) {
this.error = `Knowledge Based Authentication Failed - make sure you provide correct details for the Knowledge Based authentication: ${data.result.issues.join(', ')}`
} else {
this.error = 'Knowledge Based Authentication Failed - make sure you provide correct answers for the Knowledge Based authentication.'
}
throw new Error('Knowledge Based Authentication Failed') throw new Error('Knowledge Based Authentication Failed')
} }
+130 -27
View File
@@ -127,6 +127,12 @@
type="hidden" type="hidden"
:name="`values[${field.uuid}]`" :name="`values[${field.uuid}]`"
> >
<input
v-if="isTouchAttachment"
:value="touchAttachmentUuid"
type="hidden"
name="touch_attachment_uuid"
>
<img <img
v-if="modelValue || computedPreviousValue" v-if="modelValue || computedPreviousValue"
:src="attachmentsIndex[modelValue || computedPreviousValue].url" :src="attachmentsIndex[modelValue || computedPreviousValue].url"
@@ -175,9 +181,7 @@
v-if="isShowQr" v-if="isShowQr"
class="top-0 bottom-0 right-0 left-0 absolute bg-base-content/10 rounded-2xl" class="top-0 bottom-0 right-0 left-0 absolute bg-base-content/10 rounded-2xl"
> >
<div <div class="absolute top-1.5 right-1.5">
class="absolute top-1.5 right-1.5 md:tooltip"
>
<a <a
href="#" href="#"
class="btn btn-sm btn-circle btn-normal btn-outline" class="btn btn-sm btn-circle btn-normal btn-outline"
@@ -305,14 +309,14 @@
<script> <script>
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue' import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue'
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import { isValidSignatureCanvas } from './validate_signature' import { isValidSignatureCanvas, isCanvasBlocked } from './validate_signature'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on' import AppearsOn from './appears_on'
import FileDropzone from './dropzone' import FileDropzone from './dropzone'
import MarkdownContent from './markdown_content' import MarkdownContent from './markdown_content'
import { v4 } from 'uuid' import { v4 } from 'uuid'
let isFontLoaded = false let fontLoadPromise = null
const scale = 3 const scale = 3
@@ -336,6 +340,10 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
values: {
type: Object,
required: true
},
requireSigningReason: { requireSigningReason: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -390,6 +398,11 @@ export default {
required: false, required: false,
default: '' default: ''
}, },
touchAttachmentUuid: {
type: String,
required: false,
default: ''
},
reason: { reason: {
type: String, type: String,
required: false, required: false,
@@ -401,13 +414,14 @@ export default {
default: '' default: ''
} }
}, },
emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason'], emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason', 'touch-attachment'],
data () { data () {
return { return {
isSignatureStarted: false, isSignatureStarted: false,
isShowQr: false, isShowQr: false,
isOtherReason: false, isOtherReason: false,
isUsePreviousValue: true, isUsePreviousValue: true,
isTouchAttachment: false,
isTextSignature: this.field.preferences?.format === 'typed' || this.field.preferences?.format === 'typed_or_upload', isTextSignature: this.field.preferences?.format === 'typed' || this.field.preferences?.format === 'typed_or_upload',
uploadImageInputKey: Math.random().toString() uploadImageInputKey: Math.random().toString()
} }
@@ -448,14 +462,7 @@ export default {
} }
}, },
async mounted () { async mounted () {
this.$nextTick(() => { this.$nextTick(() => this.setCanvasSize())
if (this.$refs.canvas) {
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth * scale / 3
this.$refs.canvas.getContext('2d').scale(scale, scale)
}
})
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas) this.pad = new SignaturePad(this.$refs.canvas)
@@ -470,13 +477,18 @@ export default {
this.$emit('start') this.$emit('start')
}) })
this.intersectionObserver = new IntersectionObserver((entries, observer) => { this.intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => { entries.forEach(entry => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale this.setCanvasSize()
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth * scale / 3
this.$refs.canvas.getContext('2d').scale(scale, scale) if (this.isTextSignature) {
this.$nextTick(() => {
if (this.$refs.textInput) {
this.initTypedSignature()
}
})
}
this.intersectionObserver?.disconnect() this.intersectionObserver?.disconnect()
} }
@@ -484,13 +496,66 @@ export default {
}) })
this.intersectionObserver.observe(this.$refs.canvas) this.intersectionObserver.observe(this.$refs.canvas)
this.resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(() => {
if (!this.$refs.canvas) return
const { width, height } = this.$refs.canvas
this.setCanvasSize()
if (this.$refs.canvas.width !== width || this.$refs.canvas.height !== height) {
this.redrawCanvas(width, height)
}
})
})
this.resizeObserver.observe(this.$refs.canvas.parentNode)
if (this.isTextSignature) {
this.loadFont()
}
} }
}, },
beforeUnmount () { beforeUnmount () {
this.intersectionObserver?.disconnect() this.intersectionObserver?.disconnect()
this.resizeObserver?.disconnect()
this.stopCheckSignature() this.stopCheckSignature()
}, },
methods: { methods: {
setCanvasSize () {
const canvas = this.$refs.canvas
if (canvas) {
const width = canvas.parentNode.clientWidth
const height = width / 3
if (canvas.width !== width * scale || canvas.height !== height * scale) {
canvas.width = width * scale
canvas.height = height * scale
canvas.getContext('2d').scale(scale, scale)
}
}
},
redrawCanvas (oldWidth, oldHeight) {
const canvas = this.$refs.canvas
if (this.pad && !this.isTextSignature && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
const sx = canvas.width / oldWidth
const sy = canvas.height / oldHeight
const scaledData = this.pad.toData().map((stroke) => ({
...stroke,
points: stroke.points.map((p) => ({ ...p, x: p.x * sx, y: p.y * sy }))
}))
this.pad.fromData(scaledData)
} else if (this.isTextSignature && this.$refs.textInput) {
this.updateWrittenSignature({ target: this.$refs.textInput })
}
},
remove () { remove () {
this.$emit('update:model-value', '') this.$emit('update:model-value', '')
@@ -498,17 +563,17 @@ export default {
this.isSignatureStarted = false this.isSignatureStarted = false
}, },
loadFont () { loadFont () {
if (!isFontLoaded) { if (!fontLoadPromise) {
const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript-Regular.otf) format("opentype")`) const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript-Regular.otf) format("opentype")`)
font.load().then((loadedFont) => { fontLoadPromise = font.load().then((loadedFont) => {
document.fonts.add(loadedFont) document.fonts.add(loadedFont)
isFontLoaded = true
}).catch((error) => { }).catch((error) => {
console.error('Font loading failed:', error) console.error('Font loading failed:', error)
}) })
} }
return fontLoadPromise
}, },
showQr () { showQr () {
this.isShowQr = true this.isShowQr = true
@@ -608,14 +673,29 @@ export default {
if (this.isTextSignature) { if (this.isTextSignature) {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.textInput.focus() if (this.$refs.textInput) {
if (!this.submitter.name) {
this.$refs.textInput.focus()
}
this.loadFont() this.initTypedSignature()
this.$emit('start') this.$emit('start')
}
}) })
} }
}, },
async initTypedSignature () {
if (this.submitter.name) {
this.$refs.textInput.value = this.submitter.name
}
await this.loadFont()
if (this.$refs.textInput.value) {
this.updateWrittenSignature({ target: this.$refs.textInput })
}
},
drawImage (event) { drawImage (event) {
this.remove() this.remove()
this.clear() this.clear()
@@ -694,6 +774,13 @@ export default {
}, },
async submit () { async submit () {
if (this.modelValue || this.computedPreviousValue) { if (this.modelValue || this.computedPreviousValue) {
if (this.touchAttachmentUuid && this.computedPreviousValue === this.touchAttachmentUuid && !Object.values(this.values).includes(this.touchAttachmentUuid)) {
this.isTouchAttachment = true
this.$emit('touch-attachment', this.touchAttachmentUuid)
} else {
this.isTouchAttachment = false
}
if (this.computedPreviousValue) { if (this.computedPreviousValue) {
this.$emit('update:model-value', this.computedPreviousValue) this.$emit('update:model-value', this.computedPreviousValue)
} }
@@ -703,7 +790,15 @@ export default {
if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) { if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) {
if (this.field.required === true || this.pad.toData().length > 0) { if (this.field.required === true || this.pad.toData().length > 0) {
alert(this.t('signature_is_too_small_or_simple_please_redraw')) if (isCanvasBlocked()) {
alert(this.t('browser_privacy_settings_block_canvas'))
if (window.Rollbar) {
window.Rollbar.info('Canvas blocked')
}
} else {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
}
return Promise.reject(new Error('Image too small or simple')) return Promise.reject(new Error('Image too small or simple'))
} else { } else {
@@ -759,7 +854,15 @@ export default {
} }
}).catch((error) => { }).catch((error) => {
if (this.field.required === true) { if (this.field.required === true) {
alert(this.t('signature_is_too_small_or_simple_please_redraw')) if (isCanvasBlocked()) {
alert(this.t('browser_privacy_settings_block_canvas'))
if (window.Rollbar) {
window.Rollbar.info('Canvas blocked')
}
} else {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
}
return reject(error) return reject(error)
} else { } else {
@@ -35,4 +35,24 @@ function isValidSignatureCanvas (data) {
return validStrokes.length > 0 return validStrokes.length > 0
} }
export { isValidSignatureCanvas } function isCanvasBlocked () {
try {
const testCanvas = document.createElement('canvas')
testCanvas.width = 2
testCanvas.height = 2
const ctx = testCanvas.getContext('2d')
ctx.fillStyle = 'rgb(255, 0, 0)'
ctx.fillRect(0, 0, 2, 2)
const pixel = ctx.getImageData(0, 0, 1, 1).data
return pixel[0] !== 255 || pixel[1] !== 0 || pixel[2] !== 0 || pixel[3] !== 255
} catch (e) {
return true
}
}
export { isValidSignatureCanvas, isCanvasBlocked }
@@ -119,6 +119,7 @@ export default {
docId: this.eidEasyData.doc_id, docId: this.eidEasyData.doc_id,
language: this.locale, language: this.locale,
countryCode: this.countryCode, countryCode: this.countryCode,
sandbox: ['demo.docuseal.tech'].includes(location.host),
enabledMethods: { enabledMethods: {
signature: this.eidEasyData.available_methods signature: this.eidEasyData.available_methods
}, },
+80 -20
View File
@@ -8,7 +8,7 @@
@touchstart="startTouchDrag" @touchstart="startTouchDrag"
> >
<div <div
v-if="isSelected || isDraw" v-if="isSelected || isDraw || isInMultiSelection"
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none" class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none"
:class="activeBorderClasses" :class="activeBorderClasses"
/> />
@@ -24,7 +24,7 @@
:style="{ left: (cellW / area.w * 100) + '%' }" :style="{ left: (cellW / area.w * 100) + '%' }"
> >
<span <span
v-if="index === 0 && editable" v-if="index === 0 && editable && !isInMultiSelection"
class="h-2.5 w-2.5 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-ew-resize z-10" class="h-2.5 w-2.5 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-ew-resize z-10"
style="left: -4px" style="left: -4px"
@mousedown.stop="startResizeCell" @mousedown.stop="startResizeCell"
@@ -32,7 +32,7 @@
</div> </div>
</div> </div>
<div <div
v-if="field?.type && (isSelected || isNameFocus)" v-if="field?.type && (isSelected || isNameFocus) && !isInMultiSelection"
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10 field-area-controls" class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10 field-area-controls"
style="top: -25px; height: 25px" style="top: -25px; height: 25px"
@mousedown.stop @mousedown.stop
@@ -48,7 +48,7 @@
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'" :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="selectedAreasRef.value = [area]"
/> />
<FieldType <FieldType
v-model="field.type" v-model="field.type"
@@ -57,7 +57,7 @@
:button-classes="'px-1'" :button-classes="'px-1'"
:menu-classes="'bg-white rounded-t-none'" :menu-classes="'bg-white rounded-t-none'"
@update:model-value="[maybeUpdateOptions(), save()]" @update:model-value="[maybeUpdateOptions(), save()]"
@click="selectedAreaRef.value = area" @click="selectedAreasRef.value = [area]"
/> />
<span <span
v-if="field.type !== 'checkbox' || field.name" v-if="field.type !== 'checkbox' || field.name"
@@ -134,6 +134,7 @@
@focusout="maybeBlurSettings" @focusout="maybeBlurSettings"
> >
<FieldSettings <FieldSettings
v-if="isMobile"
:field="field" :field="field"
:default-field="defaultField" :default-field="defaultField"
:editable="editable" :editable="editable"
@@ -145,9 +146,17 @@
@click-formula="isShowFormulaModal = true" @click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true" @click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true" @click-description="isShowDescriptionModal = true"
@add-custom-field="$emit('add-custom-field')"
@click-condition="isShowConditionsModal = true" @click-condition="isShowConditionsModal = true"
@scroll-to="[selectedAreaRef.value = $event, $emit('scroll-to', $event)]" @save="save"
@scroll-to="[selectedAreasRef.value = [$event], $emit('scroll-to', $event)]"
/> />
<div
v-else
class="whitespace-normal"
>
The dots menu is retired in favor of the field context menu. Right-click the field to access field settings. Double-click the field to set a default value.
</div>
</ul> </ul>
</span> </span>
</div> </div>
@@ -250,7 +259,8 @@
</span> </span>
<div <div
v-else-if="field.type === 'cells' && field.default_value" v-else-if="field.type === 'cells' && field.default_value"
class="w-full flex items-center" class="w-full flex"
:class="fontClasses"
> >
<div <div
v-for="(char, index) in field.default_value" v-for="(char, index) in field.default_value"
@@ -266,7 +276,7 @@
ref="defaultValueSelect" ref="defaultValueSelect"
class="bg-transparent outline-none focus:outline-none w-full" class="bg-transparent outline-none focus:outline-none w-full"
@change="[field.default_value = $event.target.value, field.readonly = !!field.default_value?.length, save()]" @change="[field.default_value = $event.target.value, field.readonly = !!field.default_value?.length, save()]"
@focus="selectedAreaRef.value = area" @focus="selectedAreasRef.value = [area]"
@keydown.enter="onDefaultValueEnter" @keydown.enter="onDefaultValueEnter"
> >
<option <option
@@ -293,7 +303,7 @@
:class="{ 'cursor-text': isValueInput }" :class="{ 'cursor-text': isValueInput }"
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : (field.type === 'date' ? field.preferences?.format || t('type_value') : t('type_value'))" :placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : (field.type === 'date' ? field.preferences?.format || t('type_value') : t('type_value'))"
@blur="onDefaultValueBlur" @blur="onDefaultValueBlur"
@focus="selectedAreaRef.value = area" @focus="selectedAreasRef.value = [area]"
@paste.prevent="onPaste" @paste.prevent="onPaste"
@keydown.enter="onDefaultValueEnter" @keydown.enter="onDefaultValueEnter"
>{{ field.default_value }}</span> >{{ field.default_value }}</span>
@@ -319,6 +329,7 @@
<span <span
v-if="field?.type && editable" v-if="field?.type && editable"
class="h-4 w-4 lg:h-2.5 lg:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize" class="h-4 w-4 lg:h-2.5 lg:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
:class="{ 'z-30': isInMultiSelection }"
@mousedown.stop="startResize" @mousedown.stop="startResize"
@touchstart="startTouchResize" @touchstart="startTouchResize"
/> />
@@ -331,6 +342,7 @@
:editable="editable && !defaultField" :editable="editable && !defaultField"
:default-field="defaultField" :default-field="defaultField"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@save="save"
@close="isShowFormulaModal = false" @close="isShowFormulaModal = false"
/> />
</Teleport> </Teleport>
@@ -343,6 +355,7 @@
:editable="editable && !defaultField" :editable="editable && !defaultField"
:default-field="defaultField" :default-field="defaultField"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@save="save"
@close="isShowFontModal = false" @close="isShowFontModal = false"
/> />
</Teleport> </Teleport>
@@ -354,6 +367,7 @@
:item="field" :item="field"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
:default-field="defaultField" :default-field="defaultField"
@save="save"
@close="isShowConditionsModal = false" @close="isShowConditionsModal = false"
/> />
</Teleport> </Teleport>
@@ -366,6 +380,7 @@
:editable="editable && !defaultField" :editable="editable && !defaultField"
:default-field="defaultField" :default-field="defaultField"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@save="save"
@close="isShowDescriptionModal = false" @close="isShowDescriptionModal = false"
/> />
</Teleport> </Teleport>
@@ -398,7 +413,7 @@ export default {
FieldSubmitter, FieldSubmitter,
IconX IconX
}, },
inject: ['template', 'selectedAreaRef', 'save', 't', 'isInlineSize'], inject: ['template', 'save', 't', 'isInlineSize', 'selectedAreasRef', 'isCmdKeyRef', 'getFieldTypeIndex'],
props: { props: {
area: { area: {
type: Object, type: Object,
@@ -463,9 +478,19 @@ export default {
type: Object, type: Object,
required: false, required: false,
default: null default: null
},
isMobile: {
type: Boolean,
required: false,
default: false
},
isSelectMode: {
type: Boolean,
required: false,
default: false
} }
}, },
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to'], emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field'],
data () { data () {
return { return {
isShowFormulaModal: false, isShowFormulaModal: false,
@@ -571,7 +596,7 @@ export default {
return this.$el.getRootNode().querySelector('#docuseal_modal_container') return this.$el.getRootNode().querySelector('#docuseal_modal_container')
}, },
defaultName () { defaultName () {
return this.buildDefaultName(this.field, this.template.fields) return this.buildDefaultName(this.field)
}, },
fontClasses () { fontClasses () {
if (!this.field.preferences) { if (!this.field.preferences) {
@@ -646,7 +671,10 @@ export default {
] ]
}, },
isSelected () { isSelected () {
return this.selectedAreaRef.value === this.area return this.selectedAreasRef.value.includes(this.area)
},
isInMultiSelection () {
return this.selectedAreasRef.value.length >= 2 && this.isSelected
}, },
positionStyle () { positionStyle () {
const { x, y, w, h } = this.area const { x, y, w, h } = this.area
@@ -683,10 +711,10 @@ export default {
buildAreaOptionValue (area) { buildAreaOptionValue (area) {
const option = this.optionsUuidIndex[area.option_uuid] const option = this.optionsUuidIndex[area.option_uuid]
return option.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}` return option?.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}`
}, },
maybeToggleDefaultValue () { maybeToggleDefaultValue () {
if (!this.editable) { if (!this.editable || this.isCmdKeyRef.value) {
return return
} }
@@ -770,7 +798,7 @@ export default {
} }
}, },
onNameFocus (e) { onNameFocus (e) {
this.selectedAreaRef.value = this.area this.selectedAreasRef.value = [this.area]
this.isNameFocus = true this.isNameFocus = true
this.$refs.name.style.minWidth = this.$refs.name.clientWidth + 'px' this.$refs.name.style.minWidth = this.$refs.name.clientWidth + 'px'
@@ -906,6 +934,15 @@ export default {
if (e.target.id === 'mask') { if (e.target.id === 'mask') {
this.area.w = e.offsetX / e.target.clientWidth - this.area.x this.area.w = e.offsetX / e.target.clientWidth - this.area.x
this.area.h = e.offsetY / e.target.clientHeight - this.area.y this.area.h = e.offsetY / e.target.clientHeight - this.area.y
if (this.isInMultiSelection) {
this.selectedAreasRef.value.forEach((area) => {
if (area !== this.area) {
area.w = this.area.w
area.h = this.area.h
}
})
}
} }
}, },
drag (e) { drag (e) {
@@ -931,7 +968,7 @@ export default {
const rect = e.target.getBoundingClientRect() const rect = e.target.getBoundingClientRect()
this.selectedAreaRef.value = this.area this.selectedAreasRef.value = [this.area]
this.dragFrom = { x: rect.left - e.touches[0].clientX, y: rect.top - e.touches[0].clientY } this.dragFrom = { x: rect.left - e.touches[0].clientX, y: rect.top - e.touches[0].clientY }
@@ -980,13 +1017,23 @@ export default {
e.preventDefault() e.preventDefault()
if (e.metaKey || e.ctrlKey) {
if (!this.selectedAreasRef.value.includes(this.area)) {
this.selectedAreasRef.value.push(this.area)
} else {
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(this.area), 1)
}
return
}
if (this.editable) { if (this.editable) {
this.isDragged = true this.isDragged = true
} }
const rect = e.target.getBoundingClientRect() const rect = e.target.getBoundingClientRect()
this.selectedAreaRef.value = this.area this.selectedAreasRef.value = [this.area]
this.dragFrom = { x: rect.left - e.clientX, y: rect.top - e.clientY } this.dragFrom = { x: rect.left - e.clientX, y: rect.top - e.clientY }
@@ -1055,7 +1102,9 @@ export default {
this.$emit('stop-drag') this.$emit('stop-drag')
}, },
startResize () { startResize () {
this.selectedAreaRef.value = this.area if (!this.selectedAreasRef.value.includes(this.area)) {
this.selectedAreasRef.value = [this.area]
}
this.$el.getRootNode().addEventListener('mousemove', this.resize) this.$el.getRootNode().addEventListener('mousemove', this.resize)
this.$el.getRootNode().addEventListener('mouseup', this.stopResize) this.$el.getRootNode().addEventListener('mouseup', this.stopResize)
@@ -1071,7 +1120,9 @@ export default {
this.save() this.save()
}, },
startTouchResize (e) { startTouchResize (e) {
this.selectedAreaRef.value = this.area if (!this.selectedAreasRef.value.includes(this.area)) {
this.selectedAreasRef.value = [this.area]
}
this.$refs?.name?.blur() this.$refs?.name?.blur()
@@ -1088,6 +1139,15 @@ export default {
this.area.w = (e.touches[0].clientX - rect.left) / rect.width - this.area.x this.area.w = (e.touches[0].clientX - rect.left) / rect.width - this.area.x
this.area.h = (e.touches[0].clientY - rect.top) / rect.height - this.area.y this.area.h = (e.touches[0].clientY - rect.top) / rect.height - this.area.y
if (this.isInMultiSelection) {
this.selectedAreasRef.value.forEach((area) => {
if (area !== this.area) {
area.w = this.area.w
area.h = this.area.h
}
})
}
}, },
stopTouchResize () { stopTouchResize () {
this.$el.getRootNode().removeEventListener('touchmove', this.touchResize) this.$el.getRootNode().removeEventListener('touchmove', this.touchResize)
File diff suppressed because it is too large. Load diff
@@ -9,7 +9,7 @@
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl"> <div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium"> <div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title"> <span class="modal-title">
{{ t('condition') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item, template.fields) }} {{ t('condition') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item) }}
</span> </span>
<a <a
href="#" href="#"
@@ -83,7 +83,7 @@
class="text-base-content" class="text-base-content"
:selected="condition.field_uuid === f.uuid" :selected="condition.field_uuid === f.uuid"
> >
{{ f.name || buildDefaultName(f, template.fields) }} {{ f.name || buildDefaultName(f) }}
</option> </option>
</select> </select>
<select <select
@@ -124,6 +124,16 @@
{{ option.value || `${t('option')} ${index + 1}` }} {{ option.value || `${t('option')} ${index + 1}` }}
</option> </option>
</select> </select>
<input
v-else-if="conditionField(condition)?.type === 'number' && ['equal', 'not_equal', 'greater_than', 'less_than'].includes(condition.action)"
v-model="condition.value"
type="number"
step="any"
class="input input-bordered input-sm w-full bg-white h-11 pl-4 text-base font-normal"
:class="{ 'text-gray-300': !condition.value }"
:placeholder="t('type_value')"
required
>
</div> </div>
</div> </div>
<a <a
@@ -154,7 +164,7 @@
<script> <script>
export default { export default {
name: 'ConditionModal', name: 'ConditionModal',
inject: ['t', 'save', 'template', 'withConditions'], inject: ['t', 'template', 'withConditions'],
props: { props: {
item: { item: {
type: Object, type: Object,
@@ -168,9 +178,14 @@ export default {
buildDefaultName: { buildDefaultName: {
type: Function, type: Function,
required: true required: true
},
excludeFieldUuids: {
type: Array,
required: false,
default: () => []
} }
}, },
emits: ['close'], emits: ['close', 'save'],
data () { data () {
return { return {
conditions: this.item.conditions?.[0] ? JSON.parse(JSON.stringify(this.item.conditions)) : [{}] conditions: this.item.conditions?.[0] ? JSON.parse(JSON.stringify(this.item.conditions)) : [{}]
@@ -183,14 +198,14 @@ export default {
fields () { fields () {
if (this.item.submitter_uuid) { if (this.item.submitter_uuid) {
return this.template.fields.reduce((acc, f) => { return this.template.fields.reduce((acc, f) => {
if (f !== this.item && !this.excludeTypes.includes(f.type) && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) { if (f !== this.item && !this.excludeTypes.includes(f.type) && !this.excludeFieldUuids.includes(f.uuid) && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
acc.push(f) acc.push(f)
} }
return acc return acc
}, []) }, [])
} else { } else {
return this.template.fields return this.template.fields.filter((f) => !this.excludeFieldUuids.includes(f.uuid))
} }
} }
}, },
@@ -217,6 +232,8 @@ export default {
actions.push('equal', 'not_equal') actions.push('equal', 'not_equal')
} else if (['multiple'].includes(field.type)) { } else if (['multiple'].includes(field.type)) {
actions.push('contains', 'does_not_contain') actions.push('contains', 'does_not_contain')
} else if (field.type === 'number') {
actions.push('not_empty', 'empty', 'equal', 'not_equal', 'greater_than', 'less_than')
} else { } else {
actions.push('not_empty', 'empty') actions.push('not_empty', 'empty')
} }
@@ -234,7 +251,7 @@ export default {
delete this.item.conditions delete this.item.conditions
} }
this.save() this.$emit('save')
this.$emit('close') this.$emit('close')
} }
} }
@@ -6,18 +6,24 @@
<span <span
ref="contenteditable" ref="contenteditable"
dir="auto" dir="auto"
:contenteditable="editable" :contenteditable="editable && (!editableOnButton || isEditable)"
style="min-width: 2px" :data-placeholder="placeholder"
:class="[iconInline ? 'inline' : 'block', hideIcon ? 'focus:block' : '']" :data-empty="isEmpty"
class="peer outline-none" :style="{ minWidth }"
:class="[iconInline ? (isEmpty ? 'inline-block' : 'inline') : 'block', hideIcon ? 'focus:block' : '']"
class="peer relative inline-block outline-none before:pointer-events-none before:absolute before:left-0 before:top-0 before:select-none before:whitespace-pre before:text-neutral-400 before:content-[attr(data-placeholder)] before:opacity-0 data-[empty=true]:before:opacity-100"
@paste.prevent="onPaste" @paste.prevent="onPaste"
@keydown.enter.prevent="blurContenteditable" @keydown.enter.prevent="blurContenteditable"
@input="updateInputValue"
@cut="updateInputValue"
@focus="$emit('focus', $event)" @focus="$emit('focus', $event)"
@blur="onBlur" @blur="onBlur"
@click="editable && (!editableOnButton || isEditable) ? '' : $emit('click-contenteditable')"
> >
{{ value }} {{ value }}
</span> </span>
<span <span
v-if="withButton"
class="relative inline" class="relative inline"
:class="{ 'peer-focus:hidden': hideIcon, 'peer-focus:invisible': !hideIcon }" :class="{ 'peer-focus:hidden': hideIcon, 'peer-focus:invisible': !hideIcon }"
> >
@@ -28,7 +34,7 @@
:class="{ invisible: !editable, 'absolute top-1/2 -translate-y-1/2': !iconInline || floatIcon, 'inline align-bottom': iconInline, 'left-0': floatIcon }" :class="{ invisible: !editable, 'absolute top-1/2 -translate-y-1/2': !iconInline || floatIcon, 'inline align-bottom': iconInline, 'left-0': floatIcon }"
:width="iconWidth + 4" :width="iconWidth + 4"
:stroke-width="iconStrokeWidth" :stroke-width="iconStrokeWidth"
@click="[focusContenteditable(), selectOnEditClick && selectContent()]" @click="clickEdit"
/> />
</span> </span>
</div> </div>
@@ -49,6 +55,16 @@ export default {
required: false, required: false,
default: '' default: ''
}, },
placeholder: {
type: String,
required: false,
default: ''
},
withButton: {
type: Boolean,
required: false,
default: true
},
iconInline: { iconInline: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -74,6 +90,16 @@ export default {
required: false, required: false,
default: false default: false
}, },
editableOnButton: {
type: Boolean,
required: false,
default: false
},
minWidth: {
type: String,
required: false,
default: '2px'
},
editable: { editable: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -85,21 +111,34 @@ export default {
default: 2 default: 2
} }
}, },
emits: ['update:model-value', 'focus', 'blur'], emits: ['update:model-value', 'focus', 'blur', 'click-contenteditable'],
data () { data () {
return { return {
isEditable: false,
inputValue: '',
value: '' value: ''
} }
}, },
computed: {
isEmpty () {
return !this.inputValue.replace(/\u200B/g, '').replace(/\u00A0/g, ' ').trim()
}
},
watch: { watch: {
modelValue: { modelValue: {
handler (value) { handler (value) {
this.value = value this.value = value || ''
}, },
immediate: true immediate: true
} }
}, },
mounted () {
this.updateInputValue()
},
methods: { methods: {
updateInputValue () {
this.inputValue = this.$refs.contenteditable?.textContent || ''
},
onPaste (e) { onPaste (e) {
const text = (e.clipboardData || window.clipboardData).getData('text/plain') const text = (e.clipboardData || window.clipboardData).getData('text/plain')
@@ -110,6 +149,20 @@ export default {
selection.getRangeAt(0).insertNode(document.createTextNode(text)) selection.getRangeAt(0).insertNode(document.createTextNode(text))
selection.collapseToEnd() selection.collapseToEnd()
} }
this.updateInputValue()
},
clickEdit (e) {
this.focusContenteditable()
if (this.selectOnEditClick) {
this.selectContent()
}
},
setText (text) {
this.$refs.contenteditable.innerText = text
this.updateInputValue()
}, },
selectContent () { selectContent () {
const el = this.$refs.contenteditable const el = this.$refs.contenteditable
@@ -126,13 +179,24 @@ export default {
}, },
onBlur (e) { onBlur (e) {
setTimeout(() => { setTimeout(() => {
this.value = this.$refs.contenteditable.innerText.trim() || this.modelValue if (this.$refs.contenteditable) {
this.$emit('update:model-value', this.value) this.value = this.$refs.contenteditable.innerText.trim() || this.modelValue
this.$emit('update:model-value', this.value)
}
this.$emit('blur', e) this.$emit('blur', e)
this.isEditable = false
}, 1) }, 1)
}, },
focusContenteditable () { focusContenteditable () {
this.$refs.contenteditable.focus() this.isEditable = true
this.$nextTick(() => {
this.$refs.contenteditable.focus()
this.updateInputValue()
})
}, },
blurContenteditable () { blurContenteditable () {
this.$refs.contenteditable.blur() this.$refs.contenteditable.blur()
@@ -0,0 +1,271 @@
<template>
<div
class="list-field group"
>
<div
class="border border-dashed border-base-300 hover:border-base-content/20 rounded relative group fields-list-item transition-colors"
:style="{ backgroundColor: backgroundColor }"
>
<div class="flex items-center justify-between relative group/contenteditable-container">
<div
v-if="!isNew"
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
@click="$emit('click', field)"
/>
<div
class="absolute top-0 bottom-0 left-0 flex items-center transition-all cursor-grab group-hover:bg-base-200/50"
@click="$emit('click', field)"
>
<IconDrag style="margin-left: 1px" />
</div>
<div class="flex items-center p-1 pl-6 space-x-1">
<FieldType
v-model="field.type"
:editable="false"
:button-width="20"
@click="$emit('click', field)"
/>
<Contenteditable
ref="name"
:model-value="field.name"
:placeholder="'Field Name'"
:icon-inline="true"
:icon-width="18"
:min-width="isNew ? '100px' : '2px'"
:icon-stroke-width="1.6"
:editable-on-button="!isNew"
:with-button="!isNew"
:class="{ 'cursor-pointer': !isNew }"
@click-contenteditable="$emit('click', field)"
@focus="onNameFocus"
@blur="onNameBlur"
/>
</div>
<div
class="flex items-center space-x-1"
>
<PaymentSettings
v-if="field.type === 'payment' && !isNew"
:field="field"
:with-condition="false"
:with-force-open="false"
@click-description="isShowDescriptionModal = true"
@click-formula="isShowFormulaModal = true"
/>
<span
v-else-if="!isNew"
class="dropdown dropdown-end field-settings-dropdown"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
>
<label
tabindex="0"
:title="t('settings')"
class="cursor-pointer text-transparent group-hover:text-base-content"
>
<IconSettings
:width="18"
:stroke-width="1.6"
/>
</label>
<ul
v-if="renderDropdown"
tabindex="0"
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10"
:style="{ backgroundColor: dropdownBgColor }"
draggable="true"
@dragstart.prevent.stop
@click="closeDropdown"
>
<FieldSettings
:field="field"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:background-color="dropdownBgColor"
:with-areas="false"
:with-copy-to-all-pages="false"
:with-condition="false"
@click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true"
@save="$emit('save')"
/>
</ul>
</span>
<button
v-if="isNew && !$refs.name"
class="relative text-base-content pr-1 field-save-button"
:title="t('save')"
@click="field.name ? $emit('save', field) : focusName()"
>
<IconCheck
:width="18"
:stroke-width="2"
/>
</button>
<button
class="relative group-hover:text-base-content pr-1 field-remove-button"
:class="isNew ? 'text-base-content' : 'text-transparent group-hover:text-base-content'"
:title="t('remove')"
@click="onRemoveClick"
>
<IconTrashX
:width="18"
:stroke-width="1.6"
/>
</button>
</div>
</div>
</div>
<Teleport
v-if="isShowFormulaModal"
:to="modalContainerEl"
>
<FormulaModal
:field="field"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@save="$emit('save')"
@close="isShowFormulaModal = false"
/>
</Teleport>
<Teleport
v-if="isShowFontModal"
:to="modalContainerEl"
>
<FontModal
:field="field"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@save="$emit('save')"
@close="isShowFontModal = false"
/>
</Teleport>
<Teleport
v-if="isShowDescriptionModal"
:to="modalContainerEl"
>
<DescriptionModal
:field="field"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@save="$emit('save')"
@close="isShowDescriptionModal = false"
/>
</Teleport>
</div>
</template>
<script>
import Contenteditable from './contenteditable'
import FieldType from './field_type'
import PaymentSettings from './payment_settings'
import FieldSettings from './field_settings'
import FormulaModal from './formula_modal'
import FontModal from './font_modal'
import DescriptionModal from './description_modal'
import { IconTrashX, IconSettings, IconCheck } from '@tabler/icons-vue'
import IconDrag from './icon_drag'
export default {
name: 'CustomField',
components: {
Contenteditable,
IconSettings,
IconCheck,
FieldSettings,
PaymentSettings,
IconDrag,
FormulaModal,
FontModal,
DescriptionModal,
IconTrashX,
FieldType
},
inject: ['backgroundColor', 't'],
props: {
field: {
type: Object,
required: true
},
isNew: {
type: Boolean,
required: false,
default: false
},
withSignatureId: {
type: Boolean,
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
}
},
emits: ['remove', 'save', 'click'],
data () {
return {
isShowFormulaModal: false,
isShowFontModal: false,
isShowDescriptionModal: false,
renderDropdown: false
}
},
computed: {
dropdownBgColor () {
return ['', null, 'transparent'].includes(this.backgroundColor) ? 'white' : this.backgroundColor
},
modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
}
},
created () {
this.field.preferences ||= {}
},
mounted () {
if (this.isNew) {
this.focusName()
}
},
methods: {
buildDefaultName () {
return this.t('custom')
},
focusName () {
setTimeout(() => {
this.$refs.name.clickEdit()
}, 1)
},
onNameFocus (e) {
if (!this.field.name) {
setTimeout(() => {
this.$refs.name.$refs.contenteditable.innerText = ' '
}, 1)
}
},
closeDropdown () {
this.$el.getRootNode().activeElement.blur()
},
onRemoveClick () {
if (this.isNew || window.confirm(this.t('are_you_sure_'))) {
this.$emit('remove', this.field)
}
},
onNameBlur (e) {
const text = this.$refs.name.$refs.contenteditable.innerText.trim()
if (text) {
this.field.name = text
} else {
this.$refs.name.setText(this.field.name)
}
if (this.field.name) {
this.$emit('save')
}
}
}
}
</script>
@@ -9,7 +9,7 @@
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl"> <div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium"> <div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title"> <span class="modal-title">
{{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field) }}
</span> </span>
<a <a
href="#" href="#"
@@ -67,7 +67,7 @@
<script> <script>
export default { export default {
name: 'DescriptionModal', name: 'DescriptionModal',
inject: ['t', 'save', 'template'], inject: ['t', 'template'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -88,7 +88,7 @@ export default {
required: true required: true
} }
}, },
emits: ['close'], emits: ['close', 'save'],
data () { data () {
return { return {
description: this.field.description, description: this.field.description,
@@ -103,7 +103,7 @@ export default {
this.field.description = this.description this.field.description = this.description
this.field.title = this.title this.field.title = this.title
this.save() this.$emit('save')
this.$emit('close') this.$emit('close')
}, },
resizeTextarea () { resizeTextarea () {
+37 -3
View File
@@ -13,17 +13,28 @@
:with-signature-id="withSignatureId" :with-signature-id="withSignatureId"
:with-prefillable="withPrefillable" :with-prefillable="withPrefillable"
:is-drag="isDrag" :is-drag="isDrag"
:is-mobile="isMobile"
:with-field-placeholder="withFieldPlaceholder" :with-field-placeholder="withFieldPlaceholder"
:default-fields="defaultFields" :default-fields="defaultFields"
:drag-field-placeholder="dragFieldPlaceholder" :drag-field-placeholder="dragFieldPlaceholder"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:draw-field="drawField" :draw-field="drawField"
:draw-field-type="drawFieldType" :draw-field-type="drawFieldType"
:draw-custom-field="drawCustomField"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
:total-pages="sortedPreviewImages.length" :total-pages="sortedPreviewImages.length"
:image="image" :image="image"
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })" :attachment-uuid="document.uuid"
:with-fields-detection="withFieldsDetection"
@drop-field="$emit('drop-field', { ...$event, attachment_uuid: document.uuid })"
@remove-area="$emit('remove-area', $event)" @remove-area="$emit('remove-area', $event)"
@copy-field="$emit('copy-field', $event)"
@paste-field="$emit('paste-field', { ...$event, attachment_uuid: document.uuid })"
@add-custom-field="$emit('add-custom-field', $event)"
@set-draw="$emit('set-draw', $event)"
@copy-selected-areas="$emit('copy-selected-areas')"
@delete-selected-areas="$emit('delete-selected-areas')"
@autodetect-fields="$emit('autodetect-fields', $event)"
@scroll-to="scrollToArea" @scroll-to="scrollToArea"
@draw="$emit('draw', { area: {...$event.area, attachment_uuid: document.uuid }, isTooSmall: $event.isTooSmall })" @draw="$emit('draw', { area: {...$event.area, attachment_uuid: document.uuid }, isTooSmall: $event.isTooSmall })"
/> />
@@ -88,6 +99,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
isMobile: {
type: Boolean,
required: false,
default: false
},
allowDraw: { allowDraw: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -107,6 +123,11 @@ export default {
required: false, required: false,
default: null default: null
}, },
drawCustomField: {
type: Object,
required: false,
default: null
},
baseUrl: { baseUrl: {
type: String, type: String,
required: false, required: false,
@@ -116,9 +137,14 @@ export default {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
},
withFieldsDetection: {
type: Boolean,
required: false,
default: false
} }
}, },
emits: ['draw', 'drop-field', 'remove-area'], emits: ['draw', 'drop-field', 'remove-area', 'paste-field', 'copy-field', 'copy-selected-areas', 'delete-selected-areas', 'autodetect-fields', 'add-custom-field', 'set-draw'],
data () { data () {
return { return {
pageRefs: [] pageRefs: []
@@ -160,7 +186,15 @@ export default {
methods: { methods: {
scrollToArea (area) { scrollToArea (area) {
this.$nextTick(() => { this.$nextTick(() => {
this.pageRefs[area.page].areaRefs.find((e) => e.area === area).$el.scrollIntoView({ behavior: 'smooth', block: 'center' }) const pageRef = this.pageRefs[area.page]
if (pageRef && pageRef.areaRefs) {
const areaRef = pageRef.areaRefs.find((e) => e.area === area)
if (areaRef && areaRef.$el) {
areaRef.$el.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
}
}) })
}, },
setPageRefs (el) { setPageRefs (el) {
@@ -8,6 +8,13 @@
class="fixed z-20 pointer-events-none" class="fixed z-20 pointer-events-none"
:editable="false" :editable="false"
/> />
<CustomField
v-else-if="dragPlaceholder && isCustom && !isMask && field"
ref="dragPlaceholder"
:style="dragPlaceholderStyle"
:field="field"
class="fixed z-20 pointer-events-none opacity-90"
/>
<div <div
v-else-if="dragPlaceholder && (isDefault || isRequired) && !isMask && field" v-else-if="dragPlaceholder && (isDefault || isRequired) && !isMask && field"
ref="dragPlaceholder" ref="dragPlaceholder"
@@ -57,6 +64,7 @@
<script> <script>
import Field from './field' import Field from './field'
import CustomField from './custom_field'
import IconDrag from './icon_drag' import IconDrag from './icon_drag'
import FieldType from './field_type' import FieldType from './field_type'
@@ -64,6 +72,7 @@ export default {
name: 'DragPlaceholder', name: 'DragPlaceholder',
components: { components: {
Field, Field,
CustomField,
IconDrag IconDrag
}, },
inject: ['t', 'backgroundColor'], inject: ['t', 'backgroundColor'],
@@ -87,6 +96,11 @@ export default {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
},
isCustom: {
type: Boolean,
required: false,
default: false
} }
}, },
data () { data () {
+64 -16
View File
@@ -1,6 +1,7 @@
<template> <template>
<div <div
class="list-field group mb-2" class="list-field group"
:class="`list-field-${field.type}`"
> >
<div <div
class="border border-base-300 rounded relative group fields-list-item" class="border border-base-300 rounded relative group fields-list-item"
@@ -18,7 +19,7 @@
:button-width="20" :button-width="20"
:menu-classes="'mt-1.5'" :menu-classes="'mt-1.5'"
:menu-style="{ backgroundColor: dropdownBgColor }" :menu-style="{ backgroundColor: dropdownBgColor }"
@update:model-value="[maybeUpdateOptions(), save()]" @update:model-value="[maybeUpdateOptions(), $emit('save')]"
@click="scrollToFirstArea" @click="scrollToFirstArea"
/> />
<Contenteditable <Contenteditable
@@ -92,9 +93,12 @@
<PaymentSettings <PaymentSettings
v-if="field.type === 'payment'" v-if="field.type === 'payment'"
:field="field" :field="field"
:with-custom-fields="withCustomFields"
@click-condition="isShowConditionsModal = true" @click-condition="isShowConditionsModal = true"
@click-description="isShowDescriptionModal = true" @click-description="isShowDescriptionModal = true"
@add-custom-field="$emit('add-custom-field', $event)"
@click-formula="isShowFormulaModal = true" @click-formula="isShowFormulaModal = true"
@save="$emit('save')"
/> />
<span <span
v-else v-else
@@ -128,12 +132,15 @@
:with-signature-id="withSignatureId" :with-signature-id="withSignatureId"
:with-prefillable="withPrefillable" :with-prefillable="withPrefillable"
:background-color="dropdownBgColor" :background-color="dropdownBgColor"
:with-custom-fields="withCustomFields"
@click-formula="isShowFormulaModal = true" @click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true" @click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true" @click-description="isShowDescriptionModal = true"
@click-condition="isShowConditionsModal = true" @click-condition="isShowConditionsModal = true"
@set-draw="$emit('set-draw', $event)" @set-draw="$emit('set-draw', $event)"
@add-custom-field="$emit('add-custom-field', $event)"
@remove-area="removeArea" @remove-area="removeArea"
@save="$emit('save')"
@scroll-to="$emit('scroll-to', $event)" @scroll-to="$emit('scroll-to', $event)"
/> />
</ul> </ul>
@@ -154,8 +161,10 @@
v-if="field.options && withOptions && (isExpandOptions || field.options.length < 5)" v-if="field.options && withOptions && (isExpandOptions || field.options.length < 5)"
ref="options" ref="options"
class="border-t border-base-300 mx-2 pt-2 space-y-1.5" class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
draggable="true"
@dragover="onOptionDragover" @dragover="onOptionDragover"
@drop="reorderOptions" @drop="reorderOptions"
@dragstart.prevent.stop
> >
<div <div
v-for="(option, index) in field.options" v-for="(option, index) in field.options"
@@ -184,7 +193,8 @@
required required
:placeholder="`${t('option')} ${index + 1}`" :placeholder="`${t('option')} ${index + 1}`"
@keydown.enter="option.value ? addOptionAt(index + 1) : null" @keydown.enter="option.value ? addOptionAt(index + 1) : null"
@blur="save" @blur="$emit('save')"
@paste="onOptionPaste($event, index)"
> >
<button <button
:title="t('draw')" :title="t('draw')"
@@ -208,7 +218,8 @@
dir="auto" dir="auto"
@keydown.enter="option.value ? addOptionAt(index + 1) : null" @keydown.enter="option.value ? addOptionAt(index + 1) : null"
@focus="maybeFocusOnOptionArea(option)" @focus="maybeFocusOnOptionArea(option)"
@blur="save" @blur="$emit('save')"
@paste="onOptionPaste($event, index)"
> >
<button <button
v-if="editable && !defaultField" v-if="editable && !defaultField"
@@ -259,6 +270,7 @@
:editable="editable && !defaultField" :editable="editable && !defaultField"
:default-field="defaultField" :default-field="defaultField"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@save="$emit('save')"
@close="isShowFormulaModal = false" @close="isShowFormulaModal = false"
/> />
</Teleport> </Teleport>
@@ -271,6 +283,7 @@
:editable="editable && !defaultField" :editable="editable && !defaultField"
:default-field="defaultField" :default-field="defaultField"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@save="$emit('save')"
@close="isShowFontModal = false" @close="isShowFontModal = false"
/> />
</Teleport> </Teleport>
@@ -282,6 +295,7 @@
:item="field" :item="field"
:default-field="defaultField" :default-field="defaultField"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@save="$emit('save')"
@close="isShowConditionsModal = false" @close="isShowConditionsModal = false"
/> />
</Teleport> </Teleport>
@@ -294,6 +308,7 @@
:editable="editable && !defaultField" :editable="editable && !defaultField"
:default-field="defaultField" :default-field="defaultField"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@save="$emit('save')"
@close="isShowDescriptionModal = false" @close="isShowDescriptionModal = false"
/> />
</Teleport> </Teleport>
@@ -330,7 +345,7 @@ export default {
IconMathFunction, IconMathFunction,
FieldType FieldType
}, },
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't', 'locale'], inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale', 'getFieldTypeIndex'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -341,6 +356,11 @@ export default {
required: false, required: false,
default: null default: null
}, },
withCustomFields: {
type: Boolean,
required: false,
default: false
},
withPrefillable: { withPrefillable: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -362,12 +382,11 @@ export default {
default: true default: true
} }
}, },
emits: ['set-draw', 'remove', 'scroll-to'], emits: ['set-draw', 'remove', 'scroll-to', 'save', 'add-custom-field'],
data () { data () {
return { return {
isExpandOptions: false, isExpandOptions: false,
isNameFocus: false, isNameFocus: false,
showPaymentModal: false,
isShowFormulaModal: false, isShowFormulaModal: false,
isShowFontModal: false, isShowFontModal: false,
isShowConditionsModal: false, isShowConditionsModal: false,
@@ -398,7 +417,7 @@ export default {
return this.$el.getRootNode().querySelector('#docuseal_modal_container') return this.$el.getRootNode().querySelector('#docuseal_modal_container')
}, },
defaultName () { defaultName () {
return this.buildDefaultName(this.field, this.template.fields) return this.buildDefaultName(this.field)
}, },
areas () { areas () {
return this.field.areas || [] return this.field.areas || []
@@ -416,9 +435,9 @@ export default {
removeArea (area) { removeArea (area) {
this.field.areas.splice(this.field.areas.indexOf(area), 1) this.field.areas.splice(this.field.areas.indexOf(area), 1)
this.save() this.$emit('save')
}, },
buildDefaultName (field, fields) { buildDefaultName (field) {
if (field.type === 'payment' && field.preferences?.price && !field.preferences?.formula) { if (field.type === 'payment' && field.preferences?.price && !field.preferences?.formula) {
const { price, currency } = field.preferences || {} const { price, currency } = field.preferences || {}
@@ -429,7 +448,7 @@ export default {
return `${this.fieldNames[field.type]} ${formattedPrice}` return `${this.fieldNames[field.type]} ${formattedPrice}`
} else { } else {
const typeIndex = fields.filter((f) => f.type === field.type).indexOf(field) const typeIndex = this.getFieldTypeIndex(field)
if (field.type === 'heading' || field.type === 'strikethrough') { if (field.type === 'heading' || field.type === 'strikethrough') {
return `${this.fieldNames[field.type]} ${typeIndex + 1}` return `${this.fieldNames[field.type]} ${typeIndex + 1}`
@@ -451,7 +470,7 @@ export default {
const area = this.field.areas.find((a) => a.option_uuid === option.uuid) const area = this.field.areas.find((a) => a.option_uuid === option.uuid)
if (area) { if (area) {
this.selectedAreaRef.value = area this.selectedAreasRef.value = [area]
} }
}, },
scrollToFirstArea () { scrollToFirstArea () {
@@ -460,6 +479,35 @@ export default {
closeDropdown () { closeDropdown () {
this.$el.getRootNode().activeElement.blur() this.$el.getRootNode().activeElement.blur()
}, },
onOptionPaste (e, index) {
const text = e.clipboardData.getData('text')
if (text.includes('\n')) {
e.preventDefault()
this.isExpandOptions = true
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l)
if (lines.length > 0) {
const currentOption = this.field.options[index]
currentOption.value = (currentOption.value + lines[0]).trim()
const newOptions = lines.slice(1).map((line) => ({ value: line, uuid: v4() }))
this.field.options.splice(index + 1, 0, ...newOptions)
this.$nextTick(() => {
const inputs = this.$refs.options.querySelectorAll('input')
inputs[index + newOptions.length]?.focus()
})
this.$emit('save')
}
}
},
addOptionAt (index) { addOptionAt (index) {
this.isExpandOptions = true this.isExpandOptions = true
@@ -473,7 +521,7 @@ export default {
inputs[insertAt]?.focus() inputs[insertAt]?.focus()
}) })
this.save() this.$emit('save')
}, },
removeOption (option) { removeOption (option) {
this.field.options.splice(this.field.options.indexOf(option), 1) this.field.options.splice(this.field.options.indexOf(option), 1)
@@ -484,7 +532,7 @@ export default {
this.field.areas.splice(this.field.areas.findIndex((a) => a.option_uuid === option.uuid), 1) this.field.areas.splice(this.field.areas.findIndex((a) => a.option_uuid === option.uuid), 1)
} }
this.save() this.$emit('save')
}, },
maybeUpdateOptions () { maybeUpdateOptions () {
delete this.field.default_value delete this.field.default_value
@@ -526,7 +574,7 @@ export default {
this.isNameFocus = false this.isNameFocus = false
this.save() this.$emit('save')
}, },
onOptionDragstart (event, option) { onOptionDragstart (event, option) {
this.optionDragRef = option this.optionDragRef = option
@@ -587,7 +635,7 @@ export default {
if (newOrder.length === this.field.options.length) { if (newOrder.length === this.field.options.length) {
this.field.options.splice(0, this.field.options.length, ...newOrder) this.field.options.splice(0, this.field.options.length, ...newOrder)
this.save() this.$emit('save')
} }
this.optionDragRef = null this.optionDragRef = null
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,49 @@
<template>
<Teleport :to="modalContainerEl">
<div class="modal modal-open items-start !animate-none overflow-y-auto">
<div
class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')"
/>
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title">
{{ title }}
</span>
<a
href="#"
class="text-xl modal-close-button"
@click.prevent="$emit('close')"
>&times;</a>
</div>
<form @submit.prevent="$emit('save')">
<slot />
<button
class="base-button w-full mt-4 modal-save-button"
type="submit"
>
{{ t('save') }}
</button>
</form>
</div>
</div>
</Teleport>
</template>
<script>
export default {
name: 'ContextModal',
inject: ['t'],
props: {
title: {
type: String,
required: true
},
modalContainerEl: {
type: Element,
required: true
}
},
emits: ['close', 'save']
}
</script>
@@ -0,0 +1,147 @@
<template>
<div
class="relative"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
@click.stop="isOpen ? close() : open()"
>
<span class="flex items-center space-x-2">
<component
:is="icon || 'span'"
class="w-4 h-4"
/>
<span>{{ label }}</span>
</span>
<IconChevronRight class="w-4 h-4" />
</button>
<div
v-if="isOpen"
ref="submenu"
class="absolute p-1 z-50 left-full bg-white shadow-lg rounded-lg border border-neutral-200 cursor-default"
style="min-width: 170px"
:style="submenuStyle"
:class="menuClass"
@click.stop
>
<slot>
<button
v-for="option in options"
:key="option.value"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between space-x-2 text-sm cursor-pointer"
@click="handleSelect(option.value)"
>
<span class="whitespace-nowrap">{{ option.label }}</span>
<IconCheck
v-if="modelValue === option.value"
class="w-4 h-4"
/>
</button>
</slot>
</div>
</div>
</template>
<script>
import { IconChevronRight, IconCheck } from '@tabler/icons-vue'
export default {
name: 'ContextSubmenu',
components: {
IconChevronRight,
IconCheck
},
props: {
icon: {
type: [Function],
required: false,
default: null
},
label: {
type: String,
required: true
},
options: {
type: Array,
default: () => []
},
modelValue: {
type: [String, Number],
default: null
},
menuClass: {
type: String,
default: ''
}
},
emits: ['select', 'update:modelValue'],
data () {
return {
isOpen: false,
topOffset: 0
}
},
computed: {
submenuStyle () {
return {
top: this.topOffset + 'px'
}
}
},
beforeUnmount () {
this.clearTimeout()
},
methods: {
handleMouseEnter () {
clearTimeout(this.closeTimeout)
this.openTimeout = setTimeout(() => this.open(), 200)
},
handleMouseLeave () {
clearTimeout(this.openTimeout)
this.closeTimeout = setTimeout(() => this.close(), 200)
},
open () {
this.clearTimeout()
this.isOpen = true
this.topOffset = 0
this.$nextTick(() => setTimeout(() => this.adjustPosition(), 0))
},
clearTimeout () {
if (this.openTimeout) {
clearTimeout(this.openTimeout)
}
if (this.closeTimeout) {
clearTimeout(this.closeTimeout)
}
},
close () {
this.clearTimeout()
this.isOpen = false
},
handleSelect (value) {
this.$emit('select', value)
this.$emit('update:modelValue', value)
},
adjustPosition () {
if (!this.$refs.submenu) return
const rect = this.$refs.submenu.getBoundingClientRect()
const overflow = rect.bottom - window.innerHeight
if (overflow > 0) {
this.topOffset = -overflow - 4
} else {
this.topOffset = 0
}
}
}
}
</script>
@@ -1,16 +1,16 @@
<template> <template>
<div <div
v-if="field.type === 'verification'" v-if="field.type === 'verification'"
class="py-1.5 px-1 relative" class="field-settings-verification-method py-1.5 px-1 relative"
@click.stop @click.stop
> >
<select <select
:placeholder="t('method')" :placeholder="t('method')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent" class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
@change="[field.preferences ||= {}, field.preferences.method = $event.target.value, save()]" @change="[field.preferences ||= {}, field.preferences.method = $event.target.value, $emit('save')]"
> >
<option <option
v-for="method in ['QeS', 'AeS']" v-for="method in verificationMethods"
:key="method" :key="method"
:value="method.toLowerCase()" :value="method.toLowerCase()"
:selected="method.toLowerCase() === field.preferences?.method || (method === 'QeS' && !field.preferences?.method)" :selected="method.toLowerCase() === field.preferences?.method || (method === 'QeS' && !field.preferences?.method)"
@@ -26,42 +26,16 @@
{{ t('method') }} {{ t('method') }}
</label> </label>
</div> </div>
<div
v-if="['cells'].includes(field.type)"
class="py-1.5 px-1 relative"
@click.stop
>
<select
class="select select-bordered select-xs w-full max-w-xs h-7 !outline-0 font-normal bg-transparent"
@change="[field.preferences ||= {}, field.preferences.align = $event.target.value, save()]"
>
<option
v-for="value in ['left', 'right', field.type === 'cells' ? null : 'center'].filter(Boolean)"
:key="value"
:selected="field.preferences?.align ? value === field.preferences.align : value === 'left'"
:value="value"
>
{{ t(value) }}
</option>
</select>
<label
:style="{ backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('align') }}
</label>
</div>
<div <div
v-if="['select', 'radio'].includes(field.type) && !defaultField" v-if="['select', 'radio'].includes(field.type) && !defaultField"
class="py-1.5 px-1 relative" class="field-settings-default-value py-1.5 px-1 relative"
@click.stop @click.stop
> >
<select <select
:placeholder="t('default_value')" :placeholder="t('default_value')"
dir="auto" dir="auto"
class="select select-bordered select-xs w-full max-w-xs h-7 !outline-0 font-normal bg-transparent" class="select select-bordered select-xs w-full max-w-xs h-7 !outline-0 font-normal bg-transparent"
@change="[field.default_value = $event.target.value, !field.default_value && delete field.default_value, save()]" @change="[field.default_value = $event.target.value, !field.default_value && delete field.default_value, $emit('save')]"
> >
<option <option
value="" value=""
@@ -88,7 +62,7 @@
</div> </div>
<div <div
v-if="['text', 'number'].includes(field.type) && !defaultField" v-if="['text', 'number'].includes(field.type) && !defaultField"
class="py-1.5 px-1 relative" class="field-settings-default-value py-1.5 px-1 relative"
@click.stop @click.stop
> >
<input <input
@@ -97,7 +71,7 @@
dir="auto" dir="auto"
:type="field.type" :type="field.type"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent" class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
@blur="save" @blur="$emit('save')"
> >
<label <label
v-if="field.default_value" v-if="field.default_value"
@@ -110,7 +84,7 @@
</div> </div>
<div <div
v-if="['text', 'cells'].includes(field.type)" v-if="['text', 'cells'].includes(field.type)"
class="py-1.5 px-1 relative" class="field-settings-validation py-1.5 px-1 relative"
@click.stop @click.stop
> >
<select <select
@@ -148,7 +122,7 @@
</div> </div>
<div <div
v-if="['text', 'cells'].includes(field.type) && field.validation && lengthValidation" v-if="['text', 'cells'].includes(field.type) && field.validation && lengthValidation"
class="py-1.5 px-1 relative flex space-x-1" class="field-settings-length-validation py-1.5 px-1 relative flex space-x-1"
@click.stop @click.stop
> >
<div class="w-1/2 relative"> <div class="w-1/2 relative">
@@ -159,7 +133,7 @@
:value="lengthValidation.min" :value="lengthValidation.min"
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent" class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
@input="field.validation.pattern = `.{${$event.target.value || 0},${lengthValidation.max || ''}}`" @input="field.validation.pattern = `.{${$event.target.value || 0},${lengthValidation.max || ''}}`"
@blur="save" @blur="$emit('save')"
> >
<label <label
v-if="lengthValidation.min" v-if="lengthValidation.min"
@@ -178,7 +152,7 @@
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent" class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
:value="lengthValidation.max" :value="lengthValidation.max"
@input="field.validation.pattern = `.{${lengthValidation.min},${$event.target.value || ''}}`" @input="field.validation.pattern = `.{${lengthValidation.min},${$event.target.value || ''}}`"
@blur="save" @blur="$emit('save')"
> >
<label <label
v-if="lengthValidation.max" v-if="lengthValidation.max"
@@ -192,7 +166,7 @@
</div> </div>
<div <div
v-if="field.type === 'number'" v-if="field.type === 'number'"
class="py-1.5 px-1 relative flex space-x-1" class="field-settings-number-range py-1.5 px-1 relative flex space-x-1"
@click.stop @click.stop
> >
<div class="w-1/2 relative"> <div class="w-1/2 relative">
@@ -203,7 +177,7 @@
:value="field.validation?.min" :value="field.validation?.min"
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent" class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
@input="[field.validation ||= {}, $event.target.value ? field.validation.min = $event.target.value : delete field.validation.min]" @input="[field.validation ||= {}, $event.target.value ? field.validation.min = $event.target.value : delete field.validation.min]"
@blur="save" @blur="$emit('save')"
> >
<label <label
v-if="field.validation?.min" v-if="field.validation?.min"
@@ -222,7 +196,7 @@
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent" class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
:value="field.validation?.max" :value="field.validation?.max"
@input="[field.validation ||= {}, $event.target.value ? field.validation.max = $event.target.value : delete field.validation.max]" @input="[field.validation ||= {}, $event.target.value ? field.validation.max = $event.target.value : delete field.validation.max]"
@blur="save" @blur="$emit('save')"
> >
<label <label
v-if="field.validation?.max" v-if="field.validation?.max"
@@ -236,13 +210,13 @@
</div> </div>
<div <div
v-if="field.type === 'number'" v-if="field.type === 'number'"
class="py-1.5 px-1 relative" class="field-settings-number-format py-1.5 px-1 relative"
@click.stop @click.stop
> >
<select <select
:placeholder="t('format')" :placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent" class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
@change="[field.preferences ||= {}, field.preferences.format = $event.target.value, save()]" @change="[field.preferences ||= {}, field.preferences.format = $event.target.value, $emit('save')]"
> >
<option <option
v-for="format in numberFormats" v-for="format in numberFormats"
@@ -263,7 +237,7 @@
</div> </div>
<div <div
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation" v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
class="py-1.5 px-1 relative" class="field-settings-custom-validation py-1.5 px-1 relative"
@click.stop @click.stop
> >
<input <input
@@ -272,7 +246,7 @@
:placeholder="t('regexp_validation')" :placeholder="t('regexp_validation')"
dir="auto" dir="auto"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent" class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
@blur="save" @blur="$emit('save')"
> >
<label <label
v-if="field.validation.pattern" v-if="field.validation.pattern"
@@ -285,7 +259,7 @@
</div> </div>
<div <div
v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation" v-if="['text', 'cells'].includes(field.type) && field.validation && !validations[field.validation.pattern] && !lengthValidation"
class="py-1.5 px-1 relative" class="field-settings-error-message py-1.5 px-1 relative"
@click.stop @click.stop
> >
<input <input
@@ -293,7 +267,7 @@
:placeholder="t('error_message')" :placeholder="t('error_message')"
dir="auto" dir="auto"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent" class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
@blur="save" @blur="$emit('save')"
> >
<label <label
v-if="field.validation.message" v-if="field.validation.message"
@@ -306,14 +280,14 @@
</div> </div>
<div <div
v-if="field.type === 'date'" v-if="field.type === 'date'"
class="py-1.5 px-1 relative" class="field-settings-date-format py-1.5 px-1 relative"
@click.stop @click.stop
> >
<select <select
v-model="field.preferences.format" v-model="field.preferences.format"
:placeholder="t('format')" :placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent" class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
@change="save" @change="$emit('save')"
> >
<option <option
v-for="format in dateFormats" v-for="format in dateFormats"
@@ -333,13 +307,13 @@
</div> </div>
<div <div
v-if="field.type === 'signature'" v-if="field.type === 'signature'"
class="py-1.5 px-1 relative" class="field-settings-signature-format py-1.5 px-1 relative"
@click.stop @click.stop
> >
<select <select
:placeholder="t('format')" :placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent" class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
@change="[field.preferences.format = $event.target.value, save()]" @change="[field.preferences.format = $event.target.value, $emit('save')]"
> >
<option <option
value="any" value="any"
@@ -348,7 +322,7 @@
{{ t('any') }} {{ t('any') }}
</option> </option>
<option <option
v-for="type in ['drawn', 'typed', 'drawn_or_typed', 'drawn_or_upload', 'upload']" v-for="type in signatureFormats"
:key="type" :key="type"
:value="type" :value="type"
:selected="field.preferences?.format === type" :selected="field.preferences?.format === type"
@@ -366,6 +340,7 @@
</div> </div>
<li <li
v-if="[true, false].includes(withSignatureId) && field.type === 'signature'" v-if="[true, false].includes(withSignatureId) && field.type === 'signature'"
class="field-settings-signature-id"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
@@ -374,13 +349,14 @@
type="checkbox" type="checkbox"
:disabled="!editable || (defaultField && [true, false].includes(defaultField.required))" :disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
class="toggle toggle-xs" class="toggle toggle-xs"
@change="[field.preferences ||= {}, field.preferences.with_signature_id = $event.target.checked, save()]" @change="[field.preferences ||= {}, field.preferences.with_signature_id = $event.target.checked, $emit('save')]"
> >
<span class="label-text">{{ t('signature_id') }}</span> <span class="label-text">{{ t('signature_id') }}</span>
</label> </label>
</li> </li>
<li <li
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification' && field.type !== 'strikethrough' && field.type !== 'heading'" v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification' && field.type !== 'strikethrough' && field.type !== 'heading'"
class="field-settings-required"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
@@ -389,13 +365,14 @@
type="checkbox" type="checkbox"
:disabled="!editable || (defaultField && [true, false].includes(defaultField.required))" :disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
class="toggle toggle-xs" class="toggle toggle-xs"
@update:model-value="save" @update:model-value="$emit('save')"
> >
<span class="label-text">{{ t('required') }}</span> <span class="label-text">{{ t('required') }}</span>
</label> </label>
</li> </li>
<li <li
v-if="field.type == 'stamp'" v-if="field.type == 'stamp'"
class="field-settings-with-logo"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
@@ -403,13 +380,14 @@
:checked="field.preferences?.with_logo != false" :checked="field.preferences?.with_logo != false"
type="checkbox" type="checkbox"
class="toggle toggle-xs" class="toggle toggle-xs"
@change="[field.preferences ||= {}, field.preferences.with_logo = field.preferences.with_logo == false, save()]" @change="[field.preferences ||= {}, field.preferences.with_logo = field.preferences.with_logo == false, $emit('save')]"
> >
<span class="label-text">{{ t('with_logo') }}</span> <span class="label-text">{{ t('with_logo') }}</span>
</label> </label>
</li> </li>
<li <li
v-if="field.type == 'checkbox'" v-if="field.type == 'checkbox'"
class="field-settings-checked"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
@@ -417,13 +395,14 @@
v-model="field.default_value" v-model="field.default_value"
type="checkbox" type="checkbox"
class="toggle toggle-xs" class="toggle toggle-xs"
@update:model-value="[field.default_value = $event, field.readonly = $event, save()]" @update:model-value="[field.default_value = $event, field.readonly = $event, $emit('save')]"
> >
<span class="label-text">{{ t('checked') }}</span> <span class="label-text">{{ t('checked') }}</span>
</label> </label>
</li> </li>
<li <li
v-if="field.type == 'date'" v-if="field.type == 'date'"
class="field-settings-set-signing-date"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
@@ -431,13 +410,14 @@
v-model="field.readonly" v-model="field.readonly"
type="checkbox" type="checkbox"
class="toggle toggle-xs" class="toggle toggle-xs"
@update:model-value="[field.default_value = $event ? '{{date}}' : null, field.readonly = $event, save()]" @update:model-value="[field.default_value = $event ? '{{date}}' : null, field.readonly = $event, $emit('save')]"
> >
<span class="label-text">{{ t('set_signing_date') }}</span> <span class="label-text">{{ t('set_signing_date') }}</span>
</label> </label>
</li> </li>
<li <li
v-if="['text', 'number'].includes(field.type)" v-if="['text', 'number', 'radio', 'multiple', 'select'].includes(field.type)"
class="field-settings-read-only"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
@@ -446,13 +426,14 @@
type="checkbox" type="checkbox"
class="toggle toggle-xs" class="toggle toggle-xs"
:disabled="!editable || (defaultField && [true, false].includes(defaultField.readonly))" :disabled="!editable || (defaultField && [true, false].includes(defaultField.readonly))"
@update:model-value="save" @update:model-value="$emit('save')"
> >
<span class="label-text">{{ t('read_only') }}</span> <span class="label-text">{{ t('read_only') }}</span>
</label> </label>
</li> </li>
<li <li
v-if="withPrefillable && ['text', 'number', 'cells', 'date', 'checkbox', 'select', 'radio', 'phone'].includes(field['type'])" v-if="withPrefillable && prefillableFieldTypes.includes(field['type'])"
class="field-settings-prefillable"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
@@ -461,7 +442,7 @@
type="checkbox" type="checkbox"
:disabled="!editable || (defaultField && [true, false].includes(defaultField.prefillable))" :disabled="!editable || (defaultField && [true, false].includes(defaultField.prefillable))"
class="toggle toggle-xs" class="toggle toggle-xs"
@update:model-value="save" @update:model-value="$emit('save')"
> >
<span class="label-text">{{ t('prefillable') }}</span> <span class="label-text">{{ t('prefillable') }}</span>
</label> </label>
@@ -470,7 +451,10 @@
v-if="field.type != 'stamp'" v-if="field.type != 'stamp'"
class="pb-0.5 mt-0.5" class="pb-0.5 mt-0.5"
> >
<li v-if="['text', 'number', 'date', 'select', 'heading'].includes(field.type)"> <li
v-if="['text', 'number', 'date', 'select', 'heading', 'cells'].includes(field.type)"
class="field-settings-font"
>
<label <label
class="label-text cursor-pointer text-center w-full flex items-center" class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-font')" @click="$emit('click-font')"
@@ -485,6 +469,7 @@
</li> </li>
<li <li
v-if="field.type != 'stamp' && field.type != 'heading' && field.type != 'strikethrough'" v-if="field.type != 'stamp' && field.type != 'heading' && field.type != 'strikethrough'"
class="field-settings-description"
> >
<label <label
class="label-text cursor-pointer text-center w-full flex items-center" class="label-text cursor-pointer text-center w-full flex items-center"
@@ -499,7 +484,8 @@
</label> </label>
</li> </li>
<li <li
v-if="field.type != 'stamp' && field.type != 'heading'" v-if="withCondition && field.type != 'stamp' && field.type != 'heading'"
class="field-settings-condition"
> >
<label <label
class="label-text cursor-pointer text-center w-full flex items-center" class="label-text cursor-pointer text-center w-full flex items-center"
@@ -513,7 +499,10 @@
</span> </span>
</label> </label>
</li> </li>
<li v-if="field.type == 'number'"> <li
v-if="field.type == 'number'"
class="field-settings-formula"
>
<label <label
class="label-text cursor-pointer text-center w-full flex items-center" class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-formula')" @click="$emit('click-formula')"
@@ -526,11 +515,15 @@
</span> </span>
</label> </label>
</li> </li>
<hr class="pb-0.5 mt-0.5"> <hr
v-if="withCopyToAllPages || withAreas || withCustomFields"
class="pb-0.5 mt-0.5"
>
<template v-if="withAreas"> <template v-if="withAreas">
<li <li
v-for="(area, index) in sortedAreas" v-for="(area, index) in sortedAreas"
:key="index" :key="index"
class="field-settings-area"
> >
<a <a
href="#" href="#"
@@ -550,7 +543,10 @@
/> />
</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)"
class="field-settings-draw-new-area"
>
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"
@@ -564,7 +560,10 @@
</a> </a>
</li> </li>
</template> </template>
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells', 'stamp'].includes(field.type)"> <li
v-if="withCopyToAllPages && field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells', 'stamp'].includes(field.type)"
class="field-settings-copy-to-all-pages"
>
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"
@@ -577,10 +576,26 @@
{{ t('copy_to_all_pages') }} {{ t('copy_to_all_pages') }}
</a> </a>
</li> </li>
<li
v-if="withCustomFields"
class="field-settings-save-as-custom-field"
>
<a
href="#"
class="text-sm py-1 px-2"
@click.prevent="$emit('add-custom-field', field)"
>
<IconForms
:width="20"
:stroke-width="1.6"
/>
{{ t('save_as_custom_field') }}
</a>
</li>
</template> </template>
<script> <script>
import { IconRouteAltLeft, IconTypography, IconShape, IconX, IconMathFunction, IconNewSection, IconInfoCircle, IconCopy } from '@tabler/icons-vue' import { IconRouteAltLeft, IconTypography, IconShape, IconX, IconMathFunction, IconNewSection, IconInfoCircle, IconCopy, IconForms } from '@tabler/icons-vue'
export default { export default {
name: 'FieldSettings', name: 'FieldSettings',
@@ -589,17 +604,33 @@ export default {
IconInfoCircle, IconInfoCircle,
IconMathFunction, IconMathFunction,
IconRouteAltLeft, IconRouteAltLeft,
IconForms,
IconCopy, IconCopy,
IconNewSection, IconNewSection,
IconTypography, IconTypography,
IconX IconX
}, },
inject: ['template', 'save', 't'], inject: ['template', 't'],
props: { props: {
field: { field: {
type: Object, type: Object,
required: true required: true
}, },
withCondition: {
type: Boolean,
required: false,
default: true
},
withCustomFields: {
type: Boolean,
required: false,
default: false
},
withCopyToAllPages: {
type: Boolean,
required: false,
default: true
},
withSignatureId: { withSignatureId: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -636,7 +667,7 @@ export default {
default: true default: true
} }
}, },
emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition', 'click-font', 'remove-area'], emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition', 'click-font', 'remove-area', 'save', 'add-custom-field'],
data () { data () {
return { return {
selectedValidation: '' selectedValidation: ''
@@ -686,7 +717,7 @@ export default {
}, },
lengthValidation () { lengthValidation () {
if (this.field.validation?.pattern && this.selectedValidation !== 'custom') { if (this.field.validation?.pattern && this.selectedValidation !== 'custom') {
return this.field.validation.pattern.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups return this.parseLengthPattern(this.field.validation.pattern)
} else { } else {
return null return null
} }
@@ -703,6 +734,15 @@ export default {
'^[a-zA-Z]+$': 'letters_only' '^[a-zA-Z]+$': 'letters_only'
} }
}, },
signatureFormats () {
return ['drawn', 'typed', 'drawn_or_typed', 'drawn_or_upload', 'upload']
},
verificationMethods () {
return ['QeS', 'AeS']
},
prefillableFieldTypes () {
return ['text', 'number', 'cells', 'date', 'checkbox', 'select', 'radio', 'phone']
},
sortedAreas () { sortedAreas () {
return (this.field.areas || []).sort((a, b) => { return (this.field.areas || []).sort((a, b) => {
return this.schemaAttachmentsIndexes[a.attachment_uuid] - this.schemaAttachmentsIndexes[b.attachment_uuid] return this.schemaAttachmentsIndexes[a.attachment_uuid] - this.schemaAttachmentsIndexes[b.attachment_uuid]
@@ -730,7 +770,7 @@ export default {
delete this.field.validation delete this.field.validation
} }
this.save() this.$emit('save')
}, },
copyToAllPages (field) { copyToAllPages (field) {
const areaString = JSON.stringify(field.areas[0]) const areaString = JSON.stringify(field.areas[0])
@@ -747,7 +787,7 @@ export default {
this.$emit('scroll-to', this.field.areas[this.field.areas.length - 1]) this.$emit('scroll-to', this.field.areas[this.field.areas.length - 1])
this.save() this.$emit('save')
}, },
formatNumber (number, format) { formatNumber (number, format) {
if (format === 'comma') { if (format === 'comma') {
@@ -766,6 +806,9 @@ export default {
return number return number
} }
}, },
parseLengthPattern (pattern) {
return pattern?.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups || null
},
formatDate (date, format) { formatDate (date, format) {
const monthFormats = { const monthFormats = {
M: 'numeric', M: 'numeric',
+229 -6
View File
@@ -15,7 +15,8 @@
</div> </div>
<div <div
ref="fields" ref="fields"
class="fields mb-1 mt-2" class="fields mt-2"
:class="{ 'mb-1': !withCustomFields || !customFields.length }"
@dragover.prevent="onFieldDragover" @dragover.prevent="onFieldDragover"
@drop="fieldsDragFieldRef.value ? reorderFields() : null" @drop="fieldsDragFieldRef.value ? reorderFields() : null"
> >
@@ -24,13 +25,17 @@
:key="field.uuid" :key="field.uuid"
: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="getFieldTypeIndex(field)"
:editable="editable" :editable="editable"
:with-signature-id="withSignatureId" :with-signature-id="withSignatureId"
:with-prefillable="withPrefillable" :with-prefillable="withPrefillable"
:default-field="defaultFieldsIndex[field.name]" :default-field="defaultFieldsIndex[field.name]"
:draggable="editable" :draggable="editable"
:with-custom-fields="withCustomFields"
class="mb-1.5"
@add-custom-field="addCustomField"
@dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]" @dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
@save="save"
@dragend="[fieldsDragFieldRef.value = null, $emit('set-drag-placeholder', null)]" @dragend="[fieldsDragFieldRef.value = null, $emit('set-drag-placeholder', null)]"
@remove="removeField" @remove="removeField"
@scroll-to="$emit('scroll-to-area', $event)" @scroll-to="$emit('scroll-to-area', $event)"
@@ -105,7 +110,92 @@
</div> </div>
</div> </div>
<div <div
v-if="editable && !onlyDefinedFields" v-if="editable && withCustomFields && (customFields.length || newCustomField)"
class="tabs w-full mb-1.5"
>
<a
class="tab tab-bordered w-1/2 border-base-300"
:class="{ 'tab-active': !showCustomTab }"
:style="{ '--tab-border': showCustomTab ? '0px' : '0.5px' }"
@click="setFieldsTab('default')"
>{{ t('default') }}</a>
<a
class="tab tab-bordered w-1/2 border-base-300"
:class="{ 'tab-active': showCustomTab }"
:style="{ '--tab-border': showCustomTab ? '0.5px' : '0px' }"
@click="setFieldsTab('custom')"
>{{ t('custom') }}</a>
</div>
<div
v-if="showCustomTab && editable && (customFields.length || newCustomField)"
ref="customFields"
class="custom-fields"
@dragover.prevent="onCustomFieldDragover"
@drop="customDragFieldRef.value ? reorderCustomFields() : null"
>
<template v-if="isShowCustomFieldSearch">
<input
v-model="customFieldsSearch"
:placeholder="t('search_field')"
class="input input-ghost input-xs px-0 text-base mb-1 !outline-0 !rounded bg-transparent w-full"
>
<hr class="mb-2">
</template>
<div
ref="customFieldsList"
class="overflow-auto relative"
:style="{
maxHeight: isShowCustomFieldSearch ? '320px' : '',
minHeight: '320px'
}"
>
<div
v-if="!filteredCustomFields.length && customFieldsSearch"
class="top-0 bottom-0 text-center absolute flex items-center justify-center w-full flex-col"
>
<div>
{{ t('field_not_found') }}
</div>
<a
href="#"
class="link"
@click.prevent="customFieldsSearch = ''"
>
{{ t('clear') }}
</a>
</div>
<CustomField
v-if="newCustomField"
:key="newCustomField.uuid"
ref="newCustomField"
:data-uuid="newCustomField.uuid"
:is-new="true"
:field="newCustomField"
:draggable="false"
class="mb-1.5"
@save="onNewCustomFieldSave"
@remove="newCustomField = null"
/>
<CustomField
v-for="field in filteredCustomFields"
:key="field.uuid"
:data-uuid="field.uuid"
:field="field"
:draggable="true"
class="mb-1.5"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
@save="saveCustomFields"
@click="$emit('set-draw-custom-field', field)"
@dragstart="[customDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
@dragend="[customDragFieldRef.value = null, $emit('set-drag-placeholder', null)]"
@remove="removeCustomField"
@set-draw="$emit('set-draw', $event)"
/>
</div>
</div>
<div
v-if="editable && !onlyDefinedFields && (!showCustomTab || (!customFields.length && !newCustomField))"
id="field-types-grid" id="field-types-grid"
class="grid grid-cols-3 gap-1 pb-2 fields-grid" class="grid grid-cols-3 gap-1 pb-2 fields-grid"
> >
@@ -266,15 +356,18 @@
<script> <script>
import Field from './field' import Field from './field'
import CustomField from './custom_field'
import FieldType from './field_type' import FieldType from './field_type'
import FieldSubmitter from './field_submitter' import FieldSubmitter from './field_submitter'
import { IconLock, IconCirclePlus, IconInnerShadowTop, IconSparkles } from '@tabler/icons-vue' import { IconLock, IconCirclePlus, IconInnerShadowTop, IconSparkles } from '@tabler/icons-vue'
import IconDrag from './icon_drag' import IconDrag from './icon_drag'
import { v4 } from 'uuid'
export default { export default {
name: 'TemplateFields', name: 'TemplateFields',
components: { components: {
Field, Field,
CustomField,
FieldType, FieldType,
IconCirclePlus, IconCirclePlus,
IconSparkles, IconSparkles,
@@ -283,12 +376,22 @@ export default {
IconDrag, IconDrag,
IconLock IconLock
}, },
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'baseFetch'], inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'baseFetch', 'selectedAreasRef', 'getFieldTypeIndex'],
props: { props: {
fields: { fields: {
type: Array, type: Array,
required: true required: true
}, },
customFields: {
type: Array,
required: false,
default: () => []
},
withCustomFields: {
type: Boolean,
required: false,
default: false
},
withFieldsSearch: { withFieldsSearch: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -372,12 +475,15 @@ export default {
default: false default: false
} }
}, },
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder', 'select-submitter'], emits: ['add-field', 'set-draw', 'set-draw-type', 'set-draw-custom-field', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder', 'select-submitter'],
data () { data () {
return { return {
fieldPagesLoaded: null, fieldPagesLoaded: null,
analyzingProgress: 0, analyzingProgress: 0,
defaultFieldsSearch: '' newCustomField: null,
showCustomTab: false,
defaultFieldsSearch: '',
customFieldsSearch: ''
} }
}, },
computed: { computed: {
@@ -430,6 +536,23 @@ export default {
} else { } else {
return this.submitterDefaultFields return this.submitterDefaultFields
} }
},
isShowCustomFieldSearch () {
return this.customFields.length > 8
},
filteredCustomFields () {
if (this.customFieldsSearch) {
return this.customFields.filter((f) => f.name.toLowerCase().includes(this.customFieldsSearch.toLowerCase()))
} else {
return this.customFields
}
}
},
mounted () {
try {
this.showCustomTab = localStorage.getItem('docuseal_builder_tab') === 'custom'
} catch (e) {
console.error(e)
} }
}, },
methods: { methods: {
@@ -440,6 +563,61 @@ export default {
this.$emit('set-drag', field) this.$emit('set-drag', field)
}, },
onNewCustomFieldSave () {
if (this.newCustomField.name) {
this.customFields.unshift(this.newCustomField)
this.newCustomField = null
this.saveCustomFields()
} else {
this.newCustomField = null
}
},
addCustomField (field) {
const customField = JSON.parse(JSON.stringify(field))
customField.uuid = v4()
delete customField.submitter_uuid
delete customField.prefillable
delete customField.conditions
customField.areas?.forEach((area) => {
delete area.attachment_uuid
delete area.page
})
if (customField.name) {
this.customFields.unshift(customField)
this.saveCustomFields()
} else {
this.newCustomField = customField
}
this.setFieldsTab('custom')
},
setFieldsTab (type) {
try {
localStorage.setItem('docuseal_builder_tab', type)
} catch (e) {
console.error(e)
}
this.showCustomTab = type === 'custom'
},
saveCustomFields () {
return this.baseFetch('/account_custom_fields', {
method: 'POST',
body: JSON.stringify({
value: this.customFields
}),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
const fields = await resp.json()
this.customFields.splice(0, this.customFields.length, ...fields)
})
},
detectFields () { detectFields () {
const fields = [] const fields = []
@@ -610,9 +788,54 @@ export default {
}) })
}) })
field.areas?.forEach((area) => {
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(area), 1)
})
if (save) { if (save) {
this.save() this.save()
} }
},
removeCustomField (field) {
this.customFields.splice(this.customFields.indexOf(field), 1)
if (!this.customFields.length) {
this.setFieldsTab('default')
}
this.saveCustomFields()
},
onCustomFieldDragover (e) {
if (this.customDragFieldRef.value && this.customFields.includes(this.customDragFieldRef.value)) {
const container = this.$refs.customFieldsList
const targetField = e.target.closest('[data-uuid]')
const dragField = container.querySelector(`[data-uuid="${this.customDragFieldRef.value.uuid}"]`)
if (dragField && targetField && targetField !== dragField) {
const fields = Array.from(container.children)
const currentIndex = fields.indexOf(dragField)
const targetIndex = fields.indexOf(targetField)
if (currentIndex < targetIndex) {
targetField.after(dragField)
} else {
targetField.before(dragField)
}
}
}
},
reorderCustomFields () {
if (!this.customFields.includes(this.customDragFieldRef.value)) {
return
}
const reorderedFields = Array.from(this.$refs.customFieldsList.children).map((el) => {
return this.customFields.find((f) => f.uuid === el.dataset.uuid)
}).filter(Boolean)
this.customFields.splice(0, this.customFields.length, ...reorderedFields)
this.saveCustomFields()
} }
} }
} }
@@ -9,7 +9,7 @@
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl"> <div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium"> <div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title"> <span class="modal-title">
{{ t('font') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }} {{ t('font') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field) }}
</span> </span>
<a <a
href="#" href="#"
@@ -171,7 +171,7 @@
contenteditable="true" contenteditable="true"
class="outline-none whitespace-nowrap truncate" class="outline-none whitespace-nowrap truncate"
> >
{{ field.default_value || field.name || buildDefaultName(field, template.fields) }} {{ field.default_value || field.name || buildDefaultName(field) }}
</span> </span>
</div> </div>
</div> </div>
@@ -196,7 +196,7 @@ export default {
components: { components: {
IconChevronDown IconChevronDown
}, },
inject: ['t', 'save', 'template'], inject: ['t', 'template'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -217,7 +217,7 @@ export default {
required: true required: true
} }
}, },
emits: ['close'], emits: ['close', 'save'],
data () { data () {
return { return {
preferences: {} preferences: {}
@@ -257,6 +257,7 @@ export default {
colors () { colors () {
return [ return [
{ label: '⬛', value: 'black' }, { label: '⬛', value: 'black' },
{ label: '⬜', value: 'white' },
{ label: '🟦', value: 'blue' }, { label: '🟦', value: 'blue' },
{ label: '🟥', value: 'red' } { label: '🟥', value: 'red' }
] ]
@@ -271,6 +272,7 @@ export default {
'items-center': !this.preferences.valign || this.preferences.valign === 'center', 'items-center': !this.preferences.valign || this.preferences.valign === 'center',
'items-start': this.preferences.valign === 'top', 'items-start': this.preferences.valign === 'top',
'items-end': this.preferences.valign === 'bottom', 'items-end': this.preferences.valign === 'bottom',
'bg-black': this.preferences.color === 'white',
'font-bold': ['bold_italic', 'bold'].includes(this.preferences.font_type), 'font-bold': ['bold_italic', 'bold'].includes(this.preferences.font_type),
italic: ['bold_italic', 'italic'].includes(this.preferences.font_type) italic: ['bold_italic', 'italic'].includes(this.preferences.font_type)
} }
@@ -322,8 +324,7 @@ export default {
Object.assign(this.field.preferences, this.preferences) Object.assign(this.field.preferences, this.preferences)
this.save() this.$emit('save')
this.$emit('close') this.$emit('close')
} }
} }
@@ -9,7 +9,7 @@
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl"> <div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium"> <div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title"> <span class="modal-title">
{{ t('formula') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }} {{ t('formula') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field) }}
</span> </span>
<a <a
href="#" href="#"
@@ -33,7 +33,7 @@
v-for="f in fields" v-for="f in fields"
:key="f.uuid" :key="f.uuid"
class="mr-1 flex btn btn-neutral btn-outline border-base-content/20 btn-sm normal-case font-normal bg-white !rounded-xl" class="mr-1 flex btn btn-neutral btn-outline border-base-content/20 btn-sm normal-case font-normal bg-white !rounded-xl"
@click.prevent="insertTextUnderCursor(`{{${f.name || buildDefaultName(f, template.fields)}}}`)" @click.prevent="insertTextUnderCursor(`{{${f.name || buildDefaultName(f)}}}`)"
> >
<IconMathFunction <IconMathFunction
v-if="f.preferences?.formula" v-if="f.preferences?.formula"
@@ -47,7 +47,7 @@
height="20" height="20"
stroke-width="1.5" stroke-width="1.5"
/> />
{{ f.name || buildDefaultName(f, template.fields) }} {{ f.name || buildDefaultName(f) }}
</button> </button>
</div> </div>
<div> <div>
@@ -131,7 +131,7 @@ export default {
IconCodePlus, IconCodePlus,
IconMathFunction IconMathFunction
}, },
inject: ['t', 'save', 'template', 'withFormula'], inject: ['t', 'template', 'withFormula'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -152,7 +152,7 @@ export default {
required: true required: true
} }
}, },
emits: ['close'], emits: ['close', 'save'],
data () { data () {
return { return {
formula: '' formula: ''
@@ -161,7 +161,7 @@ export default {
computed: { computed: {
fields () { fields () {
return this.template.fields.reduce((acc, f) => { return this.template.fields.reduce((acc, f) => {
if (f !== this.field && ['number'].includes(f.type) && (!f.preferences?.formula || !f.preferences.formula.includes(this.field.uuid))) { if (f !== this.field && this.isNumberField(f) && (!f.preferences?.formula || !f.preferences.formula.includes(this.field.uuid))) {
acc.push(f) acc.push(f)
} }
@@ -176,12 +176,15 @@ export default {
this.formula = this.humanizeFormula(this.field.preferences.formula || '') this.formula = this.humanizeFormula(this.field.preferences.formula || '')
}, },
methods: { methods: {
isNumberField (field) {
return field.type === 'number' || (['radio', 'select'].includes(field.type) && field.options?.every((o) => String(o.value).match(/^[\d.-]+$/)))
},
humanizeFormula (text) { humanizeFormula (text) {
return text.replace(/{{(.*?)}}/g, (match, uuid) => { return text.replace(/{{(.*?)}}/g, (match, uuid) => {
const foundField = this.fields.find((f) => f.uuid === uuid) const foundField = this.template.fields.find((f) => f.uuid === uuid)
if (foundField) { if (foundField) {
return `{{${foundField.name || this.buildDefaultName(foundField, this.template.fields)}}}` return `{{${foundField.name || this.buildDefaultName(foundField)}}}`
} else { } else {
return '{{FIELD NOT FOUND}}' return '{{FIELD NOT FOUND}}'
} }
@@ -189,8 +192,8 @@ export default {
}, },
normalizeFormula (text) { normalizeFormula (text) {
return text.replace(/{{(.*?)}}/g, (match, name) => { return text.replace(/{{(.*?)}}/g, (match, name) => {
const foundField = this.fields.find((f) => { const foundField = this.template.fields.find((f) => {
return (f.name || this.buildDefaultName(f, this.template.fields)).trim() === name.trim() return (f.name || this.buildDefaultName(f)).trim() === name.trim()
}) })
if (foundField) { if (foundField) {
@@ -212,11 +215,14 @@ export default {
} else { } else {
this.field.preferences.formula = normalizedFormula this.field.preferences.formula = normalizedFormula
if (this.field.type !== 'payment') { if (this.field.type === 'payment') {
delete this.field.preferences.price
delete this.field.preferences.payment_link_id
} else {
this.field.readonly = !!normalizedFormula this.field.readonly = !!normalizedFormula
} }
this.save() this.$emit('save')
this.$emit('close') this.$emit('close')
} }
+172 -11
View File
@@ -1,10 +1,13 @@
const en = { const en = {
fixed: 'Fixed',
default: 'Default',
save_as_custom_field: 'Save as custom field',
kba: 'KBA', kba: 'KBA',
analyzing_: 'Analyzing...', analyzing_: 'Analyzing...',
download: 'Download', download: 'Download',
downloading_: 'Downloading...', downloading_: 'Downloading...',
view: 'View', view: 'View',
autodetect_fields: 'Autodetect fields', autodetect_fields: 'Autodetect Fields',
payment_link: 'Payment link', payment_link: 'Payment link',
strikeout: 'Strikeout', strikeout: 'Strikeout',
draw_strikethrough_the_document: 'Draw strikethrough the document', draw_strikethrough_the_document: 'Draw strikethrough the document',
@@ -29,6 +32,9 @@ const en = {
field_not_found: 'Field not found', field_not_found: 'Field not found',
clear: 'Clear', clear: 'Clear',
align: 'Align', align: 'Align',
resize: 'Resize',
width: 'Width',
height: 'Height',
add_all_required_fields_to_continue: 'Add all required fields to continue', add_all_required_fields_to_continue: 'Add all required fields to continue',
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Uploaded PDF contains form fields. Keep or remove them?', uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Uploaded PDF contains form fields. Keep or remove them?',
keep: 'Keep', keep: 'Keep',
@@ -46,6 +52,8 @@ const en = {
type_value: 'Type value', type_value: 'Type value',
equal: 'Equal', equal: 'Equal',
not_equal: 'Not equal', not_equal: 'Not equal',
greater_than: 'Greater than',
less_than: 'Less than',
contains: 'Contains', contains: 'Contains',
does_not_contain: 'Does not contain', does_not_contain: 'Does not contain',
not_empty: 'Not empty', not_empty: 'Not empty',
@@ -89,8 +97,9 @@ const en = {
format: 'Format', format: 'Format',
read_only: 'Read-only', read_only: 'Read-only',
page: 'Page', page: 'Page',
draw_new_area: 'Draw New Area', draw_new_area: 'Draw new area',
copy_to_all_pages: 'Copy to All Pages', copy_to_all_pages: 'Copy to all pages',
more: 'More',
add_option: 'Add option', add_option: 'Add option',
option: 'Option', option: 'Option',
options: 'Options', options: 'Options',
@@ -171,6 +180,9 @@ const en = {
numbers_only: 'Numbers only', numbers_only: 'Numbers only',
letters_only: 'Letters only', letters_only: 'Letters only',
regexp_validation: 'Regexp validation', regexp_validation: 'Regexp validation',
custom_validation: 'Custom Validation',
length_validation: 'Length Validation',
number_range: 'Number Range',
enter_pdf_password: 'Enter PDF password', enter_pdf_password: 'Enter PDF password',
wrong_password: 'Wrong password', wrong_password: 'Wrong password',
currency: 'Currency', currency: 'Currency',
@@ -181,14 +193,28 @@ const en = {
learn_more: 'Learn more', learn_more: 'Learn more',
and: 'and', and: 'and',
or: 'or', or: 'or',
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start a quick tour to learn how to create an send your first document', start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start a quick tour to learn how to create and send your first document',
start_tour: 'Start Tour', start_tour: 'Start Tour',
or_add_from: 'Or add from', or_add_from: 'Or add from',
sync: 'Sync', sync: 'Sync',
syncing: 'Syncing...' syncing: 'Syncing...',
copy: 'Copy',
paste: 'Paste',
select_fields: 'Select Fields',
draw_fields: 'Draw Fields',
align_left: 'Align Left',
align_right: 'Align Right',
align_top: 'Align Top',
align_bottom: 'Align Bottom',
fields_selected: '{count} Fields Selected',
field_added: '{count} Field Added',
fields_added: '{count} Fields Added'
} }
const es = { const es = {
fixed: 'Fijo',
default: 'Predeterminado',
save_as_custom_field: 'Guardar como personalizado',
kba: 'KBA', kba: 'KBA',
autodetect_fields: 'Autodetectar campos', autodetect_fields: 'Autodetectar campos',
analyzing_: 'Analizando...', analyzing_: 'Analizando...',
@@ -220,6 +246,9 @@ const es = {
clear: 'Borrar', clear: 'Borrar',
type_value: 'Escriba valor', type_value: 'Escriba valor',
align: 'Alinear', align: 'Alinear',
resize: 'Redimensionar',
width: 'Ancho',
height: 'Alto',
add_all_required_fields_to_continue: 'Agregar todos los campos requeridos para continuar', add_all_required_fields_to_continue: 'Agregar todos los campos requeridos para continuar',
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'El PDF cargado tiene campos. ¿Mantenerlos o eliminarlos?', uploaded_pdf_contains_form_fields_keep_or_remove_them: 'El PDF cargado tiene campos. ¿Mantenerlos o eliminarlos?',
keep: 'Mantener', keep: 'Mantener',
@@ -237,6 +266,8 @@ const es = {
price: 'Precio', price: 'Precio',
equal: 'Igual', equal: 'Igual',
not_equal: 'No es igual', not_equal: 'No es igual',
greater_than: 'Mayor que',
less_than: 'Menor que',
contains: 'Contiene', contains: 'Contiene',
does_not_contain: 'No contiene', does_not_contain: 'No contiene',
not_empty: 'No vacío', not_empty: 'No vacío',
@@ -277,6 +308,7 @@ const es = {
page: 'Página', page: 'Página',
draw_new_area: 'Dibujar nueva área', draw_new_area: 'Dibujar nueva área',
copy_to_all_pages: 'Copiar a todas las páginas', copy_to_all_pages: 'Copiar a todas las páginas',
more: 'Más',
add_option: 'Agregar opción', add_option: 'Agregar opción',
option: 'Opción', option: 'Opción',
options: 'Opciones', options: 'Opciones',
@@ -361,6 +393,9 @@ const es = {
numbers_only: 'Solo números', numbers_only: 'Solo números',
letters_only: 'Solo letras', letters_only: 'Solo letras',
regexp_validation: 'Validación de expresión regular', regexp_validation: 'Validación de expresión regular',
custom_validation: 'Validación Personalizada',
length_validation: 'Validación de Longitud',
number_range: 'Rango de Números',
enter_pdf_password: 'Ingrese la contraseña del PDF', enter_pdf_password: 'Ingrese la contraseña del PDF',
wrong_password: 'Contraseña incorrecta', wrong_password: 'Contraseña incorrecta',
currency: 'Moneda', currency: 'Moneda',
@@ -375,10 +410,24 @@ const es = {
start_tour: 'Iniciar guía', start_tour: 'Iniciar guía',
or_add_from: 'O agregar desde', or_add_from: 'O agregar desde',
sync: 'Sincronizar', sync: 'Sincronizar',
syncing: 'Sincronizando...' syncing: 'Sincronizando...',
copy: 'Copiar',
paste: 'Pegar',
select_fields: 'Seleccionar Campos',
draw_fields: 'Dibujar Campos',
align_left: 'Alinear a la izquierda',
align_right: 'Alinear a la derecha',
align_top: 'Alinear arriba',
align_bottom: 'Alinear abajo',
fields_selected: '{count} Campos Seleccionados',
field_added: '{count} Campo Añadido',
fields_added: '{count} Campos Añadidos'
} }
const it = { const it = {
fixed: 'Fisso',
default: 'Predefinito',
save_as_custom_field: 'Salva come personalizzato',
kba: 'KBA', kba: 'KBA',
autodetect_fields: 'Rileva campi', autodetect_fields: 'Rileva campi',
analyzing_: 'Analisi...', analyzing_: 'Analisi...',
@@ -409,6 +458,9 @@ const it = {
field_not_found: 'Campo non trovato', field_not_found: 'Campo non trovato',
clear: 'Cancella', clear: 'Cancella',
align: 'Allinea', align: 'Allinea',
resize: 'Ridimensiona',
width: 'Larghezza',
height: 'Altezza',
add_all_required_fields_to_continue: 'Aggiungi tutti i campi obbligatori per continuare', add_all_required_fields_to_continue: 'Aggiungi tutti i campi obbligatori per continuare',
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Il PDF caricato contiene campi del modulo. Mantenerli o rimuoverli?', uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Il PDF caricato contiene campi del modulo. Mantenerli o rimuoverli?',
keep: 'Mantieni', keep: 'Mantieni',
@@ -426,6 +478,8 @@ const it = {
type_value: 'Inserisci valore', type_value: 'Inserisci valore',
equal: 'Uguale', equal: 'Uguale',
not_equal: 'Non uguale', not_equal: 'Non uguale',
greater_than: 'Maggiore di',
less_than: 'Minore di',
contains: 'Contiene', contains: 'Contiene',
does_not_contain: 'Non contiene', does_not_contain: 'Non contiene',
not_empty: 'Non vuoto', not_empty: 'Non vuoto',
@@ -471,6 +525,7 @@ const it = {
page: 'Pagina', page: 'Pagina',
draw_new_area: 'Disegna nuova area', draw_new_area: 'Disegna nuova area',
copy_to_all_pages: 'Copia in tutte le pagine', copy_to_all_pages: 'Copia in tutte le pagine',
more: 'Altro',
add_option: 'Aggiungi opzione', add_option: 'Aggiungi opzione',
option: 'Opzione', option: 'Opzione',
options: 'Opzioni', options: 'Opzioni',
@@ -551,6 +606,9 @@ const it = {
numbers_only: 'Solo numeri', numbers_only: 'Solo numeri',
letters_only: 'Solo lettere', letters_only: 'Solo lettere',
regexp_validation: 'Validazione regexp', regexp_validation: 'Validazione regexp',
custom_validation: 'Validazione Personalizzata',
length_validation: 'Validazione Lunghezza',
number_range: 'Intervallo Numerico',
enter_pdf_password: 'Inserisci password PDF', enter_pdf_password: 'Inserisci password PDF',
wrong_password: 'Password errata', wrong_password: 'Password errata',
currency: 'Valuta', currency: 'Valuta',
@@ -565,10 +623,24 @@ const it = {
start_tour: 'Inizia il tour', start_tour: 'Inizia il tour',
or_add_from: 'O aggiungi da', or_add_from: 'O aggiungi da',
sync: 'Sincronizza', sync: 'Sincronizza',
syncing: 'Sincronizzazione...' syncing: 'Sincronizzazione...',
copy: 'Copia',
paste: 'Incolla',
select_fields: 'Seleziona Campi',
draw_fields: 'Disegna Campi',
align_left: 'Allinea a sinistra',
align_right: 'Allinea a destra',
align_top: 'Allinea in alto',
align_bottom: 'Allinea in basso',
fields_selected: '{count} Campi Selezionati',
field_added: '{count} Campo Aggiunto',
fields_added: '{count} Campi Aggiunti'
} }
const pt = { const pt = {
fixed: 'Fixo',
default: 'Padrão',
save_as_custom_field: 'Salvar como personalizado',
kba: 'KBA', kba: 'KBA',
autodetect_fields: 'Detectar campos', autodetect_fields: 'Detectar campos',
analyzing_: 'Analisando...', analyzing_: 'Analisando...',
@@ -603,6 +675,9 @@ const pt = {
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'O PDF carregado contém campos. Manter ou removê-los?', uploaded_pdf_contains_form_fields_keep_or_remove_them: 'O PDF carregado contém campos. Manter ou removê-los?',
keep: 'Manter', keep: 'Manter',
align: 'Alinhar', align: 'Alinhar',
resize: 'Redimensionar',
width: 'Largura',
height: 'Altura',
left: 'Esquerda', left: 'Esquerda',
heading: 'Cabeçalho', heading: 'Cabeçalho',
validation: 'Validação', validation: 'Validação',
@@ -617,6 +692,8 @@ const pt = {
price: 'Preço', price: 'Preço',
equal: 'Igual', equal: 'Igual',
not_equal: 'Não é igual', not_equal: 'Não é igual',
greater_than: 'Maior que',
less_than: 'Menor que',
contains: 'Contém', contains: 'Contém',
does_not_contain: 'Não contém', does_not_contain: 'Não contém',
not_empty: 'Não vazio', not_empty: 'Não vazio',
@@ -657,6 +734,7 @@ const pt = {
page: 'Página', page: 'Página',
draw_new_area: 'Desenhar nova área', draw_new_area: 'Desenhar nova área',
copy_to_all_pages: 'Copiar para todas as páginas', copy_to_all_pages: 'Copiar para todas as páginas',
more: 'Mais',
add_option: 'Adicionar opção', add_option: 'Adicionar opção',
option: 'Opção', option: 'Opção',
options: 'Opções', options: 'Opções',
@@ -741,6 +819,9 @@ const pt = {
numbers_only: 'Somente números', numbers_only: 'Somente números',
letters_only: 'Somente letras', letters_only: 'Somente letras',
regexp_validation: 'Validação de expressão regular', regexp_validation: 'Validação de expressão regular',
custom_validation: 'Validação Personalizada',
length_validation: 'Validação de Comprimento',
number_range: 'Intervalo de Números',
enter_pdf_password: 'Digite a senha do PDF', enter_pdf_password: 'Digite a senha do PDF',
wrong_password: 'Senha incorreta', wrong_password: 'Senha incorreta',
currency: 'Moeda', currency: 'Moeda',
@@ -755,10 +836,24 @@ const pt = {
start_tour: 'Iniciar tour', start_tour: 'Iniciar tour',
or_add_from: 'Ou adicionar de', or_add_from: 'Ou adicionar de',
sync: 'Sincronizar', sync: 'Sincronizar',
syncing: 'Sincronizando...' syncing: 'Sincronizando...',
copy: 'Copiar',
paste: 'Colar',
select_fields: 'Selecionar Campos',
draw_fields: 'Desenhar Campos',
align_left: 'Alinhar à esquerda',
align_right: 'Alinhar à direita',
align_top: 'Alinhar ao topo',
align_bottom: 'Alinhar à parte inferior',
fields_selected: '{count} Campos Selecionados',
field_added: '{count} Campo Adicionado',
fields_added: '{count} Campos Adicionados'
} }
const fr = { const fr = {
fixed: 'Fixe',
default: 'Par défaut',
save_as_custom_field: 'Enregistrer comme personnalisé',
kba: 'KBA', kba: 'KBA',
autodetect_fields: 'Détecter les champs', autodetect_fields: 'Détecter les champs',
analyzing_: 'Analyse...', analyzing_: 'Analyse...',
@@ -789,6 +884,9 @@ const fr = {
field_not_found: 'Champ introuvable', field_not_found: 'Champ introuvable',
clear: 'Effacer', clear: 'Effacer',
align: 'Aligner', align: 'Aligner',
resize: 'Redimensionner',
width: 'Largeur',
height: 'Hauteur',
add_all_required_fields_to_continue: 'Ajoutez tous les champs obligatoires pour continuer', add_all_required_fields_to_continue: 'Ajoutez tous les champs obligatoires pour continuer',
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Le PDF téléversé contient des champs de formulaire. Les conserver ou les supprimer ?', uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Le PDF téléversé contient des champs de formulaire. Les conserver ou les supprimer ?',
keep: 'Conserver', keep: 'Conserver',
@@ -806,6 +904,8 @@ const fr = {
type_value: 'Saisir une valeur', type_value: 'Saisir une valeur',
equal: 'Égal', equal: 'Égal',
not_equal: 'Différent', not_equal: 'Différent',
greater_than: 'Supérieur à',
less_than: 'Inférieur à',
contains: 'Contient', contains: 'Contient',
does_not_contain: 'Ne contient pas', does_not_contain: 'Ne contient pas',
not_empty: 'Non vide', not_empty: 'Non vide',
@@ -851,6 +951,7 @@ const fr = {
page: 'Page', page: 'Page',
draw_new_area: 'Dessiner une zone', draw_new_area: 'Dessiner une zone',
copy_to_all_pages: 'Copier sur toutes les pages', copy_to_all_pages: 'Copier sur toutes les pages',
more: 'Plus',
add_option: 'Ajouter une option', add_option: 'Ajouter une option',
option: 'Option', option: 'Option',
options: 'Options', options: 'Options',
@@ -931,6 +1032,9 @@ const fr = {
numbers_only: 'Chiffres uniquement', numbers_only: 'Chiffres uniquement',
letters_only: 'Lettres uniquement', letters_only: 'Lettres uniquement',
regexp_validation: 'Validation par expression régulière', regexp_validation: 'Validation par expression régulière',
custom_validation: 'Validation Personnalisée',
length_validation: 'Validation de Longueur',
number_range: 'Plage de Nombres',
enter_pdf_password: 'Saisir le mot de passe du PDF', enter_pdf_password: 'Saisir le mot de passe du PDF',
wrong_password: 'Mot de passe incorrect', wrong_password: 'Mot de passe incorrect',
currency: 'Devise', currency: 'Devise',
@@ -945,10 +1049,24 @@ const fr = {
start_tour: 'Démarrer', start_tour: 'Démarrer',
or_add_from: 'Ou ajouter depuis', or_add_from: 'Ou ajouter depuis',
sync: 'Synchroniser', sync: 'Synchroniser',
syncing: 'Synchronisation...' syncing: 'Synchronisation...',
copy: 'Copier',
paste: 'Coller',
select_fields: 'Sélectionner Champs',
draw_fields: 'Dessiner Champs',
align_left: 'Aligner à gauche',
align_right: 'Aligner à droite',
align_top: 'Aligner en haut',
align_bottom: 'Aligner en bas',
fields_selected: '{count} Champs Sélectionnés',
field_added: '{count} Champ Ajouté',
fields_added: '{count} Champs Ajoutés'
} }
const de = { const de = {
fixed: 'Fest',
default: 'Standard',
save_as_custom_field: 'Als benutzerdefiniert speichern',
kba: 'KBA', kba: 'KBA',
autodetect_fields: 'Felder erkennen', autodetect_fields: 'Felder erkennen',
analyzing_: 'Analysiere...', analyzing_: 'Analysiere...',
@@ -979,6 +1097,9 @@ const de = {
field_not_found: 'Feld nicht gefunden', field_not_found: 'Feld nicht gefunden',
clear: 'Leeren', clear: 'Leeren',
align: 'Ausrichten', align: 'Ausrichten',
resize: 'Größe ändern',
width: 'Breite',
height: 'Höhe',
add_all_required_fields_to_continue: 'Fügen Sie alle erforderlichen Felder hinzu, um fortzufahren', add_all_required_fields_to_continue: 'Fügen Sie alle erforderlichen Felder hinzu, um fortzufahren',
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Das hochgeladene PDF enthält Formularfelder. Beibehalten oder entfernen?', uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Das hochgeladene PDF enthält Formularfelder. Beibehalten oder entfernen?',
keep: 'Beibehalten', keep: 'Beibehalten',
@@ -996,6 +1117,8 @@ const de = {
type_value: 'Wert eingeben', type_value: 'Wert eingeben',
equal: 'Gleich', equal: 'Gleich',
not_equal: 'Ungleich', not_equal: 'Ungleich',
greater_than: 'Größer als',
less_than: 'Kleiner als',
contains: 'Enthält', contains: 'Enthält',
does_not_contain: 'Enthält nicht', does_not_contain: 'Enthält nicht',
not_empty: 'Nicht leer', not_empty: 'Nicht leer',
@@ -1041,6 +1164,7 @@ const de = {
page: 'Seite', page: 'Seite',
draw_new_area: 'Bereich zeichnen', draw_new_area: 'Bereich zeichnen',
copy_to_all_pages: 'Auf alle Seiten kopieren', copy_to_all_pages: 'Auf alle Seiten kopieren',
more: 'Mehr',
add_option: 'Option hinzufügen', add_option: 'Option hinzufügen',
option: 'Option', option: 'Option',
options: 'Optionen', options: 'Optionen',
@@ -1121,6 +1245,9 @@ const de = {
numbers_only: 'Nur Zahlen', numbers_only: 'Nur Zahlen',
letters_only: 'Nur Buchstaben', letters_only: 'Nur Buchstaben',
regexp_validation: 'RegExp-Validierung', regexp_validation: 'RegExp-Validierung',
custom_validation: 'Benutzerdefinierte Validierung',
length_validation: 'Längenvalidierung',
number_range: 'Zahlenbereich',
enter_pdf_password: 'PDF-Passwort eingeben', enter_pdf_password: 'PDF-Passwort eingeben',
wrong_password: 'Falsches Passwort', wrong_password: 'Falsches Passwort',
currency: 'Währung', currency: 'Währung',
@@ -1135,10 +1262,24 @@ const de = {
start_tour: 'Tour starten', start_tour: 'Tour starten',
or_add_from: 'Oder hinzufügen aus', or_add_from: 'Oder hinzufügen aus',
sync: 'Synchronisieren', sync: 'Synchronisieren',
syncing: 'Synchronisiere...' syncing: 'Synchronisiere...',
copy: 'Kopieren',
paste: 'Einfügen',
select_fields: 'Felder Auswählen',
draw_fields: 'Felder Zeichnen',
align_left: 'Links ausrichten',
align_right: 'Rechts ausrichten',
align_top: 'Oben ausrichten',
align_bottom: 'Unten ausrichten',
fields_selected: '{count} Felder Ausgewählt',
field_added: '{count} Feld Hinzugefügt',
fields_added: '{count} Felder Hinzugefügt'
} }
const nl = { const nl = {
fixed: 'Vast',
default: 'Standaard',
save_as_custom_field: 'Opslaan als aangepast',
kba: 'KBA', kba: 'KBA',
autodetect_fields: 'Velden detecteren', autodetect_fields: 'Velden detecteren',
analyzing_: 'Analyseren...', analyzing_: 'Analyseren...',
@@ -1169,6 +1310,9 @@ const nl = {
field_not_found: 'Veld niet gevonden', field_not_found: 'Veld niet gevonden',
clear: 'Wissen', clear: 'Wissen',
align: 'Uitlijnen', align: 'Uitlijnen',
resize: 'Formaat wijzigen',
width: 'Breedte',
height: 'Hoogte',
add_all_required_fields_to_continue: 'Voeg alle vereiste velden toe om door te gaan', add_all_required_fields_to_continue: 'Voeg alle vereiste velden toe om door te gaan',
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Geüploade PDF bevat formuliervelden. Behouden of verwijderen?', uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Geüploade PDF bevat formuliervelden. Behouden of verwijderen?',
keep: 'Behouden', keep: 'Behouden',
@@ -1186,6 +1330,8 @@ const nl = {
type_value: 'Typ waarde', type_value: 'Typ waarde',
equal: 'Gelijk aan', equal: 'Gelijk aan',
not_equal: 'Niet gelijk aan', not_equal: 'Niet gelijk aan',
greater_than: 'Groter dan',
less_than: 'Kleiner dan',
contains: 'Bevat', contains: 'Bevat',
does_not_contain: 'Bevat niet', does_not_contain: 'Bevat niet',
not_empty: 'Niet leeg', not_empty: 'Niet leeg',
@@ -1231,6 +1377,7 @@ const nl = {
page: 'Pagina', page: 'Pagina',
draw_new_area: 'Nieuw gebied tekenen', draw_new_area: 'Nieuw gebied tekenen',
copy_to_all_pages: 'Kopieer naar alle pag.', copy_to_all_pages: 'Kopieer naar alle pag.',
more: 'Meer',
add_option: 'Optie toevoegen', add_option: 'Optie toevoegen',
option: 'Optie', option: 'Optie',
options: 'Opties', options: 'Opties',
@@ -1311,6 +1458,9 @@ const nl = {
numbers_only: 'Alleen cijfers', numbers_only: 'Alleen cijfers',
letters_only: 'Alleen letters', letters_only: 'Alleen letters',
regexp_validation: 'Regex validatie', regexp_validation: 'Regex validatie',
custom_validation: 'Aangepaste Validatie',
length_validation: 'Lengte Validatie',
number_range: 'Getalbereik',
enter_pdf_password: 'Voer PDF-wachtwoord in', enter_pdf_password: 'Voer PDF-wachtwoord in',
wrong_password: 'Onjuist wachtwoord', wrong_password: 'Onjuist wachtwoord',
currency: 'Valuta', currency: 'Valuta',
@@ -1325,7 +1475,18 @@ const nl = {
start_tour: 'Rondleiding starten', start_tour: 'Rondleiding starten',
or_add_from: 'Of toevoegen van', or_add_from: 'Of toevoegen van',
sync: 'Synchroniseren', sync: 'Synchroniseren',
syncing: 'Synchroniseren...' syncing: 'Synchroniseren...',
copy: 'Kopiëren',
paste: 'Plakken',
select_fields: 'Velden Selecteren',
draw_fields: 'Velden Tekenen',
align_left: 'Links uitlijnen',
align_right: 'Rechts uitlijnen',
align_top: 'Boven uitlijnen',
align_bottom: 'Onder uitlijnen',
fields_selected: '{count} Velden Geselecteerd',
field_added: '{count} Veld Toegevoegd',
fields_added: '{count} Velden Toegevoegd'
} }
export { en, es, it, pt, fr, de, nl } export { en, es, it, pt, fr, de, nl }
+8 -75
View File
@@ -2,87 +2,20 @@
<svg <svg
height="40" height="40"
width="40" width="40"
version="1.1" style="color: #e0753f"
viewBox="0 0 180 180"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 511.998 511.998"
xml:space="preserve"
> >
<path <path
style="fill:#AA968C;" fill="currentColor"
d="M503.999,247.999c0,128.13-111.033,240-248,240S8,376.129,8,247.999s111.033-224,248-224 d="M 178.224 72.09 c -0.296 -1.463 -0.627 -2.919 -0.996 -4.364 -0.293 -1.151 -0.616 -2.293 -0.956 -3.433 -0.301 -1.008 -0.612 -2.014 -0.95 -3.012 -0.531 -1.578 -1.113 -3.142 -1.735 -4.694 -0.216 -0.54 -0.433 -1.082 -0.661 -1.618 -0.195 -0.462 -0.399 -0.917 -0.601 -1.375 -0.262 -0.591 -0.53 -1.177 -0.804 -1.762 -0.074 -0.159 -0.151 -0.315 -0.226 -0.474 -0.209 -0.441 -0.422 -0.881 -0.638 -1.318 -0.076 -0.154 -0.153 -0.306 -0.229 -0.459 -0.236 -0.471 -0.477 -0.939 -0.721 -1.406 -0.053 -0.101 -0.105 -0.201 -0.158 -0.302 -1.143 -2.16 -2.367 -4.269 -3.68 -6.322 -0.116 -0.181 -0.237 -0.359 -0.355 -0.539 -0.094 -0.144 -0.189 -0.288 -0.284 -0.432 -0.284 -0.431 -0.57 -0.861 -0.862 -1.287 -0.112 -0.164 -0.225 -0.326 -0.338 -0.489 -0.193 -0.279 -0.382 -0.56 -0.579 -0.836 -0.089 -0.125 -0.182 -0.249 -0.273 -0.374 -0.13 -0.182 -0.264 -0.362 -0.395 -0.542 -0.277 -0.38 -0.556 -0.76 -0.838 -1.135 -0.15 -0.199 -0.303 -0.395 -0.454 -0.593 -0.21 -0.274 -0.417 -0.552 -0.63 -0.823 -0.055 -0.069 -0.111 -0.136 -0.166 -0.205 -0.482 -0.61 -0.971 -1.216 -1.47 -1.814 -0.129 -0.155 -0.262 -0.306 -0.392 -0.461 -0.402 -0.476 -0.808 -0.95 -1.22 -1.417 -0.186 -0.212 -0.375 -0.422 -0.563 -0.631 -0.384 -0.428 -0.773 -0.854 -1.167 -1.276 -0.176 -0.189 -0.351 -0.379 -0.529 -0.567 -0.564 -0.595 -1.134 -1.186 -1.716 -1.768 -1.091 -1.091 -2.207 -2.15 -3.346 -3.178 -1.016 -0.919 -2.05 -1.815 -3.103 -2.684 -0.772 -0.636 -1.557 -1.255 -2.348 -1.864 -3.465 -2.67 -7.112 -5.075 -10.927 -7.209 -2.869 -1.604 -5.83 -3.06 -8.883 -4.351 -2.443 -1.033 -4.922 -1.948 -7.428 -2.756 -8.879 -2.863 -18.13 -4.318 -27.605 -4.318 -3.19 0 -6.354 0.169 -9.488 0.496 -4.036 0.421 -8.019 1.114 -11.94 2.073 -1.732 0.423 -3.452 0.892 -5.157 1.42 -2.856 0.883 -5.673 1.912 -8.447 3.085 -2.645 1.118 -5.222 2.357 -7.729 3.711 -2.574 1.39 -5.073 2.901 -7.494 4.533 -1.195 0.805 -2.37 1.64 -3.527 2.503 -1.156 0.864 -2.292 1.756 -3.408 2.676 -0.553 0.456 -1.1 0.919 -1.643 1.389 -1.649 1.427 -3.252 2.92 -4.806 4.473 -2.582 2.582 -4.991 5.299 -7.222 8.138 -0.892 1.135 -1.756 2.292 -2.59 3.467 -0.417 0.588 -0.827 1.18 -1.23 1.778 -0.403 0.597 -0.798 1.199 -1.186 1.806 -0.388 0.607 -0.769 1.218 -1.143 1.835 -2.241 3.697 -4.216 7.562 -5.916 11.582 -1.095 2.589 -2.059 5.217 -2.901 7.877 -0.153 0.482 -0.3 0.965 -0.444 1.449 -0.339 1.14 -0.663 2.282 -0.956 3.433 -0.369 1.446 -0.7 2.901 -0.996 4.364 -1.034 5.121 -1.618 10.343 -1.749 15.637 -0.018 0.757 -0.028 1.514 -0.028 2.274 0 1.123 0.02 2.244 0.062 3.361 0.285 7.82 1.568 15.475 3.825 22.879 0.044 0.147 0.088 0.295 0.133 0.441 0.877 2.823 1.894 5.608 3.054 8.35 0.85 2.009 1.769 3.98 2.755 5.912 0.539 1.057 1.105 2.099 1.685 3.132 4.013 7.142 8.98 13.698 14.846 19.564 7.713 7.713 16.611 13.878 26.477 18.352 0.705 0.32 1.415 0.632 2.131 0.935 2.081 0.88 4.185 1.679 6.313 2.396 9.217 3.106 18.85 4.677 28.719 4.677 8.031 0 15.902 -1.047 23.522 -3.107 0.633 -0.172 1.266 -0.35 1.895 -0.535 0.757 -0.222 1.509 -0.456 2.26 -0.698 0.717 -0.232 1.431 -0.474 2.145 -0.723 1.752 -0.616 3.49 -1.281 5.211 -2.009 0.755 -0.319 1.503 -0.651 2.247 -0.989 1.237 -0.563 2.459 -1.15 3.664 -1.766 0.644 -0.328 1.283 -0.665 1.917 -1.009 1.654 -0.896 3.274 -1.848 4.865 -2.844 5.736 -3.591 11.06 -7.827 15.912 -12.679 0.775 -0.775 1.534 -1.562 2.278 -2.36 5.204 -5.59 9.636 -11.754 13.246 -18.417 0.343 -0.634 0.68 -1.274 1.009 -1.917 0.482 -0.944 0.943 -1.9 1.392 -2.863 0.471 -1.007 0.928 -2.021 1.364 -3.049 1.22 -2.886 2.281 -5.82 3.187 -8.793 0.559 -1.833 1.056 -3.68 1.494 -5.542 0.108 -0.458 0.211 -0.916 0.312 -1.376 0.194 -0.883 0.373 -1.77 0.539 -2.659 1.02 -5.455 1.542 -11.02 1.542 -16.663 0 -6.074 -0.595 -12.058 -1.776 -17.911 z m -161.733 19.614 c -1.118 -56.662 44.604 -74.877 60.998 -67.647 2.187 0.965 4.732 2.431 7.042 2.96 5.295 1.213 13.432 -3.113 13.521 6.273 0.078 8.156 -3.389 13.108 -10.797 16.177 -7.539 3.124 -14.777 9.181 -19.95 15.493 -21.487 26.216 -31.231 68.556 -7.565 94.296 -13.679 -5.545 -42.418 -25.467 -43.248 -67.552 z m 91.109 72.619 c -0.053 0.008 -4.171 0.775 -4.171 0.775 0 0 -15.862 -22.957 -23.509 -21.719 11.291 16.04 12.649 22.625 12.649 22.625 -0.053 0.001 -0.107 0.001 -0.161 0.003 -51.831 2.131 -42.785 -64.026 -28.246 -86.502 -1.555 13.073 8.878 39.992 39.034 44.1 9.495 1.293 32.302 -3.275 41.015 -11.38 0.098 1.825 0.163 3.85 0.159 6.013 -0.046 23.538 -13.47 42.743 -36.77 46.085 z m 30.575 -15.708 c 9.647 -9.263 12.869 -27.779 9.103 -44.137 -4.608 -20.011 -28.861 -32.383 -40.744 -35.564 5.766 -8.089 27.908 -14.274 39.567 5.363 -5.172 -10.519 -13.556 -23.023 -1.732 -33.128 12.411 13.329 19.411 29.94 20.161 48.7 0.75 18.753 -6.64 41.768 -26.355 58.765 z"
S503.999,119.869,503.999,247.999z"
/>
<path
style="fill:#AA968C;"
d="M255.999,23.999C119.033,23.999,8,119.868,8,247.999c0,24.631,4.138,48.647,11.74,71.397
c25.518,34.29,66.232,56.602,112.26,56.602c53.893,0,100.6-30.495,124-75.13c23.4,44.636,70.107,75.13,124,75.13
c46.028,0,86.743-22.312,112.26-56.602c7.602-22.75,11.74-46.767,11.74-71.397C503.999,119.868,392.966,23.999,255.999,23.999z"
/> />
<circle <circle
style="fill:#C8AF9B;" fill="currentColor"
cx="256" cx="71.927"
cy="351.999" cy="32.004"
r="136" r="2.829"
/> />
<g>
<circle
style="fill:#464655;"
cx="132"
cy="203.999"
r="28"
/>
<circle
style="fill:#464655;"
cx="380"
cy="203.999"
r="28"
/>
<path
style="fill:#464655;"
d="M269.949,284.516c-7.672,10.741-20.227,10.741-27.899,0l-12.101-16.941
c-7.672-10.741-3.15-19.53,10.05-19.53h32c13.2,0,17.723,8.788,10.05,19.53L269.949,284.516z"
/>
</g>
<path
style="fill:#AA968C;"
d="M 350.964 399.998 C 316.628 399.998 299.021 351.998 255.882 351.998 C 212.742 351.998 195.135 399.998 160.801 399.998 C 145.395 399.998 131.723 394.147 120.621 374.798 C 131.595 439.03 187.893 487.998 255.881 487.998 C 323.868 487.998 380.168 439.03 391.14 374.798 C 380.04 394.148 366.368 399.998 350.964 399.998 Z"
/>
<g>
<path
style="fill:#8C7873;"
d="M32,423.998c-3.172,0-6.18-1.906-7.43-5.031c-1.641-4.105,0.359-8.758,4.461-10.402l160.008-64
c4.062-1.617,8.758,0.352,10.398,4.457s-0.359,8.758-4.461,10.402l-160.008,64C34,423.811,32.992,423.998,32,423.998z"
/>
<path
style="fill:#8C7873;"
d="M15.992,375.995c-3.547,0-6.781-2.375-7.727-5.965c-1.125-4.273,1.422-8.648,5.695-9.773l152-40
c4.289-1.121,8.648,1.426,9.773,5.703c1.125,4.273-1.422,8.648-5.695,9.773l-152,40C17.351,375.913,16.672,375.995,15.992,375.995z
"
/>
<path
style="fill:#8C7873;"
d="M7.992,335.995c-3.812,0-7.187-2.73-7.867-6.609c-0.773-4.352,2.133-8.5,6.484-9.27l136-24
c4.328-0.77,8.508,2.141,9.266,6.488c0.773,4.352-2.133,8.5-6.484,9.27l-136,24C8.922,335.956,8.453,335.995,7.992,335.995z"
/>
<path
style="fill:#8C7873;"
d="M480,423.998c3.172,0,6.18-1.906,7.43-5.031c1.641-4.105-0.359-8.758-4.461-10.402l-160.008-64
c-4.063-1.617-8.758,0.352-10.398,4.457s0.359,8.758,4.461,10.402l160.008,64C478,423.811,479.007,423.998,480,423.998z"
/>
<path
style="fill:#8C7873;"
d="M496.007,375.995c3.547,0,6.781-2.375,7.727-5.965c1.125-4.273-1.422-8.648-5.695-9.773l-152-40
c-4.289-1.121-8.648,1.426-9.773,5.703c-1.125,4.273,1.422,8.648,5.695,9.773l152,40
C494.648,375.913,495.328,375.995,496.007,375.995z"
/>
<path
style="fill:#8C7873;"
d="M504.007,335.995c3.813,0,7.188-2.73,7.867-6.609c0.773-4.352-2.133-8.5-6.484-9.27l-136-24
c-4.328-0.77-8.508,2.141-9.266,6.488c-0.773,4.352,2.133,8.5,6.484,9.27l136,24C503.078,335.956,503.546,335.995,504.007,335.995z
"
/>
</g>
</svg> </svg>
</template> </template>
+475 -58
View File
@@ -1,7 +1,7 @@
<template> <template>
<div <div
class="relative select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute" class="relative select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
:class="{ 'cursor-crosshair': allowDraw && editable, 'touch-none': !!drawField }" :class="{ 'cursor-crosshair': allowDraw && editable && !isSelectMode, 'touch-none': !!drawField }"
style="container-type: size" style="container-type: size"
:style="{ aspectRatio: `${width} / ${height}`}" :style="{ aspectRatio: `${width} / ${height}`}"
> >
@@ -17,7 +17,20 @@
<div <div
class="top-0 bottom-0 left-0 right-0 absolute" class="top-0 bottom-0 left-0 right-0 absolute"
@pointerdown="onStartDraw" @pointerdown="onStartDraw"
@contextmenu="openContextMenu"
> >
<SelectionBox
v-if="showSelectionBox"
:selection-box="selectionBox"
:page-width="width"
:page-height="height"
:is-resizing="!!resizeDirection"
:is-drawing="!!drawFieldType"
:is-drag="isDrag"
@move="onSelectionBoxMove"
@contextmenu="openSelectionContextMenu"
@close-context-menu="closeContextMenu"
/>
<FieldArea <FieldArea
v-for="(item, i) in areas" v-for="(item, i) in areas"
:key="i" :key="i"
@@ -34,31 +47,81 @@
:default-field="defaultFieldsIndex[item.field.name]" :default-field="defaultFieldsIndex[item.field.name]"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:max-page="totalPages - 1" :max-page="totalPages - 1"
:is-select-mode="isSelectMode"
:is-mobile="isMobile"
@start-resize="resizeDirection = $event" @start-resize="resizeDirection = $event"
@stop-resize="resizeDirection = null" @stop-resize="resizeDirection = null"
@remove="$emit('remove-area', item.area)" @remove="$emit('remove-area', item.area)"
@scroll-to="$emit('scroll-to', $event)" @scroll-to="$emit('scroll-to', $event)"
@add-custom-field="$emit('add-custom-field', $event)"
@contextmenu="openAreaContextMenu($event, item.area, item.field)"
/> />
<FieldArea <FieldArea
v-if="newArea" v-for="(area, index) in newAreas"
:key="index"
:is-draw="true" :is-draw="true"
:page-width="width" :page-width="width"
:page-height="height" :page-height="height"
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || dragFieldPlaceholder?.type || defaultFieldType }" :field="{ submitter_uuid: selectedSubmitter.uuid, type: newAreaFieldType }"
:area="newArea" :area="area"
/>
<div
v-if="newAreas.length > 1"
class="absolute outline-dashed outline-gray-400 pointer-events-none z-20"
:style="newAreasBoxStyle"
/>
<div
v-if="selectionRect"
class="absolute outline-dashed outline-gray-400 pointer-events-none z-20"
:style="selectionRectStyle"
/>
<FieldContextMenu
v-if="contextMenu && contextMenu.field"
:context-menu="contextMenu"
:field="contextMenu.field"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:editable="editable"
:default-field="defaultFieldsIndex[contextMenu.field.name]"
@copy="handleCopy"
@delete="handleDelete"
@close="closeContextMenu"
@set-draw="$emit('set-draw', $event)"
@scroll-to="$emit('scroll-to', $event)"
@save="save"
@add-custom-field="$emit('add-custom-field', $event)"
/>
<SelectionContextMenu
v-else-if="contextMenu && contextMenu.areas"
:context-menu="contextMenu"
:editable="editable"
:template="template"
@copy="handleSelectionCopy"
@delete="handleSelectionDelete"
@close="closeContextMenu"
/>
<PageContextMenu
v-else-if="contextMenu && !contextMenu.field && !contextMenu.areas"
:context-menu="contextMenu"
:editable="editable"
:with-fields-detection="withFieldsDetection"
@paste="handlePaste"
@autodetect-fields="handleAutodetectFields"
@close="closeContextMenu"
/> />
</div> </div>
<div <div
v-show="resizeDirection || isDrag || showMask || (drawField && isMobile) || fieldsDragFieldRef.value" v-show="resizeDirection || isDrag || showMask || (drawField && isMobile) || fieldsDragFieldRef.value || customDragFieldRef?.value || selectionRect"
id="mask" id="mask"
ref="mask" ref="mask"
class="top-0 bottom-0 left-0 right-0 absolute" class="top-0 bottom-0 left-0 right-0 absolute"
:class="{ 'z-10': !isMobile, 'cursor-grab': isDrag, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }" :class="{ 'z-10': !isMobile, 'cursor-grab': isDrag, 'cursor-nwse-resize': drawField && !isSelectMode, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
@pointermove="onPointermove" @pointermove="onPointermove"
@pointerdown="onStartDraw" @pointerdown="onStartDraw"
@contextmenu="openContextMenu"
@dragover.prevent="onDragover" @dragover.prevent="onDragover"
@dragenter="onDragenter" @dragenter="onDragenter"
@dragleave="newArea = null" @dragleave="newAreas = []"
@drop="onDrop" @drop="onDrop"
@pointerup="onPointerup" @pointerup="onPointerup"
/> />
@@ -67,13 +130,21 @@
<script> <script>
import FieldArea from './area' import FieldArea from './area'
import FieldContextMenu from './field_context_menu'
import SelectionContextMenu from './selection_context_menu'
import PageContextMenu from './page_context_menu'
import SelectionBox from './selection_box'
export default { export default {
name: 'TemplatePage', name: 'TemplatePage',
components: { components: {
FieldArea FieldArea,
FieldContextMenu,
SelectionContextMenu,
PageContextMenu,
SelectionBox
}, },
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'assignDropAreaSize'], inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'customDragFieldRef', 'assignDropAreaSize', 'selectedAreasRef', 'template', 'isSelectModeRef', 'save'],
props: { props: {
image: { image: {
type: Object, type: Object,
@@ -84,6 +155,11 @@ export default {
required: false, required: false,
default: null default: null
}, },
isMobile: {
type: Boolean,
required: false,
default: false
},
withSignatureId: { withSignatureId: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -142,6 +218,11 @@ export default {
required: false, required: false,
default: null default: null
}, },
drawCustomField: {
type: Object,
required: false,
default: null
},
editable: { editable: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -155,18 +236,68 @@ export default {
number: { number: {
type: Number, type: Number,
required: true required: true
},
attachmentUuid: {
type: String,
required: false,
default: ''
},
withFieldsDetection: {
type: Boolean,
required: false,
default: false
} }
}, },
emits: ['draw', 'drop-field', 'remove-area', 'scroll-to'], emits: ['draw', 'drop-field', 'remove-area', 'copy-field', 'paste-field', 'scroll-to', 'copy-selected-areas', 'delete-selected-areas', 'autodetect-fields', 'add-custom-field', 'set-draw'],
data () { data () {
return { return {
areaRefs: [], areaRefs: [],
showMask: false, showMask: false,
resizeDirection: null, resizeDirection: null,
newArea: null newAreas: [],
contextMenu: null,
selectionRect: null
} }
}, },
computed: { computed: {
isSelectMode () {
return this.isSelectModeRef.value && !this.drawFieldType && this.editable && !this.drawField && !this.drawCustomField
},
pageSelectedAreas () {
if (!this.selectedAreasRef.value) return []
return this.selectedAreasRef.value.filter((a) =>
a.attachment_uuid === this.attachmentUuid && a.page === this.number
)
},
showSelectionBox () {
return this.pageSelectedAreas.length >= 2 && this.editable
},
minSelectionBoxHeight () {
const ys = this.pageSelectedAreas.map((a) => a.y)
return Math.max(...ys) - Math.min(...ys)
},
minSelectionBoxWidth () {
const xs = this.pageSelectedAreas.map((a) => a.x)
return Math.max(...xs) - Math.min(...xs)
},
selectionBox () {
if (!this.pageSelectedAreas.length) return null
const minX = Math.min(...this.pageSelectedAreas.map((a) => a.x))
const minY = Math.min(...this.pageSelectedAreas.map((a) => a.y))
const maxX = Math.max(...this.pageSelectedAreas.map((a) => a.x + a.w))
const maxY = Math.max(...this.pageSelectedAreas.map((a) => a.y + a.h))
return {
x: minX,
y: minY,
w: Math.max(maxX - minX, this.minSelectionBoxWidth),
h: Math.max(maxY - minY, this.minSelectionBoxHeight)
}
},
defaultFieldsIndex () { defaultFieldsIndex () {
return this.defaultFields.reduce((acc, field) => { return this.defaultFields.reduce((acc, field) => {
acc[field.name] = field acc[field.name] = field
@@ -174,6 +305,14 @@ export default {
return acc return acc
}, {}) }, {})
}, },
newAreaFieldType () {
if (this.drawField?.type) return this.drawField.type
if (this.drawCustomField?.type) return this.drawCustomField.type
if (this.dragFieldPlaceholder?.type) return this.dragFieldPlaceholder.type
if (this.customDragFieldRef?.value?.type) return this.customDragFieldRef.value.type
return this.defaultFieldType
},
defaultFieldType () { defaultFieldType () {
if (this.drawFieldType) { if (this.drawFieldType) {
return this.drawFieldType return this.drawFieldType
@@ -185,11 +324,6 @@ export default {
return 'text' return 'text'
} }
}, },
isMobile () {
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent)
},
resizeDirectionClasses () { resizeDirectionClasses () {
return { return {
nwse: 'cursor-nwse-resize', nwse: 'cursor-nwse-resize',
@@ -201,6 +335,31 @@ export default {
}, },
height () { height () {
return this.image.metadata.height return this.image.metadata.height
},
newAreasBoxStyle () {
if (this.newAreas.length < 2) return {}
const minX = Math.min(...this.newAreas.map(a => a.x))
const minY = Math.min(...this.newAreas.map(a => a.y))
const maxX = Math.max(...this.newAreas.map(a => a.x + a.w))
const maxY = Math.max(...this.newAreas.map(a => a.y + a.h))
return {
left: minX * 100 + '%',
top: minY * 100 + '%',
width: (maxX - minX) * 100 + '%',
height: (maxY - minY) * 100 + '%'
}
},
selectionRectStyle () {
if (!this.selectionRect) return {}
return {
left: this.selectionRect.x * 100 + '%',
top: this.selectionRect.y * 100 + '%',
width: this.selectionRect.w * 100 + '%',
height: this.selectionRect.h * 100 + '%'
}
} }
}, },
beforeUpdate () { beforeUpdate () {
@@ -211,28 +370,183 @@ export default {
this.image.metadata.width = e.target.naturalWidth this.image.metadata.width = e.target.naturalWidth
this.image.metadata.height = e.target.naturalHeight this.image.metadata.height = e.target.naturalHeight
}, },
openContextMenu (event) {
if (!this.editable) {
return
}
event.preventDefault()
event.stopPropagation()
const rect = this.$refs.image.getBoundingClientRect()
this.newAreas = []
this.showMask = false
this.contextMenu = {
x: event.clientX,
y: event.clientY,
relativeX: (event.clientX - rect.left) / rect.width,
relativeY: (event.clientY - rect.top) / rect.height
}
},
openAreaContextMenu (event, area, field) {
if (!this.editable) {
return
}
event.preventDefault()
event.stopPropagation()
const rect = this.$refs.image.getBoundingClientRect()
this.newAreas = []
this.showMask = false
this.contextMenu = {
x: event.clientX,
y: event.clientY,
relativeX: (event.clientX - rect.left) / rect.width,
relativeY: (event.clientY - rect.top) / rect.height,
area,
field
}
},
openSelectionContextMenu (event) {
if (!this.editable) {
return
}
event.preventDefault()
event.stopPropagation()
const rect = this.$el.getBoundingClientRect()
this.contextMenu = {
x: event.clientX,
y: event.clientY,
relativeX: (event.clientX - rect.left) / rect.width,
relativeY: (event.clientY - rect.top) / rect.height,
areas: this.selectedAreasRef.value
}
},
handleSelectionCopy () {
this.$emit('copy-selected-areas')
this.closeContextMenu()
},
handleSelectionDelete () {
this.$emit('delete-selected-areas')
this.closeContextMenu()
},
closeContextMenu () {
this.contextMenu = null
this.newAreas = []
this.showMask = false
},
handleCopy () {
if (this.contextMenu.area) {
this.selectedAreasRef.value = [this.contextMenu.area]
this.$emit('copy-field')
}
this.closeContextMenu()
},
handleDelete () {
if (this.contextMenu.area) {
this.$emit('remove-area', this.contextMenu.area)
}
this.closeContextMenu()
},
handlePaste () {
this.newAreas = []
this.showMask = false
this.$emit('paste-field', {
page: this.number,
x: this.contextMenu.relativeX,
y: this.contextMenu.relativeY
})
this.closeContextMenu()
},
handleAutodetectFields () {
this.$emit('autodetect-fields', {
page: this.number,
attachmentUuid: this.attachmentUuid
})
this.closeContextMenu()
},
setAreaRefs (el) { setAreaRefs (el) {
if (el) { if (el) {
this.areaRefs.push(el) this.areaRefs.push(el)
} }
}, },
onDragenter (e) { onDragenter (e) {
this.newArea = {} const customField = this.customDragFieldRef?.value
const customAreas = customField?.areas || []
const dropX = (e.offsetX - 6) / this.$refs.mask.clientWidth
const dropY = e.offsetY / this.$refs.mask.clientHeight
this.assignDropAreaSize(this.newArea, this.dragFieldPlaceholder, { if (customAreas.length > 1) {
maskW: this.$refs.mask.clientWidth, const refArea = customAreas[0]
maskH: this.$refs.mask.clientHeight
})
this.newArea.x = (e.offsetX - 6) / this.$refs.mask.clientWidth this.newAreas = customAreas.map((customArea) => ({
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight - this.newArea.h / 2 x: dropX + (customArea.x - refArea.x),
y: dropY + (customArea.y - refArea.y) - (customArea.h / 2),
w: customArea.w,
h: customArea.h
}))
} else {
const newArea = {}
if (customAreas.length === 1) {
newArea.w = customAreas[0].w
newArea.h = customAreas[0].h
} else if (customField) {
this.assignDropAreaSize(newArea, customField, {
maskW: this.$refs.mask.clientWidth,
maskH: this.$refs.mask.clientHeight
})
} else {
this.assignDropAreaSize(newArea, this.dragFieldPlaceholder, {
maskW: this.$refs.mask.clientWidth,
maskH: this.$refs.mask.clientHeight
})
}
newArea.x = dropX
newArea.y = dropY - newArea.h / 2
this.newAreas = [newArea]
}
}, },
onDragover (e) { onDragover (e) {
this.newArea.x = (e.offsetX - 6) / this.$refs.mask.clientWidth const customField = this.customDragFieldRef?.value
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight - this.newArea.h / 2 const customAreas = customField?.areas || []
const dropX = (e.offsetX - 6) / this.$refs.mask.clientWidth
const dropY = e.offsetY / this.$refs.mask.clientHeight
if (customAreas.length > 1) {
const refArea = customAreas[0]
this.newAreas.forEach((newArea, index) => {
const customArea = customAreas[index]
newArea.x = dropX + (customArea.x - refArea.x)
newArea.y = dropY + (customArea.y - refArea.y) - (customArea.h / 2)
})
} else if (this.newAreas.length) {
this.newAreas[0].x = dropX
this.newAreas[0].y = dropY - this.newAreas[0].h / 2
}
}, },
onDrop (e) { onDrop (e) {
this.newArea = null this.newAreas = []
this.$emit('drop-field', { this.$emit('drop-field', {
x: e.offsetX, x: e.offsetX,
@@ -243,11 +557,25 @@ export default {
}) })
}, },
onStartDraw (e) { onStartDraw (e) {
if (e.button === 2) {
return
}
if (this.selectedAreasRef.value.length >= 2) {
this.selectedAreasRef.value = []
}
if (this.isSelectMode) {
this.startSelectionRect(e)
return
}
if (!this.allowDraw) { if (!this.allowDraw) {
return return
} }
if (this.isMobile && !this.drawField) { if (this.isMobile && !this.drawField && !this.drawCustomField) {
return return
} }
@@ -258,65 +586,154 @@ export default {
this.showMask = true this.showMask = true
this.$nextTick(() => { this.$nextTick(() => {
this.newArea = { this.newAreas = [{
initialX: e.offsetX / this.$refs.mask.clientWidth, initialX: e.offsetX / this.$refs.mask.clientWidth,
initialY: e.offsetY / this.$refs.mask.clientHeight, initialY: e.offsetY / this.$refs.mask.clientHeight,
x: e.offsetX / this.$refs.mask.clientWidth, x: e.offsetX / this.$refs.mask.clientWidth,
y: e.offsetY / this.$refs.mask.clientHeight, y: e.offsetY / this.$refs.mask.clientHeight,
w: 0, w: 0,
h: 0 h: 0
}]
})
},
startSelectionRect (e) {
this.selectedAreasRef.value = []
this.showMask = true
this.$nextTick(() => {
const x = e.offsetX / this.$refs.mask.clientWidth
const y = e.offsetY / this.$refs.mask.clientHeight
this.selectionRect = {
initialX: x,
initialY: y,
x,
y,
w: 0,
h: 0
} }
}) })
}, },
onSelectionBoxMove (dx, dy) {
let clampedDx = dx
let clampedDy = dy
this.pageSelectedAreas.forEach((area) => {
const maxDxLeft = -area.x
const maxDxRight = 1 - area.w - area.x
const maxDyTop = -area.y
const maxDyBottom = 1 - area.h - area.y
if (dx < maxDxLeft) clampedDx = Math.max(clampedDx, maxDxLeft)
if (dx > maxDxRight) clampedDx = Math.min(clampedDx, maxDxRight)
if (dy < maxDyTop) clampedDy = Math.max(clampedDy, maxDyTop)
if (dy > maxDyBottom) clampedDy = Math.min(clampedDy, maxDyBottom)
})
this.pageSelectedAreas.forEach((area) => {
area.x += clampedDx
area.y += clampedDy
})
},
onPointermove (e) { onPointermove (e) {
if (this.newArea) { if (this.selectionRect) {
const dx = e.offsetX / this.$refs.mask.clientWidth - this.newArea.initialX const dx = e.offsetX / this.$refs.mask.clientWidth - this.selectionRect.initialX
const dy = e.offsetY / this.$refs.mask.clientHeight - this.newArea.initialY const dy = e.offsetY / this.$refs.mask.clientHeight - this.selectionRect.initialY
if (dx > 0) { if (dx > 0) {
this.newArea.x = this.newArea.initialX this.selectionRect.x = this.selectionRect.initialX
} else { } else {
this.newArea.x = e.offsetX / this.$refs.mask.clientWidth this.selectionRect.x = e.offsetX / this.$refs.mask.clientWidth
} }
if (dy > 0) { if (dy > 0) {
this.newArea.y = this.newArea.initialY this.selectionRect.y = this.selectionRect.initialY
} else { } else {
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight this.selectionRect.y = e.offsetY / this.$refs.mask.clientHeight
} }
if ((this.drawField?.type || this.drawFieldType) === 'cells') { this.selectionRect.w = Math.abs(dx)
this.newArea.cell_w = this.newArea.h * (this.$refs.mask.clientHeight / this.$refs.mask.clientWidth) this.selectionRect.h = Math.abs(dy)
return
}
const drawArea = this.newAreas[0]
if (drawArea?.initialX !== undefined) {
const dx = e.offsetX / this.$refs.mask.clientWidth - drawArea.initialX
const dy = e.offsetY / this.$refs.mask.clientHeight - drawArea.initialY
if (dx > 0) {
drawArea.x = drawArea.initialX
} else {
drawArea.x = e.offsetX / this.$refs.mask.clientWidth
} }
this.newArea.w = Math.abs(dx) if (dy > 0) {
this.newArea.h = Math.abs(dy) drawArea.y = drawArea.initialY
} else {
drawArea.y = e.offsetY / this.$refs.mask.clientHeight
}
if ((this.drawField?.type || this.drawCustomField?.type || this.drawFieldType) === 'cells') {
drawArea.cell_w = drawArea.h * (this.$refs.mask.clientHeight / this.$refs.mask.clientWidth)
}
drawArea.w = Math.abs(dx)
drawArea.h = Math.abs(dy)
} }
}, },
onPointerup (e) { onPointerup (e) {
if (this.newArea) { if (this.selectionRect) {
const area = { const selRect = this.selectionRect
x: this.newArea.x, const areasToSelect = this.areas || []
y: this.newArea.y,
w: this.newArea.w, areasToSelect.forEach((item) => {
h: this.newArea.h, const area = item.area
page: this.number
if (this.rectsOverlap(selRect, area)) {
this.selectedAreasRef.value.push(area)
}
})
this.selectionRect = null
} else {
const drawArea = this.newAreas[0]
if (drawArea?.initialX !== undefined) {
const area = {
x: drawArea.x,
y: drawArea.y,
w: drawArea.w,
h: drawArea.h,
page: this.number
}
if ('cell_w' in drawArea) {
area.cell_w = drawArea.cell_w
}
const dx = Math.abs(e.offsetX - this.$refs.mask.clientWidth * drawArea.initialX)
const dy = Math.abs(e.offsetY - this.$refs.mask.clientHeight * drawArea.initialY)
const isTooSmall = dx < 8 && dy < 8
this.$emit('draw', { area, isTooSmall })
} }
if ('cell_w' in this.newArea) {
area.cell_w = this.newArea.cell_w
}
const dx = Math.abs(e.offsetX - this.$refs.mask.clientWidth * this.newArea.initialX)
const dy = Math.abs(e.offsetY - this.$refs.mask.clientHeight * this.newArea.initialY)
const isTooSmall = dx < 8 && dy < 8
this.$emit('draw', { area, isTooSmall })
} }
this.showMask = false this.showMask = false
this.newArea = null this.newAreas = []
},
rectsOverlap (r1, r2) {
return !(
r1.x + r1.w < r2.x ||
r2.x + r2.w < r1.x ||
r1.y + r1.h < r2.y ||
r2.y + r2.h < r1.y
)
} }
} }
} }
@@ -0,0 +1,159 @@
<template>
<div
ref="menu"
class="fixed z-50 p-1 bg-white shadow-lg rounded-lg border border-neutral-200 cursor-default"
style="min-width: 170px"
:style="menuStyle"
@mousedown.stop
@pointerdown.stop
>
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
:class="!hasClipboardData ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-100'"
:disabled="!hasClipboardData"
@click.stop="!hasClipboardData ? null : $emit('paste')"
>
<span class="flex items-center space-x-2">
<IconClipboard class="w-4 h-4" />
<span>{{ t('paste') }}</span>
</span>
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘V' : 'Ctrl+V' }}</span>
</button>
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
@click.stop="handleToggleSelectMode"
>
<span class="flex items-center space-x-2">
<IconClick
v-if="!isSelectModeRef.value"
class="w-4 h-4"
/>
<IconNewSection
v-else
class="w-4 h-4"
/>
<span>{{ isSelectModeRef.value ? t('draw_fields') : t('select_fields') }}</span>
</span>
<span class="text-xs text-base-content/60 ml-4">Tab</span>
</button>
<hr
v-if="showAutodetectFields"
class="my-1 border-neutral-200"
>
<button
v-if="showAutodetectFields"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="$emit('autodetect-fields')"
>
<IconSparkles class="w-4 h-4" />
<span>{{ t('autodetect_fields') }}</span>
</button>
</div>
</template>
<script>
import { IconClipboard, IconClick, IconNewSection, IconSparkles } from '@tabler/icons-vue'
export default {
name: 'PageContextMenu',
components: {
IconClipboard,
IconClick,
IconNewSection,
IconSparkles
},
inject: ['t', 'isSelectModeRef'],
props: {
contextMenu: {
type: Object,
default: null,
required: true
},
editable: {
type: Boolean,
default: true
},
withFieldsDetection: {
type: Boolean,
default: false
}
},
emits: ['paste', 'close', 'autodetect-fields'],
computed: {
isMac () {
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
},
hasClipboardData () {
try {
const clipboard = localStorage.getItem('docuseal_clipboard')
if (clipboard) {
const data = JSON.parse(clipboard)
return Date.now() - data.timestamp < 3600000
}
return false
} catch {
return false
}
},
menuStyle () {
return {
left: this.contextMenu.x + 'px',
top: this.contextMenu.y + 'px'
}
},
showAutodetectFields () {
return this.withFieldsDetection && this.editable
}
},
mounted () {
document.addEventListener('keydown', this.onKeyDown)
document.addEventListener('mousedown', this.handleClickOutside)
this.$nextTick(() => {
this.checkMenuPosition()
})
},
beforeUnmount () {
document.removeEventListener('keydown', this.onKeyDown)
document.removeEventListener('mousedown', this.handleClickOutside)
},
methods: {
checkMenuPosition () {
if (this.$refs.menu) {
const rect = this.$refs.menu.getBoundingClientRect()
const overflow = rect.bottom - window.innerHeight
if (overflow > 0) {
this.contextMenu.y = this.contextMenu.y - overflow - 4
}
}
},
onKeyDown (event) {
if (event.key === 'Escape') {
event.preventDefault()
event.stopPropagation()
this.$emit('close')
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v' && this.hasClipboardData) {
event.preventDefault()
event.stopPropagation()
this.$emit('paste')
}
},
handleClickOutside (event) {
if (this.$refs.menu && !this.$refs.menu.contains(event.target)) {
this.$emit('close')
}
},
handleToggleSelectMode () {
this.isSelectModeRef.value = !this.isSelectModeRef.value
this.$emit('close')
}
}
}
</script>
@@ -1,7 +1,7 @@
<template> <template>
<span <span
class="dropdown dropdown-end field-settings-dropdown" class="dropdown dropdown-end field-settings-dropdown"
:class="{ 'dropdown-open': ((!field.preferences?.price && !field.preferences?.formula && !field.preferences?.price_id && !field.preferences?.payment_link_id) || !isConnected) && !isLoading }" :class="{ 'dropdown-open': withForceOpen && ((!field.preferences?.price && !field.preferences?.formula && !field.preferences?.price_id && !field.preferences?.payment_link_id) || !isConnected) && !isLoading }"
> >
<label <label
tabindex="0" tabindex="0"
@@ -22,7 +22,7 @@
> >
<div <div
v-if="!('price_id' in field.preferences) && !('payment_link_id' in field.preferences)" v-if="!('price_id' in field.preferences) && !('payment_link_id' in field.preferences)"
class="py-1.5 px-1 relative" class="field-settings-currency py-1.5 px-1 relative"
@click.stop @click.stop
> >
<select <select
@@ -48,7 +48,7 @@
</label> </label>
</div> </div>
<div <div
class="py-1.5 px-1 relative" class="field-settings-price py-1.5 px-1 relative"
@click.stop @click.stop
> >
<input <input
@@ -121,7 +121,7 @@
</div> </div>
<div <div
v-if="!isConnected || isOauthSuccess" v-if="!isConnected || isOauthSuccess"
class="py-1.5 px-1 relative" class="field-settings-stripe-connect py-1.5 px-1 relative"
@click.stop @click.stop
> >
<div <div
@@ -203,7 +203,7 @@
>{{ t('learn_more') }}</a> >{{ t('learn_more') }}</a>
</div> </div>
<li <li
class="mb-1" class="field-settings-formula mb-1"
> >
<label <label
class="label-text cursor-pointer text-center w-full flex items-center" class="label-text cursor-pointer text-center w-full flex items-center"
@@ -218,7 +218,7 @@
</label> </label>
</li> </li>
<hr> <hr>
<li> <li class="field-settings-description">
<label <label
class="label-text cursor-pointer text-center w-full flex items-center" class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-description')" @click="$emit('click-description')"
@@ -231,7 +231,10 @@
</span> </span>
</label> </label>
</li> </li>
<li class="mt-1"> <li
v-if="withCondition"
class="field-settings-condition mt-1"
>
<label <label
class="label-text cursor-pointer text-center w-full flex items-center" class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-condition')" @click="$emit('click-condition')"
@@ -244,12 +247,32 @@
</span> </span>
</label> </label>
</li> </li>
<hr
v-if="withCustomFields"
class="pb-0.5 mt-0.5"
>
<li
v-if="withCustomFields"
class="field-settings-save-as-custom-field"
>
<a
href="#"
class="text-sm py-1 px-2"
@click.prevent="$emit('add-custom-field', field)"
>
<IconForms
:width="20"
:stroke-width="1.6"
/>
{{ t('save_as_custom_field') }}
</a>
</li>
</ul> </ul>
</span> </span>
</template> </template>
<script> <script>
import { IconMathFunction, IconSettings, IconCircleCheck, IconInfoCircle, IconBrandStripe, IconInnerShadowTop, IconRouteAltLeft } from '@tabler/icons-vue' import { IconMathFunction, IconSettings, IconCircleCheck, IconInfoCircle, IconBrandStripe, IconInnerShadowTop, IconRouteAltLeft, IconForms } from '@tabler/icons-vue'
import { ref } from 'vue' import { ref } from 'vue'
const isConnected = ref(false) const isConnected = ref(false)
@@ -261,6 +284,7 @@ export default {
IconCircleCheck, IconCircleCheck,
IconRouteAltLeft, IconRouteAltLeft,
IconInfoCircle, IconInfoCircle,
IconForms,
IconMathFunction, IconMathFunction,
IconInnerShadowTop, IconInnerShadowTop,
IconBrandStripe IconBrandStripe
@@ -270,9 +294,24 @@ export default {
field: { field: {
type: Object, type: Object,
required: true required: true
},
withForceOpen: {
type: Boolean,
required: false,
default: true
},
withCustomFields: {
type: Boolean,
required: false,
default: false
},
withCondition: {
type: Boolean,
required: false,
default: true
} }
}, },
emits: ['click-condition', 'click-description', 'click-formula'], emits: ['click-condition', 'click-description', 'click-formula', 'add-custom-field'],
data () { data () {
return { return {
isLoading: false isLoading: false
+2 -1
View File
@@ -119,6 +119,7 @@
<ConditionsModal <ConditionsModal
:item="item" :item="item"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@save="$emit('change')"
@close="isShowConditionsModal = false" @close="isShowConditionsModal = false"
/> />
</Teleport> </Teleport>
@@ -145,7 +146,7 @@ export default {
GoogleDriveDocumentSettings, GoogleDriveDocumentSettings,
IconSortDescending2 IconSortDescending2
}, },
inject: ['t'], inject: ['t', 'getFieldTypeIndex'],
props: { props: {
item: { item: {
type: Object, type: Object,
@@ -0,0 +1,100 @@
<template>
<div
class="absolute outline-dashed outline-gray-400 cursor-move"
:class="[isResizing || isCmdKeyRef.value ? 'z-0' : 'z-20', { 'pointer-events-none': isDrawing || fieldsDragFieldRef.value || isDrag }]"
:style="positionStyle"
@pointerdown.stop.prevent="onPointerDown"
@contextmenu.stop.prevent="openContextMenu"
/>
</template>
<script>
export default {
name: 'SelectionBox',
inject: ['save', 'selectedAreasRef', 'isCmdKeyRef', 'fieldsDragFieldRef'],
props: {
selectionBox: {
type: Object,
required: true
},
pageWidth: {
type: Number,
required: true
},
pageHeight: {
type: Number,
required: true
},
isResizing: {
type: Boolean,
default: false
},
isDrag: {
type: Boolean,
default: false
},
isDrawing: {
type: Boolean,
default: false
}
},
emits: ['move', 'contextmenu', 'close-context-menu'],
data () {
return {
isDragging: false,
dragStart: { x: 0, y: 0 }
}
},
computed: {
positionStyle () {
const { x, y, w, h } = this.selectionBox
return {
top: y * 100 + '%',
left: x * 100 + '%',
width: w * 100 + '%',
height: h * 100 + '%'
}
}
},
methods: {
openContextMenu (e) {
this.$emit('contextmenu', e)
},
onPointerDown (e) {
this.$emit('close-context-menu')
this.startDrag(e)
},
startDrag (e) {
this.isDragging = true
this.dragStart = { x: e.clientX, y: e.clientY }
document.addEventListener('pointermove', this.onDrag)
document.addEventListener('pointerup', this.stopDrag)
},
onDrag (e) {
if (!this.isDragging) return
const parent = this.$el.parentElement
const rect = parent.getBoundingClientRect()
const dx = (e.clientX - this.dragStart.x) / rect.width
const dy = (e.clientY - this.dragStart.y) / rect.height
this.$emit('move', dx, dy)
this.dragStart = { x: e.clientX, y: e.clientY }
},
stopDrag () {
this.isDragging = false
document.removeEventListener('pointermove', this.onDrag)
document.removeEventListener('pointerup', this.stopDrag)
this.save()
}
}
}
</script>
@@ -0,0 +1,348 @@
<template>
<div>
<div
v-if="!isShowFontModal && !isShowConditionsModal"
ref="menu"
class="fixed z-50 p-1 bg-white shadow-lg rounded-lg border border-neutral-200 cursor-default"
style="min-width: 170px"
:style="menuStyle"
@mousedown.stop
@pointerdown.stop
>
<ContextSubmenu
:icon="IconLayoutAlignMiddle"
:label="t('align')"
>
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="alignSelectedAreas('left')"
>
<IconLayoutAlignLeft class="w-4 h-4" />
<span>{{ t('align_left') }}</span>
</button>
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="alignSelectedAreas('right')"
>
<IconLayoutAlignRight class="w-4 h-4" />
<span>{{ t('align_right') }}</span>
</button>
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="alignSelectedAreas('top')"
>
<IconLayoutAlignTop class="w-4 h-4" />
<span>{{ t('align_top') }}</span>
</button>
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="alignSelectedAreas('bottom')"
>
<IconLayoutAlignBottom class="w-4 h-4" />
<span>{{ t('align_bottom') }}</span>
</button>
</ContextSubmenu>
<ContextSubmenu
:icon="IconAspectRatio"
:label="t('resize')"
>
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="resizeSelectedAreas('width')"
>
<IconArrowsHorizontal class="w-4 h-4" />
<span>{{ t('width') }}</span>
</button>
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="resizeSelectedAreas('height')"
>
<IconArrowsVertical class="w-4 h-4" />
<span>{{ t('height') }}</span>
</button>
</ContextSubmenu>
<hr
v-if="showFont || showCondition"
class="my-1 border-neutral-200"
>
<button
v-if="showFont"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="openFontModal"
>
<IconTypography class="w-4 h-4" />
<span>{{ t('font') }}</span>
</button>
<button
v-if="showCondition"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="openConditionModal"
>
<IconRouteAltLeft class="w-4 h-4" />
<span>{{ t('condition') }}</span>
</button>
<hr class="my-1 border-neutral-200">
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
@click.stop="$emit('copy')"
>
<span class="flex items-center space-x-2">
<IconCopy class="w-4 h-4" />
<span>{{ t('copy') }}</span>
</span>
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘C' : 'Ctrl+C' }}</span>
</button>
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm text-red-600"
@click.stop="$emit('delete')"
>
<span class="flex items-center space-x-2">
<IconTrashX class="w-4 h-4" />
<span>{{ t('remove') }}</span>
</span>
<span class="text-xs text-base-content/60 ml-4">Del</span>
</button>
</div>
<Teleport
v-if="isShowFontModal"
:to="modalContainerEl"
>
<FontModal
:field="multiSelectField"
:area="contextMenu.area"
:editable="editable"
:build-default-name="buildDefaultName"
@save="handleSaveMultiSelectFontModal"
@close="closeModal"
/>
</Teleport>
<Teleport
v-if="isShowConditionsModal"
:to="modalContainerEl"
>
<ConditionsModal
:item="multiSelectField"
:build-default-name="buildDefaultName"
:exclude-field-uuids="selectedFields.map(f => f.uuid)"
@save="handleSaveMultiSelectConditionsModal"
@close="closeModal"
/>
</Teleport>
</div>
</template>
<script>
import { IconCopy, IconTrashX, IconTypography, IconRouteAltLeft, IconLayoutAlignLeft, IconLayoutAlignRight, IconLayoutAlignTop, IconLayoutAlignBottom, IconLayoutAlignMiddle, IconAspectRatio, IconArrowsHorizontal, IconArrowsVertical } from '@tabler/icons-vue'
import FontModal from './font_modal'
import ConditionsModal from './conditions_modal'
import ContextSubmenu from './field_context_submenu'
import Field from './field'
import FieldType from './field_type'
export default {
name: 'SelectionContextMenu',
components: {
IconCopy,
IconTrashX,
IconTypography,
IconRouteAltLeft,
IconLayoutAlignLeft,
IconLayoutAlignRight,
IconLayoutAlignTop,
IconLayoutAlignBottom,
FontModal,
IconArrowsHorizontal,
IconArrowsVertical,
ConditionsModal,
ContextSubmenu
},
inject: ['t', 'save', 'selectedAreasRef', 'getFieldTypeIndex'],
props: {
contextMenu: {
type: Object,
required: true
},
editable: {
type: Boolean,
default: true
},
template: {
type: Object,
required: true
},
withCondition: {
type: Boolean,
default: true
}
},
emits: ['copy', 'delete', 'close'],
data () {
return {
isShowFontModal: false,
isShowConditionsModal: false,
multiSelectField: null
}
},
computed: {
modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
},
selectedFields () {
return this.selectedAreasRef.value.map((area) => {
return this.template.fields.find((f) => f.areas?.includes(area))
}).filter(Boolean)
},
isMac () {
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
},
menuStyle () {
return {
left: this.contextMenu.x + 'px',
top: this.contextMenu.y + 'px'
}
},
showFont () {
return true
},
showCondition () {
return this.withCondition
},
fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels
},
mounted () {
document.addEventListener('keydown', this.onKeyDown)
document.addEventListener('mousedown', this.handleClickOutside)
this.$nextTick(() => this.checkMenuPosition())
},
beforeUnmount () {
document.removeEventListener('keydown', this.onKeyDown)
document.removeEventListener('mousedown', this.handleClickOutside)
},
methods: {
IconLayoutAlignMiddle,
IconAspectRatio,
buildDefaultName: Field.methods.buildDefaultName,
checkMenuPosition () {
if (this.$refs.menu) {
const rect = this.$refs.menu.getBoundingClientRect()
const overflow = rect.bottom - window.innerHeight
if (overflow > 0) {
this.contextMenu.y = this.contextMenu.y - overflow - 4
}
}
},
onKeyDown (event) {
if (event.key === 'Escape') {
event.preventDefault()
event.stopPropagation()
this.$emit('close')
}
},
handleClickOutside (event) {
if (this.$refs.menu && !this.$refs.menu.contains(event.target)) {
this.$emit('close')
}
},
openFontModal () {
this.multiSelectField = {
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
preferences: {}
}
const preferencesStrings = this.selectedFields.map((f) => JSON.stringify(f.preferences || {}))
if (preferencesStrings.every((s) => s === preferencesStrings[0])) {
this.multiSelectField.preferences = JSON.parse(preferencesStrings[0])
}
this.isShowFontModal = true
},
openConditionModal () {
this.multiSelectField = {
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
conditions: []
}
const conditionStrings = this.selectedFields.map((f) => JSON.stringify(f.conditions || []))
if (conditionStrings.every((s) => s === conditionStrings[0])) {
this.multiSelectField.conditions = JSON.parse(conditionStrings[0])
}
this.isShowConditionsModal = true
},
closeModal () {
this.isShowFontModal = false
this.isShowConditionsModal = false
this.multiSelectField = null
this.$emit('close')
},
alignSelectedAreas (direction) {
const areas = this.selectedAreasRef.value
let targetValue
if (direction === 'left') {
targetValue = Math.min(...areas.map(a => a.x))
areas.forEach((area) => { area.x = targetValue })
} else if (direction === 'right') {
targetValue = Math.max(...areas.map(a => a.x + a.w))
areas.forEach((area) => { area.x = targetValue - area.w })
} else if (direction === 'top') {
targetValue = Math.min(...areas.map(a => a.y))
areas.forEach((area) => { area.y = targetValue })
} else if (direction === 'bottom') {
targetValue = Math.max(...areas.map(a => a.y + a.h))
areas.forEach((area) => { area.y = targetValue - area.h })
}
this.save()
this.$emit('close')
},
resizeSelectedAreas (dimension) {
const areas = this.selectedAreasRef.value
const values = areas.map(a => dimension === 'width' ? a.w : a.h).sort((a, b) => a - b)
const medianValue = values[Math.floor(values.length / 2)]
if (dimension === 'width') {
areas.forEach((area) => { area.w = medianValue })
} else if (dimension === 'height') {
areas.forEach((area) => {
const diff = medianValue - area.h
area.y = area.y - diff
area.h = medianValue
})
}
this.save()
this.$emit('close')
},
handleSaveMultiSelectFontModal () {
this.selectedFields.forEach((field) => {
field.preferences = { ...field.preferences, ...this.multiSelectField.preferences }
})
this.save()
this.closeModal()
},
handleSaveMultiSelectConditionsModal () {
this.selectedFields.forEach((field) => {
field.conditions = JSON.parse(JSON.stringify(this.multiSelectField.conditions))
})
this.save()
this.closeModal()
}
}
}
</script>
+5 -1
View File
@@ -290,7 +290,11 @@ export default {
if (resp.ok) { if (resp.ok) {
resp.json().then((data) => { resp.json().then((data) => {
this.$emit('success', data) this.$emit('success', data)
this.$refs.input.value = ''
if (this.$refs.input) {
this.$refs.input.value = ''
}
this.isLoading = false this.isLoading = false
}) })
} else if (resp.status === 422) { } else if (resp.status === 422) {
+5 -3
View File
@@ -164,11 +164,13 @@ class ProcessSubmitterCompletionJob
next_submitter_items = next_submitter_items =
if submission.template_submitters.any? { |s| s['order'] } if submission.template_submitters.any? { |s| s['order'] }
submitter_groups = submitter_groups =
submission.template_submitters.group_by.with_index { |s, index| s['order'] || index } submission.template_submitters
.group_by.with_index { |s, index| s['order'] || index }
.sort_by(&:first).pluck(1)
current_group_index = submitter_groups.find { |_, group| group.any? { |s| s['uuid'] == submitter.uuid } }&.first current_group_index = submitter_groups.index { |group| group.any? { |s| s['uuid'] == submitter.uuid } }
if submitter_groups[current_group_index + 1] && if current_group_index && submitter_groups[current_group_index + 1] &&
submitters_index.values_at(*submitter_groups[current_group_index].pluck('uuid')) submitters_index.values_at(*submitter_groups[current_group_index].pluck('uuid'))
.compact.all?(&:completed_at?) .compact.all?(&:completed_at?)
submitter_groups[current_group_index + 1] submitter_groups[current_group_index + 1]
+1 -1
View File
@@ -26,7 +26,7 @@ class SendTestWebhookRequestJob
Addressable::URI.parse(webhook_url.url).normalize Addressable::URI.parse(webhook_url.url).normalize
end end
raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' || [443, nil].exclude?(uri.port)
raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS) raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS)
end end
+12 -7
View File
@@ -14,12 +14,7 @@ class SubmitterMailer < ApplicationMailer
@email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid']) @email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid'])
end end
template_submitters_index = template_submitters_index = @email_message.blank? ? build_submitter_preferences_index(@submitter) : {}
if @email_message.blank?
build_submitter_preferences_index(@submitter)
else
{}
end
@body = @email_message&.body.presence || @body = @email_message&.body.presence ||
template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence || template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence ||
@@ -34,7 +29,9 @@ class SubmitterMailer < ApplicationMailer
assign_message_metadata('submitter_invitation', @submitter) assign_message_metadata('submitter_invitation', @submitter)
reply_to = build_submitter_reply_to(@submitter) reply_to = build_submitter_reply_to(@submitter, email_config: @email_config)
maybe_set_custom_domain(@submitter)
I18n.with_locale(@current_account.locale) do I18n.with_locale(@current_account.locale) do
subject = build_invite_subject(@subject, @email_config, submitter) subject = build_invite_subject(@subject, @email_config, submitter)
@@ -133,6 +130,8 @@ class SubmitterMailer < ApplicationMailer
assign_message_metadata('submitter_documents_copy', @submitter) assign_message_metadata('submitter_documents_copy', @submitter)
reply_to = build_submitter_reply_to(submitter, email_config: @email_config, documents_copy_email: true) reply_to = build_submitter_reply_to(submitter, email_config: @email_config, documents_copy_email: true)
maybe_set_custom_domain(@submitter)
I18n.with_locale(@current_account.locale) do I18n.with_locale(@current_account.locale) do
subject = subject =
@subject.present? ? ReplaceEmailVariables.call(@subject, submitter:) : I18n.t(:your_document_copy) @subject.present? ? ReplaceEmailVariables.call(@subject, submitter:) : I18n.t(:your_document_copy)
@@ -262,4 +261,10 @@ class SubmitterMailer < ApplicationMailer
def fetch_config_email_body(email_config, _submitter = nil) def fetch_config_email_body(email_config, _submitter = nil)
email_config ? email_config.value['body'].presence : nil email_config ? email_config.value['body'].presence : nil
end end
def maybe_set_custom_domain(submitter)
if Docuseal.multitenant? && (config = AccountConfig.find_by(account_id: submitter.account_id, key: :custom_domain))
@custom_domain = config.value
end
end
end end
+10
View File
@@ -47,14 +47,24 @@ class AccountConfig < ApplicationRecord
WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason' WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason'
RECIPIENT_FORM_FIELDS_KEY = 'recipient_form_fields' RECIPIENT_FORM_FIELDS_KEY = 'recipient_form_fields'
WITH_AUDIT_VALUES_KEY = 'with_audit_values' WITH_AUDIT_VALUES_KEY = 'with_audit_values'
WITH_AUDIT_SENDER_KEY = 'with_audit_sender'
WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone' WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone'
WITH_TIMESTAMP_SECONDS_KEY = 'with_timestamp_seconds'
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason' REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
REUSE_SIGNATURE_KEY = 'reuse_signature' REUSE_SIGNATURE_KEY = 'reuse_signature'
WITH_FIELD_LABELS_KEY = 'with_field_labels' WITH_FIELD_LABELS_KEY = 'with_field_labels'
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key' COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format' DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields'
POLICY_LINKS_KEY = 'policy_links' POLICY_LINKS_KEY = 'policy_links'
EMAIL_VARIABLES = {
SUBMITTER_INVITATION_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
SUBMITTER_COMPLETED_EMAIL_KEY => %w[template.name submission.submitters submission.link].freeze,
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[template.name documents.link account.name].freeze
}.freeze
DEFAULT_VALUES = { DEFAULT_VALUES = {
SUBMITTER_INVITATION_EMAIL_KEY => lambda { SUBMITTER_INVITATION_EMAIL_KEY => lambda {
{ {
+3 -2
View File
@@ -78,8 +78,9 @@ class Submission < ApplicationRecord
scope :active, -> { where(archived_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) } scope :archived, -> { where.not(archived_at: nil) }
scope :pending, lambda { scope :pending, lambda {
where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]) where(expire_at: nil).or(where(expire_at: Time.current..))
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists) .where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
} }
scope :completed, lambda { scope :completed, lambda {
where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]) where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
+2 -2
View File
@@ -22,7 +22,7 @@
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :password, 'Password', class: 'label' %> <%= ff.label :password, 'Password', class: 'label' %>
<%= ff.password_field :password, value: value['password'], class: 'base-input' %> <%= ff.password_field :password, class: 'base-input', required: value['password'].present?, placeholder: value['password'].present? ? '*************' : '' %>
</div> </div>
</div> </div>
<div class="grid md:grid-cols-2 gap-4"> <div class="grid md:grid-cols-2 gap-4">
@@ -39,7 +39,7 @@
<div class="form-control"> <div class="form-control">
<%= ff.label :security_label, 'SMTP Security', class: 'label' %> <%= ff.label :security_label, 'SMTP Security', class: 'label' %>
<div class="flex items-center space-x-6"> <div class="flex items-center space-x-6">
<% [%w[Auto none], %w[SSL ssl], %w[TLS tls], %w[Noverify noverify]].each do |(label, val)| %> <% [%w[STARTTLS none], %w[TLS tls], %w[SSL ssl], %w[Noverify noverify]].each do |(label, val)| %>
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %> <%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
<%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %> <%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %>
<%= label %> <%= label %>
+5
View File
@@ -0,0 +1,5 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 14l-4 -4l4 -4" />
<path d="M5 10h11a4 4 0 1 1 0 8h-1" />
</svg>

After

Width:  |  Height:  |  Size: 354 B

@@ -0,0 +1,5 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M15 14l4 -4l-4 -4" />
<path d="M19 10h-11a4 4 0 1 0 0 8h1" />
</svg>

After

Width:  |  Height:  |  Size: 356 B

+5
View File
@@ -0,0 +1,5 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 5h6a3.5 3.5 0 0 1 0 7h-6l0 -7" />
<path d="M13 12h1a3.5 3.5 0 0 1 0 7h-7v-7" />
</svg>

After

Width:  |  Height:  |  Size: 377 B

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

After

Width:  |  Height:  |  Size: 358 B

+5
View File
@@ -0,0 +1,5 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 5v5a5 5 0 0 0 10 0v-5" />
<path d="M5 19h14" />
</svg>

After

Width:  |  Height:  |  Size: 345 B

+1 -1
View File
@@ -22,7 +22,7 @@
<% if params[:modal].present? %> <% if params[:modal].present? %>
<% url_params = Rails.application.routes.recognize_path(params[:modal], method: :get) %> <% url_params = Rails.application.routes.recognize_path(params[:modal], method: :get) %>
<% if url_params[:action] == 'new' %> <% if url_params[:action] == 'new' %>
<open-modal src="<%= params[:modal] %>"></open-modal> <open-modal src="<%= url_for(url_params) %>"></open-modal>
<% end %> <% end %>
<% end %> <% end %>
<turbo-frame id="modal"></turbo-frame> <turbo-frame id="modal"></turbo-frame>
@@ -1,11 +1,5 @@
<div class="form-control"> <div class="form-control">
<div class="flex items-center"> <%= ff.label :body, t('body'), class: 'label' %>
<%= ff.label :body, t('body'), class: 'label' %> <% variables = AccountConfig::EMAIL_VARIABLES[local_assigns[:config].key] %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[local_assigns[:config].key].call['body'].scan(/{.*?}/).join(', ') %>"> <%= render 'personalization_settings/markdown_editor', name: ff.field_name(:body), value: local_assigns[:config].value['body'], variables: variables %>
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full !rounded-2xl py-2', dir: 'auto' %>
</autoresize-textarea>
</div> </div>
@@ -0,0 +1,76 @@
<% if value.to_s.start_with?('<html') %>
<autoresize-textarea>
<%= text_area_tag name, value, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto', style: 'max-height: 400px' %>
</autoresize-textarea>
<% else %>
<markdown-editor>
<template data-target="markdown-editor.linkTooltipTemplate">
<div class="hidden absolute flex bg-white border border-base-300 rounded-xl shadow p-1 gap-1 items-center z-50" contenteditable="false">
<input type="text" placeholder="<%= t('enter_a_url_or_variable_name') %>" class="rounded-lg border border-base-300 px-2 py-1 text-sm outline-none" style="field-sizing: content; min-width: 205px; max-width: 320px;" autocomplete="off">
<button type="button" data-role="link-save" class="flex items-center px-1 w-6 h-6 rounded hover:bg-success/10 cursor-pointer">
<%= svg_icon('check', class: 'w-4 h-4 text-success') %>
</button>
<button type="button" data-role="link-remove" class="flex items-center px-1 w-6 h-6 rounded hover:bg-error/10 cursor-pointer">
<%= svg_icon('x', class: 'w-4 h-4 text-error') %>
</button>
</div>
</template>
<div class="border border-base-content/20 rounded-2xl bg-white">
<div class="flex items-center px-2 py-2 border-b" style="height: 42px;">
<div class="flex items-center gap-1">
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('bold') %> (Ctrl+B)">
<button type="button" data-action="click:markdown-editor#bold" data-target="markdown-editor.boldButton" aria-label="<%= t('bold') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('bold', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('italic') %> (Ctrl+I)">
<button type="button" data-action="click:markdown-editor#italic" data-target="markdown-editor.italicButton" aria-label="<%= t('italic') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('italic', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('underline') %> (Ctrl+U)">
<button type="button" data-action="click:markdown-editor#underline" data-target="markdown-editor.underlineButton" aria-label="<%= t('underline') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('underline', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('link') %> (Ctrl+K)">
<button type="button" data-action="click:markdown-editor#linkSelection" data-target="markdown-editor.linkButton" aria-label="<%= t('link') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('link', class: 'w-4 h-4') %>
</button>
</div>
</div>
<div class="mx-2 h-5 border-l border-base-content/20"></div>
<div class="flex items-center gap-1">
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('undo') %> (Ctrl+Z)">
<button type="button" data-action="click:markdown-editor#undo" data-target="markdown-editor.undoButton" aria-label="<%= t('undo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('arrow_back_up', class: 'w-4 h-4') %>
</button>
</div>
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('redo') %> (Ctrl+Shift+Z)">
<button type="button" data-action="click:markdown-editor#redo" data-target="markdown-editor.redoButton" aria-label="<%= t('redo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('arrow_forward_up', class: 'w-4 h-4') %>
</button>
</div>
</div>
<% if local_assigns[:variables]&.any? %>
<% variable_labels = { 'account.name' => t('variables.account_name'), 'submitter.link' => t('variables.submitter_link'), 'template.name' => t('variables.template_name'), 'submission.submitters' => t('variables.submission_submitters'), 'submission.link' => t('variables.submission_link'), 'documents.link' => t('variables.documents_link') } %>
<div class="dropdown dropdown-end ml-auto">
<label tabindex="0" class="flex items-center gap-1 text-sm px-2 py-1 rounded hover:bg-base-200 cursor-pointer">
<%= t('add_variable') %>
<%= svg_icon('chevron_down', class: 'w-3.5 h-3.5') %>
</label>
<div tabindex="0" class="dropdown-content right-0 top-full mt-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50">
<% local_assigns[:variables]&.each do |variable| %>
<button type="button" data-variable="<%= variable %>" data-action="click:markdown-editor#insertVariable" class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 text-left text-sm cursor-pointer whitespace-nowrap">
<%= variable_labels.fetch(variable, "{#{variable}}") %>
</button>
<% end %>
</div>
</div>
<% end %>
</div>
<div data-target="markdown-editor.editorElement"></div>
</div>
<%= hidden_field_tag name, value, required: true, data: { target: 'markdown-editor.textarea' } %>
</markdown-editor>
<% end %>
@@ -8,12 +8,18 @@
<div class="collapse-content"> <div class="collapse-content">
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %> <%= f.fields_for :value, Struct.new(:subject, :body, :reply_to).new(*f.object.value.values_at('subject', 'body', 'reply_to')) do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :subject, t('subject'), class: 'label' %> <%= ff.label :subject, t('subject'), class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %> <%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %> <%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
<% if can?(:manage, :reply_to) || can?(:manage, :personalization_advanced) %>
<div class="form-control">
<%= ff.label :reply_to, t('reply_to'), class: 'label' %>
<%= ff.email_field :reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %>
</div>
<% end %>
<% end %> <% end %>
<div class="form-control pt-2"> <div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %> <%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
@@ -12,7 +12,7 @@
<div class="form-control"> <div class="form-control">
<div class="flex items-center"> <div class="flex items-center">
<%= ff.label :subject, t('subject'), class: 'label' %> <%= ff.label :subject, t('subject'), class: 'label' %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].call['subject'].scan(/{.*?}/).join(', ') %>"> <span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].map { |v| "{#{v}}" }.join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %> <%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span> </span>
</div> </div>
+1 -1
View File
@@ -4,7 +4,7 @@
"id": "/", "id": "/",
"icons": [ "icons": [
{ {
"src": "/logo.svg", "src": "/favicon.svg",
"type": "image/svg+xml", "type": "image/svg+xml",
"sizes": "any" "sizes": "any"
}, },
-1
View File
@@ -1,5 +1,4 @@
<% uuid = local_assigns[:uuid] || SecureRandom.uuid %> <% uuid = local_assigns[:uuid] || SecureRandom.uuid %>
<label id="<%= local_assigns[:button_id] %>" for="<%= uuid %>" class="<%= local_assigns[:btn_class] %>"><%= local_assigns[:btn_text] %></label>
<input type="checkbox" id="<%= uuid %>" class="modal-toggle"> <input type="checkbox" id="<%= uuid %>" class="modal-toggle">
<div id="<%= local_assigns[:id] %>" class="modal items-start !animate-none overflow-y-auto"> <div id="<%= local_assigns[:id] %>" class="modal items-start !animate-none overflow-y-auto">
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none"> <div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none">
+3 -31
View File
@@ -1,32 +1,4 @@
<svg height="<%= local_assigns.fetch(:height, '37') %>" width="<%= local_assigns.fetch(:width, '37') %>" class="<%= local_assigns[:class] %>" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 511.998 511.998" xml:space="preserve"> <svg class="<%= local_assigns[:class] %>" height="<%= local_assigns.fetch(:height, '37') %>" width="<%= local_assigns.fetch(:width, '37') %>" style="color: #e0753f" viewBox="0 0 180 180" xmlns="http://www.w3.org/2000/svg">
<path style="fill:#AA968C;" d="M503.999,247.999c0,128.13-111.033,240-248,240S8,376.129,8,247.999s111.033-224,248-224 <path fill="currentColor" d="M 178.224 72.09 c -0.296 -1.463 -0.627 -2.919 -0.996 -4.364 -0.293 -1.151 -0.616 -2.293 -0.956 -3.433 -0.301 -1.008 -0.612 -2.014 -0.95 -3.012 -0.531 -1.578 -1.113 -3.142 -1.735 -4.694 -0.216 -0.54 -0.433 -1.082 -0.661 -1.618 -0.195 -0.462 -0.399 -0.917 -0.601 -1.375 -0.262 -0.591 -0.53 -1.177 -0.804 -1.762 -0.074 -0.159 -0.151 -0.315 -0.226 -0.474 -0.209 -0.441 -0.422 -0.881 -0.638 -1.318 -0.076 -0.154 -0.153 -0.306 -0.229 -0.459 -0.236 -0.471 -0.477 -0.939 -0.721 -1.406 -0.053 -0.101 -0.105 -0.201 -0.158 -0.302 -1.143 -2.16 -2.367 -4.269 -3.68 -6.322 -0.116 -0.181 -0.237 -0.359 -0.355 -0.539 -0.094 -0.144 -0.189 -0.288 -0.284 -0.432 -0.284 -0.431 -0.57 -0.861 -0.862 -1.287 -0.112 -0.164 -0.225 -0.326 -0.338 -0.489 -0.193 -0.279 -0.382 -0.56 -0.579 -0.836 -0.089 -0.125 -0.182 -0.249 -0.273 -0.374 -0.13 -0.182 -0.264 -0.362 -0.395 -0.542 -0.277 -0.38 -0.556 -0.76 -0.838 -1.135 -0.15 -0.199 -0.303 -0.395 -0.454 -0.593 -0.21 -0.274 -0.417 -0.552 -0.63 -0.823 -0.055 -0.069 -0.111 -0.136 -0.166 -0.205 -0.482 -0.61 -0.971 -1.216 -1.47 -1.814 -0.129 -0.155 -0.262 -0.306 -0.392 -0.461 -0.402 -0.476 -0.808 -0.95 -1.22 -1.417 -0.186 -0.212 -0.375 -0.422 -0.563 -0.631 -0.384 -0.428 -0.773 -0.854 -1.167 -1.276 -0.176 -0.189 -0.351 -0.379 -0.529 -0.567 -0.564 -0.595 -1.134 -1.186 -1.716 -1.768 -1.091 -1.091 -2.207 -2.15 -3.346 -3.178 -1.016 -0.919 -2.05 -1.815 -3.103 -2.684 -0.772 -0.636 -1.557 -1.255 -2.348 -1.864 -3.465 -2.67 -7.112 -5.075 -10.927 -7.209 -2.869 -1.604 -5.83 -3.06 -8.883 -4.351 -2.443 -1.033 -4.922 -1.948 -7.428 -2.756 -8.879 -2.863 -18.13 -4.318 -27.605 -4.318 -3.19 0 -6.354 0.169 -9.488 0.496 -4.036 0.421 -8.019 1.114 -11.94 2.073 -1.732 0.423 -3.452 0.892 -5.157 1.42 -2.856 0.883 -5.673 1.912 -8.447 3.085 -2.645 1.118 -5.222 2.357 -7.729 3.711 -2.574 1.39 -5.073 2.901 -7.494 4.533 -1.195 0.805 -2.37 1.64 -3.527 2.503 -1.156 0.864 -2.292 1.756 -3.408 2.676 -0.553 0.456 -1.1 0.919 -1.643 1.389 -1.649 1.427 -3.252 2.92 -4.806 4.473 -2.582 2.582 -4.991 5.299 -7.222 8.138 -0.892 1.135 -1.756 2.292 -2.59 3.467 -0.417 0.588 -0.827 1.18 -1.23 1.778 -0.403 0.597 -0.798 1.199 -1.186 1.806 -0.388 0.607 -0.769 1.218 -1.143 1.835 -2.241 3.697 -4.216 7.562 -5.916 11.582 -1.095 2.589 -2.059 5.217 -2.901 7.877 -0.153 0.482 -0.3 0.965 -0.444 1.449 -0.339 1.14 -0.663 2.282 -0.956 3.433 -0.369 1.446 -0.7 2.901 -0.996 4.364 -1.034 5.121 -1.618 10.343 -1.749 15.637 -0.018 0.757 -0.028 1.514 -0.028 2.274 0 1.123 0.02 2.244 0.062 3.361 0.285 7.82 1.568 15.475 3.825 22.879 0.044 0.147 0.088 0.295 0.133 0.441 0.877 2.823 1.894 5.608 3.054 8.35 0.85 2.009 1.769 3.98 2.755 5.912 0.539 1.057 1.105 2.099 1.685 3.132 4.013 7.142 8.98 13.698 14.846 19.564 7.713 7.713 16.611 13.878 26.477 18.352 0.705 0.32 1.415 0.632 2.131 0.935 2.081 0.88 4.185 1.679 6.313 2.396 9.217 3.106 18.85 4.677 28.719 4.677 8.031 0 15.902 -1.047 23.522 -3.107 0.633 -0.172 1.266 -0.35 1.895 -0.535 0.757 -0.222 1.509 -0.456 2.26 -0.698 0.717 -0.232 1.431 -0.474 2.145 -0.723 1.752 -0.616 3.49 -1.281 5.211 -2.009 0.755 -0.319 1.503 -0.651 2.247 -0.989 1.237 -0.563 2.459 -1.15 3.664 -1.766 0.644 -0.328 1.283 -0.665 1.917 -1.009 1.654 -0.896 3.274 -1.848 4.865 -2.844 5.736 -3.591 11.06 -7.827 15.912 -12.679 0.775 -0.775 1.534 -1.562 2.278 -2.36 5.204 -5.59 9.636 -11.754 13.246 -18.417 0.343 -0.634 0.68 -1.274 1.009 -1.917 0.482 -0.944 0.943 -1.9 1.392 -2.863 0.471 -1.007 0.928 -2.021 1.364 -3.049 1.22 -2.886 2.281 -5.82 3.187 -8.793 0.559 -1.833 1.056 -3.68 1.494 -5.542 0.108 -0.458 0.211 -0.916 0.312 -1.376 0.194 -0.883 0.373 -1.77 0.539 -2.659 1.02 -5.455 1.542 -11.02 1.542 -16.663 0 -6.074 -0.595 -12.058 -1.776 -17.911 z m -161.733 19.614 c -1.118 -56.662 44.604 -74.877 60.998 -67.647 2.187 0.965 4.732 2.431 7.042 2.96 5.295 1.213 13.432 -3.113 13.521 6.273 0.078 8.156 -3.389 13.108 -10.797 16.177 -7.539 3.124 -14.777 9.181 -19.95 15.493 -21.487 26.216 -31.231 68.556 -7.565 94.296 -13.679 -5.545 -42.418 -25.467 -43.248 -67.552 z m 91.109 72.619 c -0.053 0.008 -4.171 0.775 -4.171 0.775 0 0 -15.862 -22.957 -23.509 -21.719 11.291 16.04 12.649 22.625 12.649 22.625 -0.053 0.001 -0.107 0.001 -0.161 0.003 -51.831 2.131 -42.785 -64.026 -28.246 -86.502 -1.555 13.073 8.878 39.992 39.034 44.1 9.495 1.293 32.302 -3.275 41.015 -11.38 0.098 1.825 0.163 3.85 0.159 6.013 -0.046 23.538 -13.47 42.743 -36.77 46.085 z m 30.575 -15.708 c 9.647 -9.263 12.869 -27.779 9.103 -44.137 -4.608 -20.011 -28.861 -32.383 -40.744 -35.564 5.766 -8.089 27.908 -14.274 39.567 5.363 -5.172 -10.519 -13.556 -23.023 -1.732 -33.128 12.411 13.329 19.411 29.94 20.161 48.7 0.75 18.753 -6.64 41.768 -26.355 58.765 z" />
S503.999,119.869,503.999,247.999z" /> <circle fill="currentColor" cx="71.927" cy="32.004" r="2.829" />
<path style="fill:#AA968C;" d="M255.999,23.999C119.033,23.999,8,119.868,8,247.999c0,24.631,4.138,48.647,11.74,71.397
c25.518,34.29,66.232,56.602,112.26,56.602c53.893,0,100.6-30.495,124-75.13c23.4,44.636,70.107,75.13,124,75.13
c46.028,0,86.743-22.312,112.26-56.602c7.602-22.75,11.74-46.767,11.74-71.397C503.999,119.868,392.966,23.999,255.999,23.999z" />
<circle style="fill:#C8AF9B;" cx="256" cy="351.999" r="136" />
<g>
<circle style="fill:#464655;" cx="132" cy="203.999" r="28" />
<circle style="fill:#464655;" cx="380" cy="203.999" r="28" />
<path style="fill:#464655;" d="M269.949,284.516c-7.672,10.741-20.227,10.741-27.899,0l-12.101-16.941
c-7.672-10.741-3.15-19.53,10.05-19.53h32c13.2,0,17.723,8.788,10.05,19.53L269.949,284.516z" />
</g>
<path style="fill:#AA968C;" d="M 350.964 399.998 C 316.628 399.998 299.021 351.998 255.882 351.998 C 212.742 351.998 195.135 399.998 160.801 399.998 C 145.395 399.998 131.723 394.147 120.621 374.798 C 131.595 439.03 187.893 487.998 255.881 487.998 C 323.868 487.998 380.168 439.03 391.14 374.798 C 380.04 394.148 366.368 399.998 350.964 399.998 Z" />
<g>
<path style="fill:#8C7873;" d="M32,423.998c-3.172,0-6.18-1.906-7.43-5.031c-1.641-4.105,0.359-8.758,4.461-10.402l160.008-64
c4.062-1.617,8.758,0.352,10.398,4.457s-0.359,8.758-4.461,10.402l-160.008,64C34,423.811,32.992,423.998,32,423.998z" />
<path style="fill:#8C7873;" d="M15.992,375.995c-3.547,0-6.781-2.375-7.727-5.965c-1.125-4.273,1.422-8.648,5.695-9.773l152-40
c4.289-1.121,8.648,1.426,9.773,5.703c1.125,4.273-1.422,8.648-5.695,9.773l-152,40C17.351,375.913,16.672,375.995,15.992,375.995z
" />
<path style="fill:#8C7873;" d="M7.992,335.995c-3.812,0-7.187-2.73-7.867-6.609c-0.773-4.352,2.133-8.5,6.484-9.27l136-24
c4.328-0.77,8.508,2.141,9.266,6.488c0.773,4.352-2.133,8.5-6.484,9.27l-136,24C8.922,335.956,8.453,335.995,7.992,335.995z" />
<path style="fill:#8C7873;" d="M480,423.998c3.172,0,6.18-1.906,7.43-5.031c1.641-4.105-0.359-8.758-4.461-10.402l-160.008-64
c-4.063-1.617-8.758,0.352-10.398,4.457s0.359,8.758,4.461,10.402l160.008,64C478,423.811,479.007,423.998,480,423.998z" />
<path style="fill:#8C7873;" d="M496.007,375.995c3.547,0,6.781-2.375,7.727-5.965c1.125-4.273-1.422-8.648-5.695-9.773l-152-40
c-4.289-1.121-8.648,1.426-9.773,5.703c-1.125,4.273,1.422,8.648,5.695,9.773l152,40
C494.648,375.913,495.328,375.995,496.007,375.995z" />
<path style="fill:#8C7873;" d="M504.007,335.995c3.813,0,7.188-2.73,7.867-6.609c0.773-4.352-2.133-8.5-6.484-9.27l-136-24
c-4.328-0.77-8.508,2.141-9.266,6.488c-0.773,4.352,2.133,8.5,6.484,9.27l136,24C503.078,335.956,503.546,335.995,504.007,335.995z
" />
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

+1
View File
@@ -27,4 +27,5 @@
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png"> <link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<meta name="theme-color" content="#faf7f5"> <meta name="theme-color" content="#faf7f5">
+3 -4
View File
@@ -1,4 +1,3 @@
<% link = pagy_anchor(pagy) %>
<% if pagy.pages > 1 %> <% if pagy.pages > 1 %>
<div class="flex my-6 justify-center md:justify-between"> <div class="flex my-6 justify-center md:justify-between">
<div class="hidden md:block text-sm"> <div class="hidden md:block text-sm">
@@ -12,8 +11,8 @@
<div class="flex items-center space-x-1.5"> <div class="flex items-center space-x-1.5">
<%= local_assigns[:right_additional_html] %> <%= local_assigns[:right_additional_html] %>
<div class="join"> <div class="join">
<% if pagy.prev %> <% if pagy.previous %>
<%== link.call(pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %> <%= link_to '«', url_for(params: request.query_parameters.merge('page' => pagy.previous)), class: 'join-item btn min-h-full h-10' %>
<% else %> <% else %>
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span> <span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span>
<% end %> <% end %>
@@ -23,7 +22,7 @@
<% if local_assigns[:next_page_path].present? %> <% if local_assigns[:next_page_path].present? %>
<%= link_to '»', local_assigns[:next_page_path], class: 'join-item btn min-h-full h-10' %> <%= link_to '»', local_assigns[:next_page_path], class: 'join-item btn min-h-full h-10' %>
<% elsif pagy.next %> <% elsif pagy.next %>
<%== link.call(pagy.next, '»', classes: 'join-item btn min-h-full h-10') %> <%= link_to '»', url_for(params: request.query_parameters.merge('page' => pagy.next)), class: 'join-item btn min-h-full h-10' %>
<% else %> <% else %>
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span> <span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span>
<% end %> <% end %>
+1 -4
View File
@@ -1,7 +1,4 @@
<form action="<%= url_for %>" method="get" class="items-center flex"> <form action="<%= url_for %>" method="get" class="items-center flex">
<% if params[:status].present? %>
<input name="status" value="<%= params[:status] %>" class="hidden">
<% end %>
<% Submissions::Filter::ALLOWED_PARAMS.each do |key| %> <% Submissions::Filter::ALLOWED_PARAMS.each do |key| %>
<% if params[key].present? %> <% if params[key].present? %>
<input name="<%= key %>" value="<%= params[key] %>" class="hidden"> <input name="<%= key %>" value="<%= params[key] %>" class="hidden">
@@ -9,7 +6,7 @@
<% end %> <% end %>
<% if params[:q].present? %> <% if params[:q].present? %>
<div class="relative"> <div class="relative">
<a href="<%= url_for(params.to_unsafe_h.except(:q)) %>" title="<%= t('clear') %>" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight"> <a href="<%= url_for(params: request.query_parameters.except('q')) %>" title="<%= t('clear') %>" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
&times; &times;
</a> </a>
</div> </div>
+1 -1
View File
@@ -135,7 +135,7 @@
<a href="mailto:<%= Docuseal::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline text-center"> <a href="mailto:<%= Docuseal::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline text-center">
<%= Docuseal::SUPPORT_EMAIL %> <%= Docuseal::SUPPORT_EMAIL %>
</a> </a>
<% if Docuseal.version.present? %> <% if Docuseal.version.present? && !Docuseal.multitenant? && can?(:manage, EncryptedConfig) %>
<a href="https://github.com/docusealco/docuseal/releases" target="_blank" class="badge badge-outline text-xs block mx-auto mt-4"> <a href="https://github.com/docusealco/docuseal/releases" target="_blank" class="badge badge-outline text-xs block mx-auto mt-4">
v<%= Docuseal.version %> v<%= Docuseal.version %>
</a> </a>
+1 -1
View File
@@ -1,5 +1,5 @@
<% if configs = account.account_configs.find_by(key: AccountConfig::POLICY_LINKS_KEY) %> <% if configs = account.account_configs.find_by(key: AccountConfig::POLICY_LINKS_KEY) %>
<div class="max-w-md mx-auto flex flex-wrap gap-1 justify-center text-sm text-base-content/60 mt-2"> <div class="max-w-md mx-auto flex flex-wrap gap-1 justify-center text-sm text-base-content/60 mt-2">
<%= auto_link(MarkdownToHtml.call(configs.value)) %> <%= MarkdownToHtml.call(configs.value) %>
</div> </div>
<% end %> <% end %>
@@ -8,7 +8,7 @@
</div> </div>
<div class="form-control"> <div class="form-control">
<%= fff.label :secret_access_key, class: 'label' %> <%= fff.label :secret_access_key, class: 'label' %>
<%= fff.password_field :secret_access_key, value: configs['secret_access_key'], required: true, class: 'base-input' %> <%= fff.password_field :secret_access_key, required: true, class: 'base-input', placeholder: configs['secret_access_key'].present? ? '*************' : '' %>
</div> </div>
</div> </div>
<div class="grid md:grid-cols-2 gap-4"> <div class="grid md:grid-cols-2 gap-4">
Loaded 100 of 202 files, more files were not shown because too many files have changed in this diff. Show more