mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 07:14:43 +00:00
Compare commits
619 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d813e184b | |||
| fd23674e87 | |||
| 3b9d35acf1 | |||
| 17fc6e2f1c | |||
| 2e132d8b5a | |||
| 53bef85a63 | |||
| 99ad6e36ac | |||
| bfa41ac35f | |||
| bc4414a368 | |||
| 26ae5f1d80 | |||
| be4962a02a | |||
| e72090c2e0 | |||
| ad8bca8bf4 | |||
| 22be3b37cf | |||
| cfc32ee522 | |||
| 10fbdf6612 | |||
| 3604fb6461 | |||
| 24392d0258 | |||
| 4ab1065773 | |||
| 50b0f7e5ed | |||
| be4a0505bc | |||
| bd853c6265 | |||
| 5a1efd0e27 | |||
| 71b8cc7172 | |||
| 126a36dbf3 | |||
| 90db64cc03 | |||
| 227a067c77 | |||
| dc24df0d4c | |||
| 2598962c5b | |||
| b4884e94f6 | |||
| 4741e64178 | |||
| 25fe35e212 | |||
| 92909cea09 | |||
| b16322dd5b | |||
| a04c04141c | |||
| 90f21d3ed9 | |||
| f31225e8c2 | |||
| eead1b2bc4 | |||
| f90a4e5576 | |||
| 50987ce38b | |||
| 26113ac3a6 | |||
| 0b7b72ce16 | |||
| 95d48b2808 | |||
| 8fecf94a28 | |||
| 66dd87b290 | |||
| d20e8cc37d | |||
| c189e2ed20 | |||
| 57f8f45320 | |||
| 5684baf1e0 | |||
| b6f307690d | |||
| 66c44f8ff5 | |||
| 8f3592c481 | |||
| dc8b1e0741 | |||
| b806a6afc5 | |||
| d43f8f4dd8 | |||
| 37c9bf5741 | |||
| a0d37f516f | |||
| 2764a18252 | |||
| 96cf228e74 | |||
| ff6ff3a51e | |||
| 9472a6b5b0 | |||
| 5f3a8bf391 | |||
| a0a0fdd1b5 | |||
| 7a6f901b09 | |||
| c4f05fbd9e | |||
| 50c342ec5c | |||
| a9cfd2bcfc | |||
| e2bdb2acee | |||
| b8828b503c | |||
| d63f62109b | |||
| c04483f0ea | |||
| 1efaf30fb7 | |||
| 2e462e9633 | |||
| 838df5f41d | |||
| 32445f0e19 | |||
| 5f9067baee | |||
| ad56d5ee14 | |||
| a091d121d9 | |||
| cd9c3ee3c1 | |||
| 534c3a2e10 | |||
| 7bc8e194df | |||
| de5c4d5bcd | |||
| 1e6b5c684f | |||
| 4651f6b652 | |||
| e5b0a2355f | |||
| db22fe7518 | |||
| 0dcd97f49b | |||
| de14330449 | |||
| 173658138c | |||
| 000f3c72fc | |||
| 561b16e9fb | |||
| 507ca49112 | |||
| 94d5ab4ace | |||
| 91d44a3e31 | |||
| 478167ea4f | |||
| 6b1c3bba19 | |||
| 73841f751c | |||
| 22dbe3291f | |||
| 1dac3fe434 | |||
| d0c49ac04c | |||
| 3877dbf83f | |||
| d15fd1fb84 | |||
| 0a105490fe | |||
| 0bd35b526c | |||
| 3a2910f717 | |||
| c4a91c2b34 | |||
| 5643380916 | |||
| cf6b378ccb | |||
| 66bc8cfb5f | |||
| 994bd1fe5c | |||
| 12528b6527 | |||
| d135c93939 | |||
| 637011d55a | |||
| b94e6bf54a | |||
| 8f975c2898 | |||
| d25c4fa766 | |||
| 43db15b350 | |||
| 09074e2ac0 | |||
| 04295f4540 | |||
| 11c48a495f | |||
| 942c961cd1 | |||
| 9193511d5f | |||
| e0ddf97771 | |||
| 2bd249bd5a | |||
| b72c99dbce | |||
| fc0504f91e | |||
| 1da2196b0e | |||
| 4bdb249a2b | |||
| cb38549480 | |||
| ee9ee6ee32 | |||
| 748eaaa98d | |||
| 9e4ec11e33 | |||
| 93d2b76eae | |||
| 2c8942e888 | |||
| 4424840c81 | |||
| 68392b4e74 | |||
| 0b4e345ad6 | |||
| 1cea5f5a99 | |||
| cdaadbab20 | |||
| d1ec8c6837 | |||
| cdffa66db1 | |||
| fca21ab476 | |||
| b1d2d92f54 | |||
| 10bb9112c7 | |||
| 58b337a3ee | |||
| de52f2f5e5 | |||
| f10e941529 | |||
| 78b1864d7d | |||
| ab49af9bcb | |||
| 286b8d7067 | |||
| 21c087cba7 | |||
| ddd881a52f | |||
| 12c69578af | |||
| e96cb5eb8a | |||
| c01cfe83be | |||
| b92d1ee38c | |||
| b90c67b184 | |||
| edb29977dd | |||
| e55c8f251d | |||
| f1d6bbdc67 | |||
| 8da4c2fa10 | |||
| e872fa2497 | |||
| 165bfef9b9 | |||
| 8a50c755d2 | |||
| 002bc315f6 | |||
| e87522eeab | |||
| 377244f948 | |||
| 73423a6f44 | |||
| d219e3cd7e | |||
| 09b0022195 | |||
| 8aac935641 | |||
| 16e5c13633 | |||
| fff1831768 | |||
| f91a318e94 | |||
| ed06e17b23 | |||
| 63e1be7dfe | |||
| 16044612bd | |||
| 27d8880414 | |||
| a0aae6a9de | |||
| 749043dfbb | |||
| 320fe02e35 | |||
| 6f4b9d4a4e | |||
| a589cf9471 | |||
| 7fc7283191 | |||
| af03560494 | |||
| 988d1b93a5 | |||
| ab0278198a | |||
| fbc649ab59 | |||
| 6b5adc2504 | |||
| 7c4307ef10 | |||
| de744e2f6c | |||
| c171597c7f | |||
| 3034c00bb3 | |||
| 9539c476c0 | |||
| 4552c72860 | |||
| 85f8a92721 | |||
| 049b9c78c2 | |||
| acf06084bf | |||
| 61da477733 | |||
| fdeec548a3 | |||
| ff0d486831 | |||
| d5bf54ee26 | |||
| 7616774613 | |||
| 27c4dc7eca | |||
| 4ae834d6f7 | |||
| b71a66a082 | |||
| 80cc38542c | |||
| 185aecec42 | |||
| 8c092eba78 | |||
| 781ccc701d | |||
| 009e05584a | |||
| f9d52e56a2 | |||
| 9e66f8412c | |||
| 181e42c4b3 | |||
| 15e60fc5e7 | |||
| ee357e58ac | |||
| 0d9a709fa3 | |||
| 7eae09bdb0 | |||
| e8e32e5d1f | |||
| a3d54db4c6 | |||
| 9686ae6250 | |||
| 044195f07b | |||
| 35f159266a | |||
| 3b717ae3de | |||
| 2389c3fc40 | |||
| 92da790e77 | |||
| 14418b93b1 | |||
| 1289aff493 | |||
| 7568f563cb | |||
| 5ae56bd50c | |||
| c2ded81959 | |||
| f2c307ef63 | |||
| af787887d8 | |||
| 54272d83eb | |||
| 3070fc2074 | |||
| 6a31d2c722 | |||
| f26bfe9acc | |||
| a729fd6234 | |||
| c2051c156e | |||
| 3852870c09 | |||
| a58253a6a1 | |||
| 30c98a970d | |||
| 8f2edef09e | |||
| 9e465ea47e | |||
| cdda55d528 | |||
| db302858c4 | |||
| b81cc83027 | |||
| 766d653349 | |||
| 98c04eac15 | |||
| c35dfea154 | |||
| 37a9a86a1d | |||
| e9a759055b | |||
| f6c061a57b | |||
| 91f3d8ed7e | |||
| 3b18502d3d | |||
| df8e79683a | |||
| 7080e8ea33 | |||
| c56f8cb38f | |||
| 1ceac968b0 | |||
| 938ee99cfc | |||
| 2367040e45 | |||
| df19aabfba | |||
| f7b5bcd52f | |||
| a249b0a678 | |||
| 99bc46b40f | |||
| c1e6f0970d | |||
| e8c2316bb2 | |||
| 9866e5468c | |||
| fc60df7974 | |||
| 08784d28c7 | |||
| 0440ea652f | |||
| e466562391 | |||
| b2c8667c53 | |||
| d6893079f7 | |||
| ac1d973605 | |||
| 1405fefc85 | |||
| 2a75055fb6 | |||
| f84fdd51de | |||
| b38073126e | |||
| c6e681195d | |||
| 53d924be0b | |||
| d6cf027d5d | |||
| ec332bab88 | |||
| 8b3b90a7e2 | |||
| 9047e3bed3 | |||
| 72b7391245 | |||
| 2df05f0327 | |||
| 95f3eb0e5d | |||
| ce8d964ce6 | |||
| f0d5fc4e2d | |||
| b4b17d847e | |||
| fd537e7ac0 | |||
| 8c271104e1 | |||
| 51edd60d96 | |||
| a265ca1716 | |||
| a773f63a65 | |||
| 576c8a2e24 | |||
| 5829de9ee4 | |||
| b202941b79 | |||
| 6f6fb74988 | |||
| 0137bdbe4c | |||
| 55d9511c01 | |||
| 929d29576d | |||
| 6f7128a51c | |||
| fc5c4ff2fc | |||
| a81595db2b | |||
| a5615d500d | |||
| da9266befc | |||
| 5b3cffeb73 | |||
| 8f7b8a5fdf | |||
| 6d93116993 | |||
| a0f4916d3b | |||
| ace897eaf9 | |||
| b8667c5408 | |||
| d87d66632e | |||
| 3015c2d156 | |||
| 52b9e9ee8e | |||
| 5bfad0271c | |||
| 58797891c7 | |||
| 897750ba1e | |||
| 99341e773f | |||
| 71319c1548 | |||
| c0a42f6bfc | |||
| d5abb46112 | |||
| 2508b5fc8a | |||
| 4bd1ef0da3 | |||
| 4efd748571 | |||
| 859892d1fd | |||
| 7adeaf93cd | |||
| 5dbe155ecb | |||
| 667d065bee | |||
| bb613059de | |||
| 1b2dec4c2a | |||
| 2c2ef97c30 | |||
| 6df1d68237 | |||
| 20c9faedff | |||
| d931a40ea9 | |||
| 1097b98b85 | |||
| 2e597fcf43 | |||
| bb2fa6a261 | |||
| 1dc8369487 | |||
| e6d60f1985 | |||
| 82e3cb8a4d | |||
| aeec0cbf8e | |||
| 7a2d1fc6c5 | |||
| 68033ab7d2 | |||
| 7595c60ea8 | |||
| d9318c04f8 | |||
| 2d76198290 | |||
| 02a7b6ea55 | |||
| ab57076b8f | |||
| 82fba47c98 | |||
| 5f32e2a5af | |||
| 70012fbd7c | |||
| 668f6ca975 | |||
| e78dd301ef | |||
| f8ec3daf10 | |||
| 75c929639d | |||
| e9c1242b8d | |||
| e7eb1d4f12 | |||
| abbd3e809a | |||
| f430b4f5a6 | |||
| e65234b4c8 | |||
| fe04d22c94 | |||
| 44a7a5ebdc | |||
| 119ef0d0c7 | |||
| cdb8eb7931 | |||
| aef176c0cc | |||
| 0c21b55865 | |||
| 964b1444a5 | |||
| 48de343948 | |||
| 2640934c15 | |||
| 442dd4d6db | |||
| 3168feb3d6 | |||
| fbae4933f6 | |||
| 78bb66c344 | |||
| 5af65ef74f | |||
| 4d354ae9ff | |||
| 1e57c1ace7 | |||
| fefd138dfd | |||
| 16a77d3536 | |||
| e03857bdb7 | |||
| fc679bcd72 | |||
| 652b0da021 | |||
| 2d54b0db2f | |||
| 7a29894926 | |||
| f9ae79dd5b | |||
| cadb78ca35 | |||
| 36a6b7ff1d | |||
| efee46f113 | |||
| b6639738c3 | |||
| aab07c5f49 | |||
| 33c0784bc6 | |||
| 6f5ebfc8a6 | |||
| 811091ff14 | |||
| 6666f39928 | |||
| f67db9fe5f | |||
| 45f002803e | |||
| 8c19f90e88 | |||
| a8f5d5cea7 | |||
| 6788071eec | |||
| 0c690b3033 | |||
| dcb963b623 | |||
| 04e30dbdd6 | |||
| ac6d5a7aed | |||
| 3166a31b67 | |||
| fc3283eb3a | |||
| d6a96d4aa3 | |||
| cda349d71c | |||
| 87345a3b3d | |||
| b791d0699c | |||
| 2cb15f3282 | |||
| 8b823ca0d0 | |||
| f3f08797f7 | |||
| cab9672d25 | |||
| c8d3b1bace | |||
| 5ee51b2b57 | |||
| 525dc503a6 | |||
| 12cf28792b | |||
| 33466c1d07 | |||
| 226ae2172e | |||
| 8e2700b8b8 | |||
| 753cbd4d27 | |||
| f16405ca70 | |||
| 23f052c400 | |||
| 90a0a03e15 | |||
| 2dba8ae366 | |||
| 5bb7ad571c | |||
| 0cdb7c2c4f | |||
| 11bdc522cd | |||
| 014835f90d | |||
| 05e66b60ee | |||
| 505b185cf2 | |||
| 50c32ac6ff | |||
| 878ab25fd4 | |||
| 1b31772ff6 | |||
| d4940234b7 | |||
| 9bdc5d4863 | |||
| a73fd7929c | |||
| 8269a61751 | |||
| de072d1437 | |||
| 5b24793f2b | |||
| 660f7ab8f3 | |||
| 6e3fea5fa0 | |||
| 29ecbecbc2 | |||
| ad3bbe9892 | |||
| b570ebee69 | |||
| 207780fa3f | |||
| 8aa219433b | |||
| cb1e9f2be4 | |||
| 202943f58d | |||
| 6dd15b2091 | |||
| 1c55e4f18b | |||
| 6cc36b1728 | |||
| 37751cd5c1 | |||
| 80d882cd20 | |||
| 8bef75e570 | |||
| 077eab7005 | |||
| 715c70d422 | |||
| a3c23f9d53 | |||
| c1e67a8af0 | |||
| 2fdc294293 | |||
| 1f49ba069e | |||
| 43ef0fc0d0 | |||
| a1a920e536 | |||
| dec33b4331 | |||
| 432f290633 | |||
| cf98616a27 | |||
| 1a720cf1ad | |||
| 557c6c34f2 | |||
| 943c76505d | |||
| 66eb83e6a7 | |||
| 0844427357 | |||
| 72e9d9ea2d | |||
| 3db7d47746 | |||
| 95899336b8 | |||
| f6691c110e | |||
| 749722a9df | |||
| 265b668cb4 | |||
| 0527e6a5c1 | |||
| d989b61d19 | |||
| c1f3e6ff13 | |||
| 3c0fff2d82 | |||
| bd599c264f | |||
| 0274093c89 | |||
| 5845b5bdfd | |||
| 3dd0ba6b0b | |||
| e79306f8a3 | |||
| 0171b0c2dd | |||
| a963ef53d3 | |||
| 344f302c76 | |||
| 17ded282b2 | |||
| 2ce57df485 | |||
| 849289287e | |||
| 704fbc2393 | |||
| 168fc8bd03 | |||
| c5598d9efe | |||
| f02315726d | |||
| e56dcb59c0 | |||
| c74f350ea2 | |||
| af9d015cf2 | |||
| 633e5ac5e4 | |||
| 2d0f323854 | |||
| cfe5610ec9 | |||
| 55616ac321 | |||
| 8f2d1deb38 | |||
| e2763ceb80 | |||
| 10462f3140 | |||
| 3aef92486b | |||
| c1a9ee0bbb | |||
| 398fae82cb | |||
| a16443f961 | |||
| ad4e0e1d27 | |||
| 493a2108e9 | |||
| c2c9bb64b1 | |||
| bb1426c0af | |||
| 2505768e4d | |||
| 23a1b70534 | |||
| b34616ecbc | |||
| 0111252f6f | |||
| a561cc5f73 | |||
| 519e6f1f79 | |||
| da871b5d3a | |||
| ae3f939d23 | |||
| fb6ede564e | |||
| 1c701d4fe8 | |||
| c743cc689d | |||
| 8432bec3d6 | |||
| 8e72b0b0e4 | |||
| 19e4bbc01e | |||
| 56a9e412c8 | |||
| 4b75623b86 | |||
| e1ea42a997 | |||
| ee37710e57 | |||
| ae99218789 | |||
| 9fe8214ca0 | |||
| 6ca44a5cc3 | |||
| 801abd197b | |||
| 970977e469 | |||
| f83674b934 | |||
| 57c7dc1b5e | |||
| e9277f4eeb | |||
| 4052b6fe13 | |||
| 608347b1e6 | |||
| 345fe674d4 | |||
| 92f8901a67 | |||
| fcd40308ee | |||
| 2bd023e5d0 | |||
| a697d6330e | |||
| ab56e8be20 | |||
| b9a906b569 | |||
| 02adbae758 | |||
| 866aab7fac | |||
| c81b4d855b | |||
| 4ad58fc285 | |||
| 8227dd4f6a | |||
| beb677734f | |||
| 03db636d51 | |||
| b83abc9499 | |||
| 9738cdcf8f | |||
| 179ae01551 | |||
| 295860a9de | |||
| 0a6c15b6b8 | |||
| 18f80020ee | |||
| 3768fb94bf | |||
| 7b2cadcb91 | |||
| 4eb4e8c1f8 | |||
| 984df15949 | |||
| aa3bd24332 | |||
| 250889623d | |||
| 862fa55986 | |||
| 0f2889904a | |||
| 49f14c7929 | |||
| c3f3c5d5df | |||
| 274259f305 | |||
| a15d7a9cf3 | |||
| 7d154fc28d | |||
| 6727d3be83 | |||
| 4bbd5c3bcb | |||
| 5995f2e427 | |||
| 0910c8e100 | |||
| 8df76dfa90 | |||
| 78e9e66cb0 | |||
| 71c61fc4b3 | |||
| a728aa8689 | |||
| 5a3174a720 | |||
| 652072d660 | |||
| 323f5c0c15 | |||
| 229d78c78b | |||
| 9d9a955149 | |||
| b40a047190 | |||
| ff9e415823 | |||
| 2f7a4fb2b3 | |||
| f8a654a5b6 | |||
| 4449475467 | |||
| bcbbf0dd42 | |||
| 19b9ba52d4 | |||
| bdc30db2dc | |||
| 74634ba96b | |||
| 77feb05bad | |||
| a1f05d006e | |||
| cc3dae1335 | |||
| d9debcfb0d | |||
| 8770d16d52 | |||
| 84953481e4 | |||
| df7aa59f6f | |||
| 008b83119d | |||
| 15dafcb6ff | |||
| 7d291ed888 | |||
| a7221f0750 | |||
| 9a5b2a5d30 | |||
| 7361a00d42 | |||
| e07d402782 | |||
| a34354bee1 | |||
| 84b762b529 | |||
| 2a839951b4 | |||
| 26d7c2f047 | |||
| 151a5b85f3 | |||
| 3f506f53b4 |
@@ -9,7 +9,7 @@
|
||||
"rules": {
|
||||
"vue/no-deprecated-html-element-is": 0,
|
||||
"vue/no-mutating-props": 0,
|
||||
"vue/no-v-html": 0
|
||||
"vue/one-component-per-file": 0
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2022,
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.2.2
|
||||
ruby-version: 3.3.4
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.2.2
|
||||
ruby-version: 3.3.4
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16.13.1
|
||||
node-version: 20.9.0
|
||||
- name: Cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
@@ -100,13 +100,15 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.2.2
|
||||
ruby-version: 3.3.4
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16.13.1
|
||||
node-version: 20.9.0
|
||||
- name: Install Chrome
|
||||
uses: browser-actions/setup-chrome@latest
|
||||
with:
|
||||
chrome-version: 125
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
|
||||
@@ -36,3 +36,4 @@ yarn-debug.log*
|
||||
/attachments
|
||||
/docuseal
|
||||
/ee
|
||||
dump.rdb
|
||||
|
||||
+10
-3
@@ -10,7 +10,8 @@ AllCops:
|
||||
- node_modules/**/*
|
||||
- bin/*
|
||||
- vendor/**/*
|
||||
TargetRubyVersion: '3.2'
|
||||
TargetRubyVersion: '3.3'
|
||||
SuggestExtensions: false
|
||||
|
||||
Metrics/BlockLength:
|
||||
Exclude:
|
||||
@@ -33,6 +34,7 @@ Metrics/MethodLength:
|
||||
Max: 30
|
||||
Exclude:
|
||||
- 'db/migrate/**'
|
||||
- 'spec/**/*'
|
||||
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 15
|
||||
@@ -44,7 +46,9 @@ Layout/LineLength:
|
||||
AllowedPatterns: ['\A\s*#']
|
||||
|
||||
Metrics/AbcSize:
|
||||
Max: 40
|
||||
Max: 45
|
||||
Exclude:
|
||||
- spec/**/*
|
||||
|
||||
Metrics/ModuleLength:
|
||||
Max: 500
|
||||
@@ -64,6 +68,9 @@ RSpec/ExampleLength:
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 6
|
||||
|
||||
Metrics/BlockNesting:
|
||||
Max: 4
|
||||
|
||||
Rails/I18nLocaleTexts:
|
||||
Enabled: false
|
||||
|
||||
@@ -76,5 +83,5 @@ Rails/SkipsModelValidations:
|
||||
Rails/ApplicationController:
|
||||
Enabled: false
|
||||
|
||||
Capybara/ClickLinkOrButtonStyle:
|
||||
Rails/Output:
|
||||
Enabled: false
|
||||
|
||||
+28
-13
@@ -1,12 +1,12 @@
|
||||
FROM ruby:3.2.2-alpine3.18 as fonts
|
||||
FROM ruby:3.3.4-alpine as fonts
|
||||
|
||||
WORKDIR /fonts
|
||||
|
||||
RUN apk --no-cache add fontforge wget ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf . && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansArabic/hinted/ttf/NotoSansArabic-Regular.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
||||
RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
||||
|
||||
RUN fontforge -lang=py -c 'font1 = fontforge.open("LiberationSans-Regular.ttf"); font2 = fontforge.open("NotoSansArabic-Regular.ttf"); font1.mergeFonts(font2); font1.generate("LiberationSans-Regular.ttf")'
|
||||
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
||||
|
||||
FROM ruby:3.2.2-alpine3.18 as webpack
|
||||
FROM ruby:3.3.4-alpine as webpack
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
ENV NODE_ENV=production
|
||||
@@ -23,27 +23,41 @@ RUN yarn install --network-timeout 1000000
|
||||
COPY ./bin/shakapacker ./bin/shakapacker
|
||||
COPY ./config/webpack ./config/webpack
|
||||
COPY ./config/shakapacker.yml ./config/shakapacker.yml
|
||||
COPY ./postcss.config.js ./postcss.config.js ./
|
||||
COPY ./tailwind.config.js ./tailwind.config.js ./
|
||||
COPY ./tailwind.form.config.js ./tailwind.form.config.js ./
|
||||
COPY ./tailwind.application.config.js ./tailwind.application.config.js ./
|
||||
COPY ./postcss.config.js ./postcss.config.js
|
||||
COPY ./tailwind.config.js ./tailwind.config.js
|
||||
COPY ./tailwind.form.config.js ./tailwind.form.config.js
|
||||
COPY ./tailwind.application.config.js ./tailwind.application.config.js
|
||||
COPY ./app/javascript ./app/javascript
|
||||
COPY ./app/views ./app/views
|
||||
|
||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||
|
||||
FROM ruby:3.2.2-alpine3.18 as app
|
||||
FROM ruby:3.3.4-alpine as app
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
ENV BUNDLE_WITHOUT="development:test"
|
||||
ENV LD_PRELOAD=/lib/libgcompat.so.0
|
||||
ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont && mkdir /fonts
|
||||
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||
|
||||
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||
\n\
|
||||
[provider_sect]\n\
|
||||
default = default_sect\n\
|
||||
legacy = legacy_sect\n\
|
||||
\n\
|
||||
[default_sect]\n\
|
||||
activate = 1\n\
|
||||
\n\
|
||||
[legacy_sect]\n\
|
||||
activate = 1' >> /app/openssl_legacy.cnf
|
||||
|
||||
COPY ./Gemfile ./Gemfile.lock ./
|
||||
|
||||
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
|
||||
RUN apk add --no-cache build-base && bundle install && apk del build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
|
||||
|
||||
COPY ./bin ./bin
|
||||
COPY ./app ./app
|
||||
@@ -55,7 +69,8 @@ COPY ./public ./public
|
||||
COPY ./tmp ./tmp
|
||||
COPY LICENSE README.md Rakefile config.ru .version ./
|
||||
|
||||
COPY --from=fonts /fonts/LiberationSans-Regular.ttf /fonts/LiberationSans-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||
COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||
COPY --from=webpack /app/public/packs ./public/packs
|
||||
|
||||
RUN ln -s /fonts /app/public/fonts
|
||||
@@ -65,4 +80,4 @@ WORKDIR /data/docuseal
|
||||
ENV WORKDIR=/data/docuseal
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["/app/bin/rails", "server"]
|
||||
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '3.2.2'
|
||||
ruby '3.3.4'
|
||||
|
||||
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
|
||||
gem 'aws-sdk-s3', require: false
|
||||
gem 'aws-sdk-secretsmanager', require: false
|
||||
gem 'azure-storage-blob', require: false
|
||||
gem 'bootsnap', require: false
|
||||
gem 'cancancan'
|
||||
gem 'csv'
|
||||
gem 'devise'
|
||||
gem 'devise-two-factor'
|
||||
gem 'dotenv', require: false
|
||||
@@ -21,25 +23,24 @@ gem 'image_processing'
|
||||
gem 'jwt'
|
||||
gem 'lograge'
|
||||
gem 'mysql2', require: false
|
||||
gem 'net-smtp', '0.4.0'
|
||||
gem 'oj'
|
||||
gem 'pagy'
|
||||
gem 'pdf-reader'
|
||||
gem 'pg', require: false
|
||||
gem 'premailer-rails'
|
||||
gem 'pretender'
|
||||
gem 'puma'
|
||||
gem 'puma', require: false
|
||||
gem 'rack'
|
||||
gem 'rails'
|
||||
gem 'rails_autolink'
|
||||
gem 'rails-i18n'
|
||||
gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN')
|
||||
gem 'rotp'
|
||||
gem 'rqrcode'
|
||||
gem 'ruby-vips'
|
||||
gem 'rubyXL'
|
||||
gem 'shakapacker'
|
||||
gem 'sidekiq', require: ENV.key?('REDIS_URL')
|
||||
gem 'sqlite3', require: false
|
||||
gem 'sidekiq'
|
||||
gem 'sqlite3', require: false, force_ruby_platform: true
|
||||
gem 'strip_attributes'
|
||||
gem 'turbo-rails'
|
||||
gem 'twitter_cldr', require: false
|
||||
|
||||
+193
-195
@@ -1,36 +1,35 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
Ascii85 (1.1.0)
|
||||
actioncable (7.1.2)
|
||||
actionpack (= 7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
actioncable (7.1.3.4)
|
||||
actionpack (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (7.1.2)
|
||||
actionpack (= 7.1.2)
|
||||
activejob (= 7.1.2)
|
||||
activerecord (= 7.1.2)
|
||||
activestorage (= 7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
actionmailbox (7.1.3.4)
|
||||
actionpack (= 7.1.3.4)
|
||||
activejob (= 7.1.3.4)
|
||||
activerecord (= 7.1.3.4)
|
||||
activestorage (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.1.2)
|
||||
actionpack (= 7.1.2)
|
||||
actionview (= 7.1.2)
|
||||
activejob (= 7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
actionmailer (7.1.3.4)
|
||||
actionpack (= 7.1.3.4)
|
||||
actionview (= 7.1.3.4)
|
||||
activejob (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (7.1.2)
|
||||
actionview (= 7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
actionpack (7.1.3.4)
|
||||
actionview (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4)
|
||||
@@ -38,35 +37,35 @@ GEM
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
actiontext (7.1.2)
|
||||
actionpack (= 7.1.2)
|
||||
activerecord (= 7.1.2)
|
||||
activestorage (= 7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
actiontext (7.1.3.4)
|
||||
actionpack (= 7.1.3.4)
|
||||
activerecord (= 7.1.3.4)
|
||||
activestorage (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
actionview (7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
activejob (7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
activerecord (7.1.2)
|
||||
activemodel (= 7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
activemodel (7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
activerecord (7.1.3.4)
|
||||
activemodel (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (7.1.2)
|
||||
actionpack (= 7.1.2)
|
||||
activejob (= 7.1.2)
|
||||
activerecord (= 7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
activestorage (7.1.3.4)
|
||||
actionpack (= 7.1.3.4)
|
||||
activejob (= 7.1.3.4)
|
||||
activerecord (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
marcel (~> 1.0)
|
||||
activesupport (7.1.2)
|
||||
activesupport (7.1.3.4)
|
||||
base64
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
@@ -78,26 +77,28 @@ GEM
|
||||
tzinfo (~> 2.0)
|
||||
addressable (2.8.6)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
afm (0.2.2)
|
||||
annotate (3.2.0)
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
arabic-letter-connector (0.1.1)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.874.0)
|
||||
aws-sdk-core (3.190.1)
|
||||
aws-partitions (1.913.0)
|
||||
aws-sdk-core (3.191.6)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.75.0)
|
||||
aws-sdk-core (~> 3, >= 3.188.0)
|
||||
aws-sdk-kms (1.79.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.142.0)
|
||||
aws-sdk-core (~> 3, >= 3.189.0)
|
||||
aws-sdk-s3 (1.146.1)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.8)
|
||||
aws-sdk-secretsmanager (1.91.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.8.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
azure-storage-blob (2.0.3)
|
||||
@@ -110,52 +111,55 @@ GEM
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
base64 (0.2.0)
|
||||
bcrypt (3.1.20)
|
||||
better_html (2.0.2)
|
||||
better_html (2.1.1)
|
||||
actionview (>= 6.0)
|
||||
activesupport (>= 6.0)
|
||||
ast (~> 2.0)
|
||||
erubi (~> 1.4)
|
||||
parser (>= 2.4)
|
||||
smart_properties
|
||||
bigdecimal (3.1.5)
|
||||
bigdecimal (3.1.8)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.17.0)
|
||||
bootsnap (1.18.3)
|
||||
msgpack (~> 1.2)
|
||||
builder (3.2.4)
|
||||
bullet (7.1.4)
|
||||
builder (3.3.0)
|
||||
bullet (7.1.6)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
camertron-eprun (1.1.1)
|
||||
cancancan (3.5.0)
|
||||
capybara (3.39.2)
|
||||
capybara (3.40.0)
|
||||
addressable
|
||||
matrix
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (~> 1.8)
|
||||
nokogiri (~> 1.11)
|
||||
rack (>= 1.6.0)
|
||||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
childprocess (5.0.0)
|
||||
chunky_png (1.4.0)
|
||||
cldr-plurals-runtime-rb (1.1.0)
|
||||
cmdparse (3.0.7)
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.2.2)
|
||||
concurrent-ruby (1.3.4)
|
||||
connection_pool (2.4.1)
|
||||
crack (0.4.5)
|
||||
crack (1.0.0)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
css_parser (1.16.0)
|
||||
css_parser (1.17.1)
|
||||
addressable
|
||||
csv (3.3.0)
|
||||
cuprite (0.15)
|
||||
capybara (~> 3.0)
|
||||
ferrum (~> 0.14.0)
|
||||
date (3.3.4)
|
||||
debug (1.9.1)
|
||||
debug (1.9.2)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
declarative (0.0.20)
|
||||
devise (4.9.3)
|
||||
devise (4.9.4)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
@@ -166,13 +170,12 @@ GEM
|
||||
devise (~> 4.0)
|
||||
railties (~> 7.0)
|
||||
rotp (~> 6.0)
|
||||
diff-lcs (1.5.0)
|
||||
diff-lcs (1.5.1)
|
||||
digest-crc (0.6.5)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
docile (1.4.0)
|
||||
dotenv (2.8.1)
|
||||
drb (2.2.0)
|
||||
ruby2_keywords
|
||||
dotenv (3.1.0)
|
||||
drb (2.2.1)
|
||||
email_typo (0.2.3)
|
||||
erb_lint (0.5.0)
|
||||
activesupport
|
||||
@@ -181,13 +184,13 @@ GEM
|
||||
rainbow
|
||||
rubocop
|
||||
smart_properties
|
||||
erubi (1.12.0)
|
||||
factory_bot (6.4.4)
|
||||
erubi (1.13.0)
|
||||
factory_bot (6.4.6)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot_rails (6.4.2)
|
||||
factory_bot_rails (6.4.3)
|
||||
factory_bot (~> 6.4)
|
||||
railties (>= 5.0.0)
|
||||
faker (3.2.2)
|
||||
faker (3.3.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (1.10.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
@@ -225,34 +228,34 @@ GEM
|
||||
geom2d (0.4.1)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
google-apis-core (0.11.2)
|
||||
google-apis-core (0.14.1)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
googleauth (~> 1.9)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.17.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.29.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-cloud-core (1.6.1)
|
||||
google-apis-iamcredentials_v1 (0.20.0)
|
||||
google-apis-core (>= 0.14.0, < 2.a)
|
||||
google-apis-storage_v1 (0.37.0)
|
||||
google-apis-core (>= 0.14.0, < 2.a)
|
||||
google-cloud-core (1.7.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (2.1.0)
|
||||
google-cloud-env (2.1.1)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-errors (1.3.1)
|
||||
google-cloud-storage (1.45.0)
|
||||
google-cloud-errors (1.4.0)
|
||||
google-cloud-storage (1.49.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.29.0)
|
||||
google-apis-core (~> 0.13)
|
||||
google-apis-iamcredentials_v1 (~> 0.18)
|
||||
google-apis-storage_v1 (~> 0.33)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
googleauth (~> 1.9)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.9.1)
|
||||
googleauth (1.11.0)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-env (~> 2.1)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
@@ -260,30 +263,31 @@ GEM
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
hashdiff (1.1.0)
|
||||
hashery (2.1.2)
|
||||
hexapdf (0.34.1)
|
||||
hexapdf (0.42.0)
|
||||
cmdparse (~> 3.0, >= 3.0.3)
|
||||
geom2d (~> 0.4, >= 0.4.1)
|
||||
openssl (>= 2.2.1)
|
||||
htmlentities (4.3.4)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.14.1)
|
||||
i18n (1.14.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.12.2)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
io-console (0.7.1)
|
||||
irb (1.11.0)
|
||||
rdoc
|
||||
reline (>= 0.3.8)
|
||||
io-console (0.7.2)
|
||||
irb (1.13.1)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.7.1)
|
||||
jwt (2.7.1)
|
||||
json (2.7.2)
|
||||
jwt (2.8.1)
|
||||
base64
|
||||
language_server-protocol (3.17.0.3)
|
||||
launchy (2.5.2)
|
||||
launchy (3.0.0)
|
||||
addressable (~> 2.8)
|
||||
letter_opener (1.8.1)
|
||||
launchy (>= 2.2, < 3)
|
||||
childprocess (~> 5.0)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
letter_opener_web (2.0.0)
|
||||
actionmailer (>= 5.2)
|
||||
letter_opener (~> 1.7)
|
||||
@@ -302,21 +306,21 @@ GEM
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.0.2)
|
||||
marcel (1.0.4)
|
||||
matrix (0.4.2)
|
||||
method_source (1.0.0)
|
||||
method_source (1.1.0)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.5)
|
||||
minitest (5.20.0)
|
||||
mini_portile2 (2.8.7)
|
||||
minitest (5.24.1)
|
||||
msgpack (1.7.2)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.3.0)
|
||||
multipart-post (2.4.0)
|
||||
mutex_m (0.2.0)
|
||||
mysql2 (0.5.5)
|
||||
mysql2 (0.5.6)
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.4.9)
|
||||
net-imap (0.4.14)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -325,28 +329,27 @@ GEM
|
||||
timeout
|
||||
net-smtp (0.4.0)
|
||||
net-protocol
|
||||
nio4r (2.7.0)
|
||||
nokogiri (1.15.5)
|
||||
nio4r (2.7.1)
|
||||
nokogiri (1.16.7)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-aarch64-linux)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oj (3.16.3)
|
||||
bigdecimal (>= 3.0)
|
||||
openssl (3.2.0)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.4)
|
||||
pagy (6.2.0)
|
||||
parallel (1.24.0)
|
||||
parser (3.2.2.4)
|
||||
package_json (0.1.0)
|
||||
pagy (8.1.2)
|
||||
parallel (1.25.1)
|
||||
parser (3.3.4.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pdf-reader (2.12.0)
|
||||
Ascii85 (~> 1.0)
|
||||
afm (~> 0.2.1)
|
||||
hashery (~> 2.0)
|
||||
ruby-rc4
|
||||
ttfunk
|
||||
pg (1.5.4)
|
||||
premailer (1.21.0)
|
||||
pg (1.5.6)
|
||||
premailer (1.23.0)
|
||||
addressable
|
||||
css_parser (>= 1.12.0)
|
||||
htmlentities (>= 4.0.0)
|
||||
@@ -363,34 +366,34 @@ GEM
|
||||
pry (>= 0.10.4)
|
||||
psych (5.1.2)
|
||||
stringio
|
||||
public_suffix (5.0.4)
|
||||
puma (6.4.0)
|
||||
public_suffix (5.0.5)
|
||||
puma (6.4.2)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.7.3)
|
||||
rack (2.2.8)
|
||||
racc (1.8.1)
|
||||
rack (3.1.7)
|
||||
rack-proxy (0.7.7)
|
||||
rack
|
||||
rack-session (1.0.2)
|
||||
rack (< 3)
|
||||
rack-session (2.0.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rackup (1.0.0)
|
||||
rack (< 3)
|
||||
webrick
|
||||
rails (7.1.2)
|
||||
actioncable (= 7.1.2)
|
||||
actionmailbox (= 7.1.2)
|
||||
actionmailer (= 7.1.2)
|
||||
actionpack (= 7.1.2)
|
||||
actiontext (= 7.1.2)
|
||||
actionview (= 7.1.2)
|
||||
activejob (= 7.1.2)
|
||||
activemodel (= 7.1.2)
|
||||
activerecord (= 7.1.2)
|
||||
activestorage (= 7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
rackup (2.1.0)
|
||||
rack (>= 3)
|
||||
webrick (~> 1.8)
|
||||
rails (7.1.3.4)
|
||||
actioncable (= 7.1.3.4)
|
||||
actionmailbox (= 7.1.3.4)
|
||||
actionmailer (= 7.1.3.4)
|
||||
actionpack (= 7.1.3.4)
|
||||
actiontext (= 7.1.3.4)
|
||||
actionview (= 7.1.3.4)
|
||||
activejob (= 7.1.3.4)
|
||||
activemodel (= 7.1.3.4)
|
||||
activerecord (= 7.1.3.4)
|
||||
activestorage (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.1.2)
|
||||
railties (= 7.1.3.4)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
@@ -398,114 +401,108 @@ GEM
|
||||
rails-html-sanitizer (1.6.0)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
rails-i18n (7.0.8)
|
||||
rails-i18n (7.0.9)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 8)
|
||||
rails_autolink (1.1.8)
|
||||
actionview (> 3.1)
|
||||
activesupport (> 3.1)
|
||||
railties (> 3.1)
|
||||
railties (7.1.2)
|
||||
actionpack (= 7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
railties (7.1.3.4)
|
||||
actionpack (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
irb
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.1.0)
|
||||
rdoc (6.6.2)
|
||||
rake (13.2.1)
|
||||
rdoc (6.6.3.1)
|
||||
psych (>= 4.0.0)
|
||||
redis-client (0.19.1)
|
||||
redis-client (0.22.2)
|
||||
connection_pool
|
||||
regexp_parser (2.8.3)
|
||||
reline (0.4.1)
|
||||
regexp_parser (2.9.2)
|
||||
reline (0.5.7)
|
||||
io-console (~> 0.5)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
request_store (1.5.1)
|
||||
request_store (1.6.0)
|
||||
rack (>= 1.4)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.6)
|
||||
rollbar (3.4.2)
|
||||
rexml (3.3.6)
|
||||
strscan
|
||||
rotp (6.3.0)
|
||||
rqrcode (2.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 1.0)
|
||||
rqrcode_core (1.2.0)
|
||||
rspec-core (3.12.2)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-expectations (3.12.3)
|
||||
rspec-core (3.13.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-mocks (3.12.6)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-rails (6.1.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (6.1.2)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
rspec-core (~> 3.12)
|
||||
rspec-expectations (~> 3.12)
|
||||
rspec-mocks (~> 3.12)
|
||||
rspec-support (~> 3.12)
|
||||
rspec-support (3.12.1)
|
||||
rubocop (1.59.0)
|
||||
rspec-core (~> 3.13)
|
||||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-support (3.13.1)
|
||||
rubocop (1.65.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.2.2.4)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
regexp_parser (>= 2.4, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.30.0, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.30.0)
|
||||
parser (>= 3.2.1.0)
|
||||
rubocop-capybara (2.19.0)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-factory_bot (2.24.0)
|
||||
rubocop (~> 1.33)
|
||||
rubocop-performance (1.20.1)
|
||||
rubocop-ast (1.31.3)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-performance (1.21.0)
|
||||
rubocop (>= 1.48.1, < 2.0)
|
||||
rubocop-ast (>= 1.30.0, < 2.0)
|
||||
rubocop-rails (2.23.1)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rails (2.24.1)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.33.0, < 2.0)
|
||||
rubocop-ast (>= 1.30.0, < 2.0)
|
||||
rubocop-rspec (2.25.0)
|
||||
rubocop (~> 1.40)
|
||||
rubocop-capybara (~> 2.17)
|
||||
rubocop-factory_bot (~> 2.22)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rspec (3.0.3)
|
||||
rubocop (~> 1.61)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-rc4 (0.1.5)
|
||||
ruby-vips (2.2.0)
|
||||
ruby-vips (2.2.1)
|
||||
ffi (~> 1.12)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyXL (3.4.25)
|
||||
rubyXL (3.4.27)
|
||||
nokogiri (>= 1.10.8)
|
||||
rubyzip (>= 1.3.0)
|
||||
rubyzip (2.3.2)
|
||||
semantic_range (3.0.0)
|
||||
shakapacker (7.1.0)
|
||||
shakapacker (8.0.0)
|
||||
activesupport (>= 5.2)
|
||||
package_json
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
sidekiq (7.2.0)
|
||||
sidekiq (7.2.4)
|
||||
concurrent-ruby (< 2)
|
||||
connection_pool (>= 2.3.0)
|
||||
rack (>= 2.2.4)
|
||||
redis-client (>= 0.14.0)
|
||||
signet (0.18.0)
|
||||
redis-client (>= 0.19.0)
|
||||
signet (0.19.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
@@ -517,17 +514,16 @@ GEM
|
||||
simplecov-html (0.12.3)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
smart_properties (1.17.0)
|
||||
sqlite3 (1.7.0)
|
||||
sqlite3 (1.7.3)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
sqlite3 (1.7.0-arm64-darwin)
|
||||
stringio (3.1.0)
|
||||
strip_attributes (1.13.0)
|
||||
activemodel (>= 3.0, < 8.0)
|
||||
thor (1.3.0)
|
||||
strscan (3.1.0)
|
||||
thor (1.3.1)
|
||||
timeout (0.4.1)
|
||||
trailblazer-option (0.1.2)
|
||||
ttfunk (1.7.0)
|
||||
turbo-rails (1.5.0)
|
||||
turbo-rails (2.0.5)
|
||||
actionpack (>= 6.0.0)
|
||||
activejob (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
@@ -537,7 +533,7 @@ GEM
|
||||
tzinfo
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2023.4)
|
||||
tzinfo-data (1.2024.1)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.5.0)
|
||||
@@ -549,7 +545,7 @@ GEM
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webmock (3.19.1)
|
||||
webmock (3.23.0)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
@@ -559,22 +555,25 @@ GEM
|
||||
websocket-extensions (0.1.5)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.6.12)
|
||||
zeitwerk (2.6.14)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-22
|
||||
aarch64-linux
|
||||
ruby
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
annotate
|
||||
arabic-letter-connector
|
||||
aws-sdk-s3
|
||||
aws-sdk-secretsmanager
|
||||
azure-storage-blob
|
||||
better_html
|
||||
bootsnap
|
||||
bullet
|
||||
cancancan
|
||||
capybara
|
||||
csv
|
||||
cuprite
|
||||
debug
|
||||
devise
|
||||
@@ -593,9 +592,9 @@ DEPENDENCIES
|
||||
letter_opener_web
|
||||
lograge
|
||||
mysql2
|
||||
net-smtp (= 0.4.0)
|
||||
oj
|
||||
pagy
|
||||
pdf-reader
|
||||
pg
|
||||
premailer-rails
|
||||
pretender
|
||||
@@ -605,7 +604,6 @@ DEPENDENCIES
|
||||
rails
|
||||
rails-i18n
|
||||
rails_autolink
|
||||
rollbar
|
||||
rotp
|
||||
rqrcode
|
||||
rspec-rails
|
||||
@@ -627,7 +625,7 @@ DEPENDENCIES
|
||||
webmock
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.2.2p53
|
||||
ruby 3.3.4p94
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.3
|
||||
|
||||
@@ -33,17 +33,32 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
[](https://demo.docuseal.co)
|
||||
|
||||
## Features
|
||||
- [x] PDF form fields builder (WYSIWYG)
|
||||
- [x] 11 field types available (Signature, Date, File, Checkbox etc.)
|
||||
- [x] Multiple submitters per document
|
||||
- [x] Automated emails via SMTP
|
||||
- [x] Files storage on disk or AWS S3, Google Storage, Azure Cloud
|
||||
- [x] Automatic PDF eSignature
|
||||
- [x] PDF signature verification
|
||||
- [x] Users management
|
||||
- [x] Mobile-optimized
|
||||
- [x] API and Webhooks for integrations
|
||||
- [x] Easy to deploy in minutes
|
||||
- PDF form fields builder (WYSIWYG)
|
||||
- 12 field types available (Signature, Date, File, Checkbox etc.)
|
||||
- Multiple submitters per document
|
||||
- Automated emails via SMTP
|
||||
- Files storage on disk or AWS S3, Google Storage, Azure Cloud
|
||||
- Automatic PDF eSignature
|
||||
- PDF signature verification
|
||||
- Users management
|
||||
- Mobile-optimized
|
||||
- Signing available in 13 languages
|
||||
- API and Webhooks for integrations
|
||||
- Easy to deploy in minutes
|
||||
|
||||
## Pro Features
|
||||
- Company logo and white-label
|
||||
- User roles
|
||||
- Automated reminders
|
||||
- Invitation and identify verification via SMS
|
||||
- Conditional fields and formulas
|
||||
- Bulk send with CSV, XLSX spreadsheet import
|
||||
- SSO / SAML
|
||||
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
|
||||
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
|
||||
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||
- [Learn more](https://www.docuseal.co/pricing)
|
||||
|
||||
## Deploy
|
||||
|
||||
@@ -52,9 +67,6 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|
||||
|**DigitalOcean**|**Render**|
|
||||
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|
||||
|**Koyeb**|**Elestio**|
|
||||
| [<img alt="Deploy on Koyeb" src="https://www.koyeb.com/static/images/deploy/button.svg" height="40">](https://app.koyeb.com/deploy?name=docuseal&type=docker&image=docker.io/docuseal/docuseal&env[PORT]=8000&env[DATABASE_URL]=CHANGE_ME&env[SECRET_KEY_BASE]=CHANGE_ME&ports=8000;http;/) | [<img alt="Deploy on Elestio" src="https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png">](https://dash.elest.io/deploy?soft=DocuSeal&id=339) |
|
||||
|
||||
|
||||
#### Docker
|
||||
|
||||
@@ -77,7 +89,7 @@ sudo HOST=your-domain-name.com docker compose up
|
||||
```
|
||||
|
||||
## For Businesses
|
||||
### Integrate seamless document signing into your web or mobile apps with DocuSeal!
|
||||
### Integrate seamless document signing into your web or mobile apps with DocuSeal
|
||||
|
||||
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
||||
|
||||
@@ -87,3 +99,9 @@ At DocuSeal we have expertise and technologies to make documents creation, filli
|
||||
|
||||
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
||||
Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
||||
|
||||
## Tools
|
||||
|
||||
- [Signature Maker](https://www.docuseal.co/online-signature)
|
||||
- [Sign Document Online](https://www.docuseal.co/sign-documents-online)
|
||||
- [Fill PDF Online](https://www.docuseal.co/fill-pdf)
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# Reporting a Vulnerability
|
||||
|
||||
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.co instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
|
||||
|
||||
**We have a bug bounty program to reward security researchers.**
|
||||
|
||||
Out of scope vulnerabilities:
|
||||
|
||||
- CSRF
|
||||
- DNSSEC, CAA, CSP headers
|
||||
- DNS or email security related
|
||||
- Rate Limiting
|
||||
|
||||
Note: We reserve the right to classify any reported vulnerability as out of scope for the bug bounty program.
|
||||
@@ -8,9 +8,19 @@ class AccountConfigsController < ApplicationController
|
||||
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||
AccountConfig::FORCE_MFA,
|
||||
AccountConfig::ALLOW_TO_RESUBMIT,
|
||||
AccountConfig::ESIGNING_PREFERENCE_KEY
|
||||
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
|
||||
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||
AccountConfig::FORCE_SSO_AUTH_KEY,
|
||||
AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
||||
AccountConfig::WITH_SIGNATURE_ID,
|
||||
AccountConfig::COMBINE_PDF_RESULT_KEY,
|
||||
AccountConfig::REQUIRE_SIGNING_REASON_KEY
|
||||
].freeze
|
||||
|
||||
InvalidKey = Class.new(StandardError)
|
||||
|
||||
def create
|
||||
@account_config.update!(account_config_params)
|
||||
|
||||
@@ -20,7 +30,7 @@ class AccountConfigsController < ApplicationController
|
||||
private
|
||||
|
||||
def load_account_config
|
||||
return head :not_found unless ALLOWED_KEYS.include?(account_config_params[:key])
|
||||
raise InvalidKey unless ALLOWED_KEYS.include?(account_config_params[:key])
|
||||
|
||||
@account_config =
|
||||
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
|
||||
|
||||
@@ -7,7 +7,8 @@ class AccountsController < ApplicationController
|
||||
'fr-FR' => 'French (France)',
|
||||
'es-ES' => 'Spanish (Spain)',
|
||||
'pt-PT' => 'Portuguese (Portugal)',
|
||||
'de-DE' => 'German (Germany)'
|
||||
'de-DE' => 'German (Germany)',
|
||||
'it-IT' => 'Italian (Italy)'
|
||||
}.freeze
|
||||
|
||||
before_action :load_account
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class ActiveStorageBlobsProxyController < ApiBaseController
|
||||
include ActiveStorage::Streaming
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
before_action :set_cors_headers
|
||||
before_action :set_noindex_headers
|
||||
|
||||
def show
|
||||
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
||||
|
||||
if blob_uuid.blank? || (purp.present? && purp != 'blob') || (exp && exp < Time.current.to_i)
|
||||
Rollbar.error('Blob not found') if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
end
|
||||
|
||||
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
||||
|
||||
authorization_check!(blob) if exp.blank?
|
||||
|
||||
if request.headers['Range'].present?
|
||||
send_blob_byte_range_data blob, request.headers['Range']
|
||||
else
|
||||
http_cache_forever public: true do
|
||||
response.headers['Accept-Ranges'] = 'bytes'
|
||||
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||
|
||||
send_blob_stream blob, disposition: params[:disposition]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorization_check!(blob)
|
||||
is_authorized =
|
||||
blob.attachments.all? do |a|
|
||||
a.name.in?(%w[logo preview_images]) ||
|
||||
(current_user && a.record.account.id == current_user.account_id) ||
|
||||
!a.record.account.account_configs.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
|
||||
end
|
||||
|
||||
return if is_authorized
|
||||
|
||||
Rollbar.error('Blob aunauthorized') if defined?(Rollbar)
|
||||
|
||||
raise CanCan::AccessDenied
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class ActiveStorageBlobsProxyLegacyController < ApiBaseController
|
||||
include ActiveStorage::Streaming
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
before_action :set_cors_headers
|
||||
before_action :set_noindex_headers
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def show
|
||||
Rollbar.info('Blob legacy') if defined?(Rollbar)
|
||||
|
||||
blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id] || params[:signed_id])
|
||||
|
||||
return head :not_found unless blob
|
||||
|
||||
is_permitted = blob.attachments.any? do |a|
|
||||
(current_user && a.record.account.id == current_user.account_id) ||
|
||||
a.record.account.account_configs.any? { |e| e.key == 'legacy_blob_proxy' } ||
|
||||
a.name == 'logo'
|
||||
end
|
||||
|
||||
unless is_permitted
|
||||
Rollbar.error("Blob account not found: #{blob.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: 'Not authenticated' }, status: :unauthorized
|
||||
end
|
||||
|
||||
if request.headers['Range'].present?
|
||||
send_blob_byte_range_data blob, request.headers['Range']
|
||||
else
|
||||
http_cache_forever public: true do
|
||||
response.headers['Accept-Ranges'] = 'bytes'
|
||||
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||
|
||||
send_blob_stream blob, disposition: params[:disposition]
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
end
|
||||
end
|
||||
@@ -19,15 +19,19 @@ module Api
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
rescue_from RateLimit::LimitApproached do |e|
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: 'Too many requests' }, status: :too_many_requests
|
||||
end
|
||||
|
||||
if Rails.env.production?
|
||||
rescue_from CanCan::AccessDenied do |e|
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :forbidden
|
||||
end
|
||||
|
||||
rescue_from JSON::ParserError do |e|
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_entity
|
||||
end
|
||||
@@ -35,29 +39,48 @@ module Api
|
||||
|
||||
private
|
||||
|
||||
def paginate(relation)
|
||||
result = relation.order(id: :desc)
|
||||
def paginate(relation, field: :id)
|
||||
result = relation.order(field => :desc)
|
||||
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
||||
|
||||
result = result.where(relation.arel_table[:id].lt(params[:after])) if params[:after].present?
|
||||
result = result.where(relation.arel_table[:id].gt(params[:before])) if params[:before].present?
|
||||
if field == :id
|
||||
result = result.where(id: ...params[:after].to_i) if params[:after].present?
|
||||
result = result.where(id: (params[:before].to_i + 1)...) if params[:before].present?
|
||||
else
|
||||
result = result.where(field => ...params[:after]) if params[:after].present?
|
||||
result = result.where(field => (params[:before] + 1)...) if params[:before].present?
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def authenticate_user!
|
||||
@current_user ||=
|
||||
if request.headers['X-Auth-Token'].present?
|
||||
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
|
||||
|
||||
User.joins(:access_token).active.find_by(access_token: { sha256: })
|
||||
end
|
||||
|
||||
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
|
||||
end
|
||||
|
||||
def current_user
|
||||
super || @current_user ||=
|
||||
if request.headers['X-Auth-Token'].present?
|
||||
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
|
||||
|
||||
User.joins(:access_token).active.find_by(access_token: { sha256: })
|
||||
end
|
||||
end
|
||||
|
||||
def current_account
|
||||
current_user&.account
|
||||
end
|
||||
|
||||
def set_noindex_headers
|
||||
headers['X-Robots-Tag'] = 'noindex'
|
||||
end
|
||||
|
||||
def set_cors_headers
|
||||
headers['Access-Control-Allow-Origin'] = '*'
|
||||
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
||||
headers['Access-Control-Allow-Headers'] = '*'
|
||||
headers['Access-Control-Max-Age'] = '1728000'
|
||||
headers['Access-Control-Allow-Credentials'] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class AttachmentsController < ApiBaseController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
class AttachmentsController < ActionController::API
|
||||
include ActionController::Cookies
|
||||
include ActiveStorage::SetCurrent
|
||||
|
||||
COOKIE_STORE_LIMIT = 10
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
attachment = Submitters.create_attachment!(submitter, params)
|
||||
|
||||
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])
|
||||
if params[:remember_signature] == 'true' && submitter.email.present?
|
||||
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(submitter, attachment)
|
||||
end
|
||||
|
||||
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||
end
|
||||
|
||||
def build_new_cookie_signatures_json(submitter, attachment)
|
||||
values =
|
||||
begin
|
||||
JSON.parse(cookies.encrypted[:signature_uuids].presence || '{}')
|
||||
rescue JSON::ParserError
|
||||
{}
|
||||
end
|
||||
|
||||
values[submitter.email] = attachment.uuid
|
||||
|
||||
values = values.to_a.last(COOKIE_STORE_LIMIT).to_h if values.size > COOKIE_STORE_LIMIT
|
||||
|
||||
values.to_json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class FormEventsController < ApiBaseController
|
||||
load_and_authorize_resource :submitter, parent: false
|
||||
|
||||
def index
|
||||
submitters = @submitters.where.not(completed_at: nil)
|
||||
|
||||
params[:after] = Time.zone.at(params[:after].to_i) if params[:after].present?
|
||||
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
|
||||
|
||||
submitters = paginate(
|
||||
submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob,
|
||||
combined_document_attachment: :blob }],
|
||||
documents_attachments: :blob, attachments_attachments: :blob),
|
||||
field: :completed_at
|
||||
)
|
||||
|
||||
render json: {
|
||||
data: submitters.map do |s|
|
||||
{
|
||||
event_type: 'form.completed',
|
||||
timestamp: s.completed_at,
|
||||
data: Submitters::SerializeForWebhook.call(s)
|
||||
}
|
||||
end,
|
||||
pagination: {
|
||||
count: submitters.size,
|
||||
next: submitters.last&.completed_at&.to_i,
|
||||
prev: submitters.first&.completed_at&.to_i
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class SubmissionEventsController < ApiBaseController
|
||||
load_and_authorize_resource :submission, parent: false
|
||||
|
||||
def index
|
||||
submissions = build_completed_query(@submissions)
|
||||
|
||||
params[:after] = Time.zone.at(params[:after].to_i) if params[:after].present?
|
||||
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
|
||||
|
||||
submissions = paginate(submissions.preload(
|
||||
:created_by_user, :submission_events,
|
||||
template: :folder,
|
||||
submitters: { documents_attachments: :blob, attachments_attachments: :blob },
|
||||
audit_trail_attachment: :blob
|
||||
),
|
||||
field: :completed_at)
|
||||
|
||||
render json: {
|
||||
data: submissions.map do |s|
|
||||
{
|
||||
event_type: 'submission.completed',
|
||||
timestamp: s.completed_at,
|
||||
data: Submissions::SerializeForApi.call(s, s.submitters)
|
||||
}
|
||||
end,
|
||||
pagination: {
|
||||
count: submissions.size,
|
||||
next: submissions.last&.completed_at&.to_i,
|
||||
prev: submissions.first&.completed_at&.to_i
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_completed_query(submissions)
|
||||
submissions = submissions.where(
|
||||
Submitter.where(completed_at: nil).where(
|
||||
Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
||||
).select(1).arel.exists.not
|
||||
)
|
||||
|
||||
submissions.joins(:submitters)
|
||||
.group(:id)
|
||||
.select(Submission.arel_table[Arel.star],
|
||||
Submitter.arel_table[:completed_at].maximum.as('completed_at'))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -17,11 +17,16 @@ module Api
|
||||
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
|
||||
end
|
||||
|
||||
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters,
|
||||
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
||||
template: :folder,
|
||||
combined_document_attachment: :blob,
|
||||
audit_trail_attachment: :blob))
|
||||
|
||||
render json: {
|
||||
data: submissions.as_json(serialize_params),
|
||||
data: submissions.map do |s|
|
||||
Submissions::SerializeForApi.call(s, s.submitters, params,
|
||||
with_events: false, with_documents: false, with_values: false)
|
||||
end,
|
||||
pagination: {
|
||||
count: submissions.size,
|
||||
next: submissions.last&.id,
|
||||
@@ -43,38 +48,16 @@ module Api
|
||||
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
|
||||
end
|
||||
|
||||
serialized_submitters = submitters.map { |submitter| Submitters::SerializeForApi.call(submitter) }
|
||||
|
||||
json = @submission.as_json(
|
||||
serialize_params.deep_merge(
|
||||
include: { submission_events: { only: %i[id submitter_id event_type event_timestamp] } }
|
||||
)
|
||||
)
|
||||
|
||||
if submitters.all?(&:completed_at?)
|
||||
last_submitter = submitters.max_by(&:completed_at)
|
||||
|
||||
json[:documents] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents']
|
||||
json[:status] = 'completed'
|
||||
json[:completed_at] = last_submitter.completed_at
|
||||
else
|
||||
json[:documents] = []
|
||||
json[:status] = 'pending'
|
||||
json[:completed_at] = nil
|
||||
end
|
||||
|
||||
json[:submitters] = serialized_submitters
|
||||
|
||||
render json:
|
||||
render json: Submissions::SerializeForApi.call(@submission, submitters, params)
|
||||
end
|
||||
|
||||
def create
|
||||
Params::SubmissionCreateValidator.call(params, dry_run: !Docuseal.multitenant?)
|
||||
Params::SubmissionCreateValidator.call(params)
|
||||
|
||||
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
|
||||
|
||||
if @template.fields.blank?
|
||||
Rollbar.error("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
|
||||
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_entity
|
||||
end
|
||||
@@ -84,27 +67,51 @@ module Api
|
||||
|
||||
submissions = create_submissions(@template, params)
|
||||
|
||||
submissions.each do |submission|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
|
||||
end
|
||||
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
submissions.each do |submission|
|
||||
if submission.submitters.all?(&:completed_at?) && submission.submitters.last
|
||||
ProcessSubmitterCompletionJob.perform_later(submission.submitters.last)
|
||||
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submission.submitters.last.id })
|
||||
end
|
||||
end
|
||||
|
||||
render json: submissions.flat_map(&:submitters)
|
||||
rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e
|
||||
render json: build_create_json(submissions)
|
||||
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def destroy
|
||||
@submission.update!(archived_at: Time.current)
|
||||
if params[:permanently] == 'true'
|
||||
@submission.destroy!
|
||||
else
|
||||
@submission.update!(archived_at: Time.current)
|
||||
|
||||
render json: @submission.as_json(only: %i[id], methods: %i[archived_at])
|
||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
|
||||
end
|
||||
|
||||
render json: @submission.as_json(only: %i[id archived_at])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_create_json(submissions)
|
||||
json = submissions.flat_map do |submission|
|
||||
submission.submitters.map do |s|
|
||||
Submitters::SerializeForApi.call(s, with_documents: false, with_urls: true, params:)
|
||||
end
|
||||
end
|
||||
|
||||
json = { submitters: json } if request.path.ends_with?('/init')
|
||||
|
||||
json
|
||||
end
|
||||
|
||||
def create_submissions(template, params)
|
||||
is_send_email = !params[:send_email].in?(['false', false])
|
||||
|
||||
@@ -124,7 +131,6 @@ module Api
|
||||
template:,
|
||||
user: current_user,
|
||||
source: :api,
|
||||
mark_as_sent: is_send_email,
|
||||
submitters_order: params[:submitters_order] || params[:order] || 'preserved',
|
||||
submissions_attrs:,
|
||||
params:
|
||||
@@ -142,31 +148,18 @@ module Api
|
||||
end
|
||||
end
|
||||
|
||||
def serialize_params
|
||||
{
|
||||
only: %i[id source submitters_order created_at updated_at archived_at],
|
||||
methods: %i[audit_log_url],
|
||||
include: {
|
||||
submitters: { only: %i[id slug uuid name email phone
|
||||
completed_at opened_at sent_at
|
||||
created_at updated_at external_id metadata],
|
||||
methods: %i[status application_key] },
|
||||
template: { only: %i[id name created_at updated_at] },
|
||||
created_by_user: { only: %i[id email first_name last_name] }
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def submissions_params
|
||||
permitted_attrs = [
|
||||
:send_email, :send_sms, :bcc_completed, :completed_redirect_url,
|
||||
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
||||
:expire_at,
|
||||
{
|
||||
message: %i[subject body],
|
||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||
:completed, :phone, :application_key, :external_id,
|
||||
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||
fields: [%i[name default_value title description
|
||||
readonly validation_pattern invalid_message]] }]]
|
||||
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||
:readonly, :validation_pattern, :invalid_message,
|
||||
{ default_value: [], value: [], preferences: {} }] }]]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -8,9 +8,12 @@ module Api
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
submitter.opened_at = Time.current
|
||||
submitter.save
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
||||
|
||||
SendFormViewedWebhookRequestJob.perform_later(submitter)
|
||||
SendFormViewedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id })
|
||||
|
||||
render json: {}
|
||||
end
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class SubmittersAutocompleteController < ApiBaseController
|
||||
load_and_authorize_resource :submitter, parent: false
|
||||
|
||||
SELECT_COLUMNS = %w[email phone name].freeze
|
||||
LIMIT = 100
|
||||
|
||||
def index
|
||||
submitters = search_submitters(@submitters)
|
||||
|
||||
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
|
||||
|
||||
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
|
||||
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
|
||||
|
||||
render json: attrs
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def search_submitters(submitters)
|
||||
if SELECT_COLUMNS.include?(params[:field])
|
||||
column = Submitter.arel_table[params[:field].to_sym]
|
||||
|
||||
term = "%#{params[:q].downcase}%"
|
||||
|
||||
submitters.where(column.lower.matches(term))
|
||||
else
|
||||
Submitters.search(submitters, params[:q])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -11,13 +11,21 @@ module Api
|
||||
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
|
||||
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
||||
|
||||
if params[:template_id].present?
|
||||
submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] })
|
||||
end
|
||||
|
||||
submitters = maybe_filder_by_completed_at(submitters, params)
|
||||
|
||||
submitters = paginate(
|
||||
submitters.preload(:template, :submission, :submission_events,
|
||||
documents_attachments: :blob, attachments_attachments: :blob)
|
||||
)
|
||||
|
||||
render json: {
|
||||
data: submitters.map { |s| Submitters::SerializeForApi.call(s, with_template: true, with_events: true) },
|
||||
data: submitters.map do |s|
|
||||
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:)
|
||||
end,
|
||||
pagination: {
|
||||
count: submitters.size,
|
||||
next: submitters.last&.id,
|
||||
@@ -29,7 +37,7 @@ module Api
|
||||
def show
|
||||
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
|
||||
|
||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true)
|
||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:)
|
||||
end
|
||||
|
||||
def update
|
||||
@@ -60,42 +68,101 @@ module Api
|
||||
end
|
||||
|
||||
if @submitter.completed_at?
|
||||
ProcessSubmitterCompletionJob.perform_later(@submitter)
|
||||
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => @submitter.id })
|
||||
elsif normalized_params[:send_email] || normalized_params[:send_sms]
|
||||
Submitters.send_signature_requests([@submitter])
|
||||
end
|
||||
|
||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_events: false)
|
||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: false,
|
||||
with_urls: true,
|
||||
with_events: false,
|
||||
params:)
|
||||
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def submitter_params
|
||||
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
|
||||
|
||||
submitter_params.permit(
|
||||
:send_email, :send_sms, :uuid, :name, :email, :role,
|
||||
:completed, :phone, :application_key, :external_id,
|
||||
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||
:completed, :phone, :application_key, :external_id, :go_to_last,
|
||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||
fields: [%i[name default_value readonly validation_pattern invalid_message]] }
|
||||
fields: [[:name, :uuid, :default_value, :value,
|
||||
:readonly, :validation_pattern, :invalid_message,
|
||||
{ default_value: [], value: [], preferences: {} }]] }
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def maybe_filder_by_completed_at(submitters, params)
|
||||
if params[:completed_after].present?
|
||||
submitters = submitters.where(completed_at: Time.zone.parse(params[:completed_after])..)
|
||||
end
|
||||
|
||||
if params[:completed_before].present?
|
||||
submitters = submitters.where(completed_at: ..Time.zone.parse(params[:completed_before]))
|
||||
end
|
||||
|
||||
submitters
|
||||
end
|
||||
|
||||
def assign_submitter_attrs(submitter, attrs)
|
||||
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
||||
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
|
||||
submitter.values = submitter.values.merge(attrs[:values].to_unsafe_h) if attrs[:values].present?
|
||||
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
|
||||
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
|
||||
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
|
||||
submitter.metadata = attrs[:metadata] if attrs.key?(:metadata)
|
||||
values = attrs[:values]&.to_unsafe_h || {}
|
||||
|
||||
assign_submission_fields(submitter.submission)
|
||||
|
||||
phone_field_uuid = submitter.submission.template_fields.find do |f|
|
||||
values[f['uuid']].present? && f['type'] == 'phone'
|
||||
end&.dig('uuid')
|
||||
|
||||
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
||||
|
||||
if attrs.key?(:phone)
|
||||
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '')
|
||||
elsif values[phone_field_uuid].present?
|
||||
submitter.phone = values[phone_field_uuid].to_s.gsub(/[^0-9+]/, '')
|
||||
end
|
||||
|
||||
values = values.except(phone_field_uuid)
|
||||
|
||||
submitter.values = submitter.values.merge(values) if values.present?
|
||||
submitter.metadata = attrs[:metadata] if attrs.key?(:metadata)
|
||||
|
||||
maybe_assign_completed_attributes(submitter, attrs)
|
||||
|
||||
assign_external_id(submitter, attrs)
|
||||
assign_preferences(submitter, attrs)
|
||||
|
||||
submitter
|
||||
end
|
||||
|
||||
def maybe_assign_completed_attributes(submitter, attrs)
|
||||
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
|
||||
|
||||
if attrs[:completed]
|
||||
submitter.values = Submitters::SubmitValues.merge_default_values(submitter)
|
||||
submitter.values = Submitters::SubmitValues.merge_formula_values(submitter)
|
||||
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
||||
|
||||
submitter.values = submitter.values.transform_values do |v|
|
||||
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
|
||||
end
|
||||
end
|
||||
|
||||
submitter
|
||||
end
|
||||
|
||||
def assign_external_id(submitter, attrs)
|
||||
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
|
||||
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
|
||||
|
||||
submitter
|
||||
end
|
||||
|
||||
def assign_submission_fields(submission)
|
||||
submission.template_fields ||= submission.template.fields
|
||||
submission.template_schema ||= submission.template.schema
|
||||
@@ -111,6 +178,14 @@ module Api
|
||||
end
|
||||
|
||||
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
||||
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
|
||||
if submitter_preferences.key?('go_to_last')
|
||||
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
|
||||
end
|
||||
|
||||
if submitter_preferences.key?('completed_redirect_url')
|
||||
submitter.preferences['completed_redirect_url'] = submitter_preferences['completed_redirect_url']
|
||||
end
|
||||
|
||||
return unless submitter_preferences.key?('email_message_uuid')
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class TemplateFoldersAutocompleteController < ApiBaseController
|
||||
load_and_authorize_resource :template_folder, parent: false
|
||||
|
||||
LIMIT = 100
|
||||
|
||||
def index
|
||||
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
|
||||
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
||||
|
||||
render json: template_folders.as_json(only: %i[name archived_at])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7,6 +7,11 @@ module Api
|
||||
def create
|
||||
authorize!(:manage, @template)
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@template],
|
||||
associations: [schema_documents: :preview_images_attachments]
|
||||
).call
|
||||
|
||||
cloned_template = Templates::Clone.call(
|
||||
@template,
|
||||
author: current_user,
|
||||
@@ -18,18 +23,11 @@ module Api
|
||||
cloned_template.source = :api
|
||||
cloned_template.save!
|
||||
|
||||
Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
||||
schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
||||
|
||||
render json: cloned_template.as_json(serialize_params)
|
||||
end
|
||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id)
|
||||
|
||||
private
|
||||
|
||||
def serialize_params
|
||||
{
|
||||
include: { author: { only: %i[id email first_name last_name] },
|
||||
documents: { only: %i[id uuid], methods: %i[url filename] } }
|
||||
}
|
||||
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,10 +7,31 @@ module Api
|
||||
def index
|
||||
templates = filter_templates(@templates, params)
|
||||
|
||||
templates = paginate(templates.preload(:author, documents_attachments: :blob))
|
||||
templates = paginate(templates.preload(:author, :folder))
|
||||
|
||||
schema_documents =
|
||||
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
|
||||
record_type: 'Template',
|
||||
name: :documents,
|
||||
uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
|
||||
.preload(:blob)
|
||||
|
||||
preview_image_attachments =
|
||||
ActiveStorage::Attachment.joins(:blob)
|
||||
.where(blob: { filename: ['0.png', '0.jpg'] })
|
||||
.where(record_id: schema_documents.map(&:id),
|
||||
record_type: 'ActiveStorage::Attachment',
|
||||
name: :preview_images)
|
||||
.preload(:blob)
|
||||
|
||||
render json: {
|
||||
data: templates.as_json(serialize_params),
|
||||
data: templates.map do |t|
|
||||
Templates::SerializeForApi.call(
|
||||
t,
|
||||
schema_documents.select { |e| e.record_id == t.id },
|
||||
preview_image_attachments
|
||||
)
|
||||
end,
|
||||
pagination: {
|
||||
count: templates.size,
|
||||
next: templates.last&.id,
|
||||
@@ -20,7 +41,7 @@ module Api
|
||||
end
|
||||
|
||||
def show
|
||||
render json: @template.as_json(serialize_params)
|
||||
render json: Templates::SerializeForApi.call(@template)
|
||||
end
|
||||
|
||||
def update
|
||||
@@ -28,6 +49,14 @@ module Api
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
|
||||
end
|
||||
|
||||
Array.wrap(params[:roles].presence || params.dig(:template, :roles).presence).each_with_index do |role, index|
|
||||
if (item = @template.submitters[index])
|
||||
item['name'] = role
|
||||
else
|
||||
@template.submitters << { 'name' => role, 'uuid' => SecureRandom.uuid }
|
||||
end
|
||||
end
|
||||
|
||||
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
|
||||
|
||||
if archived.in?([true, false])
|
||||
@@ -36,11 +65,13 @@ module Api
|
||||
|
||||
@template.update!(template_params)
|
||||
|
||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
||||
|
||||
render json: @template.as_json(only: %i[id updated_at])
|
||||
end
|
||||
|
||||
def destroy
|
||||
if params[:permanently] == 'true' && !Docuseal.multitenant?
|
||||
if params[:permanently] == 'true'
|
||||
@template.destroy!
|
||||
else
|
||||
@template.update!(archived_at: Time.current)
|
||||
@@ -61,29 +92,27 @@ module Api
|
||||
templates
|
||||
end
|
||||
|
||||
def serialize_params
|
||||
{
|
||||
methods: %i[application_key],
|
||||
include: { author: { only: %i[id email first_name last_name] },
|
||||
documents: { only: %i[id uuid], methods: %i[url preview_image_url filename] } }
|
||||
}
|
||||
end
|
||||
|
||||
def template_params
|
||||
permit_params = [
|
||||
permitted_params = [
|
||||
:name,
|
||||
{ schema: [%i[attachment_uuid name]],
|
||||
submitters: [%i[name uuid]],
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
|
||||
:external_id,
|
||||
{
|
||||
submitters: [%i[name uuid is_requester invite_by_uuid invite_by_uuid linked_to_uuid email]],
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description,
|
||||
{ preferences: {},
|
||||
conditions: [%i[field_uuid value action]],
|
||||
options: [%i[value uuid]],
|
||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||
validation: %i[message pattern],
|
||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||
}
|
||||
]
|
||||
|
||||
if params.key?(:template)
|
||||
params.require(:template).permit(*permit_params)
|
||||
params.require(:template).permit(permitted_params)
|
||||
else
|
||||
params.permit(*permit_params)
|
||||
params.permit(permitted_params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class TemplatesDocumentsController < ApiBaseController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def create
|
||||
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
|
||||
|
||||
documents = Templates::CreateAttachments.call(@template, params)
|
||||
|
||||
schema = documents.map do |doc|
|
||||
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
||||
end
|
||||
|
||||
render json: {
|
||||
schema:,
|
||||
documents: documents.as_json(
|
||||
methods: [:metadata],
|
||||
include: {
|
||||
preview_images: { methods: %i[url metadata filename] }
|
||||
}
|
||||
)
|
||||
}
|
||||
rescue Templates::CreateAttachments::PdfEncrypted
|
||||
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class ToolsController < ApiBaseController
|
||||
skip_authorization_check
|
||||
|
||||
def merge
|
||||
files = params[:files] || []
|
||||
|
||||
return render json: { error: 'Files are required' }, status: :unprocessable_entity if files.blank?
|
||||
return render json: { error: 'At least 2 files are required' }, status: :unprocessable_entity if files.size < 2
|
||||
|
||||
render json: {
|
||||
data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string)
|
||||
}
|
||||
end
|
||||
|
||||
def verify
|
||||
file = Base64.decode64(params[:file])
|
||||
pdf = HexaPDF::Document.new(io: StringIO.new(file))
|
||||
|
||||
trusted_certs = Accounts.load_trusted_certs(current_account)
|
||||
|
||||
is_checksum_found = ActiveStorage::Attachment.joins(:blob)
|
||||
.where(name: 'documents', record_type: 'Submitter')
|
||||
.exists?(blob: { checksum: Digest::MD5.base64digest(file) })
|
||||
|
||||
render json: {
|
||||
checksum_status: is_checksum_found ? 'verified' : 'not_found',
|
||||
signatures: pdf.signatures.map do |sig|
|
||||
{
|
||||
verification_result: sig.verify(trusted_certs:).messages,
|
||||
signer_name: sig.signer_name,
|
||||
signing_reason: sig.signing_reason,
|
||||
signing_time: sig.signing_time,
|
||||
signature_type: sig.signature_type
|
||||
}
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class UsersController < ApiBaseController
|
||||
authorize_resource :current_user
|
||||
|
||||
def show
|
||||
render json: current_user.as_json(only: %i[id first_name last_name email])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -22,9 +22,15 @@ class ApplicationController < ActionController::Base
|
||||
redirect_to request.path
|
||||
end
|
||||
|
||||
rescue_from RateLimit::LimitApproached do |e|
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
|
||||
end
|
||||
|
||||
if Rails.env.production?
|
||||
rescue_from CanCan::AccessDenied do |e|
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
redirect_to root_path, alert: e.message
|
||||
end
|
||||
@@ -46,7 +52,8 @@ class ApplicationController < ActionController::Base
|
||||
private
|
||||
|
||||
def with_browser_locale(&)
|
||||
locale = request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
|
||||
locale = params[:lang].presence
|
||||
locale ||= request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
|
||||
|
||||
locale =
|
||||
if locale.starts_with?('en-') && locale != 'en-US'
|
||||
|
||||
@@ -5,6 +5,12 @@ class ConsoleRedirectController < ApplicationController
|
||||
skip_authorization_check
|
||||
|
||||
def index
|
||||
if request.path == '/upgrade'
|
||||
params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premise"
|
||||
end
|
||||
|
||||
params[:redir] = "#{Docuseal::CONSOLE_URL}/manage" if request.path == '/manage'
|
||||
|
||||
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
|
||||
|
||||
auth = JsonWebToken.encode(uuid: true_user.uuid,
|
||||
|
||||
@@ -7,62 +7,18 @@ class DashboardController < ApplicationController
|
||||
before_action :maybe_render_landing
|
||||
before_action :maybe_redirect_mfa_setup
|
||||
|
||||
load_and_authorize_resource :template_folder, parent: false
|
||||
load_and_authorize_resource :template, parent: false
|
||||
|
||||
SHOW_TEMPLATES_FOLDERS_THRESHOLD = 9
|
||||
TEMPLATES_PER_PAGE = 12
|
||||
FOLDERS_PER_PAGE = 18
|
||||
skip_authorization_check
|
||||
|
||||
def index
|
||||
@template_folders = filter_template_folders(@template_folders)
|
||||
|
||||
@pagy, @template_folders = pagy(
|
||||
@template_folders,
|
||||
items: FOLDERS_PER_PAGE,
|
||||
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
|
||||
)
|
||||
|
||||
if @pagy.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD
|
||||
@templates = @templates.none
|
||||
if cookies.permanent[:dashboard_view] == 'submissions'
|
||||
SubmissionsDashboardController.dispatch(:index, request, response)
|
||||
else
|
||||
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
||||
@templates = filter_templates(@templates)
|
||||
|
||||
items =
|
||||
if @template_folders.size < 4
|
||||
TEMPLATES_PER_PAGE
|
||||
else
|
||||
(@template_folders.size < 7 ? 9 : 6)
|
||||
end
|
||||
|
||||
@pagy, @templates = pagy(@templates, items:)
|
||||
TemplatesDashboardController.dispatch(:index, request, response)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_template_folders(template_folders)
|
||||
rel = template_folders.joins(:active_templates)
|
||||
.order(id: :desc)
|
||||
.distinct
|
||||
|
||||
TemplateFolders.search(rel, params[:q])
|
||||
end
|
||||
|
||||
def filter_templates(templates)
|
||||
rel = templates.active.preload(:author).order(id: :desc)
|
||||
|
||||
if params[:q].blank?
|
||||
shared_template_ids =
|
||||
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
|
||||
|
||||
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
|
||||
end
|
||||
|
||||
Templates.search(rel, params[:q])
|
||||
end
|
||||
|
||||
def maybe_redirect_product_url
|
||||
return if !Docuseal.multitenant? || signed_in?
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class EmbedScriptsController < ActionController::Metal
|
||||
DUMMY_SCRIPT = <<~JAVASCRIPT.freeze
|
||||
const DummyBuilder = class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.innerHTML = `
|
||||
<div style="text-align: center; padding: 20px; font-family: Arial, sans-serif;">
|
||||
<h2>Upgrade to Pro</h2>
|
||||
<p>Unlock embedded components by upgrading to Pro</p>
|
||||
<div style="margin-top: 40px;">
|
||||
<a href="#{Docuseal::CONSOLE_URL}/on_premise" target="_blank" style="padding: 15px 25px; background-color: #222; color: white; text-decoration: none; border-radius: 5px; font-size: 16px; cursor: pointer;">
|
||||
Learn More
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
const DummyForm = class extends DummyBuilder {};
|
||||
|
||||
if (!window.customElements.get('docuseal-builder')) {
|
||||
window.customElements.define('docuseal-builder', DummyBuilder);
|
||||
}
|
||||
|
||||
if (!window.customElements.get('docuseal-form')) {
|
||||
window.customElements.define('docuseal-form', DummyForm);
|
||||
}
|
||||
JAVASCRIPT
|
||||
|
||||
def show
|
||||
headers['Content-Type'] = 'application/javascript'
|
||||
|
||||
self.response_body = DUMMY_SCRIPT
|
||||
|
||||
self.status = 200
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class EncryptedUserConfigsController < ApplicationController
|
||||
load_and_authorize_resource :encrypted_user_config
|
||||
|
||||
def destroy
|
||||
@encrypted_user_config.destroy!
|
||||
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
end
|
||||
@@ -15,6 +15,11 @@ class ErrorsController < ActionController::Base
|
||||
'/api/templates/docx'
|
||||
].freeze
|
||||
|
||||
SAFE_ERROR_MESSAGE_CLASSES = [
|
||||
ActionDispatch::Http::Parameters::ParseError,
|
||||
JSON::ParserError
|
||||
].freeze
|
||||
|
||||
def show
|
||||
if request.original_fullpath.in?(ENTERPRISE_PATHS) && error_status_code == 404
|
||||
return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found
|
||||
@@ -22,7 +27,13 @@ class ErrorsController < ActionController::Base
|
||||
|
||||
respond_to do |f|
|
||||
f.json do
|
||||
render json: { status: error_status_code }, status: error_status_code
|
||||
set_cors_headers
|
||||
|
||||
exception = request.env['action_dispatch.exception']
|
||||
|
||||
error = exception.message if exception.class.in?(SAFE_ERROR_MESSAGE_CLASSES)
|
||||
|
||||
render json: { status: error_status_code, error: }.compact, status: error_status_code
|
||||
end
|
||||
|
||||
f.html { render error_status_code.to_s, status: error_status_code }
|
||||
@@ -31,6 +42,14 @@ class ErrorsController < ActionController::Base
|
||||
|
||||
private
|
||||
|
||||
def set_cors_headers
|
||||
headers['Access-Control-Allow-Origin'] = '*'
|
||||
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
||||
headers['Access-Control-Allow-Headers'] = '*'
|
||||
headers['Access-Control-Max-Age'] = '1728000'
|
||||
headers['Access-Control-Allow-Credentials'] = true
|
||||
end
|
||||
|
||||
def error_status_code
|
||||
@error_status_code ||=
|
||||
ActionDispatch::ExceptionWrapper.new(request.env,
|
||||
|
||||
@@ -57,8 +57,10 @@ class EsignSettingsController < ApplicationController
|
||||
save_new_cert!(@encrypted_config, @cert_record)
|
||||
|
||||
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
|
||||
rescue OpenSSL::PKCS12::PKCS12Error
|
||||
@cert_record.errors.add(:password, "is invalid. Make sure you're uploading a valid .p12 file")
|
||||
rescue OpenSSL::PKCS12::PKCS12Error => e
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
@cert_record.errors.add(:password, e.message)
|
||||
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
|
||||
end
|
||||
|
||||
@@ -35,12 +35,12 @@ class NotificationsSettingsController < ApplicationController
|
||||
|
||||
def load_reminder_config
|
||||
@reminder_config =
|
||||
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::SUBMITTER_REMAILERS)
|
||||
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::SUBMITTER_REMINDERS)
|
||||
end
|
||||
|
||||
def email_config_params
|
||||
params.require(:account_config).permit!.tap do |attrs|
|
||||
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMAILERS])
|
||||
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,12 @@ class PasswordsController < Devise::PasswordsController
|
||||
attribute :user
|
||||
end
|
||||
|
||||
def create
|
||||
super do |resource|
|
||||
resource.errors.clear unless Docuseal.multitenant?
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
super do |resource|
|
||||
Current.user = resource
|
||||
|
||||
@@ -1,26 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PersonalizationSettingsController < ApplicationController
|
||||
ALLOWED_KEYS = [
|
||||
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
|
||||
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
||||
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
||||
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
|
||||
AccountConfig::FORM_COMPLETED_MESSAGE_KEY
|
||||
].freeze
|
||||
|
||||
InvalidKey = Class.new(StandardError)
|
||||
|
||||
before_action :load_and_authorize_account_config, only: :create
|
||||
|
||||
def show
|
||||
authorize!(:read, AccountConfig)
|
||||
end
|
||||
|
||||
def create
|
||||
account_config =
|
||||
current_account.account_configs.find_or_initialize_by(key: account_config_params[:key])
|
||||
if @account_config.value.is_a?(Hash)
|
||||
@account_config.value = @account_config.value.reject do |_, v|
|
||||
v.blank? && v != false
|
||||
end
|
||||
end
|
||||
|
||||
authorize!(:create, account_config)
|
||||
|
||||
account_config.update!(account_config_params)
|
||||
if @account_config.value != false && @account_config.value.blank?
|
||||
@account_config.destroy!
|
||||
else
|
||||
@account_config.save!
|
||||
end
|
||||
|
||||
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_and_authorize_account_config
|
||||
@account_config =
|
||||
current_account.account_configs.find_or_initialize_by(key: account_config_params[:key])
|
||||
|
||||
@account_config.assign_attributes(account_config_params)
|
||||
|
||||
authorize!(:create, @account_config)
|
||||
|
||||
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
|
||||
|
||||
@account_config
|
||||
end
|
||||
|
||||
def account_config_params
|
||||
attrs = params.require(:account_config).permit!
|
||||
|
||||
return attrs if attrs[:value].is_a?(String)
|
||||
|
||||
attrs[:value]&.transform_values! do |value|
|
||||
if value.in?(%w[true false])
|
||||
value == 'true'
|
||||
|
||||
@@ -6,7 +6,9 @@ class PreviewDocumentPageController < ActionController::API
|
||||
FORMAT = Templates::ProcessDocument::FORMAT
|
||||
|
||||
def show
|
||||
attachment = ActiveStorage::Attachment.find_by(uuid: params[:attachment_uuid])
|
||||
attachment_uuid = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid], purpose: :attachment)
|
||||
|
||||
attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid) if attachment_uuid
|
||||
|
||||
return head :not_found unless attachment
|
||||
|
||||
@@ -21,9 +23,10 @@ class PreviewDocumentPageController < ActionController::API
|
||||
find_or_create_document_tempfile_path(attachment)
|
||||
end
|
||||
|
||||
io = Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
||||
preview_image =
|
||||
Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
||||
|
||||
render plain: io.tap(&:rewind).read, content_type: 'image/jpeg'
|
||||
redirect_to preview_image.url, allow_other_host: true
|
||||
end
|
||||
|
||||
def find_or_create_document_tempfile_path(attachment)
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RegistrationsController < Devise::RegistrationsController
|
||||
prepend_before_action :require_no_authentication, only: [:show]
|
||||
prepend_before_action :maybe_redirect_if_signed_in, only: [:show]
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
super
|
||||
|
||||
Accounts.create_default_template(resource.account) if resource.account.persisted?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def after_sign_up_path_for(...)
|
||||
if params[:redir].present?
|
||||
return console_redirect_index_path(redir: params[:redir]) if params[:redir].starts_with?(Docuseal::CONSOLE_URL)
|
||||
|
||||
return params[:redir]
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def maybe_redirect_if_signed_in
|
||||
return unless signed_in?
|
||||
return if params[:redir].blank?
|
||||
|
||||
redirect_to after_sign_up_path_for(current_user), allow_other_host: true
|
||||
end
|
||||
|
||||
def set_flash_message(key, kind, options = {})
|
||||
return if key == :alert && kind == 'already_authenticated'
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def build_resource(_hash = {})
|
||||
account = Account.new(account_params)
|
||||
account.timezone = Accounts.normalize_timezone(account.timezone)
|
||||
|
||||
self.resource = account.users.new(user_params)
|
||||
|
||||
account.name ||= resource.full_name if params[:action] == 'create'
|
||||
end
|
||||
|
||||
def user_params
|
||||
return {} if params[:user].blank?
|
||||
|
||||
params.require(:user).permit(:first_name, :last_name, :email, :password).compact_blank.tap do |attrs|
|
||||
attrs[:password] ||= SecureRandom.hex if params[:action] == 'create'
|
||||
end
|
||||
end
|
||||
|
||||
def account_params
|
||||
return {} if params[:account].blank?
|
||||
|
||||
params.require(:account).permit(:name, :timezone).compact_blank
|
||||
end
|
||||
end
|
||||
@@ -12,13 +12,18 @@ class SendSubmissionEmailController < ApplicationController
|
||||
def create
|
||||
@submitter =
|
||||
if params[:template_slug]
|
||||
Submitter.joins(submission: :template).find_by!(email: params[:email],
|
||||
Submitter.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
|
||||
template: { slug: params[:template_slug] })
|
||||
elsif params[:submission_slug]
|
||||
Submitter.joins(:submission).find_by!(email: params[:email].to_s.downcase,
|
||||
submission: { slug: params[:submission_slug] })
|
||||
else
|
||||
Submitter.find_by!(slug: params[:submitter_slug])
|
||||
end
|
||||
|
||||
SubmitterMailer.documents_copy_email(@submitter).deliver_later!
|
||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||
|
||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
|
||||
|
||||
respond_to do |f|
|
||||
f.html { redirect_to success_send_submission_email_index_path }
|
||||
|
||||
@@ -3,13 +3,19 @@
|
||||
class SessionsController < Devise::SessionsController
|
||||
before_action :configure_permitted_parameters
|
||||
|
||||
around_action :with_browser_locale
|
||||
|
||||
def create
|
||||
if Docuseal.multitenant? && !User.exists?(email: sign_in_params[:email])
|
||||
email = sign_in_params[:email].to_s.downcase
|
||||
|
||||
if Docuseal.multitenant? && !User.exists?(email:)
|
||||
Rollbar.warning('Sign in new user') if defined?(Rollbar)
|
||||
|
||||
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
||||
notice: 'Create a new account'
|
||||
end
|
||||
|
||||
if User.exists?(email: sign_in_params[:email], otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
||||
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
||||
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
class SsoSettingsController < ApplicationController
|
||||
before_action :load_encrypted_config
|
||||
authorize_resource :encrypted_config, only: :index
|
||||
authorize_resource :encrypted_config, parent: false, except: :index
|
||||
|
||||
def index; end
|
||||
|
||||
|
||||
@@ -11,27 +11,35 @@ class StartFormController < ApplicationController
|
||||
|
||||
def show
|
||||
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||
.submitters.new(uuid: @template.submitters.first['uuid'])
|
||||
.submitters.new(uuid: (filter_undefined_submitters(@template).first ||
|
||||
@template.submitters.first)['uuid'])
|
||||
end
|
||||
|
||||
def update
|
||||
@submitter = Submitter.where(submission: @template.submissions.where(archived_at: nil))
|
||||
.order(id: :desc)
|
||||
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
||||
.find_or_initialize_by(**submitter_params.compact_blank)
|
||||
return redirect_to start_form_path(@template.slug) if @template.archived_at?
|
||||
|
||||
@submitter = find_or_initialize_submitter(@template, submitter_params)
|
||||
|
||||
if @submitter.completed_at?
|
||||
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
|
||||
else
|
||||
if @template.submitters.to_a.size > 1 && @submitter.new_record?
|
||||
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
|
||||
@error_message = 'Not found'
|
||||
|
||||
return render :show
|
||||
end
|
||||
|
||||
assign_submission_attributes(@submitter, @template) if @submitter.new_record?
|
||||
if (is_new_record = @submitter.new_record?)
|
||||
assign_submission_attributes(@submitter, @template)
|
||||
|
||||
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
|
||||
end
|
||||
|
||||
if @submitter.save
|
||||
if is_new_record
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => @submitter.submission.id })
|
||||
end
|
||||
|
||||
redirect_to submit_form_path(@submitter.slug)
|
||||
else
|
||||
render :show
|
||||
@@ -47,14 +55,21 @@ class StartFormController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def find_or_initialize_submitter(template, submitter_params)
|
||||
Submitter.where(submission: template.submissions.where(expire_at: Time.current..)
|
||||
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
|
||||
.order(id: :desc)
|
||||
.where(declined_at: nil)
|
||||
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
||||
.find_or_initialize_by(**submitter_params.compact_blank)
|
||||
end
|
||||
|
||||
def assign_submission_attributes(submitter, template)
|
||||
resubmit_submitter =
|
||||
if params[:resubmit].present?
|
||||
Submitter.where(submission: @template.submissions).find_by(slug: params[:resubmit])
|
||||
end
|
||||
(Submitter.where(submission: template.submissions).find_by(slug: params[:resubmit]) if params[:resubmit].present?)
|
||||
|
||||
submitter.assign_attributes(
|
||||
uuid: template.submitters.first['uuid'],
|
||||
uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'],
|
||||
ip: request.remote_ip,
|
||||
ua: request.user_agent,
|
||||
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
|
||||
@@ -71,11 +86,18 @@ class StartFormController < ApplicationController
|
||||
submitter.submission ||= Submission.new(template:,
|
||||
account_id: template.account_id,
|
||||
template_submitters: template.submitters,
|
||||
submitters: [submitter],
|
||||
source: :link)
|
||||
|
||||
submitter.account_id = submitter.submission.account_id
|
||||
|
||||
submitter
|
||||
end
|
||||
|
||||
def filter_undefined_submitters(template)
|
||||
Templates.filter_undefined_submitters(template)
|
||||
end
|
||||
|
||||
def submitter_params
|
||||
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
|
||||
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionsArchivedController < ApplicationController
|
||||
load_and_authorize_resource :submission, parent: false
|
||||
|
||||
def index
|
||||
@submissions = @submissions.joins(:template)
|
||||
@submissions = @submissions.where.not(archived_at: nil)
|
||||
.or(@submissions.where.not(templates: { archived_at: nil }))
|
||||
.preload(:created_by_user, template: :author)
|
||||
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
||||
|
||||
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
||||
end
|
||||
end
|
||||
@@ -6,23 +6,8 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
load_and_authorize_resource :submission, only: %i[show destroy]
|
||||
|
||||
PRELOAD_ALL_PAGES_AMOUNT = 200
|
||||
|
||||
def show
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@submission],
|
||||
associations: [:template, { template_schema_documents: :blob }]
|
||||
).call
|
||||
|
||||
total_pages =
|
||||
@submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
|
||||
|
||||
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: @submission.template_schema_documents,
|
||||
associations: [:blob, { preview_images_attachments: :blob }]
|
||||
).call
|
||||
end
|
||||
@submission = Submissions.preload_with_pages(@submission)
|
||||
|
||||
render :show, layout: 'plain'
|
||||
end
|
||||
@@ -34,6 +19,8 @@ class SubmissionsController < ApplicationController
|
||||
def create
|
||||
authorize!(:create, Submission)
|
||||
|
||||
save_template_message(@template, params) if params[:save_message] == '1'
|
||||
|
||||
if params[:is_custom_message] != '1'
|
||||
params.delete(:subject)
|
||||
params.delete(:body)
|
||||
@@ -52,24 +39,45 @@ class SubmissionsController < ApplicationController
|
||||
user: current_user,
|
||||
source: :invite,
|
||||
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||
mark_as_sent: params[:send_email] == '1',
|
||||
submissions_attrs: submissions_params[:submission].to_h.values,
|
||||
params: params.merge('send_completed_email' => true))
|
||||
end
|
||||
|
||||
submissions.each do |submission|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
|
||||
end
|
||||
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
redirect_to template_path(@template), notice: 'New recipients have been added'
|
||||
end
|
||||
|
||||
def destroy
|
||||
@submission.update!(archived_at: Time.current)
|
||||
notice =
|
||||
if params[:permanently].present?
|
||||
@submission.destroy!
|
||||
|
||||
redirect_back(fallback_location: template_path(@submission.template), notice: 'Submission has been archived')
|
||||
'Submission has been removed'
|
||||
else
|
||||
@submission.update!(archived_at: Time.current)
|
||||
|
||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
|
||||
|
||||
'Submission has been archived'
|
||||
end
|
||||
|
||||
redirect_back(fallback_location: template_path(@submission.template), notice:)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def save_template_message(template, params)
|
||||
template.preferences['request_email_subject'] = params[:subject] if params[:subject].present?
|
||||
template.preferences['request_email_body'] = params[:body] if params[:body].present?
|
||||
|
||||
template.save!
|
||||
end
|
||||
|
||||
def submissions_params
|
||||
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
||||
end
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionsDashboardController < ApplicationController
|
||||
load_and_authorize_resource :submission, parent: false
|
||||
|
||||
def index
|
||||
@submissions = @submissions.joins(:template)
|
||||
|
||||
@submissions = @submissions.where(archived_at: nil)
|
||||
.where(templates: { archived_at: nil })
|
||||
.preload(:created_by_user, template: :author)
|
||||
|
||||
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
||||
|
||||
@submissions = @submissions.pending if params[:status] == 'pending'
|
||||
@submissions = @submissions.completed if params[:status] == 'completed'
|
||||
|
||||
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
||||
end
|
||||
end
|
||||
@@ -16,13 +16,16 @@ class SubmissionsDebugController < ApplicationController
|
||||
render 'submit_form/show'
|
||||
end
|
||||
f.pdf do
|
||||
if params[:audit]
|
||||
Submissions::GenerateAuditTrail.call(@submitter.submission)
|
||||
else
|
||||
Submissions::GenerateResultAttachments.call(@submitter)
|
||||
end
|
||||
result =
|
||||
if params[:audit]
|
||||
Submissions::GenerateAuditTrail.call(@submitter.submission)
|
||||
elsif params[:combined]
|
||||
Submissions::GenerateCombinedAttachment.call(@submitter)
|
||||
else
|
||||
Submissions::GenerateResultAttachments.call(@submitter)
|
||||
end
|
||||
|
||||
send_data ActiveStorage::Attachment.where(name: params[:audit] ? :audit_trail : :documents).last.download,
|
||||
send_data Array.wrap(result).first.download,
|
||||
filename: 'debug.pdf',
|
||||
disposition: 'inline',
|
||||
type: 'application/pdf'
|
||||
|
||||
@@ -4,8 +4,20 @@ class SubmissionsDownloadController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
TTL = 40.minutes
|
||||
FILES_TTL = 5.minutes
|
||||
|
||||
def index
|
||||
submitter = Submitter.find_by(slug: params[:submitter_slug])
|
||||
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||
|
||||
signature_valid =
|
||||
if submitter&.slug == params[:submitter_slug]
|
||||
true
|
||||
else
|
||||
submitter = nil
|
||||
end
|
||||
|
||||
submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
Submissions::EnsureResultGenerated.call(submitter)
|
||||
|
||||
@@ -13,11 +25,46 @@ class SubmissionsDownloadController < ApplicationController
|
||||
|
||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||
|
||||
urls =
|
||||
Submitters.select_attachments_for_download(last_submitter).map do |attachment|
|
||||
helpers.rails_blob_url(attachment)
|
||||
end
|
||||
return head :not_found unless last_submitter.completed_at?
|
||||
|
||||
render json: urls
|
||||
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
|
||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
end
|
||||
|
||||
if params[:combined] == 'true'
|
||||
url = build_combined_url(submitter)
|
||||
|
||||
if url
|
||||
render json: [url]
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
else
|
||||
render json: build_urls(last_submitter)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def current_user_submitter?(submitter)
|
||||
current_user && current_user.account.submitters.exists?(id: submitter.id)
|
||||
end
|
||||
|
||||
def build_urls(submitter)
|
||||
Submitters.select_attachments_for_download(submitter).map do |attachment|
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
||||
end
|
||||
end
|
||||
|
||||
def build_combined_url(submitter)
|
||||
return if submitter.submission.submitters.exists?(completed_at: nil)
|
||||
return if submitter.submission.submitters.order(:completed_at).last != submitter
|
||||
|
||||
attachment = submitter.submission.combined_document_attachment
|
||||
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter)
|
||||
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,26 +4,48 @@ class SubmissionsPreviewController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
PRELOAD_ALL_PAGES_AMOUNT = 200
|
||||
TTL = 40.minutes
|
||||
|
||||
def show
|
||||
@submission = Submission.find_by!(slug: params[:slug])
|
||||
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@submission],
|
||||
associations: [:template, { template_schema_documents: :blob }]
|
||||
).call
|
||||
signature_valid =
|
||||
if submitter && submitter.submission.slug == params[:slug]
|
||||
@submission = submitter.submission
|
||||
|
||||
total_pages =
|
||||
@submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
|
||||
true
|
||||
end
|
||||
|
||||
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: @submission.template_schema_documents,
|
||||
associations: [:blob, { preview_images_attachments: :blob }]
|
||||
).call
|
||||
@submission ||= Submission.find_by!(slug: params[:slug])
|
||||
|
||||
if !@submission.submitters.all?(&:completed_at?) && current_user.blank?
|
||||
raise ActionController::RoutingError, 'Not Found'
|
||||
end
|
||||
|
||||
if !submission_valid_ttl?(@submission) && !signature_valid
|
||||
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
|
||||
|
||||
return redirect_to submissions_preview_completed_path(@submission.slug)
|
||||
end
|
||||
|
||||
@submission = Submissions.preload_with_pages(@submission)
|
||||
|
||||
render 'submissions/show', layout: 'plain'
|
||||
end
|
||||
|
||||
def completed
|
||||
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
||||
|
||||
render :completed, layout: 'form'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def submission_valid_ttl?(submission)
|
||||
return true if current_user && current_user.account.submissions.exists?(id: submission.id)
|
||||
|
||||
last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
|
||||
|
||||
last_submitter && last_submitter.completed_at > TTL.ago
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,33 +7,39 @@ class SubmitFormController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
PRELOAD_ALL_PAGES_AMOUNT = 200
|
||||
CONFIG_KEYS = [].freeze
|
||||
|
||||
def show
|
||||
@submitter = Submitter.find_by!(slug: params[:slug])
|
||||
|
||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||
return render :archived if @submitter.submission.template.archived_at? || @submitter.submission.archived_at?
|
||||
return render :expired if @submitter.submission.expired?
|
||||
return render :declined if @submitter.declined_at?
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@submitter],
|
||||
associations: [submission: [:template, { template_schema_documents: :blob }]]
|
||||
).call
|
||||
|
||||
total_pages =
|
||||
@submitter.submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
|
||||
|
||||
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: @submitter.submission.template_schema_documents,
|
||||
associations: [:blob, { preview_images_attachments: :blob }]
|
||||
).call
|
||||
end
|
||||
Submitters.preload_with_pages(@submitter)
|
||||
|
||||
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
||||
|
||||
cookies[:submitter_sid] = @submitter.signed_id
|
||||
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
|
||||
.preload(:blob).index_by(&:uuid)
|
||||
|
||||
render(@submitter.submission.template.archived_at? || @submitter.submission.archived_at? ? :archived : :show)
|
||||
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||
|
||||
return unless @form_configs[:prefill_signature]
|
||||
|
||||
if (user_signature = UserConfigs.load_signature(current_user))
|
||||
@signature_attachment = ActiveStorage::Attachment.find_or_create_by!(
|
||||
blob_id: user_signature.blob_id,
|
||||
name: 'attachments',
|
||||
record: @submitter
|
||||
)
|
||||
end
|
||||
|
||||
@signature_attachment ||=
|
||||
Submitters::MaybeAssignDefaultBrowserSignature.call(@submitter, params, cookies, @attachments_index.values)
|
||||
|
||||
@attachments_index[@signature_attachment.uuid] = @signature_attachment if @signature_attachment
|
||||
end
|
||||
|
||||
def update
|
||||
@@ -44,11 +50,15 @@ class SubmitFormController < ApplicationController
|
||||
end
|
||||
|
||||
if submitter.template.archived_at? || submitter.submission.archived_at?
|
||||
Rollbar.info("Archived template: #{submitter.template.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
if submitter.submission.expired?
|
||||
return render json: { error: 'Form has been expired.' }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
return render json: { error: 'Form has been declined.' }, status: :unprocessable_entity if submitter.declined_at?
|
||||
|
||||
Submitters::SubmitValues.call(submitter, params, request)
|
||||
|
||||
head :ok
|
||||
@@ -57,7 +67,7 @@ class SubmitFormController < ApplicationController
|
||||
end
|
||||
|
||||
def completed
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
@submitter = Submitter.completed.find_by!(slug: params[:submit_form_slug])
|
||||
end
|
||||
|
||||
def success; end
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormDeclineController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return redirect_to submit_form_path(submitter.slug) if submitter.declined_at? ||
|
||||
submitter.completed_at? ||
|
||||
submitter.submission.archived_at? ||
|
||||
submitter.submission.expired? ||
|
||||
submitter.submission.template.archived_at?
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
submitter.update!(declined_at: Time.current)
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'decline_form', request, { reason: params[:reason] })
|
||||
end
|
||||
|
||||
user = submitter.submission.created_by_user || submitter.template.author
|
||||
|
||||
SubmitterMailer.declined_email(submitter, user).deliver_later!
|
||||
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id)
|
||||
|
||||
redirect_to submit_form_path(submitter.slug)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormDownloadController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
FILES_TTL = 5.minutes
|
||||
|
||||
def index
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return redirect_to submitter_download_index_path(submitter.slug) if submitter.completed_at?
|
||||
|
||||
return head :unprocessable_entity if submitter.declined_at? ||
|
||||
submitter.submission.archived_at? ||
|
||||
submitter.submission.expired? ||
|
||||
submitter.submission.template.archived_at?
|
||||
|
||||
last_completed_submitter = submitter.submission.submitters
|
||||
.where.not(id: submitter.id)
|
||||
.where.not(completed_at: nil)
|
||||
.max_by(&:completed_at)
|
||||
|
||||
attachments =
|
||||
if last_completed_submitter
|
||||
Submitters.select_attachments_for_download(last_completed_submitter)
|
||||
else
|
||||
submitter.submission.template.schema_documents.preload(:blob)
|
||||
end
|
||||
|
||||
urls = attachments.map do |attachment|
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
||||
end
|
||||
|
||||
render json: urls
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormDrawSignatureController < ApplicationController
|
||||
layout false
|
||||
|
||||
around_action :with_browser_locale, only: %i[show]
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
def show
|
||||
@submitter = Submitter.find_by!(slug: params[:slug])
|
||||
|
||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||
|
||||
if @submitter.submission.template.archived_at? || @submitter.submission.archived_at?
|
||||
return redirect_to submit_form_path(@submitter.slug)
|
||||
end
|
||||
|
||||
render :show
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormInviteController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return head :unprocessable_entity unless can_invite?(submitter)
|
||||
|
||||
invite_submitters = filter_invite_submitters(submitter)
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
invite_submitters.each do |item|
|
||||
attrs = submitters_attributes.find { |e| e[:uuid] == item['uuid'] }
|
||||
|
||||
next unless attrs
|
||||
|
||||
submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id)
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
|
||||
end
|
||||
|
||||
submitter.submission.update!(submitters_order: :preserved)
|
||||
end
|
||||
|
||||
submitter.submission.submitters.reload
|
||||
|
||||
if invite_submitters.all? { |s| submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } }
|
||||
Submitters::SubmitValues.call(submitter, ActionController::Parameters.new(completed: 'true'), request)
|
||||
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_invite?(submitter)
|
||||
!submitter.declined_at? &&
|
||||
!submitter.completed_at? &&
|
||||
!submitter.submission.archived_at? &&
|
||||
!submitter.submission.expired? &&
|
||||
!submitter.submission.template.archived_at?
|
||||
end
|
||||
|
||||
def filter_invite_submitters(submitter)
|
||||
(submitter.submission.template_submitters || submitter.submission.template.submitters).select do |s|
|
||||
s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] }
|
||||
end
|
||||
end
|
||||
|
||||
def submitters_attributes
|
||||
params.require(:submission).permit(submitters: [%i[uuid email]]).fetch(:submitters, [])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormValuesController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
def index
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return render json: {} if submitter.completed_at? || submitter.declined_at?
|
||||
return render json: {} if submitter.submission.template.archived_at? ||
|
||||
submitter.submission.archived_at? ||
|
||||
submitter.submission.expired?
|
||||
|
||||
value = submitter.values[params['field_uuid']]
|
||||
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
||||
|
||||
render json: {
|
||||
value:,
|
||||
attachment: attachment&.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||
}, head: :ok
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmittersAutocompleteController < ApplicationController
|
||||
load_and_authorize_resource :submitter, parent: false
|
||||
|
||||
SELECT_COLUMNS = %w[email phone name].freeze
|
||||
LIMIT = 100
|
||||
|
||||
def index
|
||||
submitters = search_submitters(@submitters)
|
||||
|
||||
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
|
||||
|
||||
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
|
||||
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
|
||||
|
||||
render json: attrs
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def search_submitters(submitters)
|
||||
if SELECT_COLUMNS.include?(params[:field])
|
||||
column = Submitter.arel_table[params[:field].to_sym]
|
||||
|
||||
term = "#{params[:q].downcase}%"
|
||||
|
||||
submitters.where(column.matches(term))
|
||||
else
|
||||
Submitters.search(submitters, params[:q])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7,11 +7,13 @@ class SubmittersSendEmailController < ApplicationController
|
||||
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
|
||||
event_type: 'send_email',
|
||||
created_at: 24.hours.ago..Time.current)
|
||||
Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return redirect_back(fallback_location: submission_path(@submitter.submission),
|
||||
alert: 'Email has been sent already.')
|
||||
end
|
||||
|
||||
SendSubmitterInvitationEmailJob.perform_later('submitter_id' => @submitter.id)
|
||||
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id)
|
||||
|
||||
@submitter.sent_at ||= Time.current
|
||||
@submitter.save!
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplateDocumentsController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def create
|
||||
if params[:blobs].blank? && params[:files].blank?
|
||||
return render json: { error: 'File is missing' }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
old_fields_hash = @template.fields.hash
|
||||
|
||||
documents = Templates::CreateAttachments.call(@template, params, extract_fields: true)
|
||||
|
||||
schema = documents.map do |doc|
|
||||
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
||||
end
|
||||
|
||||
render json: {
|
||||
schema:,
|
||||
fields: old_fields_hash == @template.fields.hash ? nil : @template.fields,
|
||||
submitters: old_fields_hash == @template.fields.hash ? nil : @template.submitters,
|
||||
documents: documents.as_json(
|
||||
methods: %i[metadata signed_uuid],
|
||||
include: {
|
||||
preview_images: { methods: %i[url metadata filename] }
|
||||
}
|
||||
)
|
||||
}
|
||||
rescue Templates::CreateAttachments::PdfEncrypted
|
||||
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplateFoldersAutocompleteController < ApplicationController
|
||||
load_and_authorize_resource :template_folder, parent: false
|
||||
|
||||
LIMIT = 100
|
||||
|
||||
def index
|
||||
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
|
||||
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
||||
|
||||
render json: template_folders.as_json(only: %i[name archived_at])
|
||||
end
|
||||
end
|
||||
@@ -6,7 +6,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
|
||||
|
||||
def index
|
||||
@submissions = @submissions.where.not(archived_at: nil)
|
||||
@submissions = Submissions.search(@submissions, params[:q])
|
||||
@submissions = Submissions.search(@submissions, params[:q], search_values: true)
|
||||
|
||||
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
|
||||
@@ -8,7 +8,7 @@ class TemplatesController < ApplicationController
|
||||
def show
|
||||
submissions = @template.submissions.accessible_by(current_ability)
|
||||
submissions = submissions.active if @template.archived_at.blank?
|
||||
submissions = Submissions.search(submissions, params[:q])
|
||||
submissions = Submissions.search(submissions, params[:q], search_values: true)
|
||||
|
||||
@base_submissions = submissions
|
||||
|
||||
@@ -27,13 +27,13 @@ class TemplatesController < ApplicationController
|
||||
def edit
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@template],
|
||||
associations: [schema_documents: { preview_images_attachments: :blob }]
|
||||
associations: [schema_documents: [:blob, { preview_images_attachments: :blob }]]
|
||||
).call
|
||||
|
||||
@template_data =
|
||||
@template.as_json.merge(
|
||||
documents: @template.schema_documents.as_json(
|
||||
methods: [:metadata],
|
||||
methods: %i[metadata signed_uuid],
|
||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||
)
|
||||
).to_json
|
||||
@@ -43,6 +43,11 @@ class TemplatesController < ApplicationController
|
||||
|
||||
def create
|
||||
if @base_template
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@base_template],
|
||||
associations: [schema_documents: :preview_images_attachments]
|
||||
).call
|
||||
|
||||
@template = Templates::Clone.call(@base_template, author: current_user,
|
||||
name: params.dig(:template, :name),
|
||||
folder_name: params[:folder_name])
|
||||
@@ -51,24 +56,37 @@ class TemplatesController < ApplicationController
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||
end
|
||||
|
||||
@template.account = current_account
|
||||
if params[:account_id].present? && authorized_clone_account_id?(params[:account_id])
|
||||
@template.account_id = params[:account_id]
|
||||
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
|
||||
else
|
||||
@template.account = current_account
|
||||
end
|
||||
|
||||
if @template.save
|
||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||
|
||||
redirect_to edit_template_path(@template)
|
||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
||||
|
||||
maybe_redirect_to_template(@template)
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@template.update!(template_params)
|
||||
|
||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
def destroy
|
||||
notice =
|
||||
if params[:permanently].present?
|
||||
@template.destroy!
|
||||
|
||||
Rollbar.info("Remove template: #{@template.id}") if defined?(Rollbar)
|
||||
|
||||
'Template has been removed.'
|
||||
else
|
||||
@template.update!(archived_at: Time.current)
|
||||
@@ -82,7 +100,31 @@ class TemplatesController < ApplicationController
|
||||
private
|
||||
|
||||
def template_params
|
||||
params.require(:template).permit(:name)
|
||||
params.require(:template).permit(
|
||||
:name,
|
||||
{ schema: [%i[attachment_uuid name]],
|
||||
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid email]],
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description,
|
||||
{ preferences: {},
|
||||
conditions: [%i[field_uuid value action]],
|
||||
options: [%i[value uuid]],
|
||||
validation: %i[message pattern],
|
||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||
)
|
||||
end
|
||||
|
||||
def authorized_clone_account_id?(account_id)
|
||||
true_user.account_id.to_s == account_id.to_s || true_user.account.linked_accounts.exists?(id: account_id)
|
||||
end
|
||||
|
||||
def maybe_redirect_to_template(template)
|
||||
if template.account == current_account
|
||||
redirect_to(edit_template_path(@template))
|
||||
else
|
||||
redirect_back(fallback_location: root_path, notice: 'Template has been cloned')
|
||||
end
|
||||
end
|
||||
|
||||
def load_base_template
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesDashboardController < ApplicationController
|
||||
load_and_authorize_resource :template_folder, parent: false
|
||||
load_and_authorize_resource :template, parent: false
|
||||
|
||||
SHOW_TEMPLATES_FOLDERS_THRESHOLD = 9
|
||||
TEMPLATES_PER_PAGE = 12
|
||||
FOLDERS_PER_PAGE = 18
|
||||
|
||||
def index
|
||||
@template_folders = filter_template_folders(@template_folders)
|
||||
|
||||
@pagy, @template_folders = pagy(
|
||||
@template_folders,
|
||||
items: FOLDERS_PER_PAGE,
|
||||
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
|
||||
)
|
||||
|
||||
if @pagy.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD
|
||||
@templates = @templates.none
|
||||
else
|
||||
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
||||
@templates = filter_templates(@templates)
|
||||
|
||||
items =
|
||||
if @template_folders.size < 4
|
||||
TEMPLATES_PER_PAGE
|
||||
else
|
||||
(@template_folders.size < 7 ? 9 : 6)
|
||||
end
|
||||
|
||||
@pagy, @templates = pagy(@templates, items:)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_template_folders(template_folders)
|
||||
rel = template_folders.joins(:active_templates)
|
||||
.order(id: :desc)
|
||||
.distinct
|
||||
|
||||
TemplateFolders.search(rel, params[:q])
|
||||
end
|
||||
|
||||
def filter_templates(templates)
|
||||
rel = templates.active.preload(:author).order(id: :desc)
|
||||
|
||||
if params[:q].blank?
|
||||
if Docuseal.multitenant? && !current_account.testing?
|
||||
rel = rel.where(folder_id: current_account.default_template_folder.id)
|
||||
else
|
||||
shared_template_ids =
|
||||
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
|
||||
|
||||
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
|
||||
end
|
||||
end
|
||||
|
||||
Templates.search(rel, params[:q])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesDebugController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def show
|
||||
attachment = @template.documents.first
|
||||
|
||||
pdf = HexaPDF::Document.new(io: StringIO.new(attachment.download))
|
||||
|
||||
fields = Templates::FindAcroFields.call(pdf, attachment)
|
||||
|
||||
attachment.metadata['pdf'] ||= {}
|
||||
attachment.metadata['pdf']['fields'] = fields
|
||||
|
||||
@template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment]))
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@template],
|
||||
associations: [schema_documents: { preview_images_attachments: :blob }]
|
||||
).call
|
||||
|
||||
@template_data =
|
||||
@template.as_json.merge(
|
||||
documents: @template.schema_documents.as_json(
|
||||
methods: %i[metadata signed_uuid],
|
||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||
)
|
||||
).to_json
|
||||
|
||||
render 'templates/edit', layout: 'plain'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesFormPreviewController < ApplicationController
|
||||
layout 'form'
|
||||
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def show
|
||||
@submitter = Submitter.new(uuid: params[:uuid] || @template.submitters.first['uuid'],
|
||||
account: current_account,
|
||||
submission: @template.submissions.new(template_submitters: @template.submitters,
|
||||
account: current_account))
|
||||
|
||||
@submitter.submission.submitters = @template.submitters.map { |item| Submitter.new(uuid: item['uuid']) }
|
||||
|
||||
Submitters.preload_with_pages(@submitter)
|
||||
|
||||
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
|
||||
.preload(:blob).index_by(&:uuid)
|
||||
|
||||
@form_configs = Submitters::FormConfigs.call(@submitter)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesPreferencesController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
authorize!(:update, @template)
|
||||
|
||||
@template.preferences = @template.preferences.merge(template_params[:preferences])
|
||||
@template.preferences = @template.preferences.reject { |_, v| (v.is_a?(String) || v.is_a?(Hash)) && v.blank? }
|
||||
@template.save!
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def template_params
|
||||
params.require(:template).permit(
|
||||
preferences: %i[bcc_completed request_email_subject request_email_body
|
||||
documents_copy_email_subject documents_copy_email_body
|
||||
documents_copy_email_enabled documents_copy_email_attach_audit
|
||||
completed_notification_email_attach_documents
|
||||
completed_redirect_url
|
||||
completed_notification_email_subject completed_notification_email_body
|
||||
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
||||
[completed_message: %i[title body]]
|
||||
).tap do |attrs|
|
||||
attrs[:preferences] = attrs[:preferences].transform_values do |value|
|
||||
if %w[true false].include?(value)
|
||||
value == 'true'
|
||||
elsif value.respond_to?(:compact_blank)
|
||||
value.compact_blank
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -12,7 +12,7 @@ class TemplatesPreviewController < ApplicationController
|
||||
@template_data =
|
||||
@template.as_json.merge(
|
||||
documents: @template.schema_documents.as_json(
|
||||
methods: [:metadata],
|
||||
methods: %i[metadata signed_uuid],
|
||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||
)
|
||||
).to_json
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesRecipientsController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def create
|
||||
authorize!(:update, @template)
|
||||
|
||||
@template.submitters =
|
||||
submitters_params.map { |s| s.reject { |_, v| v.is_a?(String) && v.blank? } }
|
||||
|
||||
@template.save!
|
||||
|
||||
render json: { submitters: @template.submitters }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def submitters_params
|
||||
params.require(:template).permit(
|
||||
submitters: [%i[name uuid is_requester invite_by_uuid linked_to_uuid email option]]
|
||||
).fetch(:submitters, {}).values.filter_map do |s|
|
||||
next if s[:uuid].blank?
|
||||
|
||||
if s[:is_requester] == '1' && s[:invite_by_uuid].blank?
|
||||
s[:is_requester] = true
|
||||
else
|
||||
s.delete(:is_requester)
|
||||
end
|
||||
|
||||
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
|
||||
|
||||
option = s.delete(:option)
|
||||
|
||||
if option.present?
|
||||
case option
|
||||
when 'is_requester'
|
||||
s[:is_requester] = true
|
||||
when 'not_set'
|
||||
s.delete(:is_requester)
|
||||
s.delete(:email)
|
||||
s.delete(:linked_to_uuid)
|
||||
s.delete(:invite_by_uuid)
|
||||
when /\Alinked_to_(.*)\z/
|
||||
s[:linked_to_uuid] = ::Regexp.last_match(-1)
|
||||
when /\Ainvite_by_(.*)\z/
|
||||
s[:invite_by_uuid] = ::Regexp.last_match(-1)
|
||||
end
|
||||
end
|
||||
|
||||
s
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -12,18 +12,27 @@ class TemplatesUploadsController < ApplicationController
|
||||
|
||||
save_template!(@template, url_params)
|
||||
|
||||
documents = Templates::CreateAttachments.call(@template, url_params || params)
|
||||
|
||||
documents = Templates::CreateAttachments.call(@template, url_params || params, extract_fields: true)
|
||||
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||
|
||||
if @template.fields.blank?
|
||||
@template.fields = Templates::ProcessDocument.normalize_attachment_fields(@template, documents)
|
||||
|
||||
schema.each { |item| item['pending_fields'] = true } if @template.fields.present?
|
||||
end
|
||||
|
||||
@template.update!(schema:)
|
||||
|
||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
||||
|
||||
redirect_to edit_template_path(@template)
|
||||
rescue Templates::CreateAttachments::PdfEncrypted
|
||||
render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password)
|
||||
rescue StandardError => e
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
raise if Rails.env.local?
|
||||
|
||||
redirect_to root_path, alert: 'Unable to upload file'
|
||||
end
|
||||
|
||||
@@ -43,23 +52,17 @@ class TemplatesUploadsController < ApplicationController
|
||||
def create_file_params_from_url
|
||||
tempfile = Tempfile.new
|
||||
tempfile.binmode
|
||||
tempfile.write(conn.get(Addressable::URI.parse(params[:url]).display_uri.to_s).body)
|
||||
tempfile.write(DownloadUtils.call(params[:url]).body)
|
||||
tempfile.rewind
|
||||
|
||||
file = ActionDispatch::Http::UploadedFile.new(
|
||||
tempfile:,
|
||||
filename: File.basename(
|
||||
URI.decode_www_form_component(params[:filename].presence || params[:url])
|
||||
URI.decode_www_form_component(params[:filename].presence || params[:url]), '.*'
|
||||
),
|
||||
type: Marcel::MimeType.for(tempfile)
|
||||
)
|
||||
|
||||
{ files: [file] }
|
||||
end
|
||||
|
||||
def conn
|
||||
Faraday.new do |faraday|
|
||||
faraday.response :follow_redirects
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TimestampServerController < ApplicationController
|
||||
HASH_ALGORITHM = 'SHA256'
|
||||
|
||||
before_action :build_encrypted_config
|
||||
authorize_resource :encrypted_config
|
||||
|
||||
TimestampError = Class.new(StandardError)
|
||||
|
||||
def create
|
||||
return head :not_found if Docuseal.multitenant?
|
||||
|
||||
@@ -14,25 +18,29 @@ class TimestampServerController < ApplicationController
|
||||
else
|
||||
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
||||
end
|
||||
rescue HexaPDF::Error, SocketError, Submissions::TimestampHandler::TimestampError, OpenSSL::Timestamp::TimestampError
|
||||
rescue SocketError, TimestampError, OpenSSL::Timestamp::TimestampError
|
||||
redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def test_timeserver_url(url)
|
||||
pdf = HexaPDF::Document.new
|
||||
pdf.pages.add
|
||||
req = OpenSSL::Timestamp::Request.new
|
||||
req.algorithm = HASH_ALGORITHM
|
||||
req.message_imprint = OpenSSL::Digest.digest(HASH_ALGORITHM, 'test')
|
||||
|
||||
pkcs = Accounts.load_signing_pkcs(current_account)
|
||||
uri = Addressable::URI.parse(url)
|
||||
|
||||
pdf.sign(StringIO.new,
|
||||
reason: 'Test',
|
||||
certificate: pkcs.certificate,
|
||||
key: pkcs.key,
|
||||
signature_size: 10_000,
|
||||
certificate_chain: pkcs.ca_certs || [],
|
||||
timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url))
|
||||
conn = Faraday.new(uri.origin) do |c|
|
||||
c.basic_auth(uri.user, uri.password) if uri.password.present?
|
||||
end
|
||||
|
||||
response = conn.post(uri.path, req.to_der,
|
||||
'content-type' => 'application/timestamp-query')
|
||||
|
||||
raise TimestampError if response.status != 200 || response.body.blank?
|
||||
|
||||
response
|
||||
end
|
||||
|
||||
def load_encrypted_config
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UserConfigsController < ApplicationController
|
||||
before_action :load_user_config
|
||||
authorize_resource :user_config
|
||||
|
||||
ALLOWED_KEYS = [
|
||||
UserConfig::RECEIVE_COMPLETED_EMAIL
|
||||
].freeze
|
||||
|
||||
InvalidKey = Class.new(StandardError)
|
||||
|
||||
def create
|
||||
@user_config.update!(user_config_params)
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_user_config
|
||||
raise InvalidKey unless ALLOWED_KEYS.include?(user_config_params[:key])
|
||||
|
||||
@user_config =
|
||||
UserConfig.find_or_initialize_by(user: current_user, key: user_config_params[:key])
|
||||
end
|
||||
|
||||
def user_config_params
|
||||
params.required(:user_config).permit!.tap do |attrs|
|
||||
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UserInitialsController < ApplicationController
|
||||
before_action :load_user_config
|
||||
authorize_resource :user_config
|
||||
|
||||
def edit; end
|
||||
|
||||
def update
|
||||
file = params[:file]
|
||||
|
||||
return redirect_to settings_profile_index_path, notice: 'Unable to save initials' if file.blank?
|
||||
|
||||
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
|
||||
filename: file.original_filename,
|
||||
content_type: file.content_type)
|
||||
|
||||
attachment = ActiveStorage::Attachment.create!(
|
||||
blob:,
|
||||
name: 'initials',
|
||||
record: current_user
|
||||
)
|
||||
|
||||
if @user_config.update(value: attachment.uuid)
|
||||
redirect_to settings_profile_index_path, notice: 'Initials has been saved'
|
||||
else
|
||||
redirect_to settings_profile_index_path, notice: 'Unable to save initials'
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@user_config.destroy
|
||||
|
||||
redirect_to settings_profile_index_path, notice: 'Initials has been removed'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_user_config
|
||||
@user_config =
|
||||
UserConfig.find_or_initialize_by(user: current_user, key: UserConfig::INITIALS_KEY)
|
||||
end
|
||||
end
|
||||
@@ -1,13 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UsersController < ApplicationController
|
||||
load_and_authorize_resource :user, only: %i[index edit new update destroy]
|
||||
load_and_authorize_resource :user, only: %i[index edit update destroy]
|
||||
|
||||
before_action :build_user, only: :create
|
||||
authorize_resource :user, only: :create
|
||||
before_action :build_user, only: %i[new create]
|
||||
authorize_resource :user, only: %i[new create]
|
||||
|
||||
def index
|
||||
@pagy, @users = pagy(@users.active.order(id: :desc))
|
||||
@users =
|
||||
if params[:status] == 'archived'
|
||||
@users.archived
|
||||
else
|
||||
@users.active
|
||||
end
|
||||
|
||||
@pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
|
||||
end
|
||||
|
||||
def new; end
|
||||
@@ -15,10 +22,20 @@ class UsersController < ApplicationController
|
||||
def edit; end
|
||||
|
||||
def create
|
||||
existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
|
||||
|
||||
if existing_user
|
||||
existing_user.archived_at = nil
|
||||
existing_user.assign_attributes(user_params)
|
||||
existing_user.account = current_account
|
||||
|
||||
@user = existing_user
|
||||
end
|
||||
|
||||
if @user.save
|
||||
UserMailer.invitation_email(@user).deliver_later!
|
||||
|
||||
redirect_to settings_users_path, notice: 'User has been invited'
|
||||
redirect_back fallback_location: settings_users_path, notice: 'User has been invited'
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
|
||||
end
|
||||
@@ -27,11 +44,19 @@ class UsersController < ApplicationController
|
||||
def update
|
||||
return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
|
||||
|
||||
attrs = user_params.compact_blank
|
||||
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
|
||||
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
|
||||
|
||||
if params.dig(:user, :account_id).present?
|
||||
account = Account.accessible_by(current_ability).find(params[:user][:account_id])
|
||||
|
||||
authorize!(:manage, account)
|
||||
|
||||
@user.account = account
|
||||
end
|
||||
|
||||
if @user.update(attrs)
|
||||
redirect_to settings_users_path, notice: 'User has been updated'
|
||||
redirect_back fallback_location: settings_users_path, notice: 'User has been updated'
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
|
||||
end
|
||||
@@ -44,7 +69,7 @@ class UsersController < ApplicationController
|
||||
|
||||
@user.update!(archived_at: Time.current)
|
||||
|
||||
redirect_to settings_users_path, notice: 'User has been removed'
|
||||
redirect_back fallback_location: settings_users_path, notice: 'User has been removed'
|
||||
end
|
||||
|
||||
private
|
||||
@@ -65,6 +90,11 @@ class UsersController < ApplicationController
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:email, :first_name, :last_name, :password, :role)
|
||||
if params.key?(:user)
|
||||
params.require(:user).permit(:email, :first_name, :last_name, :password,
|
||||
:role, :archived_at, :account_id)
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,22 +9,7 @@ class VerifyPdfSignatureController < ApplicationController
|
||||
HexaPDF::Document.new(io: file.open)
|
||||
end
|
||||
|
||||
cert_data = if Docuseal.multitenant?
|
||||
Docuseal::CERTS
|
||||
else
|
||||
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||
end
|
||||
|
||||
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
||||
|
||||
custom_certs = cert_data.fetch('custom', []).map do |e|
|
||||
OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s)
|
||||
end
|
||||
|
||||
trusted_certs = [default_pkcs.certificate,
|
||||
*default_pkcs.ca_certs,
|
||||
*custom_certs.map(&:certificate),
|
||||
*custom_certs.flat_map(&:ca_certs).compact]
|
||||
trusted_certs = Accounts.load_trusted_certs(current_account)
|
||||
|
||||
render turbo_stream: turbo_stream.replace('result', partial: 'result',
|
||||
locals: { pdfs:, files: params[:files], trusted_certs: })
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WebhookPreferencesController < ApplicationController
|
||||
EVENTS = %w[
|
||||
form.viewed
|
||||
form.started
|
||||
form.completed
|
||||
form.declined
|
||||
template.created
|
||||
template.updated
|
||||
submission.created
|
||||
submission.archived
|
||||
].freeze
|
||||
|
||||
before_action :load_account_config
|
||||
authorize_resource :account_config, parent: false
|
||||
|
||||
def create
|
||||
@account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
|
||||
|
||||
@account_config.save!
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_account_config
|
||||
@account_config =
|
||||
current_account.account_configs.find_or_initialize_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
|
||||
@account_config.value ||= {}
|
||||
end
|
||||
|
||||
def account_config_params
|
||||
params.permit(:event, :value)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WebhookSecretController < ApplicationController
|
||||
before_action :load_encrypted_config
|
||||
authorize_resource :encrypted_config, parent: false
|
||||
|
||||
def index; end
|
||||
|
||||
def create
|
||||
@encrypted_config.assign_attributes(value: {
|
||||
encrypted_config_params[:key] => encrypted_config_params[:value]
|
||||
}.compact_blank)
|
||||
|
||||
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
|
||||
|
||||
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook Secret has been saved.')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_encrypted_config
|
||||
@encrypted_config =
|
||||
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_SECRET_KEY)
|
||||
end
|
||||
|
||||
def encrypted_config_params
|
||||
params.require(:encrypted_config).permit(value: %i[key value]).fetch(:value, {})
|
||||
end
|
||||
end
|
||||
@@ -17,7 +17,8 @@ class WebhookSettingsController < ApplicationController
|
||||
def update
|
||||
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
|
||||
|
||||
SendFormCompletedWebhookRequestJob.perform_later(submitter)
|
||||
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
|
||||
'encrypted_config_id' => @encrypted_config.id })
|
||||
|
||||
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook request has been sent.')
|
||||
end
|
||||
|
||||
@@ -3,8 +3,10 @@ import { encodeMethodIntoRequestBody } from '@hotwired/turbo-rails/app/javascrip
|
||||
|
||||
import { createApp, reactive } from 'vue'
|
||||
import TemplateBuilder from './template_builder/builder'
|
||||
import ImportList from './template_builder/import_list'
|
||||
|
||||
import ToggleVisible from './elements/toggle_visible'
|
||||
import ToggleCookies from './elements/toggle_cookies'
|
||||
import DisableHidden from './elements/disable_hidden'
|
||||
import TurboModal from './elements/turbo_modal'
|
||||
import FileDropzone from './elements/file_dropzone'
|
||||
@@ -20,6 +22,13 @@ import FolderAutocomplete from './elements/folder_autocomplete'
|
||||
import SignatureForm from './elements/signature_form'
|
||||
import SubmitForm from './elements/submit_form'
|
||||
import PromptPassword from './elements/prompt_password'
|
||||
import EmailsTextarea from './elements/emails_textarea'
|
||||
import ToggleOnSubmit from './elements/toggle_on_submit'
|
||||
import PasswordInput from './elements/password_input'
|
||||
import SearchInput from './elements/search_input'
|
||||
import ToggleAttribute from './elements/toggle_attribute'
|
||||
import LinkedInput from './elements/linked_input'
|
||||
import CheckboxGroup from './elements/checkbox_group'
|
||||
|
||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||
|
||||
@@ -37,23 +46,6 @@ document.addEventListener('keyup', (e) => {
|
||||
}
|
||||
})
|
||||
|
||||
window.customElements.define('toggle-visible', ToggleVisible)
|
||||
window.customElements.define('disable-hidden', DisableHidden)
|
||||
window.customElements.define('turbo-modal', TurboModal)
|
||||
window.customElements.define('file-dropzone', FileDropzone)
|
||||
window.customElements.define('menu-active', MenuActive)
|
||||
window.customElements.define('clipboard-copy', ClipboardCopy)
|
||||
window.customElements.define('dynamic-list', DynamicList)
|
||||
window.customElements.define('download-button', DownloadButton)
|
||||
window.customElements.define('set-origin-url', SetOriginUrl)
|
||||
window.customElements.define('set-timezone', SetTimezone)
|
||||
window.customElements.define('autoresize-textarea', AutoresizeTextarea)
|
||||
window.customElements.define('submitters-autocomplete', SubmittersAutocomplete)
|
||||
window.customElements.define('folder-autocomplete', FolderAutocomplete)
|
||||
window.customElements.define('signature-form', SignatureForm)
|
||||
window.customElements.define('submit-form', SubmitForm)
|
||||
window.customElements.define('prompt-password', PromptPassword)
|
||||
|
||||
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
||||
document.addEventListener('turbo:submit-end', async (event) => {
|
||||
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
|
||||
@@ -77,8 +69,37 @@ document.addEventListener('turbo:submit-end', async (event) => {
|
||||
URL.revokeObjectURL(url)
|
||||
})
|
||||
|
||||
window.customElements.define('template-builder', class extends HTMLElement {
|
||||
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
||||
|
||||
safeRegisterElement('toggle-visible', ToggleVisible)
|
||||
safeRegisterElement('disable-hidden', DisableHidden)
|
||||
safeRegisterElement('turbo-modal', TurboModal)
|
||||
safeRegisterElement('file-dropzone', FileDropzone)
|
||||
safeRegisterElement('menu-active', MenuActive)
|
||||
safeRegisterElement('clipboard-copy', ClipboardCopy)
|
||||
safeRegisterElement('dynamic-list', DynamicList)
|
||||
safeRegisterElement('download-button', DownloadButton)
|
||||
safeRegisterElement('set-origin-url', SetOriginUrl)
|
||||
safeRegisterElement('set-timezone', SetTimezone)
|
||||
safeRegisterElement('autoresize-textarea', AutoresizeTextarea)
|
||||
safeRegisterElement('submitters-autocomplete', SubmittersAutocomplete)
|
||||
safeRegisterElement('folder-autocomplete', FolderAutocomplete)
|
||||
safeRegisterElement('signature-form', SignatureForm)
|
||||
safeRegisterElement('submit-form', SubmitForm)
|
||||
safeRegisterElement('prompt-password', PromptPassword)
|
||||
safeRegisterElement('emails-textarea', EmailsTextarea)
|
||||
safeRegisterElement('toggle-cookies', ToggleCookies)
|
||||
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
|
||||
safeRegisterElement('password-input', PasswordInput)
|
||||
safeRegisterElement('search-input', SearchInput)
|
||||
safeRegisterElement('toggle-attribute', ToggleAttribute)
|
||||
safeRegisterElement('linked-input', LinkedInput)
|
||||
safeRegisterElement('checkbox-group', CheckboxGroup)
|
||||
|
||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
document.addEventListener('turbo:submit-end', this.onSubmit)
|
||||
|
||||
this.appElem = document.createElement('div')
|
||||
|
||||
this.appElem.classList.add('md:h-screen')
|
||||
@@ -86,13 +107,50 @@ window.customElements.define('template-builder', class extends HTMLElement {
|
||||
this.app = createApp(TemplateBuilder, {
|
||||
template: reactive(JSON.parse(this.dataset.template)),
|
||||
backgroundColor: '#faf7f5',
|
||||
locale: this.dataset.locale,
|
||||
withPhone: this.dataset.withPhone === 'true',
|
||||
withLogo: this.dataset.withLogo !== 'false',
|
||||
editable: this.dataset.editable !== 'false',
|
||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||
withPayment: this.dataset.withPayment === 'true',
|
||||
isPaymentConnected: this.dataset.isPaymentConnected === 'true',
|
||||
withFormula: this.dataset.withFormula === 'true',
|
||||
withSendButton: this.dataset.withSendButton !== 'false',
|
||||
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
||||
withConditions: this.dataset.withConditions === 'true',
|
||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
||||
acceptFileTypes: this.dataset.acceptFileTypes
|
||||
})
|
||||
|
||||
this.component = this.app.mount(this.appElem)
|
||||
|
||||
this.appendChild(this.appElem)
|
||||
}
|
||||
|
||||
onSubmit = (e) => {
|
||||
if (e.detail.success && e.detail?.formSubmission?.formElement?.id === 'submitters_form') {
|
||||
e.detail.fetchResponse.response.json().then((data) => {
|
||||
this.component.template.submitters = data.submitters
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
document.removeEventListener('turbo:submit-end', this.onSubmit)
|
||||
|
||||
this.app?.unmount()
|
||||
this.appElem?.remove()
|
||||
}
|
||||
})
|
||||
|
||||
safeRegisterElement('import-list', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.appElem = document.createElement('div')
|
||||
|
||||
this.app = createApp(ImportList, {
|
||||
template: JSON.parse(this.dataset.template),
|
||||
multitenant: this.dataset.multitenant === 'true',
|
||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content
|
||||
})
|
||||
|
||||
this.app.mount(this.appElem)
|
||||
|
||||
@@ -51,6 +51,10 @@ button[disabled] .enabled {
|
||||
@apply input input-bordered bg-white;
|
||||
}
|
||||
|
||||
.base-input-slim {
|
||||
@apply input input-bordered bg-white h-10;
|
||||
}
|
||||
|
||||
.base-textarea {
|
||||
@apply textarea textarea-bordered bg-white rounded-3xl;
|
||||
}
|
||||
@@ -75,16 +79,21 @@ button[disabled] .enabled {
|
||||
@apply select base-input w-full font-normal;
|
||||
}
|
||||
|
||||
.tooltip-bottom-start:before {
|
||||
transform: translateX(-30%);
|
||||
.tooltip-bottom-end:before {
|
||||
transform: translateX(-95%);
|
||||
top: var(--tooltip-offset);
|
||||
left: 100%;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.tooltip-bottom-start:after {
|
||||
transform: translateX(-75%);
|
||||
.tooltip-pre:before {
|
||||
white-space: pre;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tooltip-bottom-end:after {
|
||||
transform: translateX(-25%);
|
||||
border-color: transparent transparent var(--tooltip-color) transparent;
|
||||
top: var(--tooltip-tail-offset);
|
||||
left: 50%;
|
||||
@@ -95,7 +104,7 @@ button[disabled] .enabled {
|
||||
.autocomplete {
|
||||
background: white;
|
||||
z-index: 1000;
|
||||
font: 14px/22px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
font: 16px/25px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
@apply border border-base-300 mt-1 rounded-md;
|
||||
@@ -106,7 +115,7 @@ button[disabled] .enabled {
|
||||
}
|
||||
|
||||
.autocomplete > div {
|
||||
@apply px-2 py-1 font-normal text-sm;
|
||||
@apply px-2 py-1.5 font-normal;
|
||||
}
|
||||
|
||||
.autocomplete .group {
|
||||
@@ -125,3 +134,7 @@ button[disabled] .enabled {
|
||||
outline-offset: 3px;
|
||||
outline-color: hsl(var(--bc) / 0.2);
|
||||
}
|
||||
|
||||
select:required:invalid {
|
||||
color: gray !important;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import SignaturePad from 'signature_pad'
|
||||
import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas'
|
||||
|
||||
window.customElements.define('draw-signature', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const scale = 3
|
||||
|
||||
this.canvas.width = this.canvas.parentNode.clientWidth * scale
|
||||
this.canvas.height = this.canvas.parentNode.clientHeight * scale
|
||||
|
||||
this.canvas.getContext('2d').scale(scale, scale)
|
||||
|
||||
this.pad = new SignaturePad(this.canvas)
|
||||
|
||||
this.pad.addEventListener('endStroke', () => {
|
||||
this.updateSubmitButtonVisibility()
|
||||
})
|
||||
|
||||
this.clearButton.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
this.clearSignaturePad()
|
||||
})
|
||||
|
||||
this.form.addEventListener('submit', (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
this.submitButton.disabled = true
|
||||
|
||||
this.submitImage().then((data) => {
|
||||
this.valueInput.value = data.uuid
|
||||
|
||||
return fetch(this.form.action, {
|
||||
method: 'PUT',
|
||||
body: new FormData(this.form)
|
||||
}).then((response) => {
|
||||
this.form.classList.add('hidden')
|
||||
this.success.classList.remove('hidden')
|
||||
|
||||
return response
|
||||
})
|
||||
}).finally(() => {
|
||||
this.submitButton.disabled = false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
clearSignaturePad () {
|
||||
this.pad.clear()
|
||||
this.updateSubmitButtonVisibility()
|
||||
}
|
||||
|
||||
updateSubmitButtonVisibility () {
|
||||
if (this.pad.isEmpty()) {
|
||||
this.submitButton.style.display = 'none'
|
||||
this.placeholderButton.style.display = 'block'
|
||||
} else {
|
||||
this.submitButton.style.display = 'block'
|
||||
this.placeholderButton.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
async submitImage () {
|
||||
return new Promise((resolve, reject) => {
|
||||
cropCanvasAndExportToPNG(this.canvas, { errorOnTooSmall: true }).then(async (blob) => {
|
||||
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('file', file)
|
||||
formData.append('submitter_slug', this.dataset.slug)
|
||||
formData.append('name', 'attachments')
|
||||
formData.append('remember_signature', 'true')
|
||||
|
||||
return fetch('/api/attachments', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then((resp) => resp.json()).then((attachment) => {
|
||||
return resolve(attachment)
|
||||
})
|
||||
}).catch((error) => {
|
||||
return reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
get submitButton () {
|
||||
return this.querySelector('button[type="submit"]')
|
||||
}
|
||||
|
||||
get clearButton () {
|
||||
return this.querySelector('button[aria-label="Clear"]')
|
||||
}
|
||||
|
||||
get placeholderButton () {
|
||||
return this.querySelector('button[disabled]')
|
||||
}
|
||||
|
||||
get canvas () {
|
||||
return this.querySelector('canvas')
|
||||
}
|
||||
|
||||
get valueInput () {
|
||||
return this.querySelector('input[name^="values"]')
|
||||
}
|
||||
|
||||
get form () {
|
||||
return this.querySelector('form')
|
||||
}
|
||||
|
||||
get success () {
|
||||
return this.querySelector('#success')
|
||||
}
|
||||
})
|
||||
@@ -7,7 +7,7 @@ export default class extends HTMLElement {
|
||||
|
||||
resize () {
|
||||
if (this.textarea.clientHeight < this.textarea.scrollHeight) {
|
||||
this.textarea.style.height = `${this.textarea.scrollHeight}px`
|
||||
this.textarea.style.height = `${this.textarea.scrollHeight + 5}px`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.items.forEach((item) => {
|
||||
item.addEventListener('change', (e) => {
|
||||
this.items.forEach((item) => {
|
||||
item.checked = item === e.target && e.target.checked
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
get items () {
|
||||
return this.querySelectorAll('input[type="checkbox"]')
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,18 @@ export default targetable(class extends HTMLElement {
|
||||
|
||||
this.toggleState()
|
||||
|
||||
fetch(this.dataset.src).then((response) => response.json()).then((urls) => {
|
||||
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||
fetch(this.dataset.src).then(async (response) => {
|
||||
if (response.ok) {
|
||||
const urls = await response.json()
|
||||
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||
|
||||
if (isSafariIos && urls.length > 1) {
|
||||
this.downloadSafariIos(urls)
|
||||
if (isSafariIos && urls.length > 1) {
|
||||
this.downloadSafariIos(urls)
|
||||
} else {
|
||||
this.downloadUrls(urls)
|
||||
}
|
||||
} else {
|
||||
this.downloadUrls(urls)
|
||||
alert('Failed to download files')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -48,9 +53,9 @@ export default targetable(class extends HTMLElement {
|
||||
fileRequests.reduce(
|
||||
(prevPromise, request) => prevPromise.then(() => request()),
|
||||
Promise.resolve()
|
||||
)
|
||||
|
||||
this.toggleState()
|
||||
).finally(() => {
|
||||
this.toggleState()
|
||||
})
|
||||
}
|
||||
|
||||
downloadSafariIos (urls) {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
const emailRegexp = /([^@;,<>\s]+@[^@;,<>\s]+)/g
|
||||
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
if (this.dataset.limit) {
|
||||
this.textarea.addEventListener('input', () => {
|
||||
const emails = this.textarea.value.match(emailRegexp) || []
|
||||
|
||||
this.updateCounter(emails.length)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
updateCounter (count) {
|
||||
let counter = document.getElementById('emails_counter')
|
||||
let bulkMessage = document.getElementById('bulk_message')
|
||||
|
||||
if (count < 2) {
|
||||
counter?.remove()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if ((count + 10) > this.dataset.limit) {
|
||||
if (!counter) {
|
||||
counter = document.createElement('span')
|
||||
|
||||
counter.id = 'emails_counter'
|
||||
counter.classList.add('text-xs', 'right-0', 'absolute')
|
||||
counter.style.bottom = '-15px'
|
||||
|
||||
this.textarea.parentNode.append(counter)
|
||||
}
|
||||
|
||||
counter.innerText = `${count} / ${this.dataset.limit}`
|
||||
}
|
||||
|
||||
if (this.dataset.bulkEnabled !== 'true') {
|
||||
if (!bulkMessage) {
|
||||
bulkMessage = document.createElement('span')
|
||||
|
||||
bulkMessage.id = 'bulk_message'
|
||||
bulkMessage.classList.add('text-xs', 'left-0', 'absolute')
|
||||
bulkMessage.style.bottom = '-15px'
|
||||
|
||||
this.textarea.parentNode.append(bulkMessage)
|
||||
}
|
||||
|
||||
bulkMessage.innerHTML = '<a class="link" data-turbo="false" href="/upgrade">Upgrade</a> to bulk send multiple recipients'
|
||||
}
|
||||
}
|
||||
|
||||
get textarea () {
|
||||
return this.querySelector('textarea')
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,6 @@ export default actionable(targetable(class extends HTMLElement {
|
||||
]
|
||||
|
||||
connectedCallback () {
|
||||
if (this.dataset.isDirectUpload === 'true') {
|
||||
import('@rails/activestorage')
|
||||
}
|
||||
|
||||
this.addEventListener('drop', this.onDrop)
|
||||
|
||||
this.addEventListener('dragover', (e) => e.preventDefault())
|
||||
@@ -48,61 +44,11 @@ export default actionable(targetable(class extends HTMLElement {
|
||||
this.classList.toggle('opacity-50')
|
||||
}
|
||||
|
||||
async uploadFiles (files) {
|
||||
uploadFiles () {
|
||||
this.toggleLoading()
|
||||
|
||||
if (this.dataset.isDirectUpload === 'true') {
|
||||
const { DirectUpload } = await import('@rails/activestorage')
|
||||
|
||||
await Promise.all(
|
||||
Array.from(files).map(async (file) => {
|
||||
const upload = new DirectUpload(
|
||||
file,
|
||||
'/direct_uploads',
|
||||
this.input
|
||||
)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
upload.create((error, blob) => {
|
||||
if (error) {
|
||||
console.error(error)
|
||||
|
||||
return reject(error)
|
||||
} else {
|
||||
return resolve(blob)
|
||||
}
|
||||
})
|
||||
}).catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
})
|
||||
).then((blobs) => {
|
||||
if (this.dataset.submitOnUpload) {
|
||||
this.querySelectorAll('[name="blob_signed_ids[]"]').forEach((e) => e.remove())
|
||||
}
|
||||
|
||||
blobs.forEach((blob) => {
|
||||
const input = document.createElement('input')
|
||||
|
||||
input.type = 'hidden'
|
||||
input.name = 'blob_signed_ids[]'
|
||||
input.value = blob.signed_id
|
||||
|
||||
this.append(input)
|
||||
})
|
||||
|
||||
if (this.dataset.submitOnUpload === 'true') {
|
||||
this.closest('form').querySelector('button[type="submit"]').click()
|
||||
}
|
||||
}).finally(() => {
|
||||
if (this.dataset.submitOnUpload !== 'true') {
|
||||
this.toggleLoading()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if (this.dataset.submitOnUpload) {
|
||||
this.closest('form').querySelector('button[type="submit"]').click()
|
||||
}
|
||||
if (this.dataset.submitOnUpload) {
|
||||
this.closest('form').querySelector('button[type="submit"]').click()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -20,7 +20,7 @@ export default class extends HTMLElement {
|
||||
fetch = (text, resolve) => {
|
||||
const queryParams = new URLSearchParams({ q: text })
|
||||
|
||||
fetch('/api/template_folders_autocomplete?' + queryParams).then(async (resp) => {
|
||||
fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => {
|
||||
const items = await resp.json()
|
||||
|
||||
resolve(items)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
if (this.target) {
|
||||
this.input.value = this.target.value
|
||||
|
||||
this.target.addEventListener('input', (e) => {
|
||||
this.input.value = e.target.value
|
||||
})
|
||||
|
||||
this.target.addEventListener('linked-input.update', (e) => {
|
||||
this.input.value = e.target.value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
get input () {
|
||||
return this.querySelector('input')
|
||||
}
|
||||
|
||||
get target () {
|
||||
if (this.dataset.targetId) {
|
||||
const listItem = this.closest('[data-targets="dynamic-list.items"]')
|
||||
|
||||
if (listItem) {
|
||||
return listItem.querySelector(`#${this.dataset.targetId}`)
|
||||
} else {
|
||||
return document.getElementById(this.dataset.targetId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { target, targetable } from '@github/catalyst/lib/targetable'
|
||||
|
||||
export default targetable(class extends HTMLElement {
|
||||
static [target.static] = [
|
||||
'visiblePasswordIcon',
|
||||
'hiddenPasswordIcon',
|
||||
'passwordInput',
|
||||
'togglePasswordVisibility'
|
||||
]
|
||||
|
||||
connectedCallback () {
|
||||
this.togglePasswordVisibility.addEventListener('click', this.handleTogglePasswordVisibility)
|
||||
document.addEventListener('turbo:submit-start', this.setInitialPasswordType)
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
this.togglePasswordVisibility.removeEventListener('click', this.handleTogglePasswordVisibility)
|
||||
document.removeEventListener('turbo:submit-start', this.setInitialPasswordType)
|
||||
}
|
||||
|
||||
handleTogglePasswordVisibility = () => {
|
||||
this.passwordInput.type = this.passwordInput.type === 'password' ? 'text' : 'password'
|
||||
this.toggleIcon()
|
||||
}
|
||||
|
||||
setInitialPasswordType = () => {
|
||||
this.passwordInput.type = 'password'
|
||||
this.toggleIcon()
|
||||
}
|
||||
|
||||
toggleIcon = () => {
|
||||
this.visiblePasswordIcon.classList.toggle('hidden', this.passwordInput.type === 'password')
|
||||
this.hiddenPasswordIcon.classList.toggle('hidden', this.passwordInput.type === 'text')
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.input.addEventListener('focus', () => {
|
||||
if (this.title) {
|
||||
this.title.classList.add('hidden', 'md:block')
|
||||
this.input.classList.add('w-60')
|
||||
}
|
||||
})
|
||||
|
||||
this.input.addEventListener('blur', (e) => {
|
||||
if (this.title && !e.target.value) {
|
||||
this.title.classList.remove('hidden')
|
||||
this.input.classList.remove('w-60')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get input () {
|
||||
return this.querySelector('input')
|
||||
}
|
||||
|
||||
get title () {
|
||||
return document.querySelector(this.dataset.title)
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,12 @@ export default targetable(class extends HTMLElement {
|
||||
static [target.static] = ['canvas', 'input', 'clear', 'button']
|
||||
|
||||
async connectedCallback () {
|
||||
this.canvas.width = this.canvas.parentNode.parentNode.clientWidth
|
||||
this.canvas.height = this.canvas.parentNode.parentNode.clientWidth / 3
|
||||
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')
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export default class extends HTMLElement {
|
||||
preventSubmit: 1,
|
||||
minLength: 1,
|
||||
showOnFocus: true,
|
||||
debounceWaitMs: 200,
|
||||
onSelect: this.onSelect,
|
||||
render: this.render,
|
||||
fetch: this.fetch
|
||||
@@ -23,10 +24,13 @@ export default class extends HTMLElement {
|
||||
|
||||
if (input && item[field]) {
|
||||
input.value = item[field]
|
||||
input.dispatchEvent(new CustomEvent('linked-input.update', { bubbles: true }))
|
||||
}
|
||||
|
||||
if (textarea && item[field]) {
|
||||
textarea.value = textarea.value.replace(/[^;,\s]+$/, item[field] + ' ')
|
||||
|
||||
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -37,16 +41,16 @@ export default class extends HTMLElement {
|
||||
if (q) {
|
||||
const queryParams = new URLSearchParams({ q, field: this.dataset.field })
|
||||
|
||||
fetch('/api/submitters_autocomplete?' + queryParams).then(async (resp) => {
|
||||
this.currentFetch ||= fetch('/submitters_autocomplete?' + queryParams)
|
||||
|
||||
this.currentFetch.then(async (resp) => {
|
||||
const items = await resp.json()
|
||||
|
||||
if (q.length < 3) {
|
||||
resolve(items.filter((e) => e[this.dataset.field].startsWith(q)))
|
||||
} else {
|
||||
resolve(items)
|
||||
}
|
||||
resolve(items)
|
||||
}).catch(() => {
|
||||
resolve([])
|
||||
}).finally(() => {
|
||||
this.currentFetch = null
|
||||
})
|
||||
} else {
|
||||
resolve([])
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.input.addEventListener('change', (event) => {
|
||||
if (this.dataset.attribute) {
|
||||
this.target[this.dataset.attribute] = event.target.checked
|
||||
}
|
||||
|
||||
if (this.dataset.className) {
|
||||
this.target.classList.toggle(this.dataset.className, event.target.value !== this.dataset.value)
|
||||
if (this.dataset.className === 'hidden' && this.target.tagName === 'INPUT') {
|
||||
this.target.disabled = event.target.value !== this.dataset.value
|
||||
}
|
||||
}
|
||||
|
||||
if (this.dataset.attribute === 'disabled') {
|
||||
this.target.value = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get input () {
|
||||
return this.querySelector('input[type="checkbox"]') || this.querySelector('select')
|
||||
}
|
||||
|
||||
get target () {
|
||||
return document.getElementById(this.dataset.targetId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.button.addEventListener('click', () => {
|
||||
const expirationDate = new Date()
|
||||
|
||||
expirationDate.setFullYear(expirationDate.getFullYear() + 10)
|
||||
|
||||
const expires = expirationDate.toUTCString()
|
||||
|
||||
document.cookie = this.dataset.key + '=' + this.dataset.value + '; expires=' + expires + '; path=/'
|
||||
})
|
||||
}
|
||||
|
||||
get button () {
|
||||
return this.querySelector('button')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
document.addEventListener('turbo:submit-end', this.onSubmitEnd)
|
||||
|
||||
this.form.addEventListener('submit', this.onSubmit)
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
document.removeEventListener('turbo:submit-end', this.onSubmitEnd)
|
||||
|
||||
this.form.removeEventListener('submit', this.onSubmit)
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
this.element.classList.add('invisible')
|
||||
}
|
||||
|
||||
onSubmitEnd = (event) => {
|
||||
if (event.target === this.form) {
|
||||
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
|
||||
|
||||
if (resp?.status / 100 === 2) {
|
||||
this.element.classList.remove('invisible')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get element () {
|
||||
return document.getElementById(this.dataset.elementId)
|
||||
}
|
||||
|
||||
get form () {
|
||||
return this.closest('form')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.button.disabled = false
|
||||
|
||||
this.form.addEventListener('submit', () => {
|
||||
this.button.disabled = true
|
||||
})
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
this.button.disabled = false
|
||||
}
|
||||
|
||||
get button () {
|
||||
return this.querySelector('[type="submit"]')
|
||||
}
|
||||
|
||||
get form () {
|
||||
return this.querySelector('form') || this.closest('form')
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export default actionable(class extends HTMLElement {
|
||||
}
|
||||
|
||||
onSubmit = (e) => {
|
||||
if (e.detail.success) {
|
||||
if (e.detail.success && e.detail?.formSubmission?.formElement?.dataset?.closeOnSubmit !== 'false') {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
||||
+19
-4
@@ -2,23 +2,38 @@ import { createApp, reactive } from 'vue'
|
||||
|
||||
import Form from './submission_form/form'
|
||||
import DownloadButton from './elements/download_button'
|
||||
import ToggleSubmit from './elements/toggle_submit'
|
||||
|
||||
window.customElements.define('download-button', DownloadButton)
|
||||
window.customElements.define('submission-form', class extends HTMLElement {
|
||||
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
||||
|
||||
safeRegisterElement('download-button', DownloadButton)
|
||||
safeRegisterElement('toggle-submit', ToggleSubmit)
|
||||
safeRegisterElement('submission-form', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.appElem = document.createElement('div')
|
||||
|
||||
this.app = createApp(Form, {
|
||||
submitter: JSON.parse(this.dataset.submitter),
|
||||
inviteSubmitters: JSON.parse(this.dataset.inviteSubmitters),
|
||||
canSendEmail: this.dataset.canSendEmail === 'true',
|
||||
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
||||
previousSignatureValue: this.dataset.previousSignatureValue,
|
||||
goToLast: this.dataset.goToLast === 'true',
|
||||
isDemo: this.dataset.isDemo === 'true',
|
||||
attribution: this.dataset.attribution !== 'false',
|
||||
scrollPadding: this.dataset.scrollPadding || '-80px',
|
||||
dryRun: this.dataset.dryRun === 'true',
|
||||
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
|
||||
withSignatureId: this.dataset.withSignatureId === 'true',
|
||||
requireSigningReason: this.dataset.requireSigningReason === 'true',
|
||||
withConfetti: this.dataset.withConfetti !== 'false',
|
||||
withDisclosure: this.dataset.withDisclosure === 'true',
|
||||
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||
rememberSignature: this.dataset.rememberSignature === 'true',
|
||||
values: reactive(JSON.parse(this.dataset.values)),
|
||||
completedButton: JSON.parse(this.dataset.completedButton),
|
||||
completedButton: JSON.parse(this.dataset.completedButton || '{}'),
|
||||
withQrButton: true,
|
||||
completedMessage: JSON.parse(this.dataset.completedMessage || '{}'),
|
||||
completedRedirectUrl: this.dataset.completedRedirectUrl,
|
||||
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
||||
fields: JSON.parse(this.dataset.fields)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import Rollbar from 'rollbar/dist/rollbar.umd'
|
||||
|
||||
const token = document.querySelector('meta[name="rollbar-token"]')?.getAttribute('content')
|
||||
|
||||
if (token) {
|
||||
window.Rollbar ||= new Rollbar({
|
||||
accessToken: token,
|
||||
captureUncaught: true,
|
||||
captureUnhandledRejections: true,
|
||||
captureIp: false,
|
||||
autoInstrument: false,
|
||||
ignoredMessages: [
|
||||
/Failed to fetch/i,
|
||||
/NetworkError/i,
|
||||
/Load failed/i,
|
||||
/Clipboard write is not allowed/i
|
||||
],
|
||||
transform (payload) {
|
||||
payload.body.telemetry = []
|
||||
|
||||
if (payload.request.query_string) {
|
||||
payload.request.query_string = ''
|
||||
}
|
||||
|
||||
if (payload.request.url) {
|
||||
payload.request.url = payload.request.url.replace(/(\/[sdep]\/)(\w{5})[^/]+/, '$1$2')
|
||||
}
|
||||
|
||||
return payload
|
||||
},
|
||||
payload: {
|
||||
client: {
|
||||
javascript: {
|
||||
source_map_enabled: true
|
||||
}
|
||||
},
|
||||
environment: 'production'
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex absolute lg:text-base -outline-offset-1"
|
||||
class="field-area flex absolute lg:text-base -outline-offset-1"
|
||||
dir="auto"
|
||||
:style="computedStyle"
|
||||
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
|
||||
:class="{ 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
|
||||
>
|
||||
<div
|
||||
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
||||
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
||||
class="absolute top-0 bottom-0 right-0 left-0 items-center justify-center h-full w-full"
|
||||
>
|
||||
<span
|
||||
@@ -41,7 +41,8 @@
|
||||
<div
|
||||
v-if="isActive"
|
||||
ref="scrollToElem"
|
||||
class="absolute -top-20"
|
||||
class="absolute"
|
||||
:style="{ top: scrollPadding }"
|
||||
/>
|
||||
<img
|
||||
v-if="field.type === 'image' && image"
|
||||
@@ -53,18 +54,44 @@
|
||||
class="object-contain mx-auto"
|
||||
:src="stamp.url"
|
||||
>
|
||||
<img
|
||||
<div
|
||||
v-else-if="field.type === 'signature' && signature"
|
||||
class="object-contain mx-auto"
|
||||
:src="signature.url"
|
||||
class="flex flex-col justify-between h-full overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="flex-grow flex overflow-hidden"
|
||||
style="min-height: 50%"
|
||||
>
|
||||
<img
|
||||
class="object-contain mx-auto"
|
||||
:src="signature.url"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="withSignatureId"
|
||||
class="w-full mt-1 text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
|
||||
>
|
||||
<div class="truncate uppercase">
|
||||
ID: {{ signature.uuid }}
|
||||
</div>
|
||||
<div>
|
||||
{{ t('reason') }}: {{ values[field.preferences?.reason_field_uuid] || t('digitally_signed_by') }} {{ submitter.name }}
|
||||
<template v-if="submitter.email">
|
||||
<{{ submitter.email }}>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
{{ new Date(signature.created_at).toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'short' }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
v-else-if="field.type === 'initials' && initials"
|
||||
class="object-contain mx-auto"
|
||||
:src="initials.url"
|
||||
>
|
||||
<div
|
||||
v-else-if="field.type === 'file' || field.type === 'payment'"
|
||||
v-else-if="(field.type === 'file' || field.type === 'payment') && attachments.length"
|
||||
class="px-0.5 flex flex-col justify-center"
|
||||
>
|
||||
<a
|
||||
@@ -74,7 +101,7 @@
|
||||
:href="attachment.url"
|
||||
>
|
||||
<IconPaperclip
|
||||
class="inline w-[1.5vw] h-[1.5vw] lg:w-4 lg:h-4"
|
||||
class="inline w-[1.6vw] h-[1.6vw] lg:w-4 lg:h-4"
|
||||
/>
|
||||
{{ attachment.filename }}
|
||||
</a>
|
||||
@@ -157,23 +184,33 @@
|
||||
ref="textContainer"
|
||||
dir="auto"
|
||||
class="flex items-center px-0.5 w-full"
|
||||
:class="alignClasses[field.preferences?.align]"
|
||||
>
|
||||
<span v-if="Array.isArray(modelValue)">
|
||||
<span
|
||||
v-if="field && field.name && withFieldPlaceholder && !modelValue && modelValue !== 0"
|
||||
class="whitespace-pre-wrap text-gray-400"
|
||||
:class="{ 'w-full': field.preferences?.align }"
|
||||
>{{ field.name }}</span>
|
||||
<span v-else-if="Array.isArray(modelValue)">
|
||||
{{ modelValue.join(', ') }}
|
||||
</span>
|
||||
<span v-else-if="field.type === 'date'">
|
||||
{{ formattedDate }}
|
||||
</span>
|
||||
<span v-else-if="field.type === 'number'">
|
||||
{{ formatNumber(modelValue, field.preferences?.format) }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="whitespace-pre-wrap"
|
||||
:class="{ 'w-full': field.preferences?.align }"
|
||||
>{{ modelValue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp } from '@tabler/icons-vue'
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1 } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'FieldArea',
|
||||
@@ -187,11 +224,31 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
submitter: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
withSignatureId: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
isValueSet: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
scrollPadding: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '-80px'
|
||||
},
|
||||
withFieldPlaceholder: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
submittable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -202,6 +259,11 @@ export default {
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
values: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -240,6 +302,7 @@ export default {
|
||||
signature: this.t('signature'),
|
||||
initials: this.t('initials'),
|
||||
date: this.t('date'),
|
||||
number: this.t('number'),
|
||||
image: this.t('image'),
|
||||
file: this.t('file'),
|
||||
select: this.t('select'),
|
||||
@@ -252,6 +315,13 @@ export default {
|
||||
phone: this.t('phone')
|
||||
}
|
||||
},
|
||||
alignClasses () {
|
||||
return {
|
||||
center: 'text-center',
|
||||
left: 'text-left',
|
||||
right: 'text-right'
|
||||
}
|
||||
},
|
||||
option () {
|
||||
return this.field.options.find((o) => o.uuid === this.area.option_uuid)
|
||||
},
|
||||
@@ -260,6 +330,7 @@ export default {
|
||||
text: IconTextSize,
|
||||
signature: IconWritingSign,
|
||||
date: IconCalendarEvent,
|
||||
number: IconSquareNumber1,
|
||||
image: IconPhoto,
|
||||
initials: IconLetterCaseUpper,
|
||||
file: IconPaperclip,
|
||||
@@ -326,27 +397,35 @@ export default {
|
||||
computedStyle () {
|
||||
const { x, y, w, h } = this.area
|
||||
|
||||
return {
|
||||
const style = {
|
||||
top: y * 100 + '%',
|
||||
left: x * 100 + '%',
|
||||
width: w * 100 + '%',
|
||||
height: h * 100 + '%'
|
||||
}
|
||||
|
||||
if (this.field.preferences?.font_size) {
|
||||
style.fontSize = this.field.preferences.font_size + 'pt'
|
||||
}
|
||||
|
||||
return style
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue () {
|
||||
if (this.field.type === 'text' && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.modelValue.length)) {
|
||||
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? this.modelValue.length : 0
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.modelValue}`.length)) {
|
||||
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.field.type === 'text' && this.$refs.textContainer) {
|
||||
this.$nextTick(() => {
|
||||
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? this.modelValue.length : 0
|
||||
})
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer) {
|
||||
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? `${this.modelValue || (this.withFieldPlaceholder ? this.field.name : '')}`.length : 0
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
optionValue (option) {
|
||||
@@ -360,6 +439,17 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
formatNumber (number, format) {
|
||||
if (format === 'comma') {
|
||||
return new Intl.NumberFormat('en-US').format(number)
|
||||
} else if (format === 'dot') {
|
||||
return new Intl.NumberFormat('de-DE').format(number)
|
||||
} else if (format === 'space') {
|
||||
return new Intl.NumberFormat('fr-FR').format(number)
|
||||
} else {
|
||||
return number
|
||||
}
|
||||
},
|
||||
formatDate (date, format) {
|
||||
const monthFormats = {
|
||||
M: 'numeric',
|
||||
@@ -375,6 +465,7 @@ export default {
|
||||
|
||||
const yearFormats = {
|
||||
YYYY: 'numeric',
|
||||
YYY: 'numeric',
|
||||
YY: '2-digit'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<template
|
||||
v-for="step in steps"
|
||||
v-for="(step, stepIndex) in steps"
|
||||
:key="step[0].uuid"
|
||||
>
|
||||
<template
|
||||
@@ -18,15 +18,20 @@
|
||||
<FieldArea
|
||||
:ref="setAreaRef"
|
||||
v-model="values[field.uuid]"
|
||||
:values="values"
|
||||
:field="field"
|
||||
:area="area"
|
||||
:submittable="true"
|
||||
:submittable="submittable"
|
||||
:field-index="fieldIndex"
|
||||
:scroll-padding="scrollPadding"
|
||||
:submitter="submitter"
|
||||
:with-field-placeholder="withFieldPlaceholder"
|
||||
:with-signature-id="withSignatureId"
|
||||
:is-active="currentStep === step"
|
||||
:with-label="withLabel"
|
||||
:with-label="withLabel && !withFieldPlaceholder && step.length < 2"
|
||||
:is-value-set="step.some((f) => f.uuid in values)"
|
||||
:attachments-index="attachmentsIndex"
|
||||
@click="$emit('focus-step', step)"
|
||||
@click="$emit('focus-step', stepIndex)"
|
||||
/>
|
||||
</Teleport>
|
||||
</template>
|
||||
@@ -48,11 +53,35 @@ export default {
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
withSignatureId: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
submittable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
submitter: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
values: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
withFieldPlaceholder: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
scrollPadding: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '-80px'
|
||||
},
|
||||
attachmentsIndex: {
|
||||
type: Object,
|
||||
required: false,
|
||||
@@ -63,6 +92,11 @@ export default {
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
scrollEl: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
currentStep: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -97,7 +131,12 @@ export default {
|
||||
if (container.style.overflow === 'hidden') {
|
||||
this.scrollInContainer(areaRef.$el)
|
||||
} else {
|
||||
areaRef.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
const targetRect = areaRef.$refs.scrollToElem.getBoundingClientRect()
|
||||
const root = this.$root.$el?.parentNode?.classList?.contains('ds') ? this.$root.$el : document.body
|
||||
const rootRect = root.getBoundingClientRect()
|
||||
const scrollEl = this.scrollEl || window
|
||||
|
||||
scrollEl.scrollTo({ top: targetRect.top - rootRect.top, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -116,7 +155,7 @@ export default {
|
||||
|
||||
const targetTopRelativeToBox = targetRect.top - boxRect.top
|
||||
|
||||
scrollbox.scrollTop = targetTopRelativeToBox - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding
|
||||
scrollbox.scrollTo({ top: targetTopRelativeToBox - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding, behavior: 'smooth' })
|
||||
},
|
||||
setAreaRef (el) {
|
||||
if (el) {
|
||||
|
||||
@@ -41,11 +41,18 @@
|
||||
:name="`values[${field.uuid}][]`"
|
||||
>
|
||||
</template>
|
||||
<div
|
||||
v-if="field.description && !modelValue.length"
|
||||
dir="auto"
|
||||
class="mb-3 px-1"
|
||||
>
|
||||
<MarkdownContent :string="field.description" />
|
||||
</div>
|
||||
<FileDropzone
|
||||
:message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||
:submitter-slug="submitterSlug"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
:multiple="true"
|
||||
:dry-run="dryRun"
|
||||
@upload="onUpload"
|
||||
/>
|
||||
</div>
|
||||
@@ -53,12 +60,14 @@
|
||||
|
||||
<script>
|
||||
import FileDropzone from './dropzone'
|
||||
import MarkdownContent from './markdown_content'
|
||||
import { IconPaperclip, IconTrashX } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'AttachmentStep',
|
||||
components: {
|
||||
FileDropzone,
|
||||
MarkdownContent,
|
||||
IconPaperclip,
|
||||
IconTrashX
|
||||
},
|
||||
@@ -72,16 +81,16 @@ export default {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
dryRun: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
attachmentsIndex: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
isDirectUpload: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
required: false,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user