Compare commits

...
Author SHA1 Message Date
prafull-opensignlabs 336cfb3482 Delete apps/OpenSignServer/package-lock.json 2026-02-06 12:45:57 +00:00
AmolandGitHub f1278d32a8 Merge pull request #2091 from cozyClassic/feature/kr-trans-0.9 2026-02-05 12:07:19 +05:30
KIM 79d184cf88 feat: Add Korean language support
Add Korean translation file (kr/translation.json)
Update i18n.js to include 'kr' in whitelist
Add Korean option to SelectLanguage component

Manual inspection of full translation.
TODO: check full-text about result of word translation
2026-01-20 13:50:30 +09:00
prafull-opensignlabsandGitHub cd2904b9f0 Merge pull request #2082 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSign/npm_and_yarn-e7552e82bb
build(deps): bump react-router from 7.11.0 to 7.12.0 in /apps/OpenSign in the npm_and_yarn group across 1 directory
2026-01-09 14:50:50 +05:30
prafull-opensignlabsandGitHub fce20a165b Merge pull request #2083 from OpenSignLabs/updates-20846982071
fix: handle overlapping of date modal
2026-01-09 14:45:10 +05:30
prafull-opensignlabs 3a737ea1b7 fix: handle overlapping of date modal 2026-01-09 09:13:24 +00:00
dependabot[bot]andGitHub e035592e94 build(deps): bump react-router
Bumps the npm_and_yarn group with 1 update in the /apps/OpenSign directory: [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router).


Updates `react-router` from 7.11.0 to 7.12.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.12.0/packages/react-router)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.12.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-09 00:30:55 +00:00
prafull-opensignlabsandGitHub 0891cf55fc Merge pull request #2080 from OpenSignLabs/updates-20783798675
refactor: spelling correction
2026-01-07 19:25:17 +05:30
prafull-opensignlabs 5d328b51c3 refactor: spelling correction 2026-01-07 13:54:39 +00:00
prafull-opensignlabsandGitHub 336114f6ec Merge pull request #2076 from OpenSignLabs/updates-20777102041
v2.35.0
2026-01-07 15:12:13 +05:30
prafull-opensignlabs eb8e7b089b v2.35.0 2026-01-07 09:40:36 +00:00
prafull-opensignlabsandGitHub bce0e4971e Merge pull request #2059 from OpenSignLabs/updates-20428403989
v2.34.0
2025-12-22 15:27:36 +05:30
prafull-opensignlabs 4fb8aa9a16 Delete apps/OpenSign/package-lock.json 2025-12-22 09:56:25 +00:00
prafull-opensignlabsandGitHub 8a2329dd60 Merge pull request #2050 from OpenSignLabs/updates-20027001996
v2.33.0
2025-12-08 17:16:35 +05:30
prafull-opensignlabs 75a40fbf43 Delete apps/OpenSignServer/package-lock.json 2025-12-08 11:45:29 +00:00
prafull-opensignlabsandGitHub 334edb3c0c Merge pull request #2042 from OpenSignLabs/updates-19765002150
v2.32.0
2025-11-28 18:54:17 +05:30
prafull-opensignlabs 6465aa7c2c Delete apps/OpenSignServer/package-lock.json 2025-11-28 13:19:46 +00:00
prafull-opensignlabsandGitHub 4593ce7fec Merge pull request #2039 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/npm_and_yarn-42c243e405
build(deps): bump body-parser from 2.2.0 to 2.2.1 in /apps/OpenSignServer in the npm_and_yarn group across 1 directory
2025-11-27 00:09:48 +05:30
dependabot[bot]andGitHub b4ffdf0dc9 build(deps): bump body-parser
Bumps the npm_and_yarn group with 1 update in the /apps/OpenSignServer directory: [body-parser](https://github.com/expressjs/body-parser).


Updates `body-parser` from 2.2.0 to 2.2.1
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/v2.2.0...v2.2.1)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-version: 2.2.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 18:22:09 +00:00
AmolandGitHub 3c8b8835c7 Merge pull request #2029 from OpenSignLabs/build_deps
fix: remove type parameter from parse.save method
2025-11-20 20:17:26 +05:30
prafull-opensignlabs 20752161d3 fix: remove type parameter from parse.save method 2025-11-20 20:08:06 +05:30
prafull-opensignlabsandGitHub 002b0d5277 Merge pull request #2027 from OpenSignLabs/build_deps
fix: handle password validation in change password page
2025-11-20 17:13:20 +05:30
prafull-opensignlabsandGitHub 47348b990e Merge pull request #1957 from Miracee97/patch-1
Update README.md
2025-11-20 17:09:04 +05:30
prafull-opensignlabs ac86b6ef61 fix: add password validation in change password page 2025-11-20 17:00:39 +05:30
prafull-opensignlabsandGitHub 7894a6edb8 Merge pull request #2025 from OpenSignLabs/build_deps
build(deps): update dependencies
2025-11-20 16:21:33 +05:30
prafull-opensignlabs dac8957904 build(deps): update dependencies 2025-11-20 16:19:19 +05:30
AmolandGitHub ef686aec6a Refactor Docker workflow for multi-arch builds 2025-11-20 12:19:09 +05:30
AmolandGitHub f31155e0b4 Merge pull request #2022 from OpenSignLabs/updates-19528160086
v2.31.0
2025-11-20 12:17:56 +05:30
AmolandGitHub 78e4857d1a Merge pull request #2023 from OpenSignLabs/all-contributors/add-ehowe
docs: add ehowe as a contributor for code
2025-11-20 12:17:29 +05:30
allcontributors[bot]andGitHub c3144366d9 docs: update .all-contributorsrc 2025-11-20 06:47:02 +00:00
allcontributors[bot]andGitHub f00a7c9e50 docs: update README.md 2025-11-20 06:47:01 +00:00
prafull-opensignlabs 803d25e9d1 Fix: Document title changes are not reflected when the document is sent to signers.
Merge pull request #1498 from nxglabs/staging
2025-11-20 06:44:56 +00:00
prafull-opensignlabsandGitHub ca1b87309f Merge pull request #2011 from OpenSignLabs/updates-18996309218
feat: handle undefine username/password for SMTP
2025-11-01 17:24:58 +05:30
prafull-opensignlabs 0b4548130d feat: handle undefine username/password for SMTP 2025-11-01 11:53:04 +00:00
prafull-opensignlabsandGitHub e8b3c7fed8 Merge pull request #1994 from OpenSignLabs/updates-18555669223
v2.30.0
2025-10-16 14:19:28 +05:30
prafull-opensignlabs 2ce43ec2c8 v2.30.0 2025-10-16 08:48:28 +00:00
prafull-opensignlabsandGitHub d5af1e8a17 Merge pull request #1973 from OpenSignLabs/updates-18164868546
v2.29.2
2025-10-01 19:38:24 +05:30
prafull-opensignlabs fab7998e22 refactor: handle session expired modal 2025-10-01 14:07:45 +00:00
prafull-opensignlabsandGitHub 4a4db6851e Merge pull request #1969 from OpenSignLabs/updates-18129802734
v2.29.1
2025-09-30 17:57:55 +05:30
prafull-opensignlabs 870973e7f3 Merge pull request 2025-09-30 12:26:28 +00:00
prafull-opensignlabsandGitHub 4146680c1e Merge pull request #1966 from OpenSignLabs/updates-18086083096
v2.29.0
2025-09-29 10:24:16 +05:30
prafull-opensignlabs d9fcc69ca4 v2.29.0 2025-09-29 04:47:23 +00:00
RaistlinandGitHub a878ca1a96 Update README.md
updated link to api documentation v1.1
2025-09-23 11:33:29 +02:00
prafull-opensignlabsandGitHub d1c405912b Merge pull request #1945 from OpenSignLabs/updates-17798985943
refactor: remove unnecessary code
2025-09-17 18:51:19 +05:30
prafull-opensignlabs 745aca1754 Merge pull request #1298 from nxglabs/sync-to-public_repo-17798472199
Merge pull request #1297 from nxglabs/staging
2025-09-17 13:19:46 +00:00
prafull-opensignlabsandGitHub 672eeea851 Merge pull request #1943 from OpenSignLabs/updates-17788011031
build(deps): build dependencies and dark mode ui
2025-09-17 11:21:02 +05:30
prafull-opensignlabs 98efff65c9 Merge pull request #1295 from nxglabs/sync-to-public_repo-17787708673
fix: show signer name in typed signature instead in selfsign flow
2025-09-17 05:48:01 +00:00
prafull-opensignlabsandGitHub fe21978538 Merge pull request #1941 from OpenSignLabs/updates-17737216200
v2.28.0
2025-09-15 20:23:17 +05:30
prafull-opensignlabs ca4ccfd889 Merge pull request #1292 from nxglabs/sync-to-public_repo-17730712175
Merge pull request #1291 from nxglabs/staging
2025-09-15 14:51:16 +00:00
prafull-opensignlabsandGitHub 6cb2d69566 Merge pull request #1931 from OpenSignLabs/updates-17548337132
fix: getting error 'user not found' while deleting user
2025-09-08 16:22:46 +05:30
prafull-opensignlabs 98b9ebb228 fix: getting error 'user not found' while deleting user 2025-09-08 10:51:52 +00:00
prafull-opensignlabsandGitHub 1c2f453496 Merge pull request #1929 from OpenSignLabs/updates-17540166746
v2.27.2
2025-09-08 10:30:18 +05:30
prafull-opensignlabs b01d8125d9 Merge pull request #1250 from nxglabs/sync-to-public_repo-17512737859
Merge pull request
2025-09-08 04:57:25 +00:00
prafull-opensignlabsandGitHub 85bc2fbbb0 Merge pull request #1902 from OpenSignLabs/updates-16958097538
fix: save sign, initials not working
2025-08-14 12:27:31 +05:30
prafull-opensignlabs e857724502 Merge pull request #1137 from nxglabs/signers_completed_main
fix: not able update public username and save sign, initials not working
2025-08-14 06:53:43 +00:00
prafull-opensignlabsandGitHub 086d8ee5be Merge pull request #1889 from OpenSignLabs/updates-16770567266
fix: managesign report alignment and loader issue
2025-08-06 13:06:40 +05:30
prafull-opensignlabs 04543e826a Merge pull request #1105 from nxglabs/raktima-main-patch-2
fix: managesign report alignment and loader issue
2025-08-06 07:35:09 +00:00
prafull-opensignlabsandGitHub 04f6e38778 Merge pull request #1881 from OpenSignLabs/updates-16679049560
refactor: remove form-name from form title
2025-08-01 21:03:59 +05:30
prafull-opensignlabs 1b5c85fd5f Merge pull request #1085 from nxglabs/signers_completed_main
refactor: remove form-name from form title
2025-08-01 15:33:06 +00:00
prafull-opensignlabsandGitHub 123425d7d1 Merge pull request #1880 from OpenSignLabs/updates-16678047946
feat: allow preferences menu for all user
2025-08-01 20:18:07 +05:30
prafull-opensignlabs a5b9d8178c Merge pull request #1084 from nxglabs/signers_completed_main
feat: allow preferences menu for all user and add smtp setting in it
2025-08-01 14:46:44 +00:00
prafull-opensignlabsandGitHub aefd2ff933 Merge pull request #1868 from OpenSignLabs/updates-16625254839
refactor: correct grammar and normalise font size of signer email in report
2025-07-30 19:54:36 +05:30
nxglabs fe93be8be7 Merge pull request #1070 from nxglabs/signers_completed_main 2025-07-30 14:22:07 +00:00
prafull-opensignlabsandGitHub 369e3660fc Merge pull request #1865 from OpenSignLabs/updates-16598173522
fix: add dynamic truncate for email in list of signers
2025-07-29 19:29:57 +05:30
nxglabs 378c0f057b Merge pull request #1066 from nxglabs/signers_completed_main 2025-07-29 13:54:44 +00:00
prafull-opensignlabsandGitHub 4f3c034cec Merge pull request #1864 from OpenSignLabs/updates-16597572999
Fix: show signers list in completed documents
2025-07-29 19:04:08 +05:30
nxglabs e7d3453744 Merge pull request #1064 from nxglabs/signers_completed_main 2025-07-29 13:30:45 +00:00
prafull-opensignlabsandGitHub 47cc0ef580 Merge pull request #1858 from OpenSignLabs/updates-16525889391
remove checkbox from sign agreement screen
2025-07-25 21:18:33 +05:30
prafull-opensignlabs 04f71be6a2 Merge pull request #1044 from nxglabs/raktima-patch-main-2
fix: remove agreement checkbox and remove agreement checkbox tour
2025-07-25 15:38:38 +00:00
prafull-opensignlabsandGitHub 07603f4bc2 Merge pull request #1856 from OpenSignLabs/imgbot
[ImgBot] Optimize images
2025-07-24 14:40:19 +05:30
ImgBotApp ea16180551 [ImgBot] Optimize images
*Total -- 487.98kb -> 238.96kb (51.03%)

/apps/OpenSign/public/logo512.png -- 10.72kb -> 2.72kb (74.62%)
/apps/OpenSignServer/logo.png -- 86.48kb -> 24.83kb (71.28%)
/apps/OpenSign/src/assets/images/logo.png -- 86.48kb -> 24.83kb (71.28%)
/apps/OpenSign/public/static/js/assets/images/logo.2a7bff0c1189183fafe71d7d5b94e0cd.png -- 86.48kb -> 24.83kb (71.28%)
/apps/OpenSign/public/logo192.png -- 2.13kb -> 0.65kb (69.69%)
/apps/OpenSign/src/assets/images/dp.png -- 12.83kb -> 4.25kb (66.9%)
/apps/OpenSign/src/assets/images/folder.png -- 4.39kb -> 1.93kb (56.05%)
/apps/OpenSign/public/static/js/assets/images/logo-dark.png -- 95.21kb -> 57.21kb (39.91%)
/apps/OpenSignServer/public/assets/images/parse-logo.png -- 5.16kb -> 3.81kb (26.21%)
/apps/OpenSign/src/assets/images/pdf3.png -- 11.66kb -> 9.77kb (16.21%)
/apps/OpenSign/src/assets/images/login_img.svg -- 40.75kb -> 39.10kb (4.04%)
/apps/OpenSign/src/assets/images/recreatedoc.png -- 40.69kb -> 40.08kb (1.48%)
/apps/OpenSign/src/assets/images/pad.svg -- 5.00kb -> 4.94kb (1.07%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2025-07-24 08:28:02 +00:00
prafull-opensignlabsandGitHub 75ad566d54 Merge pull request #1841 from OpenSignLabs/updates-16144605887
v2.26.1
2025-07-08 19:01:19 +05:30
nxglabs fda9fb534b Merge pull request #963 from nxglabs/staging
v2.26.1
2025-07-08 13:27:37 +00:00
prafull-opensignlabsandGitHub cb3e31ebbf Merge pull request #1838 from OpenSignLabs/updates-16124490027
dark-theme logo
2025-07-07 23:43:31 +05:30
nxglabs 400525debb Merge pull request #960 from nxglabs/fix_customlogo
fix: custom applogo not visible in header
2025-07-07 18:07:24 +00:00
prafull-opensignlabsandGitHub 1cc017bb05 Merge pull request #1829 from OpenSignLabs/updates-16070658635
fix: quicksend screen get black out while sending document
2025-07-04 15:06:16 +05:30
prafull-opensignlabs 531dd11185 Merge pull request #942 from nxglabs/staging
fix: quicksend screen get black out while sending document
2025-07-04 09:32:50 +00:00
prafull-opensignlabsandGitHub 1e80754750 Merge pull request #1827 from OpenSignLabs/updates-16068582405
revert: css of drive
2025-07-04 13:14:27 +05:30
prafull-opensignlabs 0d286938c7 Merge pull request #940 from nxglabs/staging
revert: css of drive
2025-07-04 07:43:32 +00:00
prafull-opensignlabsandGitHub 360c13df25 Merge pull request #1825 from OpenSignLabs/updates-16055306436
v2.26.0
2025-07-04 10:35:33 +05:30
nxglabs 71b9bd6367 Merge pull request #937 from nxglabs/tenant_mailtemplate
fix: if user not provide default value use it from template in createdocument from template API
2025-07-03 16:04:35 +00:00
prafull-opensignlabsandGitHub 4a5b35e147 Merge pull request #1818 from OpenSignLabs/updates-15966812948
fix: handle undefine appId
2025-06-30 13:09:02 +05:30
prafull-opensignlabs 7930a8f439 Merge pull request #917 from nxglabs/handle_appid
fix: handle undefine appId
2025-06-30 07:37:32 +00:00
prafull-opensignlabsandGitHub 7204183542 Merge pull request #1804 from OpenSignLabs/updates-15754544632
fix: resolve issues
2025-06-19 15:03:08 +05:30
nxglabs 5561673fdc Merge pull request #866 from nxglabs/fix_main_issues 2025-06-19 09:31:17 +00:00
prafull-opensignlabsandGitHub 0df44062d6 Merge pull request #1803 from OpenSignLabs/updates-15736278136
fix: correct baseapi of docxtopdf and decryptpdf
2025-06-18 20:25:53 +05:30
nxglabs 8983440763 Merge pull request #864 from nxglabs/staging 2025-06-18 14:54:13 +00:00
prafull-opensignlabsandGitHub 5a28c0ed06 Merge pull request #1802 from OpenSignLabs/updates-15735030679
v2.25.0
2025-06-18 19:43:43 +05:30
nxglabs cbca22c308 Merge pull request #861 from nxglabs/staging
fix: handle password protected pdf
2025-06-18 14:03:05 +00:00
prafull-opensignlabsandGitHub b0e0b38e8f Merge pull request #1789 from OpenSignLabs/updates-15483575034
v2.24.1
2025-06-06 11:17:12 +05:30
prafull-opensignlabs 49c23fb52b Merge pull request #753 from OpenSignLabs/staging
v2.24.1
2025-06-06 05:33:37 +00:00
prafull-opensignlabsandGitHub da4b604710 Merge pull request #1785 from OpenSignLabs/updates-15425322075
handle crashing pattern for text input widgets
2025-06-04 00:17:30 +05:30
prafull-opensignlabs 82d518b3c9 Merge pull request #739 from OpenSignLabs/staging
disable signing request email when send_email flag is false
2025-06-03 18:45:49 +00:00
prafull-opensignlabsandGitHub dcbde2b661 Merge pull request #1784 from OpenSignLabs/updates-15419310132
v2.23.1
2025-06-03 19:28:35 +05:30
prafull-opensignlabs 27b0b426ad v2.23.1
Enable widget placement on click in desktop view
Disable signature types not working
Change icon of agreement sign checkbox
Update dependencies
2025-06-03 13:55:57 +00:00
prafull-opensignlabsandGitHub 8788afaa8e Merge pull request #1766 from OpenSignLabs/updates-15350472621
fix: add verify-document route
2025-05-30 21:19:35 +05:30
prafull-opensignlabs 428aa65b38 Merge pull request #710 from OpenSignLabs/staging
fix: add verify-document route
2025-05-30 15:45:27 +00:00
AmolandGitHub 295d942427 feat: add deploy to digitalocean button 2025-05-30 20:33:43 +05:30
prafull-opensignlabsandGitHub c4e28e6e6e Merge pull request #1764 from OpenSignLabs/updates-15349515719
v2.23.0
2025-05-30 20:26:41 +05:30
prafull-opensignlabs b19fb29b82 Merge pull request #708 from OpenSignLabs/staging
refactor: unnecessary code
2025-05-30 14:54:53 +00:00
prafull-opensignlabsandGitHub 3d223ba343 Merge pull request #1756 from OpenSignLabs/updates-15275255429
v2.22.0
2025-05-27 18:13:12 +05:30
prafull-opensignlabs 98754c776f Merge pull request #675 from OpenSignLabs/staging
v2.22.0
2025-05-27 12:28:40 +00:00
prafull-opensignlabsandGitHub 1550e3a3d0 Merge pull request #1748 from OpenSignLabs/updates-15156338467
v2.21.2
2025-05-21 13:09:14 +05:30
prafull-opensignlabs 3500883205 v2.21.2
fix: handle subscription error in publicsign
2025-05-21 07:37:15 +00:00
prafull-opensignlabsandGitHub d888897b62 Update README.md 2025-05-20 20:10:44 +05:30
prafull-opensignlabsandGitHub 8f2433fbea Merge pull request #1737 from OpenSignLabs/updates-15043583319
v2.21.1
2025-05-15 16:48:53 +05:30
prafull-opensignlabs b671dbac19 v2.21.1
fix: handle undefined placeholders condition
2025-05-15 11:17:30 +00:00
prafull-opensignlabsandGitHub 79cdbc9c8c Merge pull request #1735 from OpenSignLabs/updates-15024699698
fix: resolve error cannot read undefined props
2025-05-14 20:55:08 +05:30
prafull-opensignlabs e585b7c943 fix: resolve error cannot read undefined props 2025-05-14 15:24:27 +00:00
prafull-opensignlabsandGitHub 5336e2cb7c Merge pull request #1734 from OpenSignLabs/prafull-opensignlabs-patch-7
refactor: remove unnecessary file
2025-05-14 20:26:36 +05:30
prafull-opensignlabsandGitHub 72401291f8 Delete apps/OpenSign/vite.public-template.config.js 2025-05-14 20:24:54 +05:30
prafull-opensignlabsandGitHub 7b34314b9e Merge pull request #1733 from OpenSignLabs/updates-15023728155
fix: correct build commands for selfhosting
2025-05-14 20:13:14 +05:30
prafull-opensignlabs b6f1cceeca Merge pull request #612 from OpenSignLabs/staging
fix: correct build commands for selfhosting
2025-05-14 14:42:30 +00:00
prafull-opensignlabsandGitHub c422cdac06 Merge pull request #1732 from OpenSignLabs/updates-15023371589
fix: edit template option not visible in mobile view
2025-05-14 20:00:02 +05:30
prafull-opensignlabsandGitHub ab08d2615d Update package.json 2025-05-14 19:59:01 +05:30
prafull-opensignlabs 2b0c7488d9 Merge pull request #610 from OpenSignLabs/staging
fix: edit template option not visible in mobile view
2025-05-14 14:27:57 +00:00
prafull-opensignlabsandGitHub 6927540741 Merge pull request #1731 from OpenSignLabs/updates-15022725718
v2.21.0
2025-05-14 19:33:19 +05:30
prafull-opensignlabs 1ceeee36ac v2.21.0
Merge pull request #601 from OpenSignLabs/staging
2025-05-14 14:00:50 +00:00
prafull-opensignlabsandGitHub c4779c14c1 feat: add all three docker command 2025-05-14 19:11:18 +05:30
prafull-opensignlabsandGitHub e48869cb2f feat: add deploy to digitalocean button 2025-05-14 18:36:47 +05:30
prafull-opensignlabsandGitHub 81d3969d1f Merge pull request #1719 from OpenSignLabs/updates-14856876951
v2.20.1
2025-05-06 15:35:42 +05:30
prafull-opensignlabs 5819103f57 v2.20.1 2025-05-06 10:01:20 +00:00
prafull-opensignlabsandGitHub 6653b7ae5a Merge pull request #1717 from OpenSignLabs/updates-14829517615
v2.20.0
2025-05-05 10:46:07 +05:30
prafull-opensignlabs b50c1e70ee v2.20.0 2025-05-05 05:12:54 +00:00
prafull-opensignlabsandGitHub 365c2a217e Merge pull request #1709 from OpenSignLabs/prafull-opensignlabs-patch-5
fix: quick send not working
2025-04-29 12:11:39 +05:30
prafull-opensignlabsandGitHub b1b9b20e97 Update Validate.js 2025-04-29 12:10:14 +05:30
prafull-opensignlabsandGitHub 75c0cdb5f9 fix: quick send not working 2025-04-29 12:09:05 +05:30
prafull-opensignlabsandGitHub 740ccca1f6 Merge pull request #1704 from OpenSignLabs/updates-14689408163
Merge pull request #547 from OpenSignLabs/staging
2025-04-27 12:21:00 +05:30
prafull-opensignlabs f3b79cb71f Merge pull request #547 from OpenSignLabs/staging
refactor: remove extra comment
2025-04-27 06:50:12 +00:00
prafull-opensignlabsandGitHub f95ea0b9e9 Merge pull request #1702 from OpenSignLabs/updates-14689261849
fix: update migration script
2025-04-27 12:01:50 +05:30
prafull-opensignlabs 647a9ce5b4 Merge pull request #545 from OpenSignLabs/staging
fix: update migration script
2025-04-27 06:30:15 +00:00
prafull-opensignlabsandGitHub 3f3ca668f5 Merge pull request #1700 from OpenSignLabs/updates-14666117248
fix: return https protocol for signing urls
2025-04-25 19:17:21 +05:30
prafull-opensignlabs 2fd6d1a37a fix: return https protocol for signing urls 2025-04-25 13:46:14 +00:00
prafull-opensignlabsandGitHub 307137f926 Merge pull request #1699 from OpenSignLabs/updates-14660165068
remove non-exist class from migration script
2025-04-25 13:51:06 +05:30
prafull-opensignlabs 2394880dfc Merge pull request #541 from OpenSignLabs/staging
remove non-exist defaultOtp class from migration
2025-04-25 08:19:46 +00:00
AmolandGitHub 414cdcd4b5 Merge pull request #1698 from OpenSignLabs/prafull-opensignlabs-patch-4
fix: Workflow does not contain permissions
2025-04-25 13:29:43 +05:30
prafull-opensignlabsandGitHub 1fa6b25890 fix: Workflow does not contain permissions 2025-04-25 13:29:03 +05:30
prafull-opensignlabsandGitHub ff2fd44b19 Merge pull request #1697 from OpenSignLabs/updates-14659684433
build(deps): update react-router package to fix vulnerabilities
2025-04-25 13:22:28 +05:30
prafull-opensignlabs ad466fb066 build(deps): update react-router package to fix vulnerabilities 2025-04-25 07:51:45 +00:00
AmolandGitHub 0f179fed67 Merge pull request #1696 from OpenSignLabs/updates-14659482113
v2.19.0
2025-04-25 13:13:08 +05:30
nxglabs 3ded6c35a1 Merge pull request #536 from OpenSignLabs/doc_load_drive
refactor: remove protected fields migration
2025-04-25 07:39:21 +00:00
AmolandGitHub 64293db3a1 Merge pull request #1695 from OpenSignLabs/prafull-opensignlabs-patch-3
Update Docker.yml
2025-04-25 13:06:15 +05:30
prafull-opensignlabsandGitHub 18a4c8523a Update Docker.yml 2025-04-25 13:03:50 +05:30
prafull-opensignlabsandGitHub f7b39409a8 Merge pull request #1692 from OpenSignLabs/updates-14590419587
fix: normalize emails, migrate user logic to cloud function
2025-04-22 14:09:18 +05:30
prafull-opensignlabs 6283524d2f Merge pull request #524 from OpenSignLabs/publicurl_adduser
fix: correct public URL, normalize emails, migrate user/orgAdmin logic to cloud function
2025-04-22 08:36:56 +00:00
prafull-opensignlabsandGitHub 216288fd09 Merge pull request #1689 from OpenSignLabs/updates-14509601184
v2.18.0
2025-04-17 12:06:43 +05:30
prafull-opensignlabs 3b1d8a48b7 v2.18.0
New Features:
1. Added support to recreate a new document from declined document without having to redo everything from scratch.
2. Added replace pdf file feature for Templates. PDF underlying a Template can now be replaced while keeping all the fields the same.
3. Introduced the ability to replace pdf file for in UpdateTemplate and CreateDocumentfromtemplate API.
4. Revamped the preferences menu UI.

Bug Fixes:
1. Fixed issue with capital letters in contact emails via API.
2. Better validations & error handling in APIs
2025-04-17 06:34:06 +00:00
prafull-opensignlabsandGitHub ec68770ff0 Merge pull request #1681 from OpenSignLabs/updates-14398222591
v2.17.0
2025-04-11 13:20:43 +05:30
prafull-opensignlabs f5d2027e3a refactor: resolve warnings 2025-04-11 07:45:23 +00:00
prafull-opensignlabsandGitHub 8af0010f55 Merge pull request #1672 from OpenSignLabs/updates-14279056892
v2.16.0
2025-04-05 11:36:59 +05:30
prafull-opensignlabs d7b7ad6285 Merge pull request 2025-04-05 06:01:24 +00:00
prafull-opensignlabsandGitHub fca45ced38 Merge pull request #1663 from OpenSignLabs/updates-14122971328
v2.15.0
2025-03-28 11:37:55 +05:30
prafull-opensignlabs ac8ab70246 v2.15.0
v2.15.0
2025-03-28 06:02:14 +00:00
prafull-opensignlabsandGitHub 8373679f27 Merge pull request #1661 from OpenSignLabs/do_deploy
feat: update proxy file
2025-03-24 13:47:33 +05:30
prafull-opensignlabs 0a23aabbe4 feat: update proxy file 2025-03-24 13:45:45 +05:30
prafull-opensignlabsandGitHub 4a85599703 Merge pull request #1660 from OpenSignLabs/updates-14030275569
feat: changes to implement one click deployment
2025-03-24 13:42:41 +05:30
prafull-opensignlabs 3e9b84de72 feat: changes to implement one click deployment
feat: changes to implement one click deployment
2025-03-24 08:11:52 +00:00
prafull-opensignlabsandGitHub 4313d8d0e4 Merge pull request #1654 from OpenSignLabs/updates-14016837992
refactor: remove unnecessary code
2025-03-23 13:11:28 +05:30
prafull-opensignlabs 3088fdd163 Merge pull request #403 from OpenSignLabs/staging
refactor: remove unnecessary code
2025-03-23 07:40:01 +00:00
ZathielandGitHub 8c86fe27ec Merge pull request #1616 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSign/i18next-browser-languagedetector-8.0.4
build(deps): bump i18next-browser-languagedetector from 8.0.2 to 8.0.4 in /apps/OpenSign
2025-03-21 21:50:38 +01:00
ZathielandGitHub 01e09998e5 Merge pull request #1650 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/npm_and_yarn-1c007eb283
build(deps): bump axios from 1.8.3 to 1.8.4 in /apps/OpenSignServer in the npm_and_yarn group across 1 directory
2025-03-21 21:49:55 +01:00
ZathielandGitHub 55fdb52bf3 Merge pull request #1651 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSign/npm_and_yarn-1c007eb283
build(deps): bump axios from 1.8.3 to 1.8.4 in /apps/OpenSign in the npm_and_yarn group across 1 directory
2025-03-21 21:49:17 +01:00
dependabot[bot]andGitHub 08f46a393b build(deps): bump axios
Bumps the npm_and_yarn group with 1 update in the /apps/OpenSign directory: [axios](https://github.com/axios/axios).


Updates `axios` from 1.8.3 to 1.8.4
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.8.3...v1.8.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-21 14:41:30 +00:00
dependabot[bot]andGitHub 9345da543a build(deps): bump axios
Bumps the npm_and_yarn group with 1 update in the /apps/OpenSignServer directory: [axios](https://github.com/axios/axios).


Updates `axios` from 1.8.3 to 1.8.4
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.8.3...v1.8.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-21 14:41:20 +00:00
dependabot[bot]andGitHub 208891e10d build(deps): bump i18next-browser-languagedetector in /apps/OpenSign
Bumps [i18next-browser-languagedetector](https://github.com/i18next/i18next-browser-languageDetector) from 8.0.2 to 8.0.4.
- [Changelog](https://github.com/i18next/i18next-browser-languageDetector/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next-browser-languageDetector/compare/v8.0.2...v8.0.4)

---
updated-dependencies:
- dependency-name: i18next-browser-languagedetector
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-21 14:41:17 +00:00
prafull-opensignlabsandGitHub db0fa61b2b Merge pull request #1648 from OpenSignLabs/updates-13994222227
v2.14.0
2025-03-21 20:09:57 +05:30
prafull-opensignlabs 221f098de6 v2.14.0
v2.14.0
2025-03-21 14:38:32 +00:00
prafull-opensignlabsandGitHub 9658761ede Merge pull request #1646 from OpenSignLabs/prafull-opensignlabs-patch-2
add deprecated warning for env
2025-03-20 16:03:28 +05:30
prafull-opensignlabsandGitHub 6aad832e37 refacctor: add deprecated warning for env 2025-03-20 11:52:18 +05:30
prafull-opensignlabsandGitHub e4b6be51d9 Merge pull request #1644 from OpenSignLabs/updates-13949958694
Merge pull request #395 from OpenSignLabs
2025-03-19 20:41:10 +05:30
prafull-opensignlabs a3b2c797fb Merge pull request #395 from OpenSignLabs/fix_dailyemailquota
fix: dailyemailquota not updating for user and default value not updating of checkbox in API
2025-03-19 15:08:50 +00:00
ZathielandGitHub 22d27abb76 Merge pull request #1631 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/prettier-3.5.3
build(deps-dev): bump prettier from 3.5.0 to 3.5.3 in /apps/OpenSignServer
2025-03-16 20:06:24 +01:00
dependabot[bot]andGitHub 45e0679ca7 build(deps-dev): bump prettier in /apps/OpenSignServer
Bumps [prettier](https://github.com/prettier/prettier) from 3.5.0 to 3.5.3.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.5.0...3.5.3)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 00:48:25 +00:00
prafull-opensignlabsandGitHub 2b2aa20deb Merge pull request #1627 from OpenSignLabs/updates-13647519163
Merge pull request #357 from OpenSignLabs/revert_mail
2025-03-04 11:50:01 +05:30
prafull-opensignlabs 8d6ed3d2b5 Merge pull request #357 from OpenSignLabs/revert_mail
revert: revert mail sending
2025-03-04 06:19:26 +00:00
prafull-opensignlabsandGitHub 6064f66cdf Merge pull request #1625 from mubix/fix-install-doc
Fix Install Documentation
2025-03-04 11:36:42 +05:30
prafull-opensignlabsandGitHub 0ba29eb799 Merge pull request #1626 from OpenSignLabs/updates-13647292049
Merge pull request #356 from OpenSignLabs/correct_cond
2025-03-04 11:34:02 +05:30
prafull-opensignlabs d0dd571478 Merge pull request #356 from OpenSignLabs/mismatch_attach
fix: correct condition
2025-03-04 06:02:50 +00:00
Rob FullerandGitHub ddb63d81a7 fix install doc 2025-03-03 14:04:43 -05:00
prafull-opensignlabsandGitHub f78451d3f9 Merge pull request #1621 from OpenSignLabs/updates-13637250862
Merge pull request #349 from OpenSignLabs/crash_pdf
2025-03-04 00:08:15 +05:30
ZathielandGitHub 28630e2d23 Merge pull request #1618 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/mongodb-6.14.0
build(deps): bump mongodb from 6.13.0 to 6.14.0 in /apps/OpenSignServer
2025-03-03 19:00:15 +01:00
ZathielandGitHub c5022eaee6 Merge pull request #1620 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/posthog-node-4.8.1
build(deps): bump posthog-node from 4.5.0 to 4.8.1 in /apps/OpenSignServer
2025-03-03 18:58:01 +01:00
prafull-opensignlabs 1b7f07a2c4 Merge pull request #349 from OpenSignLabs/mismatch_attach
feat: restrict emails
2025-03-03 17:57:33 +00:00
dependabot[bot]andGitHub 2484c67406 build(deps): bump posthog-node in /apps/OpenSignServer
Bumps [posthog-node](https://github.com/PostHog/posthog-js-lite/tree/HEAD/posthog-node) from 4.5.0 to 4.8.1.
- [Release notes](https://github.com/PostHog/posthog-js-lite/releases)
- [Changelog](https://github.com/PostHog/posthog-js-lite/blob/main/posthog-node/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js-lite/commits/posthog-node-v4.8.1/posthog-node)

---
updated-dependencies:
- dependency-name: posthog-node
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-03 00:42:08 +00:00
dependabot[bot]andGitHub abfc3f32f2 build(deps): bump mongodb from 6.13.0 to 6.14.0 in /apps/OpenSignServer
Bumps [mongodb](https://github.com/mongodb/node-mongodb-native) from 6.13.0 to 6.14.0.
- [Release notes](https://github.com/mongodb/node-mongodb-native/releases)
- [Changelog](https://github.com/mongodb/node-mongodb-native/blob/main/HISTORY.md)
- [Commits](https://github.com/mongodb/node-mongodb-native/compare/v6.13.0...v6.14.0)

---
updated-dependencies:
- dependency-name: mongodb
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-03 00:41:39 +00:00
prafull-opensignlabsandGitHub 5c06ff4242 Merge pull request #1613 from OpenSignLabs/updates-13494498125
Merge pull request #343 from OpenSignLabs/staging
2025-02-24 14:44:07 +05:30
prafull-opensignlabs 1899acfffc Merge pull request #343 from OpenSignLabs/staging
fix: selected date resets to the current date when navigating between document pages.
2025-02-24 09:12:24 +00:00
ZathielandGitHub 796ff058c8 Merge pull request #1581 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSign/webpack-cli-6.0.1 2025-02-24 09:57:37 +01:00
ZathielandGitHub c88cb5357c Merge pull request #1609 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/ws-8.18.1 2025-02-24 09:56:17 +01:00
dependabot[bot]andGitHub a6a0325d4b build(deps): bump ws from 8.18.0 to 8.18.1 in /apps/OpenSignServer
Bumps [ws](https://github.com/websockets/ws) from 8.18.0 to 8.18.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.0...8.18.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 08:47:11 +00:00
ZathielandGitHub b18d17c44d Merge pull request #1610 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/babel/eslint-parser-7.26.8 2025-02-24 09:45:44 +01:00
ZathielandGitHub a73779e74b Merge pull request #1612 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSign/i18next-browser-languagedetector-8.0.4 2025-02-24 09:44:44 +01:00
dependabot[bot]andGitHub 5b943c1965 build(deps): bump i18next-browser-languagedetector in /apps/OpenSign
Bumps [i18next-browser-languagedetector](https://github.com/i18next/i18next-browser-languageDetector) from 8.0.2 to 8.0.4.
- [Changelog](https://github.com/i18next/i18next-browser-languageDetector/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next-browser-languageDetector/compare/v8.0.2...v8.0.4)

---
updated-dependencies:
- dependency-name: i18next-browser-languagedetector
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 00:52:19 +00:00
dependabot[bot]andGitHub f73844d730 build(deps-dev): bump @babel/eslint-parser in /apps/OpenSignServer
Bumps [@babel/eslint-parser](https://github.com/babel/babel/tree/HEAD/eslint/babel-eslint-parser) from 7.26.5 to 7.26.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.8/eslint/babel-eslint-parser)

---
updated-dependencies:
- dependency-name: "@babel/eslint-parser"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 00:08:02 +00:00
prafull-opensignlabsandGitHub aaadab540a Merge pull request #1607 from OpenSignLabs/updates-13456995803
Merge pull request #339 from OpenSignLabs/staging
2025-02-21 18:11:59 +05:30
prafull-opensignlabsandGitHub 83bc466df4 Merge pull request #1606 from OpenSignLabs/updates-13455918957
Merge pull request #336 from OpenSignLabs/staging
2025-02-21 18:10:43 +05:30
prafull-opensignlabs d85ed954f1 Merge pull request #339 from OpenSignLabs/staging
fix: remove logo from certificate for white labeled user
2025-02-21 12:40:33 +00:00
ZathielandGitHub 369deb6e26 Merge pull request #1584 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSign/regex-parser-2.3.1 2025-02-21 12:44:54 +01:00
ZathielandGitHub 295d1af910 Merge pull request #1589 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/eslint-9.20.1 2025-02-21 12:41:57 +01:00
dependabot[bot]andGitHub 080d7c08dc build(deps-dev): bump eslint in /apps/OpenSignServer
Bumps [eslint](https://github.com/eslint/eslint) from 9.20.0 to 9.20.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.20.0...v9.20.1)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-21 11:41:35 +00:00
ZathielandGitHub c877f9fc5d Merge pull request #1590 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSignServer/babel/eslint-parser-7.26.8 2025-02-21 12:40:28 +01:00
prafull-opensignlabs 99c7a58d62 Merge pull request #336 from OpenSignLabs/staging
style: correct logo aspect ratio in guest login
2025-02-21 11:32:27 +00:00
dependabot[bot]andGitHub 97642bf857 build(deps-dev): bump webpack-cli from 5.1.4 to 6.0.1 in /apps/OpenSign
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 5.1.4 to 6.0.1.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/webpack-cli@5.1.4...webpack-cli@6.0.1)

---
updated-dependencies:
- dependency-name: webpack-cli
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-21 07:21:34 +00:00
dependabot[bot]andGitHub 2f67ac7b0a build(deps): bump regex-parser from 2.3.0 to 2.3.1 in /apps/OpenSign
Bumps [regex-parser](https://github.com/IonicaBizau/regex-parser.js) from 2.3.0 to 2.3.1.
- [Release notes](https://github.com/IonicaBizau/regex-parser.js/releases)
- [Commits](https://github.com/IonicaBizau/regex-parser.js/compare/2.3.0...2.3.1)

---
updated-dependencies:
- dependency-name: regex-parser
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-21 07:21:32 +00:00
prafull-opensignlabsandGitHub 23aecb45d9 Merge pull request #1601 from OpenSignLabs/updates-13451970686
v2.13.1
2025-02-21 12:50:13 +05:30
prafull-opensignlabs 92b507e621 v2.13.1
fix: Adapter prototype don't match expected prototype during
fix: drive get crash if signer array is null
2025-02-21 07:18:47 +00:00
prafull-opensignlabsandGitHub 1f6c2c7d38 Merge pull request #1597 from OpenSignLabs/updates-13420682171
Merge pull request #316 from OpenSignLabs/patch
2025-02-21 12:45:33 +05:30
prafull-opensignlabs c1be197346 Merge pull request #316 from OpenSignLabs/fix_emailresetdate
fix: Adapter prototype don't match expected prototype
2025-02-19 19:22:54 +00:00
dependabot[bot]andGitHub 24a95e8852 build(deps-dev): bump @babel/eslint-parser in /apps/OpenSignServer
Bumps [@babel/eslint-parser](https://github.com/babel/babel/tree/HEAD/eslint/babel-eslint-parser) from 7.26.5 to 7.26.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.8/eslint/babel-eslint-parser)

---
updated-dependencies:
- dependency-name: "@babel/eslint-parser"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 00:56:20 +00:00
340 changed files with 43633 additions and 69526 deletions
+9
View File
@@ -241,6 +241,15 @@
"contributions": [
"translation"
]
},
{
"login": "ehowe",
"name": "Eugene Howe",
"avatar_url": "https://avatars.githubusercontent.com/u/756984?v=4",
"profile": "https://github.com/ehowe",
"contributions": [
"code"
]
}
]
}
+4 -3
View File
@@ -8,13 +8,11 @@ PUBLIC_URL=https://localhost:3001
GENERATE_SOURCEMAP=false
# Set it to the URL from where APIs will be accessible, for local development it should be localhost:3000/api/app (use your local port number instead)
# REACT_APP_SERVERURL=http://localhost:8080/app
# A 12 character long random app identifier. The value of this should be same as APP_ID which is a variable used by backend API.
# (DEPRECATED) This should not be changed if provided; it should be 'opensign'.
REACT_APP_APPID=opensign
# Backend ExpressJS config ****************************************************************************************************************************************************************************************
# A 12 character long random app identifier. The value of this should be same as REACT_APP_APPID which is a variable used by Frontend React App.
APP_ID=opensign
# Name of the app. It will be visible in the verification emails sent out.
appName=open_sign_server
# A 12 character long random secret key that allows access to all the data. It is used in Parse dashboard config to view all the data in the database.
@@ -111,3 +109,6 @@ CRUxFgQUDYlgGVxSxuOknhQc256x3++7BDwwMTAhMAkGBSsOAwIaBQAEFFjASdYl
# Provide Pass pharse of above PFX or p12 document
PASS_PHRASE=opensign
# (DEPRECATED) This should not be changed if provided; it should be 'opensign'.
APP_ID=opensign
+31 -12
View File
@@ -1,8 +1,15 @@
name: ci
# 👇 add this block
permissions:
contents: read # allow checkout & metadata-action to read repo
id-token: write # needed by docker/metadata-action v4
on:
push:
branches:
- 'main'
- 'staging'
jobs:
docker:
@@ -14,34 +21,46 @@ jobs:
dockerfile: apps/OpenSign/Dockerhubfile
- image: opensign/opensignserver
dockerfile: apps/OpenSignServer/Dockerhubfile
steps:
-
name: Checkout
- name: Checkout
uses: actions/checkout@v3
-
name: Login to Docker Hub
# 👇 Enable emulation so we can build for arm64 on amd64 runners
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: linux/amd64,linux/arm64
# 👇 Enable Buildx for multi-arch builds
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Extract metadata (tags, labels) for Docker
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ matrix.image }}
-
name: Debug - List files opensign
- name: Debug - List files opensign
run: ls -R apps/OpenSign/ # Adjust the path as needed
-
name: Debug - List files opensignserver
- name: Debug - List files opensignserver
run: ls -R apps/OpenSignServer/ # Adjust the path as needed
-
name: Build and push
- name: Build and push (multi-arch)
uses: docker/build-push-action@v4
with:
context: .
file: ${{ matrix.dockerfile }}
push: true
# 👇 Build for both AMD64 & ARM64
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+2 -2
View File
@@ -1,7 +1,7 @@
{$HOST_URL} {
reverse_proxy client:3000
handle_path /app/* {
handle_path /api/* {
reverse_proxy server:8080
rewrite * /app{uri}
rewrite * {uri}
}
}
+20 -3
View File
@@ -60,7 +60,7 @@ Welcome to OpenSign, the premier open source docusign alternative - document e-s
- **PDF Template Creation:** OpenSign™ allows you to create and store PDF document templates for repeated use thereby saving you a lot of time & collect e-signatures seamlessly.
- **OpenSign™ Drive:** It is a centralised secure vault for your digital documents that makes storing, signing, organizing, sharing & archieving your docs a breeze.
- **Audit Trails & completion certificate:** Being a security focused solution, OpenSign™ makes it a top priority to save detailed logs for tracking document activities along with time-stamps, IP addresses, email IDs & phone numbers. A completion certificate is generated as soon as document is completed which contains all the document related logs for added safety.
- **API Support:** OpenSign™ API allows seamless integration into existing systems and software. You can generate an API key from the app and refer the [official API docs](https://docs.opensignlabs.com/docs/API-docs/opensign-api-v-1) to start integrating it in your existing applications.
- **API Support:** OpenSign™ API allows seamless integration into existing systems and software. You can generate an API key from the app and refer the [official API docs](https://docs.opensignlabs.com/docs/API-docs/v1.1) to start integrating it in your existing applications.
- **Integrations:** The open source document signing experience becomes even more seamless because of integrations with various Cloud storage systems, CRMs & enterprise platforms. We also have a Zapier integration that allows you to integrate it with virtually any application.
<img alt="Login page" src="https://github.com/OpenSignLabs/OpenSign/assets/171803164/8016edb6-7d6e-4185-ab72-82d2c4a16032" height='200' >
<img alt="Dash_board" src="https://github.com/OpenSignLabs/OpenSign/assets/171803164/6e7b96f7-3154-4c25-bc1e-caf33925cc47" height='200'>
@@ -73,11 +73,27 @@ Welcome to OpenSign, the premier open source docusign alternative - document e-s
---
### Installation
### Deploy
Note: The default MongoDB instance used in deployment is not persistant and will be cleared on every restart. To retain your data, configure and supply your own MongoDB connection URL.
#### DigitalOcean
[![Deploy on DigitalOcean](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/OpenSignLabs/Deploy-OpenSign-to-Digital-Ocean/tree/main&refcode=30db1c901ab0)
#### Docker
The simplest way to install OpenSign on your own server is using official docker images by running the following command -
**Command for linux/MacOS**
```
export HOST_URL=https://opensign.yourdomain.com && curl --remote-name-all https://raw.githubusercontent.com/OpenSignLabs/OpenSign/docker_beta/docker-compose.yml https://raw.githubusercontent.com/OpenSignLabs/OpenSign/docker_beta/Caddyfile https://raw.githubusercontent.com/OpenSignLabs/OpenSign/docker_beta/.env.local_dev && mv .env.local_dev .env.prod && docker compose up --force-recreate
export HOST_URL=https://opensign.yourdomain.com && curl --remote-name-all https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev && mv .env.local_dev .env.prod && docker compose up --force-recreate
```
**Command for Windows (Powershell)**
```
$env:HOST_URL="https://opensign.yourdomain.com"; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml -OutFile docker-compose.yml; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile -OutFile Caddyfile; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev -OutFile .env.local_dev; Rename-Item -Path .env.local_dev -NewName .env.prod; docker compose up --force-recreate
```
**Command for Windows (CMD/Terminal)**
```
set HOST_URL=https://opensign.yourdomain.com && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev && rename .env.local_dev .env.prod && docker compose up --force-recreate
```
Make sure that you have `Docker` and `git` installed before you run this command -
@@ -149,6 +165,7 @@ We would like to thank all our contributors and users for their support and feed
<td align="center" valign="top" width="14.28%"><a href="https://github.com/daniel-mutwiri"><img src="https://avatars.githubusercontent.com/u/8936960?v=4?s=100" width="100px;" alt="Daniel Mutwiri"/><br /><sub><b>Daniel Mutwiri</b></sub></a><br /><a href="#code-daniel-mutwiri" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zathiel"><img src="https://avatars.githubusercontent.com/u/26553418?v=4?s=100" width="100px;" alt="Zathiel"/><br /><sub><b>Zathiel</b></sub></a><br /><a href="#code-Zathiel" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1024mb"><img src="https://avatars.githubusercontent.com/u/9301204?v=4?s=100" width="100px;" alt="1024mb"/><br /><sub><b>1024mb</b></sub></a><br /><a href="#translation-1024mb" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ehowe"><img src="https://avatars.githubusercontent.com/u/756984?v=4?s=100" width="100px;" alt="Eugene Howe"/><br /><sub><b>Eugene Howe</b></sub></a><br /><a href="#code-ehowe" title="Code">💻</a></td>
</tr>
</tbody>
</table>
-25
View File
@@ -1,25 +0,0 @@
# Use an official Node runtime as the base image
FROM node:18
# Set the working directory inside the container
WORKDIR /usr/src/app
# Copy package.json and package-lock.json first to leverage Docker cache
COPY ./package*.json ./
# Install application dependencies
RUN npm install
# Copy the current directory contents into the container
COPY ./ .
COPY ./.husky .
# Make port 3000 available to the world outside this container
EXPOSE 3000
# Define environment variables if needed
# ENV NODE_ENV production
# Run the application
ENTRYPOINT npm run start
+11 -3
View File
@@ -1,5 +1,5 @@
# Use an official Node runtime as the base image
FROM node:18
FROM node:22.14.0
# Set the working directory inside the container
WORKDIR /usr/src/app
@@ -13,16 +13,24 @@ RUN npm install
# Copy the current directory contents into the container
COPY apps/OpenSign/ .
COPY apps/OpenSign/.husky .
COPY apps/OpenSign/entrypoint.sh .
# make the entrypoint.sh file executable
RUN chmod +x entrypoint.sh
# Define environment variables if needed
ENV NODE_ENV=production
ENV REACT_APP_DEPLOYMENT=free_selfhost
ENV GENERATE_SOURCEMAP=false
# build
RUN npm run build
# Inject env.js loader into index.html
RUN sed -i '/<head>/a\<script src="/env.js"></script>' build/index.html
# Make port 3000 available to the world outside this container
EXPOSE 3000
ENTRYPOINT ["./entrypoint.sh"]
# Run the application
CMD ["npm", "start"]
+213
View File
@@ -0,0 +1,213 @@
# Tailwind Dark Mode Usage Guide
## Overview
This guide shows how to use the new Tailwind utilities for better dark mode visibility in OpenSign.
## Button Styling
### VS Code-style Disabled Buttons
```jsx
// Option 1: Direct VS Code styling
<button className="op-btn op-btn-primary op-btn-vscode-disabled" disabled>
Disabled Button
</button>
// Option 2: Themed disabled styling
<button className="op-btn btn-themed-disabled">
Themed Button
</button>
// Option 3: Conditional styling
<button
className={`op-btn op-btn-primary ${isDisabled ? 'op-btn-vscode-disabled' : ''}`}
disabled={isDisabled}
>
Dynamic Button
</button>
```
## Icon Styling
### Theme-aware Icons
```jsx
// Better visibility in dark mode
<i className="fa-light fa-folder icon-improved"></i>
// Muted but still visible
<i className="fa-light fa-plus icon-muted"></i>
// Disabled state
<i className="fa-light fa-trash icon-disabled"></i>
```
### CSS Variable Approach
```jsx
// Using CSS variables
<i className="fa-light fa-search icon-themed"></i>
<i className="fa-light fa-settings icon-themed-muted"></i>
// Inline styles with CSS variables
<i
className="fa-light fa-plus"
style={{ color: 'var(--icon-color)' }}
/>
```
### Legacy JavaScript Function (Still Supported)
```jsx
// Existing approach - still works
<i
className="fa-light fa-plus"
style={{ color: getThemeIconColor() }}
/>
```
## Text Styling
### Improved Gray Text
```jsx
// These automatically improve in dark mode
<span className="text-gray-500">More visible in dark mode</span>
<span className="text-gray-400">Muted but readable</span>
<span className="text-gray-600">Clear text</span>
```
## Complete Examples
### Toolbar with Better Visibility
```jsx
const Toolbar = () => (
<div className="flex space-x-2 p-2">
<button className="p-2 hover:bg-gray-200 rounded">
<i className="fa-light fa-plus icon-improved"></i>
</button>
<button className="p-2 hover:bg-gray-200 rounded" disabled>
<i className="fa-light fa-trash icon-disabled"></i>
</button>
<button className="p-2 hover:bg-gray-200 rounded">
<i className="fa-light fa-edit icon-improved"></i>
</button>
</div>
);
```
### Form with Disabled States
```jsx
const Form = ({ isSubmitting }) => (
<form>
<input className="op-input" />
<button
className={`op-btn op-btn-primary ${isSubmitting ? 'op-btn-vscode-disabled' : ''}`}
disabled={isSubmitting}
>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
```
## React-Tour and Tooltip Dark Mode Support
### React-Tour Modals
The react-tour modals now automatically support dark mode with VS Code-inspired styling:
```jsx
// These components automatically get dark mode styling
<Tour
onRequestClose={closeTour}
steps={tourConfig}
isOpen={isOpen}
rounded={5}
/>
```
### ReactTooltip Components
All ReactTooltip instances now support dark mode:
```jsx
// Automatically styled for dark mode
<ReactTooltip id="my-tooltip" className="z-[999]">
<div className="max-w-[200px]">
<p>Tooltip content</p>
</div>
</ReactTooltip>
```
### HoverCard Balloon UI
The balloon tooltips in OpenSign Drive now properly support dark mode:
```jsx
// These automatically get dark styling in dark mode
<HoverCard>
<HoverCardContent>
Document information
</HoverCardContent>
</HoverCard>
```
## Dark Mode Features Added
### 1. **React-Tour Modal Styling**
- Background: `#1F2937` (VS Code modal background)
- Text: `#E5E7EB` (soft white for readability)
- Borders: `#374151` (subtle borders)
- Buttons: VS Code-style primary/secondary buttons
### 2. **ReactTooltip Styling**
- Background: `#1F2937` with proper contrast
- Border: `#374151` for definition
- Box shadow: Enhanced for dark backgrounds
- Text: `#E5E7EB` for optimal readability
### 3. **HoverCard Balloon UI**
- Background: `#1F2937` (matches VS Code)
- Text: `#E5E7EB` for readability
- Arrow: Automatically matches background color
- Enhanced shadows for dark backgrounds
### 4. **React-Datepicker Support**
- Calendar background: `#1F2937`
- Selected dates: VS Code blue (`#007ACC`)
- Hover states: Proper contrast ratios
- Navigation arrows: Themed appropriately
## CSS Classes Reference
| Class | Purpose | Dark Mode Color |
|-------|---------|----------------|
| `icon-improved` | Better icon visibility | `#CCCCCC` |
| `icon-muted` | Muted but visible icons | `#999999` |
| `icon-disabled` | Disabled icon state | `#858585` |
| `op-btn-vscode-disabled` | VS Code disabled button | Background: `#3C3C3C` |
| `btn-themed-disabled` | Themed disabled button | Uses CSS variables |
| `icon-themed` | Variable-based icon color | `var(--icon-color)` |
| `.reactour__helper` | `#1F2937` background |
| `.react-tooltip` | `#1F2937` background |
| `.HoverCardContent` | `#1F2937` background |
| `.react-datepicker` | `#1F2937` background |
## Migration Guide
### From JavaScript Function to Tailwind
```jsx
// Before
<i style={{ color: getThemeIconColor() }} className="fa-light fa-plus" />
// After
<i className="fa-light fa-plus icon-improved" />
```
### From Hardcoded Colors to Theme-aware
```jsx
// Before
<i className="fa-light fa-plus text-gray-500" />
// After (automatic improvement)
<i className="fa-light fa-plus text-gray-500" />
// OR explicitly
<i className="fa-light fa-plus icon-improved" />
```
## Migration Notes
All existing tooltip and tour components will automatically inherit the new dark mode styling when the theme is set to `opensigndark`. No code changes required for existing implementations.
+29
View File
@@ -0,0 +1,29 @@
#!/bin/sh
ENV_FILE=./build/env.js
DOTENV_FILE=./.env.prod # ✅ use .env.prod
echo "Generating runtime env file at $ENV_FILE..."
echo "window.RUNTIME_ENV = {" > $ENV_FILE
# List of keys to include
RUNTIME_KEYS="REACT_APP_SERVERURL"
for key in $RUNTIME_KEYS; do
# First check docker env (-e), fallback to .env file
value=$(printenv "$key")
if [ -z "$value" ] && [ -f "$DOTENV_FILE" ]; then
# fallback: read from .env
value=$(grep "^$key=" "$DOTENV_FILE" | cut -d '=' -f2- | tr -d '\r\n' | sed 's/"/\\"/g')
else
value=$(echo "$value" | sed 's/"/\\"/g')
fi
echo " $key: \"$value\"," >> $ENV_FILE
done
echo "};" >> $ENV_FILE
exec "$@"
@@ -0,0 +1,90 @@
/**
* Tailwind Dark Mode Usage Examples for OpenSign
*
* This file demonstrates how to use the new Tailwind utilities
* for better dark mode visibility of buttons and icons.
*/
// Example 1: VS Code-style disabled buttons
const DisabledButtonExamples = () => {
return (
<div>
{/* Option A: Using the VS Code disabled style */}
<button className="op-btn op-btn-primary op-btn-vscode-disabled" disabled>
VS Code Style Disabled Button
</button>
{/* Option B: Using themed disabled style */}
<button className="op-btn btn-themed-disabled">
Themed Disabled Button
</button>
{/* Option C: Conditional styling */}
<button
className={`op-btn op-btn-primary ${isDisabled ? "op-btn-vscode-disabled" : ""}`}
disabled={isDisabled}
>
Conditional Button
</button>
</div>
);
};
// Example 2: Icon visibility improvements
const IconExamples = () => {
return (
<div>
{/* Theme-aware icons with better visibility */}
<i className="fa-light fa-folder icon-improved"></i>
<i className="fa-light fa-plus icon-muted"></i>
<i className="fa-light fa-trash icon-disabled"></i>
{/* Using CSS variables */}
<i className="fa-light fa-search icon-themed"></i>
<i className="fa-light fa-settings icon-themed-muted"></i>
{/* Gray text that automatically improves in dark mode */}
<span className="text-gray-500">
This text is now more visible in dark mode
</span>
<span className="text-gray-400">Muted but still readable</span>
</div>
);
};
// Example 3: Using CSS variables in inline styles
const InlineStyleExamples = () => {
return (
<div>
{/* Using CSS variables directly */}
<i className="fa-light fa-plus" style={{ color: "var(--icon-color)" }} />
{/* Using the existing JavaScript function */}
<i className="fa-light fa-minus" style={{ color: getThemeIconColor() }} />
</div>
);
};
// Example 4: Toolbar with improved icons
const ToolbarExample = () => {
return (
<div className="flex space-x-2 p-2">
<button className="p-2 hover:bg-gray-200 rounded">
<i className="fa-light fa-plus icon-improved"></i>
</button>
<button className="p-2 hover:bg-gray-200 rounded" disabled>
<i className="fa-light fa-trash icon-disabled"></i>
</button>
<button className="p-2 hover:bg-gray-200 rounded">
<i className="fa-light fa-edit icon-improved"></i>
</button>
</div>
);
};
export {
DisabledButtonExamples,
IconExamples,
InlineStyleExamples,
ToolbarExample
};
@@ -0,0 +1,94 @@
/**
* Tailwind Dark Mode Usage Examples for OpenSign
*
* This file demonstrates how to use the new Tailwind utilities
* for better dark mode visibility of buttons and icons.
*/
// Example 1: VS Code-style disabled buttons
const DisabledButtonExamples = () => {
return (
<div>
{/* Option A: Using the VS Code disabled style */}
<button className="op-btn op-btn-primary op-btn-vscode-disabled" disabled>
VS Code Style Disabled Button
</button>
{/* Option B: Using themed disabled style */}
<button className="op-btn btn-themed-disabled">
Themed Disabled Button
</button>
{/* Option C: Conditional styling */}
<button
className={`op-btn op-btn-primary ${isDisabled ? 'op-btn-vscode-disabled' : ''}`}
disabled={isDisabled}
>
Conditional Button
</button>
</div>
);
};
// Example 2: Icon visibility improvements
const IconExamples = () => {
return (
<div>
{/* Theme-aware icons with better visibility */}
<i className="fa-light fa-folder icon-improved"></i>
<i className="fa-light fa-plus icon-muted"></i>
<i className="fa-light fa-trash icon-disabled"></i>
{/* Using CSS variables */}
<i className="fa-light fa-search icon-themed"></i>
<i className="fa-light fa-settings icon-themed-muted"></i>
{/* Gray text that automatically improves in dark mode */}
<span className="text-gray-500">This text is now more visible in dark mode</span>
<span className="text-gray-400">Muted but still readable</span>
</div>
);
};
// Example 3: Using CSS variables in inline styles
const InlineStyleExamples = () => {
return (
<div>
{/* Using CSS variables directly */}
<i
className="fa-light fa-plus"
style={{ color: 'var(--icon-color)' }}
/>
{/* Using the existing JavaScript function */}
<i
className="fa-light fa-minus"
style={{ color: getThemeIconColor() }}
/>
</div>
);
};
// Example 4: Toolbar with improved icons
const ToolbarExample = () => {
return (
<div className="flex space-x-2 p-2">
<button className="p-2 hover:bg-gray-200 rounded">
<i className="fa-light fa-plus icon-improved"></i>
</button>
<button className="p-2 hover:bg-gray-200 rounded" disabled>
<i className="fa-light fa-trash icon-disabled"></i>
</button>
<button className="p-2 hover:bg-gray-200 rounded">
<i className="fa-light fa-edit icon-improved"></i>
</button>
</div>
);
};
export {
DisabledButtonExamples,
IconExamples,
InlineStyleExamples,
ToolbarExample
};
@@ -1,20 +1,22 @@
<!DOCTYPE html>
<html lang="en" data-theme="opensigncss">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="The fastest way to sign PDFs & request signatures from others" />
<!-- <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> -->
<!-- <link rel="apple-touch-icon" href="/logo192.png" /> -->
<link rel="manifest" href="/manifest.json" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdn.opensignlabs.com/fonts.css" />
<title>OpenSign™</title>
<link rel="stylesheet" href="https://cdn.opensignlabs.com/fonts.css" />
<title>...Loading</title>
</head>
<body>
<div id="root" style="touch-action:pan-x pan-y;"></div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>
-28358
View File
File diff suppressed because it is too large Load Diff
+75 -71
View File
@@ -1,74 +1,71 @@
{
"name": "open_sign",
"version": "0.1.0",
"version": "2.35.0",
"private": true,
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
"@lottiefiles/dotlottie-react": "^0.12.3",
"@formkit/auto-animate": "^0.9.0",
"@imgly/background-removal": "^1.7.0",
"@lottiefiles/dotlottie-react": "^0.17.13",
"@pdf-lib/fontkit": "^1.1.1",
"@radix-ui/themes": "^3.1.6",
"@react-pdf/renderer": "^4.1.6",
"@reduxjs/toolkit": "^2.5.1",
"axios": "^1.7.9",
"css-minimizer-webpack-plugin": "^7.0.0",
"@radix-ui/themes": "^3.3.0",
"@reactour/tour": "^3.8.0",
"@reduxjs/toolkit": "^2.11.2",
"axios": "^1.13.4",
"body-scroll-lock": "^4.0.0-beta.0",
"date-fns-tz": "^3.2.0",
"file-saver": "^2.0.5",
"i18next": "^23.16.8",
"i18next-browser-languagedetector": "^8.0.2",
"i18next-http-backend": "^3.0.1",
"i18next": "^25.8.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"jszip": "^3.10.1",
"jwt-decode": "^4.0.0",
"moment": "^2.30.1",
"nth-check": "^2.1.1",
"parse": "^5.3.0",
"parse": "^8.0.3",
"pdf-lib": "^1.17.1",
"pkijs": "^3.3.3",
"print-js": "^1.6.0",
"radix-ui": "^1.0.1",
"react": "^18.2.0",
"react-bootstrap": "^2.10.9",
"react-confetti": "^6.2.2",
"react-cookie": "^7.2.2",
"react-datepicker": "^7.6.0",
"prismjs": "^1.30.0",
"quill-html-edit-button": "^3.0.0",
"radix-ui": "^1.4.3",
"react": "^19.2.4",
"react-bootstrap": "^2.10.10",
"react-confetti": "^6.4.0",
"react-datepicker": "^9.1.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dnd-multi-backend": "^9.0.0",
"react-dnd-touch-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-dom": "^19.2.4",
"react-gtm-module": "^2.0.11",
"react-helmet": "^6.1.0",
"react-i18next": "^15.4.0",
"react-konva": "^18.2.10",
"react-pdf": "^9.2.1",
"react-quill-new": "^3.3.3",
"react-i18next": "^16.5.4",
"react-konva": "^19.2.1",
"react-pdf": "^10.2.0",
"react-quill-new": "^3.8.3",
"react-redux": "^9.2.0",
"react-rnd": "^10.4.14",
"react-router": "^7.1.5",
"react-scripts": "5.0.1",
"react-rnd": "^10.5.2",
"react-router": "^7.13.0",
"react-scrollbars-custom": "^4.1.1",
"react-select": "^5.10.0",
"react-signature-canvas": "^1.0.7",
"react-syntax-highlighter": "^15.6.1",
"react-select": "^5.10.2",
"react-signature-canvas": "^1.1.0-alpha.2",
"react-timezone-select": "^3.2.8",
"react-tooltip": "^5.28.0",
"react-web-share": "^2.0.2",
"reactour": "^1.19.4",
"react-to-print": "^3.2.0",
"react-tooltip": "^5.30.0",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"regex-parser": "^2.3.0",
"serve": "^14.2.4",
"styled-components": "^5.3.0",
"web-vitals": "^4.2.4",
"ws": "^8.18.0",
"xlsx": "^0.18.5"
"regex-parser": "^2.3.1",
"serve": "^14.2.5",
"web-vitals": "^5.1.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
},
"scripts": {
"build": "npm run version && react-scripts build",
"start-dev": "react-scripts start",
"build": "npm run version && NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
"start-dev": "vite",
"dev": "vite",
"preview": "vite preview",
"start": "serve -s build",
"version": "curl -s https://api.github.com/repos/opensignlabs/opensign/releases/latest | grep '\"tag_name\":' | awk -F '\"' '{print $4}' > ./public/version.txt",
"version-win": "powershell -Command \"Invoke-RestMethod -Uri 'https://api.github.com/repos/opensignlabs/opensign/releases/latest' | Select-Object -ExpandProperty tag_name | Out-File -FilePath ./public/version.txt\"",
"build-win": "npm run version-win && react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"build-win": "npm run version-win && vite build",
"test": "vitest run",
"test:watch": "vitest",
"release": "standard-version",
"commit": "cz"
},
@@ -96,33 +93,40 @@
}
},
"devDependencies": {
"@babel/core": "^7.26.8",
"@babel/preset-env": "^7.26.8",
"@babel/preset-react": "^7.26.3",
"@babel/runtime-corejs2": "^7.26.7",
"autoprefixer": "^10.4.20",
"babel-loader": "^9.2.1",
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.0",
"@babel/preset-react": "^7.28.5",
"@babel/runtime-corejs2": "^7.28.6",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.2.10",
"@vitejs/plugin-react": "^5.1.3",
"@vitest/ui": "^4.0.18",
"autoprefixer": "^10.4.24",
"babel-loader": "^10.0.0",
"commitizen": "^4.3.1",
"concurrently": "^9.1.2",
"css-loader": "^7.1.2",
"daisyui": "^4.12.23",
"dotenv": "^16.4.7",
"dotenv-webpack": "^8.1.0",
"eslint": "^9.20.0",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react": "^7.37.4",
"lint-staged": "^15.4.3",
"mini-css-extract-plugin": "^2.9.2",
"postcss": "^8.5.1",
"prettier": "^3.5.0",
"pretty-quick": "^4.0.0",
"tailwindcss": "^3.4.17",
"terser-webpack-plugin": "^5.3.11",
"webpack-cli": "^5.1.4"
"concurrently": "^9.2.1",
"css-loader": "^7.1.3",
"daisyui": "^4.12.24",
"eslint": "^9.39.2",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react": "^7.37.5",
"jsdom": "^28.0.0",
"lint-staged": "^16.2.7",
"postcss": "^8.5.6",
"prettier": "^3.8.1",
"pretty-quick": "^4.2.2",
"rollup-plugin-node-polyfills": "^0.2.1",
"tailwindcss": "^3.4.19",
"tmp": "^0.2.5",
"vite": "^7.3.1",
"vite-plugin-svgr": "^4.5.0",
"vite-tsconfig-paths": "^6.0.5",
"vitest": "^4.0.18"
},
"overrides": {
"nth-check": "$nth-check",
"ws": "$ws"
"tmp": "$tmp"
},
"engines": {
"node": "18 || 20 || 22"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

+4
View File
@@ -0,0 +1,4 @@
Name,Email,Phone,Company,JobTitle
John Doe,john@example.com,1234567890,abc firm,dev
Jane Smith,jane@example.com,9876543210,xyz firm,manager
Foo Bar,foo@example.com,5555555555,xyz firm,director
1 Name Email Phone Company JobTitle
2 John Doe john@example.com 1234567890 abc firm dev
3 Jane Smith jane@example.com 9876543210 xyz firm manager
4 Foo Bar foo@example.com 5555555555 xyz firm director
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.
+1
View File
@@ -0,0 +1 @@
import "@testing-library/jest-dom";
-175
View File
@@ -1,175 +0,0 @@
import React, { useState, useEffect, lazy } from "react";
import { Routes, Route, BrowserRouter } from "react-router";
import { pdfjs } from "react-pdf";
import Login from "./pages/Login";
import Form from "./pages/Form";
import Report from "./pages/Report";
import Dashboard from "./pages/Dashboard";
import HomeLayout from "./layout/HomeLayout";
import PageNotFound from "./pages/PageNotFound";
import ValidateRoute from "./primitives/ValidateRoute";
import Validate from "./primitives/Validate";
import TemplatePlaceholder from "./pages/TemplatePlaceholder";
import SignYourSelf from "./pages/SignyourselfPdf";
import DraftDocument from "./components/pdf/DraftDocument";
import PlaceHolderSign from "./pages/PlaceHolderSign";
import PdfRequestFiles from "./pages/PdfRequestFiles";
import LazyPage from "./primitives/LazyPage";
import Loader from "./primitives/Loader";
import UserList from "./pages/UserList";
import { serverUrl_fn } from "./constant/appinfo";
import DocSuccessPage from "./pages/DocSuccessPage";
const DebugPdf = lazy(() => import("./pages/DebugPdf"));
const ForgetPassword = lazy(() => import("./pages/ForgetPassword"));
const GuestLogin = lazy(() => import("./pages/GuestLogin"));
const ChangePassword = lazy(() => import("./pages/ChangePassword"));
const UserProfile = lazy(() => import("./pages/UserProfile"));
const Opensigndrive = lazy(() => import("./pages/Opensigndrive"));
const ManageSign = lazy(() => import("./pages/Managesign"));
const AddAdmin = lazy(() => import("./pages/AddAdmin"));
const UpdateExistUserAdmin = lazy(() => import("./pages/UpdateExistUserAdmin"));
const Preferences = lazy(() => import("./pages/Preferences"));
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
const AppLoader = () => {
return (
<div className="flex justify-center items-center h-[100vh]">
<Loader />
</div>
);
};
function App() {
const [isloading, setIsLoading] = useState(true);
useEffect(() => {
handleCredentials();
}, []);
const handleCredentials = () => {
const appId = process.env.REACT_APP_APPID
? process.env.REACT_APP_APPID
: "opensign";
const baseurl = serverUrl_fn();
try {
localStorage.setItem("baseUrl", `${baseurl}/`);
localStorage.setItem("parseAppId", appId);
setIsLoading(false);
} catch (error) {
console.log("err ", error);
}
};
return (
<div className="bg-base-200">
{isloading ? (
<AppLoader />
) : (
<BrowserRouter>
<Routes>
<Route element={<ValidateRoute />}>
<Route exact path="/" element={<Login />} />
<Route
path="/addadmin"
element={<LazyPage Page={AddAdmin} />}
/>
<Route
path="/upgrade-2.1"
element={<LazyPage Page={UpdateExistUserAdmin} />}
/>
</Route>
<Route element={<Validate />}>
<Route
path="/load/template/:templateId"
element={<TemplatePlaceholder />}
/>
<Route
exact
path="/load/placeholdersign/:docId"
element={<PlaceHolderSign />}
/>
<Route
exact
path="/load/recipientSignPdf/:docId/:contactBookId"
element={<PdfRequestFiles />}
/>
</Route>
<Route
path="/loadmf/signmicroapp/login/:id/:userMail/:contactBookId/:serverUrl"
element={<LazyPage Page={GuestLogin} />}
/>
<Route
path="/login/:id/:userMail/:contactBookId/:serverUrl"
element={<LazyPage Page={GuestLogin} />}
/>
<Route
path="/login/:base64url"
element={<LazyPage Page={GuestLogin} />}
/>
<Route path="/debugpdf" element={<LazyPage Page={DebugPdf} />} />
<Route
path="/forgetpassword"
element={<LazyPage Page={ForgetPassword} />}
/>
<Route element={<HomeLayout />}>
<Route
path="/changepassword"
element={<LazyPage Page={ChangePassword} />}
/>
<Route path="/form/:id" element={<Form />} />
<Route path="/report/:id" element={<Report />} />
<Route path="/dashboard/:id" element={<Dashboard />} />
<Route
path="/profile"
element={<LazyPage Page={UserProfile} />}
/>
<Route
path="/opensigndrive"
element={<LazyPage Page={Opensigndrive} />}
/>
<Route
path="/managesign"
element={<LazyPage Page={ManageSign} />}
/>
<Route
path="/template/:templateId"
element={<TemplatePlaceholder />}
/>
{/* signyouself route with no rowlevel data using docId from url */}
<Route path="/signaturePdf/:docId" element={<SignYourSelf />} />
{/* draft document route to handle and navigate route page accordiing to document status */}
<Route path="/draftDocument" element={<DraftDocument />} />
{/* recipient placeholder set route with no rowlevel data using docId from url*/}
<Route
path="/placeHolderSign/:docId"
element={<PlaceHolderSign />}
/>
{/* for user signature (need your sign route) with row level data */}
<Route path="/pdfRequestFiles" element={<PdfRequestFiles />} />
{/* for user signature (need your sign route) with no row level data */}
<Route
path="/pdfRequestFiles/:docId"
element={<PdfRequestFiles />}
/>
{/* recipient signature route with no rowlevel data using docId from url */}
<Route
path="/recipientSignPdf/:docId/:contactBookId"
element={<PdfRequestFiles />}
/>
<Route
path="/recipientSignPdf/:docId"
element={<PdfRequestFiles />}
/>
<Route path="/users" element={<UserList />} />
<Route
path="/preferences"
element={<LazyPage Page={Preferences} />}
/>
</Route>
<Route path="/success" element={<DocSuccessPage />} />
<Route path="*" element={<PageNotFound />} />
</Routes>
</BrowserRouter>
)}
</div>
);
}
export default App;
+148
View File
@@ -0,0 +1,148 @@
import { useState, useEffect } from "react";
import { lazyWithRetry, hideUpgradeProgress } from "./utils";
import { Routes, Route, BrowserRouter } from "react-router";
import { pdfjs } from "react-pdf";
import Form from "./pages/Form";
import Report from "./pages/Report";
import Dashboard from "./pages/Dashboard";
import HomeLayout from "./layout/HomeLayout";
import PageNotFound from "./pages/PageNotFound";
import ValidateRoute from "./primitives/ValidateRoute";
import Validate from "./primitives/Validate";
import TemplatePlaceholder from "./pages/TemplatePlaceholder";
import SignYourSelf from "./pages/SignyourselfPdf";
import DraftDocument from "./components/pdf/DraftDocument";
import PlaceHolderSign from "./pages/PlaceHolderSign";
import PdfRequestFiles from "./pages/PdfRequestFiles";
import Lazy from "./primitives/LazyPage";
import Loader from "./primitives/Loader";
import UserList from "./pages/UserList";
import { serverUrl_fn } from "./constant/appinfo";
import DocSuccessPage from "./pages/DocSuccessPage";
import DragProvider from "./components/DragProivder";
import Title from "./components/Title";
const DebugPdf = lazyWithRetry(() => import("./pages/DebugPdf"));
const ForgetPassword = lazyWithRetry(() => import("./pages/ForgetPassword"));
const GuestLogin = lazyWithRetry(() => import("./pages/GuestLogin"));
const ChangePassword = lazyWithRetry(() => import("./pages/ChangePassword"));
const UserProfile = lazyWithRetry(() => import("./pages/UserProfile"));
const Opensigndrive = lazyWithRetry(() => import("./pages/Opensigndrive"));
const ManageSign = lazyWithRetry(() => import("./pages/Managesign"));
const AddAdmin = lazyWithRetry(() => import("./pages/AddAdmin"));
const UpdateExistUserAdmin = lazyWithRetry(
() => import("./pages/UpdateExistUserAdmin")
);
const Preferences = lazyWithRetry(() => import("./pages/Preferences"));
const Login = lazyWithRetry(() => import("./pages/Login"));
const VerifyDocument = lazyWithRetry(() => import("./pages/VerifyDocument"));
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
const AppLoader = () => {
return (
<div className="flex justify-center items-center h-[100vh]">
<Loader />
</div>
);
};
function App() {
const [isloading, setIsLoading] = useState(true);
useEffect(() => {
// initialize creds
const id = process.env.REACT_APP_APPID ?? "opensign";
localStorage.setItem("parseAppId", id);
localStorage.setItem("baseUrl", `${serverUrl_fn()}/`);
hideUpgradeProgress();
localStorage.removeItem("showUpgradeProgress");
setIsLoading(false);
}, []);
return (
<div className="bg-base-200">
{isloading ? (
<AppLoader />
) : (
<BrowserRouter>
<Title />
<Routes>
<Route element={<ValidateRoute />}>
<Route exact path="/" element={<Lazy Page={Login} />} />
<Route path="/addadmin" element={<Lazy Page={AddAdmin} />} />
<Route
path="/upgrade-2.1"
element={<Lazy Page={UpdateExistUserAdmin} />}
/>
</Route>
<Route element={<Validate />}>
<Route
exact
path="/load/recipientSignPdf/:docId/:contactBookId"
element={<DragProvider Page={PdfRequestFiles} />}
/>
</Route>
<Route
path="/login/:base64url"
element={<Lazy Page={GuestLogin} />}
/>
<Route path="/debugpdf" element={<Lazy Page={DebugPdf} />} />
<Route
path="/forgetpassword"
element={<Lazy Page={ForgetPassword} />}
/>
<Route element={<HomeLayout />}>
<Route path="/users" element={<UserList />} />
<Route
path="/changepassword"
element={<Lazy Page={ChangePassword} />}
/>
<Route path="/form/:id" element={<Form />} />
<Route path="/report/:id" element={<Report />} />
<Route path="/dashboard/:id" element={<Dashboard />} />
<Route path="/profile" element={<Lazy Page={UserProfile} />} />
<Route path="/drive" element={<Lazy Page={Opensigndrive} />} />
<Route path="/managesign" element={<Lazy Page={ManageSign} />} />
<Route
path="/template/:templateId"
element={<DragProvider Page={TemplatePlaceholder} />}
/>
{/* signyouself route with no rowlevel data using docId from url */}
<Route
path="/signaturePdf/:docId"
element={<DragProvider Page={SignYourSelf} />}
/>
{/* draft document route to handle and navigate route page according to document status */}
<Route
path="/draftDocument"
element={<DragProvider Page={DraftDocument} />}
/>
{/* recipient placeholder set route with no rowlevel data using docId from url*/}
<Route
path="/placeHolderSign/:docId"
element={<DragProvider Page={PlaceHolderSign} />}
/>
{/* recipient signature route with no rowlevel data using docId from url */}
<Route
path="/recipientSignPdf/:docId/:contactBookId"
element={<DragProvider Page={PdfRequestFiles} />}
/>
<Route
path="/recipientSignPdf/:docId"
element={<DragProvider Page={PdfRequestFiles} />}
/>
<Route
path="/verify-document"
element={<Lazy Page={VerifyDocument} />}
/>
<Route
path="/preferences"
element={<Lazy Page={Preferences} />}
/>
</Route>
<Route path="/success" element={<DocSuccessPage />} />
<Route path="*" element={<PageNotFound />} />
</Routes>
</BrowserRouter>
)}
</div>
);
}
export default App;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

-300
View File
@@ -1,300 +0,0 @@
import React, { useState, useEffect } from "react";
import Parse from "parse";
import Loader from "../primitives/Loader";
import { useTranslation } from "react-i18next";
import { emailRegex } from "../constant/const";
const AddSigner = (props) => {
const { t } = useTranslation();
const [name, setName] = useState("");
const [phone, setPhone] = useState("");
const [email, setEmail] = useState("");
const [addYourself, setAddYourself] = useState(false);
const [isLoader, setIsLoader] = useState(false);
const [isUserExist, setIsUserExist] = useState(false);
useEffect(() => {
checkUserExist();
}, []);
// Load user details from localStorage when the component mounts
useEffect(() => {
const savedUserDetails = JSON.parse(
localStorage.getItem("UserInformation")
);
if (savedUserDetails && addYourself) {
setName(savedUserDetails.name);
setPhone(savedUserDetails?.phone || "");
setEmail(savedUserDetails.email);
}
}, [addYourself]);
const checkUserExist = async () => {
const user = Parse.User.current();
try {
const query = new Parse.Query("contracts_Contactbook");
query.equalTo("CreatedBy", user);
query.notEqualTo("IsDeleted", true);
query.equalTo("Email", user.getEmail());
const res = await query.first();
// console.log(res);
if (!res) {
setIsUserExist(true);
}
} catch (err) {
console.log("err", err);
}
};
// Define a function to handle form submission
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
if (!emailRegex.test(email)) {
alert("Please enter a valid email address.");
} else {
setIsLoader(true);
if (localStorage.getItem("TenantId")) {
try {
const user = Parse.User.current();
const query = new Parse.Query("contracts_Contactbook");
query.equalTo("CreatedBy", user);
query.notEqualTo("IsDeleted", true);
query.equalTo("Email", email);
const res = await query.first();
// console.log(res);
if (!res) {
const contactQuery = new Parse.Object("contracts_Contactbook");
contactQuery.set("Name", name);
if (phone) {
contactQuery.set("Phone", phone);
}
contactQuery.set("Email", email);
contactQuery.set("UserRole", "contracts_Guest");
contactQuery.set("TenantId", {
__type: "Pointer",
className: "partners_Tenant",
objectId: localStorage.getItem("TenantId")
});
try {
const _users = Parse.Object.extend("User");
const _user = new _users();
_user.set("name", name);
_user.set("username", email);
_user.set("email", email);
_user.set("password", email);
if (phone) {
_user.set("phone", phone);
}
const user = await _user.save();
if (user) {
const currentUser = Parse.User.current();
contactQuery.set(
"CreatedBy",
Parse.User.createWithoutData(currentUser.id)
);
contactQuery.set("UserId", user);
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(true);
acl.setReadAccess(currentUser.id, true);
acl.setWriteAccess(currentUser.id, true);
contactQuery.setACL(acl);
const res = await contactQuery.save();
const parseData = JSON.parse(JSON.stringify(res));
if (props.details) {
props.details({
value: parseData[props.valueKey],
label: parseData[props.displayKey],
email: parseData?.Email
});
}
if (props.closePopup) {
props.closePopup();
}
if (props.handleUserData) {
props.handleUserData(parseData);
}
setIsLoader(false);
// Reset the form fields
setAddYourself(false);
setName("");
setPhone("");
setEmail("");
}
} catch (err) {
console.log("err ", err);
if (err.code === 202) {
const params = { email: email };
const userRes = await Parse.Cloud.run("getUserId", params);
const currentUser = Parse.User.current();
contactQuery.set(
"CreatedBy",
Parse.User.createWithoutData(currentUser.id)
);
contactQuery.set("UserId", {
__type: "Pointer",
className: "_User",
objectId: userRes.id
});
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(true);
acl.setReadAccess(currentUser.id, true);
acl.setWriteAccess(currentUser.id, true);
contactQuery.setACL(acl);
const res = await contactQuery.save();
const parseData = JSON.parse(JSON.stringify(res));
if (props.details) {
props.details({
value: parseData[props.valueKey],
label: parseData[props.displayKey],
email: parseData?.Email
});
}
if (props.closePopup) {
props.closePopup();
}
if (props.handleUserData) {
props.handleUserData(parseData);
}
setIsLoader(false);
// Reset the form fields
setAddYourself(false);
setName("");
setPhone("");
setEmail("");
}
}
} else {
alert(t("add-signer-alert"));
setIsLoader(false);
}
} catch (err) {
console.log("err in fetch contact", err);
setIsLoader(false);
alert(t("something-went-wrong-mssg"));
}
} else {
setIsLoader(false);
alert(t("something-went-wrong-mssg"));
}
}
};
// Define a function to handle the "add yourself" checkbox
const handleAddYourselfChange = () => {
if (addYourself) {
setAddYourself(false);
setName("");
setPhone("");
setEmail("");
} else {
setAddYourself(true);
}
};
const handleReset = () => {
setAddYourself(false);
};
return (
<div className="h-full p-[20px]">
{isLoader && (
<div className="fixed inset-0 flex justify-center items-center bg-black bg-opacity-30">
<Loader />
</div>
)}
<div className="w-full mx-auto p-2 text-base-content">
<p className="mb-3 text-xs text-gray-500">{t("add-signer-note")}</p>
{isUserExist && (
<div className="mb-3 flex items-center">
<input
type="checkbox"
id="addYourself"
checked={addYourself}
onChange={handleAddYourselfChange}
className="op-checkbox op-checkbox-sm"
/>
<label htmlFor="addYourself" className="ml-2 mb-0">
{t("add-yourself")}
</label>
</div>
)}
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label htmlFor="name" className="block text-xs font-semibold">
{t("name")}
<span className="text-[red] text-[13px]"> *</span>
</label>
<input
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
required
disabled={addYourself}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-3">
<label htmlFor="email" className="block text-xs font-semibold">
{t("email")}
<span className="text-[red] text-[13px]"> *</span>
</label>
<input
type="email"
id="email"
value={email}
onChange={(e) =>
setEmail(e.target.value?.toLowerCase()?.replace(/\s/g, ""))
}
required
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
disabled={addYourself}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-3">
<label htmlFor="phone" className="block text-xs font-semibold">
{t("phone")}
</label>
<input
type="text"
id="phone"
value={phone}
onChange={(e) => setPhone(e.target.value)}
disabled={addYourself}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("phone-optional")}
/>
</div>
<div className="mt-4 flex gap-x-2 justify-start">
<button type="submit" className="op-btn op-btn-primary">
{t("submit")}
</button>
<button
type="button"
onClick={() => handleReset()}
className="op-btn op-btn-secondary"
>
{t("reset")}
</button>
</div>
</form>
</div>
</div>
);
};
export default AddSigner;
@@ -1,6 +1,5 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import Parse from "parse";
import Title from "./Title";
import Loader from "../primitives/Loader";
import {
copytoData,
@@ -9,7 +8,11 @@ import {
import {
emailRegex,
} from "../constant/const";
import { useTranslation } from "react-i18next";
import {
useTranslation
} from "react-i18next";
import { withSessionValidation } from "../utils";
function generatePassword(length) {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
@@ -66,181 +69,70 @@ const AddUser = (props) => {
}
};
// Define a function to handle form submission
const handleSubmit = async (e) => {
const handleSubmit = withSessionValidation(async (e) => {
e.preventDefault();
e.stopPropagation();
if (!emailRegex.test(formdata.email)) {
alert("Please enter a valid email address.");
alert(t("valid-email-alert"));
} else {
const localUser = JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
setIsFormLoader(true);
const res = await checkUserExist();
if (res) {
props.setIsAlert({ type: "danger", msg: t("user-already-exist") });
props.showAlert("danger", t("user-already-exist"));
setIsFormLoader(false);
setTimeout(() => props.setIsAlert({ type: "success", msg: "" }), 1000);
} else {
try {
const extUser = new Parse.Object("contracts_Users");
extUser.set("Name", formdata.name);
if (formdata.phone) {
extUser.set("Phone", formdata.phone);
}
extUser.set("Email", formdata.email);
extUser.set("UserRole", `contracts_${formdata.role}`);
if (formdata?.team) {
extUser.set("TeamIds", [
{
__type: "Pointer",
className: "contracts_Teams",
objectId: formdata.team
}
]);
}
if (localUser && localUser.OrganizationId) {
extUser.set("OrganizationId", {
__type: "Pointer",
className: "contracts_Organizations",
objectId: localUser.OrganizationId.objectId
});
}
if (localUser && localUser.Company) {
extUser.set("Company", localUser.Company);
}
if (localStorage.getItem("TenantId")) {
extUser.set("TenantId", {
__type: "Pointer",
className: "partners_Tenant",
objectId: localStorage.getItem("TenantId")
});
}
if (localStorage.getItem("TenantId")) {
const timezone = usertimezone;
if (timezone) {
extUser.set("Timezone", timezone);
}
try {
const _users = Parse.Object.extend("User");
const _user = new _users();
_user.set("name", formdata.name);
_user.set("username", formdata.email);
_user.set("email", formdata.email);
_user.set("password", formdata.password);
if (formdata.phone) {
_user.set("phone", formdata.phone);
const params = {
name: formdata.name,
email: formdata.email,
phone: formdata.phone,
password: formdata.password,
role: formdata.role,
team: formdata.team,
timezone: timezone,
tenantId: localStorage.getItem("TenantId"),
organization: {
objectId: localUser?.OrganizationId?.objectId,
company: localUser?.Company
},
};
const res = await Parse.Cloud.run("adduser", params);
const parseData = JSON.parse(JSON.stringify(res));
if (props.closePopup) {
props.closePopup();
}
const user = await _user.save();
if (user) {
const currentUser = Parse.User.current();
extUser.set(
"CreatedBy",
Parse.User.createWithoutData(currentUser.id)
);
extUser.set("UserId", user);
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(true);
acl.setReadAccess(currentUser.id, true);
acl.setWriteAccess(currentUser.id, true);
extUser.setACL(acl);
const res = await extUser.save();
const parseData = JSON.parse(JSON.stringify(res));
if (props.closePopup) {
props.closePopup();
if (props.handleUserData) {
if (formdata?.team) {
const team = teamList.find((x) => x.objectId === formdata.team);
parseData.TeamIds = parseData.TeamIds.map((y) =>
y.objectId === team.objectId ? team : y
);
}
if (props.handleUserData) {
if (formdata?.team) {
const team = teamList.find(
(x) => x.objectId === formdata.team
);
parseData.TeamIds = parseData.TeamIds.map((y) =>
y.objectId === team.objectId ? team : y
);
}
props.handleUserData(parseData);
}
setIsFormLoader(false);
setFormdata({
name: "",
email: "",
phone: "",
team: "",
role: ""
});
props.handleUserData(parseData);
}
setIsFormLoader(false);
setFormdata({
name: "",
email: "",
phone: "",
team: "",
role: ""
});
props.showAlert("success", t("user-created-successfully"));
} catch (err) {
console.log("err ", err);
if (err.code === 202) {
const params = { email: formdata.email };
const userRes = await Parse.Cloud.run("getUserId", params);
const currentUser = Parse.User.current();
extUser.set(
"CreatedBy",
Parse.User.createWithoutData(currentUser.id)
);
extUser.set("UserId", {
__type: "Pointer",
className: "_User",
objectId: userRes.id
});
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(true);
acl.setReadAccess(currentUser.id, true);
acl.setWriteAccess(currentUser.id, true);
extUser.setACL(acl);
const res = await extUser.save();
const parseData = JSON.parse(JSON.stringify(res));
if (props.closePopup) {
props.closePopup();
}
if (props.handleUserData) {
if (formdata?.team) {
const team = teamList.find(
(x) => x.objectId === formdata.team
);
parseData.TeamIds = parseData.TeamIds.map((y) =>
y.objectId === team.objectId ? team : y
);
}
props.handleUserData(parseData);
}
setIsFormLoader(false);
setFormdata({
name: "",
email: "",
phone: "",
team: "",
role: ""
});
}
console.log("err", err);
setIsFormLoader(false);
props.showAlert("danger", t("something-went-wrong-mssg"));
}
} catch (err) {
console.log("err", err);
setIsFormLoader(false);
props.setIsAlert({
type: "danger",
msg: t("something-went-wrong-mssg")
});
} finally {
setTimeout(
() => props.setIsAlert({ type: "success", msg: "" }),
1500
);
} else {
props.showAlert("danger", t("something-went-wrong-mssg"));
}
}
}
};
});
// Define a function to handle the "add yourself" checkbox
const handleReset = () => {
@@ -259,12 +151,10 @@ const AddUser = (props) => {
const copytoclipboard = (text) => {
copytoData(text);
props.setIsAlert({ type: "success", msg: t("copied") });
setTimeout(() => props.setIsAlert({ type: "success", msg: "" }), 1500); // Reset copied state after 1.5 seconds
props.showAlert("success", t("copied"));
};
return (
<div className="shadow-md rounded-box my-[1px] p-3 bg-base-100 relative">
<Title title={t("add-user")} />
{isFormLoader && (
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-base-content/30 z-50">
<Loader />
@@ -275,7 +165,7 @@ const AddUser = (props) => {
<div className="mb-3">
<label
htmlFor="name"
className="block text-xs text-gray-700 font-semibold"
className="block text-xs font-semibold"
>
{t("name")}
<span className="text-[red] text-[13px]"> *</span>
@@ -291,12 +181,13 @@ const AddUser = (props) => {
onInput={(e) => e.target.setCustomValidity("")}
required
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-name")}
/>
</div>
<div className="mb-3">
<label
htmlFor="email"
className="block text-xs text-gray-700 font-semibold"
className="block text-xs font-semibold"
>
{t("email")}
<span className="text-[red] text-[13px]"> *</span>
@@ -312,10 +203,11 @@ const AddUser = (props) => {
}
onInput={(e) => e.target.setCustomValidity("")}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
placeholder={t("enter-email")}
/>
</div>
<div className="mb-3">
<label className="block text-xs text-gray-700 font-semibold">
<label className="block text-xs font-semibold">
{t("password")}
</label>
<div className="flex justify-between items-center op-input op-input-bordered op-input-sm text-base-content w-full h-full text-[13px]">
@@ -332,10 +224,9 @@ const AddUser = (props) => {
<div className="mb-3">
<label
htmlFor="phone"
className="block text-xs text-gray-700 font-semibold"
className="block text-xs font-semibold"
>
{t("phone")}
{/* <span className="text-[red] text-[13px]"> *</span> */}
</label>
<input
type="text"
@@ -349,7 +240,7 @@ const AddUser = (props) => {
<div className="mb-3">
<label
htmlFor="phone"
className="block text-xs text-gray-700 font-semibold"
className="block text-xs font-semibold"
>
{t("Role")}
<span className="text-[red] text-[13px]"> *</span>
-287
View File
@@ -1,287 +0,0 @@
import React, { useState, useEffect, useRef } from "react";
import axios from "axios";
import SuggestionInput from "./shared/fields/SuggestionInput";
import Loader from "../primitives/Loader";
import { useTranslation } from "react-i18next";
import {
emailRegex,
} from "../constant/const";
const BulkSendUi = (props) => {
const { t } = useTranslation();
const [forms, setForms] = useState([]);
const formRef = useRef(null);
const [scrollOnNextUpdate, setScrollOnNextUpdate] = useState(false);
const [isSubmit, setIsSubmit] = useState(false);
const [isSignatureExist, setIsSignatureExist] = useState();
const [isDisableBulkSend, setIsDisableBulkSend] = useState(false);
const [isLoader, setIsLoader] = useState(false);
useEffect(() => {
signatureExist();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
//function to check atleast one signature field exist
const signatureExist = async () => {
setIsDisableBulkSend(false);
const getPlaceholder = props?.Placeholders;
const checkIsSignatureExistt = getPlaceholder?.every((placeholderObj) =>
placeholderObj?.placeHolder?.some((holder) =>
holder?.pos?.some((posItem) => posItem?.type === "signature")
)
);
setIsSignatureExist(checkIsSignatureExistt);
setIsLoader(false);
};
useEffect(() => {
if (scrollOnNextUpdate && formRef.current) {
formRef.current.scrollIntoView({
behavior: "smooth",
block: "end",
inline: "nearest"
});
setScrollOnNextUpdate(false);
}
}, [forms, scrollOnNextUpdate]);
useEffect(() => {
(() => {
if (props?.Placeholders?.length > 0) {
let users = [];
props?.Placeholders?.forEach((element) => {
if (!element.signerObjId) {
users = [
...users,
{
fieldId: element.Id,
email: "",
label: element.Role,
signer: {}
}
];
}
});
setForms((prevForms) => [...prevForms, { Id: 1, fields: users }]);
}
})();
// eslint-disable-next-line
}, []);
const handleInputChange = (index, signer, fieldIndex) => {
const newForms = [...forms];
newForms[index].fields[fieldIndex].email = signer?.Email
? signer?.Email
: signer || "";
newForms[index].fields[fieldIndex].signer = signer?.objectId ? signer : "";
setForms(newForms);
};
const handleRemoveForm = (index) => {
const updatedForms = forms.filter((_, i) => i !== index);
setForms(updatedForms);
};
function validateEmails(data) {
for (const item of data) {
for (const field of item.fields) {
if (!emailRegex.test(field.email)) {
alert(`Invalid email found: ${field.email}`);
return false;
}
}
}
return true;
}
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
setIsSubmit(true);
if (validateEmails(forms)) {
// Create a copy of Placeholders array from props.item
let Placeholders = [...props.Placeholders];
// Initialize an empty array to store updated documents
let Documents = [];
// Loop through each form
forms.forEach((form) => {
//checking if user enter email which already exist as a signer then add user in a signers array
let existSigner = [];
form.fields.map((data) => {
if (data.signer) {
existSigner.push(data.signer);
}
});
// Map through the copied Placeholders array to update email values
const updatedPlaceholders = Placeholders.map((placeholder) => {
// Find the field in the current form that matches the placeholder Id
const field = form.fields.find(
(element) => parseInt(element.fieldId) === placeholder.Id
);
// If a matching field is found, update the email value in the placeholder
const signer = field?.signer?.objectId ? field.signer : "";
if (field) {
if (signer) {
return {
...placeholder,
signerObjId: field?.signer?.objectId || "",
signerPtr: signer
};
} else {
return {
...placeholder,
email: field.email,
signerObjId: field?.signer?.objectId || "",
signerPtr: signer
};
}
}
// If no matching field is found, keep the placeholder as is
return placeholder;
});
// Push a new document object with updated Placeholders into the Documents array
if (existSigner?.length > 0) {
Documents.push({
...props.item,
Placeholders: updatedPlaceholders,
Signers: props.item.Signers
? [...props.item.Signers, ...existSigner]
: [...existSigner]
});
} else {
Documents.push({
...props.item,
Placeholders: updatedPlaceholders,
SignatureType: props.signatureType
});
}
});
await batchQuery(Documents);
} else {
setIsSubmit(false);
}
};
const batchQuery = async (Documents) => {
const token =
{ "X-Parse-Session-Token": localStorage.getItem("accesstoken") };
const functionsUrl = `${localStorage.getItem(
"baseUrl"
)}functions/batchdocuments`;
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
...token,
};
const params = { Documents: JSON.stringify(Documents) };
try {
const res = await axios.post(functionsUrl, params, { headers: headers });
// console.log("res ", res);
if (res.data && res.data.result) {
props.handleClose("success", Documents?.length);
}
} catch (err) {
console.log("Err ", err);
props.handleClose("error", 0);
} finally {
setIsSubmit(false);
}
};
return (
<>
{isLoader ? (
<div className="w-full h-[100px] flex justify-center items-center z-[999]">
<Loader />
</div>
) : (
<>
{isSubmit && (
<div className="absolute z-[999] h-full w-full flex justify-center items-center bg-black bg-opacity-30">
<Loader />
</div>
)}
{!isDisableBulkSend ? (
<>
{props.Placeholders?.length > 0 ? (
isSignatureExist ? (
<>
{props.Placeholders?.some((x) => !x.signerObjId) ? (
<>
<form onSubmit={handleSubmit}>
<div className="min-h-max max-h-[250px] overflow-y-auto">
{forms?.map((form, index) => (
<div
key={form.Id}
className="p-3 op-card border-[1px] border-gray-400 mt-3 mx-4 mb-4 bg-base-200 text-base-content grid grid-cols-1 md:grid-cols-2 gap-2 relative"
>
{form?.fields?.map((field, fieldIndex) => (
<div
className="flex flex-col"
key={field.fieldId}
>
<label>{field.label}</label>
<SuggestionInput
required
type="email"
value={field.value}
index={fieldIndex}
onChange={(signer) =>
handleInputChange(
index,
signer,
fieldIndex
)
}
/>
</div>
))}
{forms?.length > 1 && (
<button
onClick={() => handleRemoveForm(index)}
className="absolute right-3 top-1 text-[red] border-[1px] border-[red] rounded-lg w-[1.7rem] h-[1.7rem]"
>
<i className="fa-light fa-trash"></i>
</button>
)}
<div ref={formRef}></div>
</div>
))}
</div>
<div className="flex flex-col mx-4 mb-4 gap-3">
<button
type="submit"
className="op-btn op-btn-accent focus:outline-none"
>
<i className="fa-light fa-paper-plane"></i>{" "}
<span>{t("send")}</span>
</button>
</div>
</form>
</>
) : (
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
{t("quick-send-alert-1")}
</div>
)}
</>
) : (
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
{t("quick-send-alert-2")}
</div>
)
) : (
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
{t("quick-send-alert-3")}
</div>
)}
</>
) : (
<>
</>
)}
</>
)}
</>
);
};
export default BulkSendUi;
@@ -0,0 +1,62 @@
import { useState, useEffect } from "react";
import ModalUi from "../primitives/ModalUi";
import { useTranslation } from "react-i18next";
const ColumnSelector = ({
isOpen,
allColumns = [],
visibleColumns = [],
columnLabels = {},
onApply,
onClose
}) => {
const { t } = useTranslation();
const [selected, setSelected] = useState(visibleColumns);
const [names, setNames] = useState(columnLabels);
useEffect(() => {
setSelected(visibleColumns);
setNames(columnLabels);
}, [visibleColumns, columnLabels]);
const handleChange = (col) => {
setSelected((prev) =>
prev.includes(col) ? prev.filter((c) => c !== col) : [...prev, col]
);
};
const handleApply = () => {
onApply && onApply(selected, names);
onClose && onClose();
};
return (
<ModalUi isOpen={isOpen} title={t("select-columns")} handleClose={onClose}>
<div className="p-[20px] flex flex-col gap-2">
{allColumns.map((col, i) => (
<div key={col} className="flex justify-between items-center gap-2">
<span className="flex justify-center items-center h-full">
<input
id={col + "_" + i}
type="checkbox"
checked={selected.includes(col)}
onChange={() => handleChange(col)}
className="mb-0 cursor-pointer"
/>
<span className="whitespace-nowrap ml-1">
{t(`report-heading.${col}`, { defaultValue: col })}
</span>
</span>
</div>
))}
<div className="flex justify-start mt-2">
<button onClick={handleApply} className="op-btn op-btn-primary">
{t("apply")}
</button>
</div>
</div>
</ModalUi>
);
};
export default ColumnSelector;
@@ -0,0 +1,35 @@
import { useTranslation } from "react-i18next";
const DateFormat = ({ selectDate, dateFormatList, handleChangeFormat }) => {
const selectedFormatIndex = dateFormatList?.findIndex(
(item) => item.format === selectDate?.format
);
const { t } = useTranslation();
return (
<div className="flex flex-col md:items-center md:flex-row gap-y-3 my-2">
<span className="capitalize">{t("format")}: </span>
<select
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs md:ml-2"
value={selectedFormatIndex >= 0 ? selectedFormatIndex : ""}
onChange={(e) => handleChangeFormat(e)}
>
<option value="" disabled>
{t("select-date-format")}
</option>
{dateFormatList.map((data, ind) => {
return (
<option className="text-[13px]" value={ind} key={ind}>
{data?.date ? data?.date : "nodata"}
</option>
);
})}
</select>
<span className="text-xs text-gray-400 ml-1 uppercase">
{selectDate.format}
</span>
</div>
);
};
export default DateFormat;
@@ -0,0 +1,88 @@
import { forwardRef } from "react";
import ReactDatePicker from "react-datepicker";
import {
getDefaultDate,
getMonth,
getYear,
months,
years
} from "../constant/Utils";
import { useTranslation } from "react-i18next";
const DatePicker = ({
selectDate,
format,
minDate,
maxDate,
onChange,
handleClear
}) => {
const { t } = useTranslation();
const CustomInput = forwardRef(({ value, onClick }, ref) => (
<div
className="border-gray-400 rounded-[50px] border-[1px] px-3 text-xs py-2 focus:outline-none hover:border-base-content "
onClick={onClick}
ref={ref}
>
{value}
<i
className={`${value ? "ml-[15px]" : "w-20"} fa-light fa-calendar `}
></i>
</div>
));
CustomInput.displayName = "CustomInput";
return (
<div>
<span>{t("default-date")}: </span>
<ReactDatePicker
renderCustomHeader={({ date, changeYear, changeMonth }) => (
<div className="flex justify-start md:ml-2">
<select
className="bg-transparent outline-none"
value={months[getMonth(date)]}
onChange={({ target: { value } }) =>
changeMonth(months.indexOf(value))
}
>
{months.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<select
className="bg-transparent outline-none"
value={getYear(date)}
onChange={({ target: { value } }) => changeYear(value)}
>
{years.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
)}
closeOnScroll={true}
selected={getDefaultDate(selectDate?.date, selectDate?.format)}
popperPlacement="top-end"
customInput={<CustomInput />}
onChange={(date) => onChange(date)}
dateFormat={
selectDate ? selectDate?.format : format ? format : "MM/dd/yyyy"
}
portalId="root-portal"
/>
<span
onClick={() => handleClear()}
className="underline text-blue-500 cursor-pointer ml-2"
>
{t("clear")}
</span>
</div>
);
};
export default DatePicker;
@@ -0,0 +1,27 @@
import { HTML5Backend } from "react-dnd-html5-backend";
import {
DndProvider,
MouseTransition
} from "react-dnd-multi-backend";
import LazyPage from "../primitives/LazyPage";
import { GuidelinesProvider } from "../context/GuidelinesContext";
const HTML5toTouch = {
backends: [
{
id: "html5",
backend: HTML5Backend,
transition: MouseTransition
}
]
};
export default function DragProvider({ Page, lazy = false }) {
return (
<DndProvider options={HTML5toTouch}>
<GuidelinesProvider>
{lazy ? <LazyPage Page={Page} /> : <Page />}
</GuidelinesProvider>
</DndProvider>
);
}
@@ -4,6 +4,7 @@ import axios from "axios";
import { openInNewTab } from "../constant/Utils";
import { useTranslation } from "react-i18next";
const Footer = () => {
const appName = "OpenSign™";
const { t } = useTranslation();
const [showButton, setShowButton] = useState(false);
const [version, setVersion] = useState("");
@@ -39,7 +40,6 @@ const Footer = () => {
};
}, []);
const appName = "OpenSign™";
const openUrl = () => {
openInNewTab(
"https://github.com/OpenSignLabs/OpenSign/releases/tag/" + version
-187
View File
@@ -1,187 +0,0 @@
import React, { useState, useEffect } from "react";
import dp from "../assets/images/dp.png";
import FullScreenButton from "./FullScreenButton";
import { useNavigate } from "react-router";
import Parse from "parse";
import { useWindowSize } from "../hook/useWindowSize";
import {
getAppLogo,
openInNewTab,
saveLanguageInLocal
} from "../constant/Utils";
import { useTranslation } from "react-i18next";
const Header = ({ showSidebar, setIsMenu }) => {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const { width } = useWindowSize();
const username = localStorage.getItem("username") || "";
const image = localStorage.getItem("profileImg") || dp;
const [isOpen, setIsOpen] = useState(false);
const [applogo, setAppLogo] = useState(
localStorage.getItem("appLogo") || " "
);
const toggleDropdown = () => {
setIsOpen(!isOpen);
if (width <= 768) {
setIsMenu(false);
}
};
useEffect(() => {
initializeHead();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function initializeHead() {
const applogo = await getAppLogo();
if (applogo?.logo) {
setAppLogo(applogo?.logo);
} else {
setAppLogo(localStorage.getItem("appLogo") || "");
}
}
const closeDropdown = async () => {
setIsOpen(false);
try {
await Parse.User.logOut();
} catch (err) {
console.log("Err while logging out", err);
}
let appdata = localStorage.getItem("userSettings");
let applogo = localStorage.getItem("appLogo");
let defaultmenuid = localStorage.getItem("defaultmenuid");
let PageLanding = localStorage.getItem("PageLanding");
let baseUrl = localStorage.getItem("baseUrl");
let appid = localStorage.getItem("parseAppId");
localStorage.clear();
saveLanguageInLocal(i18n);
localStorage.setItem("appLogo", applogo);
localStorage.setItem("defaultmenuid", defaultmenuid);
localStorage.setItem("PageLanding", PageLanding);
localStorage.setItem("userSettings", appdata);
localStorage.setItem("baseUrl", baseUrl);
localStorage.setItem("parseAppId", appid);
navigate("/");
};
//handle to close profile drop down menu onclick screen
useEffect(() => {
const closeMenuOnOutsideClick = (e) => {
if (isOpen && !e.target.closest("#profile-menu")) {
setIsOpen(false);
}
};
document.addEventListener("click", closeMenuOnOutsideClick);
return () => {
// Cleanup the event listener when the component unmounts
document.removeEventListener("click", closeMenuOnOutsideClick);
};
}, [isOpen]);
return (
<div>
<div className="op-navbar bg-base-100 shadow">
<div className="flex-none">
<button
className="op-btn op-btn-square op-btn-ghost focus:outline-none hover:bg-transparent op-btn-sm no-animation"
onClick={showSidebar}
>
<i className="fa-light fa-bars text-xl text-base-content"></i>
</button>
</div>
<div className="flex-1 ml-2">
<div className="h-[25px] md:h-[40px] w-auto overflow-hidden">
<img
className="object-contain h-full w-auto"
src={applogo}
alt="img"
/>
</div>
</div>
<div id="profile-menu" className="flex-none gap-2">
<div>
<FullScreenButton />
</div>
{width >= 768 && (
<div
onClick={toggleDropdown}
className="cursor-pointer w-[35px] h-[35px] rounded-full ring-[1px] ring-offset-2 ring-gray-400 overflow-hidden"
>
<img
className="w-[35px] h-[35px] object-contain"
src={image}
alt="img"
/>
</div>
)}
{width >= 768 && (
<div
onClick={toggleDropdown}
className="cursor-pointer text-base-content text-sm"
>
{username && username}
</div>
)}
<div className="op-dropdown op-dropdown-end" id="profile-menu">
<div
tabIndex={0}
role="button"
className="op-btn op-btn-ghost op-btn-xs w-[10px] h-[20px] hover:bg-transparent"
>
<i
tabIndex={0}
role="button"
onClick={toggleDropdown}
className="fa-light fa-angle-down text-base-content"
></i>
</div>
<ul
tabIndex={0}
className={`mt-3 z-[1] p-2 shadow op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-52 ${
isOpen ? "" : "hidden"
}`}
>
<li onClick={() => openInNewTab("https://docs.opensignlabs.com")}>
<span>
<i className="fa-light fa-book"></i> {t("docs")}
</span>
</li>
<li
onClick={() => {
setIsOpen(false);
navigate("/profile");
}}
>
<span>
<i className="fa-light fa-user"></i> {t("profile")}
</span>
</li>
<li
onClick={() => {
setIsOpen(false);
navigate("/changepassword");
}}
>
<span>
<i className="fa-light fa-lock"></i> {t("change-password")}
</span>
</li>
<li onClick={closeDropdown}>
<span>
<i className="fa-light fa-arrow-right-from-bracket"></i>{" "}
{t("log-out")}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
);
};
export default Header;
+273
View File
@@ -0,0 +1,273 @@
import { useState, useEffect } from "react";
import dp from "../assets/images/dp.png";
import FullScreenButton from "./FullScreenButton";
import ThemeToggle from "./ThemeToggle";
import { useNavigate } from "react-router";
import Parse from "parse";
import { useWindowSize } from "../hook/useWindowSize";
import {
getAppLogo,
openInNewTab,
saveLanguageInLocal
} from "../constant/Utils";
import { useTranslation } from "react-i18next";
import { appInfo } from "../constant/appinfo";
import { useDispatch } from "react-redux";
import { toggleSidebar } from "../redux/reducers/sidebarReducer";
import { sessionStatus } from "../redux/reducers/userReducer";
const Header = ({ isConsole, setIsLoggingOut }) => {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const { width } = useWindowSize();
const dispatch = useDispatch();
const username = localStorage.getItem("username") || "";
const image = localStorage.getItem("profileImg") || dp;
const [isOpen, setIsOpen] = useState(false);
const [applogo, setAppLogo] = useState("");
const [isDarkTheme, setIsDarkTheme] = useState();
const toggleDropdown = () => {
setIsOpen(!isOpen);
closeSidebar();
};
const closeSidebar = () => {
if (width && width <= 768) {
dispatch(toggleSidebar(false));
}
};
useEffect(() => {
initializeHead();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
closeSidebar();
}, [width]);
const showSidebar = () => {
dispatch(toggleSidebar());
};
async function initializeHead() {
const applogo = await getAppLogo();
if (applogo?.logo) {
setAppLogo(applogo?.logo);
} else {
const logo = localStorage.getItem("appLogo") || appInfo.applogo;
setAppLogo(logo);
}
}
const handleLogout = async () => {
setIsOpen(false);
setIsLoggingOut(true);
try {
await Parse.User.logOut();
} catch (err) {
console.log("Err while logging out", err);
} finally {
dispatch(sessionStatus(true));
}
let appdata = localStorage.getItem("userSettings");
let applogo = localStorage.getItem("appLogo");
let defaultmenuid = localStorage.getItem("defaultmenuid");
let PageLanding = localStorage.getItem("PageLanding");
let baseUrl = localStorage.getItem("baseUrl");
let appid = localStorage.getItem("parseAppId");
let favicon = localStorage.getItem("favicon");
localStorage.clear();
saveLanguageInLocal(i18n);
localStorage.setItem("appLogo", applogo);
localStorage.setItem("defaultmenuid", defaultmenuid);
localStorage.setItem("PageLanding", PageLanding);
localStorage.setItem("userSettings", appdata);
localStorage.setItem("baseUrl", baseUrl);
localStorage.setItem("parseAppId", appid);
localStorage.setItem("favicon", favicon);
setIsLoggingOut(false);
navigate("/");
};
//handle to close profile drop down menu onclick screen
useEffect(() => {
const closeMenuOnOutsideClick = (e) => {
if (isOpen && !e.target.closest("#profile-menu")) {
setIsOpen(false);
}
};
document.addEventListener("click", closeMenuOnOutsideClick);
return () => {
// Cleanup the event listener when the component unmounts
document.removeEventListener("click", closeMenuOnOutsideClick);
};
}, [isOpen]);
useEffect(() => {
const updateThemeStatus = () => {
const isDarkTheme =
document.documentElement.getAttribute("data-theme") === "opensigndark";
setIsDarkTheme(isDarkTheme);
};
updateThemeStatus();
const observer = new MutationObserver(() => {
updateThemeStatus();
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"]
});
return () => observer.disconnect();
}, []);
return (
<>
<div className="op-navbar bg-base-100 shadow touch-none">
<div className="flex-none">
<button
className="op-btn op-btn-square op-btn-ghost focus:outline-none hover:bg-transparent op-btn-sm no-animation"
onClick={showSidebar}
>
<i className="fa-light fa-bars text-xl text-base-content"></i>
</button>
</div>
<div className="flex-1 ml-2">
<div
onClick={() => navigate("/dashboard/35KBoSgoAK")}
className="h-[25px] md:h-[40px] w-auto overflow-hidden cursor-pointer"
>
{applogo && (
<img
className="object-contain h-full w-auto"
src={
isDarkTheme
? "/static/js/assets/images/logo-dark.png"
: applogo
}
alt="logo"
/>
)}
</div>
</div>
<div id="profile-menu" className="flex-none gap-2">
<div>
<FullScreenButton />
</div>
{width >= 768 && (
<div
onClick={toggleDropdown}
className="cursor-pointer w-[35px] h-[35px] rounded-full ring-[1px] ring-offset-2 ring-gray-400 overflow-hidden"
>
<img
className="w-[35px] h-[35px] object-contain"
src={image}
alt="img"
/>
</div>
)}
{width >= 768 && (
<div
onClick={toggleDropdown}
role="button"
tabIndex="0"
className="cursor-pointer text-base-content text-sm"
>
{username && username}
</div>
)}
<div
className="op-dropdown op-dropdown-open op-dropdown-end"
id="profile-menu"
>
<div
tabIndex={0}
role="button"
onClick={toggleDropdown}
className="op-btn op-btn-ghost op-btn-xs w-[10px] h-[20px] hover:bg-transparent"
>
<i className="fa-light fa-angle-down text-base-content"></i>
</div>
<ul
tabIndex={0}
className={`mt-4 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-56 ${
isOpen ? "" : "hidden"
}`}
>
{!isConsole && (
<>
<li
onClick={() =>
openInNewTab("https://docs.opensignlabs.com")
}
>
<span>
<i className="fa-light fa-book"></i> {t("docs")}
</span>
</li>
<li
onClick={() => {
setIsOpen(false);
navigate("/profile");
}}
>
<span>
<i className="fa-light fa-user"></i> {t("profile")}
</span>
</li>
<li
onClick={() => {
setIsOpen(false);
navigate("/changepassword");
}}
>
<span>
<i className="fa-light fa-lock"></i>{" "}
{t("change-password")}
</span>
</li>
<li
onClick={() => {
setIsOpen(false);
navigate("/verify-document");
}}
>
<span>
<i className="fa-light fa-check-square"></i>{" "}
{t("verify-document")}
</span>
</li>
<li>
<span>
<i className="fa-light fa-moon"></i>
{t("dark-mode")}
<span className="text-[10px] font-semibold bg-base-300 text-base-content px-1 rounded-md">
BETA
</span>
<ThemeToggle />
</span>
</li>
</>
)}
<li onClick={handleLogout}>
<span>
<i className="fa-light fa-arrow-right-from-bracket"></i>{" "}
{t("log-out")}
</span>
</li>
</ul>
</div>
</div>
</div>
</>
);
};
export default Header;
@@ -1,7 +1,9 @@
import React from "react";
import { Document, Page } from "react-pdf";
import { Stage, Layer, Rect, Text } from "react-konva";
import { useTranslation } from "react-i18next";
const RenderDebugPdf = (props) => {
const { t } = useTranslation();
return (
<div>
<div className="sticky top-0 p-[10px] z-10 bg-white border-[1px] border-[gray] my-[5px]">
@@ -12,10 +14,9 @@ const RenderDebugPdf = (props) => {
onMouseMove={props.handleMouseMoveDiv}
>
<Document
onLoadError={() => {
props.setPdfLoadFail(false);
}}
loading={"Loading Document.."}
onLoadError={() => props.setPdfLoadFail(false)}
loading={t("loading-doc")}
error={<p className="mx-2">{t("failed-to-load-refresh-page")}</p>}
onLoadSuccess={props.pageDetails}
ref={props.pdfRef}
file={props.pdfUrl}
@@ -26,7 +26,7 @@ function RotateAlert(props) {
props.setShowRotateAlert({ status: false, degree: 0 })
}
type="button"
className="op-btn op-btn-ghost shadow-md"
className="op-btn op-btn-ghost text-base-content shadow-md"
>
{t("no")}
</button>
@@ -0,0 +1,41 @@
import { useEffect, useState } from "react";
const ThemeToggle = () => {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const storedTheme = localStorage.getItem("theme");
if (storedTheme === "dark") {
setIsDark(true);
document.documentElement.setAttribute("data-theme", "opensigndark");
} else {
document.documentElement.setAttribute("data-theme", "opensigncss");
}
}, []);
const handleChange = () => {
const newTheme = !isDark;
setIsDark(newTheme);
if (newTheme) {
document.documentElement.setAttribute("data-theme", "opensigndark");
localStorage.setItem("theme", "dark");
} else {
document.documentElement.setAttribute("data-theme", "opensigncss");
localStorage.setItem("theme", "light");
}
};
return (
<>
<input
id="dark-mode-toggle"
type="checkbox"
className="op-toggle checked:[--tglbg:#3368ff] transition-all checked:bg-white"
checked={isDark}
onChange={handleChange}
/>
</>
);
};
export default ThemeToggle;
-19
View File
@@ -1,19 +0,0 @@
import React from "react";
import { Helmet } from "react-helmet";
function Title({ title, drive }) {
return (
<Helmet>
<title>{drive ? title : `${title} - OpenSign™`}</title>
<meta name="description" content={`${title} - OpenSign™`} />
<link
rel="icon"
type="image/png"
href={localStorage.getItem("fev_Icon")}
sizes="40x40"
/>
</Helmet>
);
}
export default Title;
+81
View File
@@ -0,0 +1,81 @@
import { useLocation, matchPath } from "react-router";
import { useTranslation } from "react-i18next";
import { useMemo } from "react";
import { useManifestUrl } from "../hook/useManifestUrl";
const TITLE_MAP = {
"/": "login",
// Homelayout
"/dashboard/35KBoSgoAK": "Dashboard",
"/form/sHAnZphf69": "Sign Yourself",
"/form/8mZzFxbG1z": "Request Signatures",
"/form/template": "New Template",
"/report/6TeaPr321t": "Templates",
"/report/4Hhwbp482K": "Need your sign",
"/report/1MwEuxLEkF": "In Progress",
"/report/kQUoW4hUXz": "Completed",
"/report/ByHuevtCFY": "Drafts",
"/report/UPr2Fm5WY3": "Declined",
"/report/zNqBHXHsYH": "Expired",
"/report/contacts": "Contactbook",
"/drive": "Drive",
"/managesign": "My Signature",
"/preferences": "Preferences",
"/users": "Users",
"/profile": "profile",
"/changepassword": "change-password",
"/verify-document": "verify-document",
"/signaturePdf/:docId": "Sign Yourself",
"/placeHolderSign/:docId": "Request Signatures",
"/template/:templateId": "New Template",
"/recipientSignPdf/:docId": "Request Signatures",
"/recipientSignPdf/:docId/:contactBookId": "Request Signatures",
"/load/recipientSignPdf/:docId/:contactBookId": "Request Signatures",
// alone
"/debugpdf": "Debug Pdf",
"/forgetpassword": "forgot-password",
"/success": "success",
"/addadmin": "add-admin",
"/upgrade-2.1": "add-admin",
"/draftDocument": "New Document",
"/login/:base64url": "Request Signatures",
};
function resolveTitle(pathname, override) {
if (override) return override;
for (let [pattern, label] of Object.entries(TITLE_MAP)) {
if (matchPath({ path: pattern, end: true }, pathname)) {
return label;
}
}
return "";
}
export default function Title() {
const { pathname, state } = useLocation();
const { t } = useTranslation();
const appName =
"OpenSign™";
const logo = useMemo(() => localStorage.getItem("favicon"), []);
const prefix = useMemo(
() => resolveTitle(pathname, state?.title),
[pathname, state?.title]
);
const title = useMemo(
() => (prefix ? `${t(prefix)} - ${appName}` : appName),
[t, prefix, appName]
);
const manifestUrl = useManifestUrl(appName, logo);
return (
<>
<title>{title}</title>
<meta name="description" content={title} />
{logo && <link rel="icon" type="image/png" href={logo} />}
<link rel="manifest" href={manifestUrl} />
</>
);
}
@@ -0,0 +1,605 @@
import {
useState,
useEffect,
useMemo
} from "react";
import axios from "axios";
import SuggestionInput from "../shared/fields/SuggestionInput";
import Loader from "../../primitives/Loader";
import { useTranslation } from "react-i18next";
import {
emailRegex,
} from "../../constant/const";
import {
convertPdfArrayBuffer,
getSignedUrl,
isBase64,
getBase64FromUrl,
} from "../../constant/Utils";
import {
formatCSVDate,
handleEmbedPrefillToDoc,
normalizeKey
} from "../../utils";
import RenderWidgets from "./components/RenderWidgets";
import { useDispatch, useSelector } from "react-redux";
import { setBulkLoader } from "../../redux/reducers/widgetSlice";
const EXCLUDED_PREFILL_TYPES = new Set(["image", "draw"]);
const EXCLUDED_WIDGET_TYPES = new Set([
"image",
"signature",
"stamp",
"initials"
]);
const ALL_EXCLUDED_TYPES = new Set([
...EXCLUDED_PREFILL_TYPES,
...EXCLUDED_WIDGET_TYPES
]);
const requiredAsteriskCls = (isRequired = false) => {
return isRequired ? "after:content-['_*'] after:text-red-500" : "";
};
const BulkSendUi = (props) => {
const { t } = useTranslation();
const appName =
"OpenSign™";
const dispatch = useDispatch();
const { isBulkLoader } = useSelector((state) => state.widget);
const [forms, setForms] = useState([]);
const [isSignatureExist, setIsSignatureExist] = useState();
const [isLoader, setIsLoader] = useState(false);
const [signers, setSigners] = useState([]);
const [emails, setEmails] = useState([]);
const [isVacantRoles, setIsVacantRoles] = useState(false);
const [fields, setFields] = useState([]);
const [headers, setHeaders] = useState([]);
const tokenHeader = useMemo(() => {
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId")
};
return {
...headers,
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
};
}, [props?.jwttoken]);
// Initialize form state (fields, widgets/responses, emails, headers, forms, signers, allowedForm) from template placeholders with existing values.
const initializeWidgetsWithValues = async () => {
if (props?.Placeholders?.length > 0) {
const roles = props?.Placeholders?.filter((p) => !p.signerObjId) || [];
const emails = [];
const users = await Promise.all(
roles.map(async (role) => {
const isPrefill = role?.Role?.toLowerCase() === "prefill";
const uniqueNames = new Set();
const pages = role?.placeHolder ?? [];
const widgets = await Promise.all(
pages.flatMap((page) =>
page.pos
.filter((widget) => {
const widgetName = widget?.options?.name;
if (!widgetName) return false; //|| widget?.options?.isReadOnly
// exclude certain widget types for non-prefill
if (EXCLUDED_WIDGET_TYPES.has(widget.type) && !isPrefill)
return false;
// unique by widget name
if (uniqueNames.has(widgetName)) return false;
uniqueNames.add(widgetName);
return true;
})
.map(async (widget) => {
let response = "";
if (ALL_EXCLUDED_TYPES.has(widget.type)) {
const url =
widget.options?.response ||
widget.options?.defaultValue ||
"";
if (url && !isBase64(url)) {
const signedUrl = await getSignedUrl(
url,
"", // docId
props.item.objectId, // templateId
);
response = await getBase64FromUrl(signedUrl, true);
} else {
response = "";
}
} else {
if (widget.type === "date") {
const value =
widget.options?.response ||
widget.options?.defaultValue ||
"";
response = value ? formatCSVDate(widget, value) : "";
} else {
response =
widget.options?.response ||
widget.options?.defaultValue ||
"";
}
}
const label = `${role.Role}::${widget.options?.name}`;
return {
...widget,
label: label,
response,
pageNumber: page.pageNumber
};
})
)
);
const signerEmail =
normalizeKey(role?.email) || role?.signerPtr?.Email;
if (signerEmail) emails.push(signerEmail);
return {
fieldId: role.Id,
email: "",
label: role.Role,
signer: {},
widgets: widgets
};
})
);
setFields(users);
setEmails(emails);
// Build headers with required ordering:
// 1) all role emails first
// 2) then all widgets
const emailHeaders = [];
const widgetHeaders = [];
for (const role of users) {
const isPrefill = role?.label?.toLowerCase() === "prefill";
if (!isPrefill) {
emailHeaders.push({
label: `${role.label}::Email`,
role: role.label,
type: "role",
isRequired: true
});
}
for (const widget of role?.widgets ?? []) {
const isRequired =
widget?.options?.status === "required" && isPrefill;
widgetHeaders.push({
label: widget?.label,
role: role?.label,
type: widget?.type,
isRequired
});
}
}
setHeaders([...emailHeaders, ...widgetHeaders]);
setForms((prev) => [...prev, { Id: 1, fields: users }]);
const signer = props.item?.Signers?.filter((x) => x?.objectId);
setSigners(signer);
}
};
useEffect(() => {
signatureExist();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const checkSignatureAndRoles = (placeholders = []) => {
const filtered = placeholders.filter((x) => x?.Role !== "prefill");
const isSignExist = filtered.every((p) =>
p?.placeHolder?.some((h) => h?.pos?.some((p) => p?.type === "signature"))
);
setIsSignatureExist(isSignExist);
setIsVacantRoles(placeholders.some((x) => !x.signerObjId));
};
//function to check at least one signature field exist
const signatureExist = async () => {
setIsLoader(true);
await initializeWidgetsWithValues();
checkSignatureAndRoles(props?.Placeholders);
setIsLoader(false);
};
const handleInputChange = (formIndex, signer, role) => {
const newForms = [...forms];
const email = signer?.Email ? signer?.Email : signer || "";
const fieldId = newForms[formIndex].fields.findIndex(
(f) => f.label === role
);
newForms[formIndex].fields[fieldId].email = normalizeKey(email);
newForms[formIndex].fields[fieldId].signer = signer?.objectId ? signer : "";
setForms(newForms);
};
const handleWidgetDetails = (
value = "",
formIndex,
RoleIndex = 0,
widgetIndex
) => {
const newForms = [...forms];
newForms[formIndex].fields[RoleIndex].widgets[widgetIndex].response = value;
setForms(newForms);
};
function validateEmails(data) {
for (const item of data) {
let email = "";
const filterFields = item.fields.filter((p) => p.label !== "prefill");
for (const field of filterFields) {
if (!emailRegex.test(field.email)) {
alert(t("invalid-email-found", { email: field.email }));
return false;
} else if (email === field.email || emails?.includes(field.email)) {
alert(t("duplicate-email-found", { email: field.email }));
return false;
} else {
email = field.email;
}
}
}
return true;
}
// `updateWidgetValues` will update responses for all widget types
const updateWidgetValues = (widgetValues = [], placeholders = []) => {
if (!widgetValues?.length) return placeholders;
const responseByName = new Map(
widgetValues
.map((w) => [w?.options?.name, w?.response])
.filter(([name]) => !!name)
);
return placeholders.map((ph) => {
const updatedPages = (ph?.placeHolder ?? []).map((page) => {
const updatedPos = (page?.pos ?? []).map((widgetDetails) => {
const name = widgetDetails?.options?.name;
if (!name || !responseByName.has(name)) return widgetDetails;
const newResponse = responseByName.get(name);
return {
...widgetDetails,
options: {
...widgetDetails.options,
// update response; if you DON'T want undefined to overwrite, keep the ?? fallback
response:
newResponse ??
widgetDetails.options?.response ??
widgetDetails.options?.defaultValue
}
};
});
return { ...page, pos: updatedPos };
});
return { ...ph, placeHolder: updatedPages };
});
};
// `removeWidgetValues` will clear out responses for draw/image/signature/stamp/initials widgets
const removeWidgetValues = (widgetValues = [], placeholders = []) => {
if (!widgetValues?.length) return placeholders;
return placeholders.map((ph) => {
const updatedPages = (ph?.placeHolder ?? []).map((page) => {
const updatedPos = (page?.pos ?? []).map((widgetDetails) => {
const isImageType = ALL_EXCLUDED_TYPES.has(widgetDetails?.type);
return {
...widgetDetails,
options: {
...widgetDetails.options,
// update response; if you DON'T want undefined to overwrite, keep the ?? fallback
response: isImageType ? "" : widgetDetails.options?.response,
defaultValue: isImageType
? ""
: widgetDetails.options?.defaultValue
}
};
});
return { ...page, pos: updatedPos };
});
return { ...ph, placeHolder: updatedPages };
});
};
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
dispatch(setBulkLoader(true));
if (!forms.length) {
alert(t("bulk-send-no-records"));
dispatch(setBulkLoader(false));
return;
}
if (validateEmails(forms)) {
// Create a copy of Placeholders array from props.item
let Placeholders = [...props.Placeholders];
// Initialize an empty array to store updated documents
let Documents = [];
let error = "";
// Loop through each form
for (const form of forms) {
//checking if user enter email which already exist as a signer then add user in a signers array
let existSigner = [];
form.fields.map((data) => {
if (data.signer) {
existSigner.push(data.signer);
}
});
// Map through the copied Placeholders array to update email values
const updatedPlaceholders = Placeholders.map((placeholder) => {
// Find the field in the current form that matches the placeholder Id
const field = form.fields.find(
(element) => parseInt(element.fieldId) === placeholder.Id
);
// If a matching field is found, update the email value in the placeholder
const signer = field?.signer?.objectId ? field.signer : "";
if (field) {
if (signer) {
return {
...placeholder,
signerObjId: field?.signer?.objectId || "",
signerPtr: signer
};
} else {
return {
...placeholder,
email: normalizeKey(field.email),
signerObjId: field?.signer?.objectId || "",
signerPtr: signer
};
}
}
// If no matching field is found, keep the placeholder as is
return placeholder;
});
const widgetValues = form?.fields?.flatMap((f) => f.widgets);
const updateWidgetPlaceholder = updateWidgetValues(
widgetValues,
updatedPlaceholders
);
const url = props.item?.URL || props.item?.SignedUrl;
let signedUrl = await getSignedUrl(
url,
"", //docId
props.item.objectId, // templateId
);
const prefillField = form?.fields?.find((f) => f.label === "prefill");
const prefills = prefillField?.widgets ?? [];
if (prefills?.length > 0) {
const prefillDetails = updateWidgetPlaceholder?.find(
(x) => x.Role === "prefill"
);
const pdfArrayBuffer = await convertPdfArrayBuffer(signedUrl);
if (pdfArrayBuffer === "Error") {
error = t("something-went-wrong-mssg");
dispatch(setBulkLoader(false));
break;
}
signedUrl = await handleEmbedPrefillToDoc(
prefillDetails,
1, // scale
pdfArrayBuffer,
[], // prefillImg,
props.item?.ExtUserPtr?.UserId?.objectId // userId
);
if (signedUrl?.error) {
error = signedUrl.error.includes("not compatible")
? t("pdf-uncompatible", { appName: appName })
: signedUrl?.error;
dispatch(setBulkLoader(false));
break;
}
}
const placeholders = removeWidgetValues(
widgetValues,
updateWidgetPlaceholder
);
if (!signedUrl) {
error = t("something-went-wrong-mssg");
dispatch(setBulkLoader(false));
break;
}
// Push a new document object with updated Placeholders into the Documents array
if (existSigner?.length > 0) {
Documents.push({
...props.item,
URL: signedUrl,
Placeholders: placeholders,
Signers: signers ? [...signers, ...existSigner] : [...existSigner]
});
} else {
Documents.push({
...props.item,
URL: signedUrl,
Placeholders: placeholders,
SignatureType: props.signatureType,
Signers: signers
});
}
}
if (error) {
alert(error);
} else {
await batchQuery(Documents);
}
} else {
dispatch(setBulkLoader(false));
}
};
const batchQuery = async (Documents) => {
const functionsUrl = `${localStorage.getItem("baseUrl")}functions/batchdocuments`;
const headers = {
...tokenHeader,
};
const params = { Documents: JSON.stringify(Documents) };
try {
const res = await axios.post(functionsUrl, params, { headers: headers });
if (res.data && res.data.result) {
props.handleClose("success", Documents?.length);
}
} catch (err) {
const message =
err?.response?.data?.error || err?.message || "something went wrong.";
console.error("Error sending documents:", message);
props.handleClose("error", 0, message);
} finally {
dispatch(setBulkLoader(false));
}
};
if (isLoader) {
return (
<div className="w-full h-[100px] flex justify-center items-center z-[999]">
<Loader />
</div>
);
}
return (
<div className="relative">
{/* {isBulkLoader && (
<div className="absolute z-[999] h-full w-full flex justify-center items-center bg-black bg-opacity-30">
<Loader />
</div>
)} */}
{props.Placeholders?.length > 0 ? (
isSignatureExist ? (
isVacantRoles ? (
<>
<form onSubmit={handleSubmit}>
<div className="min-h-max max-h-[250px] overflow-auto">
{
forms?.length > 0 && (
<div className="mx-4">
<table className="op-table border-collapse w-full">
<thead className="text-[13px] text-center">
<tr className="border-y-[1px]">
{/* Roles + widgets */}
{headers?.map((header) => (
<th
key={header.label}
className={`${requiredAsteriskCls(header?.isRequired)} p-2`}
>
{header.label}
</th>
))}
{forms?.length > 1 && (
<th className="p-2">{t("action")}</th>
)}
</tr>
</thead>
<tbody className="text-[12px] text-base-content">
{forms.map((form, formIndex) => {
const fields = form?.fields ?? [];
const emailFields = fields.filter(
(f) => f.label !== "prefill"
);
const widgets = fields.flatMap(
(f, fieldIndex) =>
(f.widgets ?? []).map(
(widget, widgetIndex) => ({
widget,
fieldIndex, // keep the fieldIndex that produced this widget
widgetIndex
})
)
);
return (
<tr key={form.Id}>
{/* Email cell (label + SuggestionInput) */}
{emailFields?.map((field, fieldIndex) => (
<td key={field.fieldId} className="p-2">
<div className="flex flex-col min-w-max">
<SuggestionInput
required
type="email"
value={field.email ?? ""}
index={fieldIndex}
onChange={(signer) =>
handleInputChange(
formIndex,
signer,
field.label
)
}
/>
</div>
</td>
))}
{widgets.map(
({ widget, fieldIndex, widgetIndex }) => (
<td key={widget.key} className="p-2">
<RenderWidgets
widget={widget}
formIndex={formIndex}
prefillIndex={widgetIndex}
handleWidgetDetails={(value) =>
handleWidgetDetails(
value,
formIndex,
fieldIndex,
widgetIndex
)
}
/>
</td>
)
)}
</tr>
);
})}
</tbody>
</table>
</div>
)
}
</div>
<div className="flex flex-row flex-wrap pb-3 pt-2 px-3 gap-3 justify-center">
<button
type="submit"
className="op-btn op-btn-accent w-[150px] focus:outline-none"
>
<i className="fa-light fa-paper-plane"></i>
<span>{t("send")}</span>
</button>
</div>
</form>
</>
) : (
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
{t("quick-send-alert-1")}
</div>
)
) : (
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
{t("quick-send-alert-2")}
</div>
)
) : (
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
{t("quick-send-alert-3")}
</div>
)}
</div>
);
};
export default BulkSendUi;
@@ -0,0 +1,582 @@
import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
import DatePicker from "react-datepicker";
import SignatureCanvas from "react-signature-canvas";
import { useTranslation } from "react-i18next";
import moment from "moment";
import {
changeDateToMomentFormat,
compressedFileSize,
getMonth,
getYear,
months,
radioButtonWidget,
textInputWidget,
textWidget,
years
} from "../../../constant/Utils";
import PenColorComponent from "../../pdf/tab/PenColorComponent";
import { getDatePickerDate, toHtmlPattern } from "../../../utils";
import CellsInput from "../../shared/fields/CellsInput";
import { emailRegex } from "../../../constant/const";
const inputOpt = new Set(["text", "email", "number"]);
const PREFILL_PREFIX = "prefill::";
const inputTypes = {
email: emailRegex,
number: /^[0-9\s]*$/,
ssn: /^(?!000|666|9\d{2})\d{3}-(?!00)\d{2}-(?!0000)\d{4}$/
};
const inputValidation = (pattern, type) => {
if (pattern) return pattern;
return inputTypes[type] || "";
};
const canavasTheme = `opensigncss:bg-white opensigndark:bg-[#121212] opensigndark:border-[#f6f3f4]/20 opensigncss:border-gray-300 opensigndark:hover:border-white opensigncss:hover:border-black border-[1px]`;
const DateWidget = ({ widget, isRequired, onChange }) => {
const format = widget?.options?.validation?.format || "MM/dd/yyyy";
const PrefillDateInput = forwardRef(({ value, onClick }, ref) => (
<div
style={{ fontFamily: "Arial, sans-serif" }}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full"
onClick={onClick}
ref={ref}
>
{value ? value : format}
<i className="fa-light fa-calendar ml-[5px]"></i>
</div>
));
PrefillDateInput.displayName = "PrefillDateInput";
const handleDate = (widget) => {
// The getDatePickerDate function retrieves the date in the correct format supported by the DatePicker.
try {
return getDatePickerDate(widget?.response, format);
} catch (err) {
console.error("handleDate error ", err);
return;
}
};
//function to set date with required date format onchange date
const handleOnDateChange = (date) => {
let updateDate = date;
let newDate;
const isSpecialDateFormat =
format && ["dd-MM-yyyy", "dd.MM.yyyy", "dd/MM/yyyy"].includes(format);
if (isSpecialDateFormat) {
newDate = moment(updateDate).format(changeDateToMomentFormat(format));
} else {
//using moment package is used to change date as per the format provided in selectDate obj e.g. - MM/dd/yyyy -> 03/12/2024
newDate = new Date(updateDate);
newDate = moment(newDate.getTime()).format(
changeDateToMomentFormat(format)
);
}
onChange(newDate);
};
return (
<div className="flex flex-col">
<div className="min-w-max">
<DatePicker
portalId="datepicker-portal-root"
id={`widget-${widget?.options?.name}-${widget.key}`}
renderCustomHeader={({ date, changeYear, changeMonth }) => (
<div className="flex justify-start ml-2">
<select
className="bg-transparent outline-none"
value={months[getMonth(date)]}
onChange={({ target: { value } }) =>
changeMonth(months.indexOf(value))
}
>
{months.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<select
className="bg-transparent outline-none"
value={getYear(date)}
onChange={({ target: { value } }) => changeYear(value)}
>
{years.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
)}
closeOnScroll={true}
selected={handleDate(widget)}
onChange={(date) => handleOnDateChange(date, widget)}
customInput={<PrefillDateInput />}
dateFormat={widget?.options?.validation?.format || "MM/dd/yyyy"}
required={isRequired}
/>
</div>
</div>
);
};
const TextWidget = ({ widget, isRequired, onChange }) => {
const { t } = useTranslation();
const [value, setValue] = useState([]);
const inputType = widget?.options?.validation?.type || "";
const serverRegex = widget?.options?.validation?.pattern;
const isPredfineType = inputType ? inputOpt?.has(inputType) : "";
const regExpression = inputValidation(serverRegex, inputType);
const pattern = useMemo(() => toHtmlPattern(regExpression), [regExpression]);
useEffect(() => {
const response =
widget?.options?.response ?? widget?.options?.defaultValue ?? "";
const isNumber = inputType === "number" && response;
setValue(isNumber ? Number(response) : response);
}, []);
const handleInputChange = (e) => {
const text = e.target.value || "";
setValue(text);
onChange(text);
};
return (
<div className="flex flex-col">
<div className="min-w-max">
<input
type={isPredfineType ? inputType : "text"}
id={`widget-${widget?.options?.name}-${widget.key}`}
value={value}
placeholder={
widget?.options?.hint ||
t("enter-value", { value: widget?.options?.name })
}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
onChange={(e) => handleInputChange(e)}
pattern={pattern || undefined} // if no pattern, browser won't do pattern validation
onInvalid={(e) => {
const el = e.currentTarget;
// ✅ Only override message if pattern exists AND the error is patternMismatch
if (pattern && el.validity.patternMismatch) {
el.setCustomValidity(t("validation-alert-1"));
} else {
el.setCustomValidity("");
}
}}
onInput={(e) => {
e.currentTarget.setCustomValidity("");
}}
required={isRequired}
/>
</div>
</div>
);
};
const CheckboxWidget = ({ widget, isRequired, onChange }) => {
const { t } = useTranslation();
const [selectedCheckbox, setSelectedCheckbox] = useState([]);
useEffect(() => {
setSelectedCheckbox(
widget?.options?.response ?? widget?.options?.defaultValue ?? []
);
}, []);
const handleCheckboxValue = (isChecked, ind) => {
const prevArr = Array.isArray(selectedCheckbox) ? selectedCheckbox : [];
const updateResponse = isChecked
? prevArr.includes(ind)
? prevArr
: [...prevArr, ind] // add once
: prevArr.filter((v) => v !== ind); // remove
setSelectedCheckbox(updateResponse);
onChange(updateResponse);
};
const noneSelected = selectedCheckbox?.length === 0;
return (
<div className="flex flex-col">
<div className="flex flex-col gap-y-1 min-w-max">
{widget.options?.values?.map((value, ind) => (
<div
key={ind}
className="select-none-cls flex items-center text-center gap-0.5"
>
<input
id={`checkbox-${widget.key + ind}`}
className="mt-[2px] op-checkbox op-checkbox-xs"
type="checkbox"
checked={selectedCheckbox.includes(ind)}
required={isRequired && noneSelected && ind === 0} // key line
onInvalid={(e) =>
e.target.setCustomValidity(t("select-at-least-option"))
}
onChange={(e) => {
e.target.setCustomValidity(""); // ✅ clear message as soon as user changes
handleCheckboxValue(e.target.checked, ind);
}}
/>
<label
htmlFor={`checkbox-${widget.key + ind}`}
className="text-xs mb-0 text-center ml-[3px] cursor-pointer"
>
{value}
</label>
</div>
))}
</div>
</div>
);
};
const DropdownWidget = ({ widget, isRequired, onChange }) => {
const { t } = useTranslation();
const [selected, setSelected] = useState("");
useEffect(() => {
setSelected(
widget?.options?.response ?? widget?.options?.defaultValue ?? ""
);
}, []);
const handleDropdownChange = (value) => {
setSelected(value);
onChange(value);
};
return (
<div className="flex flex-col">
<div className="min-w-max">
<select
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-base-content w-full"
id={`widget-${widget?.options?.name}-${widget?.key}`}
value={selected}
onChange={(e) => handleDropdownChange(e.target.value)}
required={isRequired}
>
{/* Default/Title option */}
<option value="" disabled hidden>
{t("choose-one")}
</option>
{widget?.options?.values?.map((option, ind) => (
<option key={ind} value={option}>
{option}
</option>
))}
</select>
</div>
</div>
);
};
const RadioButtonWidget = ({ widget, isRequired, onChange, formIndex }) => {
const [selected, setSelected] = useState("");
useEffect(() => {
setSelected(
widget?.options?.response || widget?.options?.defaultValue || ""
);
}, []);
const handleRadioChange = (value) => {
setSelected(value);
onChange(value);
};
return (
<div className="flex flex-col">
<div className="flex flex-col gap-y-1 min-w-max">
{widget.options?.values?.map((data, ind) => (
<div
key={ind}
className="select-none-cls flex items-center text-center gap-0.5"
>
<input
id={`radio-${widget.key + ind}`}
className="mt-[2px] op-radio op-radio-xs"
name={`radio-group-${widget.key}-${formIndex}`} // same name => browser treats all radios as ONE group
type="radio"
required={isRequired && ind === 0} // ✅ set required ONCE (on first radio) to make the whole group mandatory
checked={selected === data}
onChange={() => handleRadioChange(data)}
/>
<label
htmlFor={`radio-${widget.key + ind}`}
className="text-xs mb-0 ml-[2px] cursor-pointer"
>
{data}
</label>
</div>
))}
</div>
</div>
);
};
const ImageWidget = ({ widget, isRequired, onChange }) => {
const { t } = useTranslation();
const imageRef = useRef(null);
const [img, setImg] = useState({ src: "", imgType: "" });
const [defaultImg, setDefaultImg] = useState({ src: "", imgType: "" });
useEffect(() => {
if (widget?.response) {
setDefaultImg({ src: widget?.response, imgType: "" });
}
}, []);
const onImageChange = (e) => {
if (e.target.files && e.target.files?.[0]) {
const file = e.target.files?.[0];
compressedFileSize(file, ({ src, imgType }) => {
onChange(src);
setImg({ src, imgType });
});
}
};
const handleClearImage = () => {
setDefaultImg({ src: "", imgType: "" });
setImg({ src: "", imgType: "" });
onChange("");
};
return (
<div className="flex flex-col">
<div className="flex flex-col prefillCanvas">
{defaultImg?.src || img?.src ? (
<>
<div
className={`cursor-pointer rounded-box ${canavasTheme} flex flex-col w-full h-full justify-center items-center`}
>
<img
alt={`image_${widget?.options?.name}`}
src={img.src || defaultImg?.src}
draggable="false"
className="object-contain h-full w-full aspect-[5/2]"
/>
</div>
<span
onClick={handleClearImage}
className="flex justify-start text-blue-500 underline cursor-pointer ml-1"
>
{t("clear")}
</span>
</>
) : (
<div
className={`cursor-pointer rounded-box ${canavasTheme} overflow-hidden w-full h-full aspect-[5/2] flex flex-col justify-center items-center`}
onClick={() => imageRef.current?.click()}
>
<i className="fa-light text-base-content fa-cloud-upload-alt text-[25px]"></i>
<div className="text-[10px] text-base-content">{t("upload")}</div>
<input
type="file"
onChange={(e) => onImageChange?.(e, widget)}
className="filetype w-[1px] h-[1px] opacity-0"
accept="image/png,image/jpeg"
ref={imageRef} // Assign ref dynamically
required={isRequired}
/>
</div>
)}
</div>
</div>
);
};
const DrawWidget = ({ widget, isRequired, onChange }) => {
const { t } = useTranslation();
const canvasRef = useRef(null);
const sigRequiredRef = useRef(null);
const [hasDraw, setHasDraw] = useState("");
const [penColor, setPenColor] = useState("blue");
const [image, setImage] = useState("");
useEffect(() => {
if (widget?.response) {
setImage(widget?.response);
setHasDraw("signed");
}
}, []);
const handleSignatureChange = () => {
const draw = canvasRef.current?.isEmpty?.() ? "" : "signed";
setHasDraw(draw);
onChange(canvasRef.current.toDataURL());
// clear custom error once user signs
sigRequiredRef.current?.setCustomValidity("");
};
const handleClear = () => {
setImage("");
if (canvasRef?.current) {
canvasRef.current.clear();
onChange("");
setHasDraw("");
// clear custom error once user signs
sigRequiredRef.current?.setCustomValidity("");
}
};
return (
<div className="flex flex-col">
<div className="relative">
{image ? (
<div className={`prefillCanvas ${canavasTheme} rounded-[10px]`}>
<img
alt={`draw_${widget?.options?.name}`}
src={image}
className="prefillCanvas object-contain"
/>
</div>
) : (
<SignatureCanvas
ref={canvasRef}
penColor={penColor}
canvasProps={{
className: `prefillCanvas ${canavasTheme} rounded-[10px]`
}}
onEnd={() => handleSignatureChange()}
dotSize={1}
/>
)}
{/* ✅ Native form validation hook */}
<input
ref={sigRequiredRef}
type="text"
value={hasDraw}
required={isRequired}
tabIndex={-1}
onInvalid={(e) => e.target.setCustomValidity(t("draw-required"))}
onChange={() => {}}
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 w-[1px] h-[1px] pointer-events-none"
/>
</div>
<div className="flex flex-row justify-between mt-[8px] w-[200px]">
<PenColorComponent
penColor={penColor}
setPenColor={setPenColor}
hideLabel
penSize="sm"
/>
<span
onClick={handleClear}
className="flex justify-start text-blue-500 underline cursor-pointer"
>
{t("clear")}
</span>
</div>
</div>
);
};
const CellsWidget = ({ widget, isRequired, onChange }) => {
const { t } = useTranslation();
const count = widget?.options?.cellCount || 1;
const [word, setWord] = useState("");
const inputType = widget?.options?.validation?.type || "";
const serverRegex = widget?.options?.validation?.pattern;
const regExpression = inputValidation(serverRegex, inputType);
const pattern = useMemo(() => toHtmlPattern(regExpression), [regExpression]);
useEffect(() => {
const response =
widget?.options?.response ?? widget?.options?.defaultValue ?? "";
const isNumber = inputType === "number" && response;
setWord(isNumber ? Number(response) : response);
}, []);
const handleChange = (words) => {
const value = words;
setWord(value);
onChange(value);
};
return (
<div className="flex flex-col">
<div className="relative">
<CellsInput
hint={
widget?.options?.hint ||
t("enter-value", { value: widget?.options?.name })
}
count={count}
value={word}
onChange={handleChange}
only="all"
autoFocus
/>
<input
id={`widget-${widget?.options?.name}-${widget.key}`}
value={word}
required={isRequired}
tabIndex={-1}
onChange={() => {}}
pattern={pattern || undefined} // if no pattern, browser won't do pattern validation
onInvalid={(e) => {
const el = e.currentTarget;
// ✅ Only override message if pattern exists AND the error is patternMismatch
if (pattern && el.validity.patternMismatch) {
el.setCustomValidity(t("validation-alert-1"));
} else {
el.setCustomValidity("");
}
}}
onInput={(e) => {
e.currentTarget.setCustomValidity("");
}}
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 w-[1px] h-[1px] pointer-events-none"
/>
</div>
</div>
);
};
const RenderWidgets = ({
widget,
formIndex,
prefillIndex,
handleWidgetDetails
}) => {
const label = widget?.label?.toLowerCase() ?? "";
const isPrefill = label.includes(PREFILL_PREFIX);
const commonProps = {
widget,
isRequired: widget.options?.status === "required" && isPrefill,
formIndex
};
const onChange = (value) => {
handleWidgetDetails(value, formIndex, prefillIndex);
};
switch (widget.type) {
case "date":
return <DateWidget {...commonProps} onChange={onChange} />;
case textWidget:
case textInputWidget:
case "name":
case "email":
case "company":
case "job title":
case "number":
return <TextWidget {...commonProps} onChange={onChange} />;
case "checkbox":
return <CheckboxWidget {...commonProps} onChange={onChange} />;
case radioButtonWidget:
return <RadioButtonWidget {...commonProps} onChange={onChange} />;
case "dropdown":
return <DropdownWidget {...commonProps} onChange={onChange} />;
case "image":
case "stamp":
return <ImageWidget {...commonProps} onChange={onChange} />;
case "initials":
case "draw":
return <DrawWidget {...commonProps} onChange={onChange} />;
case "cells":
return <CellsWidget {...commonProps} onChange={onChange} />;
default:
return null;
}
};
export default RenderWidgets;
@@ -29,7 +29,7 @@ const DashboardButton = (props) => {
: "cursor-default"
} w-full shadow-md px-3 py-2 op-card bg-base-100`}
>
<div className="flex flex-row items-center">
<div className="flex flex-row items-center text-base-content">
<div className="flex flex-row items-center">
<span className="rounded-full bg-base-content bg-opacity-20 w-[60px] h-[60px] self-start flex justify-center items-center">
<i
@@ -39,7 +39,7 @@ const DashboardButton = (props) => {
></i>
</span>
</div>
<div className="text-lg ml-3 text-base-content">
<div className="text-lg ml-3">
{t(`sidebar.${props.Label}`)}
{props.Label === "Sign yourself" && (
<div className="text-gray-500 text-xs mt-1">
@@ -1,10 +1,11 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import axios from "axios";
import Parse from "parse";
import getReplacedHashQuery from "../../constant/getReplacedHashQuery";
import { useNavigate } from "react-router";
import Tooltip from "../../primitives/Tooltip";
import { useTranslation } from "react-i18next";
import { withSessionValidation } from "../../utils";
const DashboardCard = (props) => {
const navigate = useNavigate();
@@ -14,7 +15,7 @@ const DashboardCard = (props) => {
const [response, setresponse] = useState("");
const [loading, setLoading] = useState(false);
const renderData = async () => {
const renderData = withSessionValidation(async () => {
if (props.Data.queryType === "function") {
setLoading(true);
try {
@@ -148,7 +149,7 @@ const DashboardCard = (props) => {
);
let arr = [];
for (const obj of listData) {
const isSigner = obj.Signers.some(
const isSigner = obj.Signers?.some(
(item) => item.UserId.objectId === currentUser.id
);
if (isSigner) {
@@ -168,31 +169,27 @@ const DashboardCard = (props) => {
}
}
setresponse(arr.length);
setLoading(false);
});
} else {
await axios.get(url, { headers: headers }).then((res) => {
if (res.data.results.length > 0) {
setLoading(false);
if (props.Data.key !== "count") {
setresponse(res.data.results[0][props.Data.key]);
} else {
setresponse(res.data[props.Data.key]);
}
if (res?.data?.[props.Data.key]) {
setresponse(parseInt(res.data[props.Data.key]));
} else if (res?.data?.results?.length > 0) {
setresponse(res.data.results.length);
} else {
setresponse(0);
setLoading(false);
}
});
}
} catch (e) {
console.error("Problem", e);
} finally {
setLoading(false);
}
}
};
});
const filterRender = async () => {
const filterRender = withSessionValidation(async () => {
if (props.FilterData && props.FilterData.queryType === "function") {
setLoading(true);
try {
@@ -270,7 +267,7 @@ const DashboardCard = (props) => {
setLoading(false);
}
}
};
});
const setFormat = (val) => {
switch (props.Format) {
@@ -346,7 +343,6 @@ const DashboardCard = (props) => {
id={props.Label}
iconColor={"white"}
message={t(`tour-mssg.${props.Label}`)}
// {props?.Data?.tourMessage}
/>
</div>
</div>
@@ -1,158 +0,0 @@
import React, { useEffect, useState } from "react";
import Parse from "parse";
import ReportTable from "../../primitives/GetReportDisplay";
import reportJson from "../../json/ReportJson";
import axios from "axios";
import Loader from "../../primitives/Loader";
import { useTranslation } from "react-i18next";
function DashboardReport(props) {
const { t } = useTranslation();
// console.log("props ", props)
const [List, setList] = useState([]);
const [isLoader, setIsLoader] = useState(true);
const [reportName, setReportName] = useState("");
const [actions, setActions] = useState([]);
const [heading, setHeading] = useState([]);
const [isNextRecord, setIsNextRecord] = useState(false);
const [isMoreDocs, setIsMoreDocs] = useState(true);
const abortController = new AbortController();
const docPerPage = 5;
useEffect(() => {
setReportName("");
getReportData(props.Record.reportId);
// Function returned from useEffect is called on unmount
return () => {
setIsLoader(true);
setList([]);
setIsNextRecord(false);
// Here it'll abort the fetch
abortController.abort();
};
// eslint-disable-next-line
}, [props.Record.reportId]);
// below useEffect call when isNextRecord state is true and fetch next record
useEffect(() => {
if (isNextRecord) {
getReportData(props.Record.reportId, List.length, 20);
}
// eslint-disable-next-line
}, [isNextRecord]);
const getReportData = async (id, skipUserRecord = 0, limit = 20) => {
setIsLoader(true);
const json = reportJson(id);
if (json) {
setActions(json.actions);
setReportName(json.reportName);
setHeading(json.heading);
const currentUser = Parse.User.current().id;
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
sessiontoken: localStorage.getItem("accesstoken")
};
try {
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
const res = await axios.post(url, params, {
headers: headers,
signal: abortController.signal // is used to cancel fetch query
});
if (id === "5Go51Q7T8r") {
const listData = res.data?.result.filter((x) => x.Signers.length > 0);
let arr = [];
for (const obj of listData) {
const isSigner = obj.Signers.some(
(item) => item.UserId.objectId === currentUser
);
if (isSigner) {
let isRecord;
if (obj?.AuditTrail && obj?.AuditTrail.length > 0) {
isRecord = obj?.AuditTrail.some(
(item) =>
item?.UserPtr?.UserId?.objectId === currentUser &&
item.Activity === "Signed"
);
} else {
isRecord = false;
}
if (isRecord === false) {
arr.push(obj);
}
}
}
if (arr.length === docPerPage) {
setIsMoreDocs(true);
} else {
setIsMoreDocs(false);
}
setList((prevRecord) =>
prevRecord.length > 0 ? [...prevRecord, ...arr] : arr
);
} else {
if (res.data.result.length >= docPerPage) {
setIsMoreDocs(true);
} else {
setIsMoreDocs(false);
}
setIsNextRecord(false);
if (!res.data.result.error) {
setList((prevRecord) =>
prevRecord.length > 0
? [...prevRecord, ...res.data.result]
: res.data.result
);
}
}
setIsLoader(false);
} catch (err) {
const isCancel = axios.isCancel(err);
if (!isCancel) {
console.log("err ", err);
setIsLoader(false);
}
setIsLoader(false);
}
} else {
setIsLoader(false);
}
};
return (
<>
{isLoader ? (
<div className="h-[250px] flex justify-center items-center">
<Loader />
</div>
) : (
<>
{reportName ? (
<ReportTable
ReportName={reportName}
List={List}
setList={setList}
actions={actions}
heading={heading}
setIsNextRecord={setIsNextRecord}
isMoreDocs={isMoreDocs}
docPerPage={docPerPage}
/>
) : (
<div className="flex items-center justify-center h-[100px] w-full bg-white rounded">
<div className="text-center">
<p className="text-xl text-black">{t("report-not-found")}</p>
</div>
</div>
)}
</>
)}
</>
);
}
export default DashboardReport;
@@ -0,0 +1,229 @@
import { useEffect, useState, useRef } from "react";
import Parse from "parse";
import DocumentsReport from "../../reports/document/DocumentsReport";
import reportJson from "../../json/ReportJson";
import axios from "axios";
import Loader from "../../primitives/Loader";
import { useTranslation } from "react-i18next";
import { withSessionValidation } from "../../utils";
function DashboardReport(props) {
const { t } = useTranslation();
const [List, setList] = useState([]);
const [isLoader, setIsLoader] = useState(true);
const [reportName, setReportName] = useState("");
const [actions, setActions] = useState([]);
const [heading, setHeading] = useState([]);
const [isNextRecord, setIsNextRecord] = useState(false);
const [isMoreDocs, setIsMoreDocs] = useState(true);
const abortController = new AbortController();
const docPerPage = 5;
const [searchTerm, setSearchTerm] = useState("");
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
const [isSearchResult, setIsSearchResult] = useState(false);
const debounceTimer = useRef(null);
useEffect(() => {
setReportName("");
setSearchTerm("");
setMobileSearchOpen(false);
getReportData(props.Record.reportId, 0, 20, "");
// Function returned from useEffect is called on unmount
return () => {
setIsLoader(true);
setList([]);
setIsNextRecord(false);
// Here it'll abort the fetch
abortController.abort();
};
// eslint-disable-next-line
}, [props.Record.reportId]);
// below useEffect call when isNextRecord state is true and fetch next record
useEffect(() => {
if (isNextRecord) {
getReportData(props.Record.reportId, List.length, 20, searchTerm);
}
// eslint-disable-next-line
}, [isNextRecord]);
const handleSearchChange = withSessionValidation(async (e) => {
const term = e.target.value.toLowerCase();
setSearchTerm(term);
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
debounceTimer.current = setTimeout(async () => {
try {
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
sessiontoken: localStorage.getItem("accesstoken")
};
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
const res = await axios.post(
url,
{
reportId: props.Record.reportId,
searchTerm: term,
skip: 0,
limit: docPerPage
},
{ headers }
);
const data = res.data?.result || [];
if (!data.error) {
setList(data);
setIsMoreDocs(data.length >= docPerPage);
setIsNextRecord(false);
setIsSearchResult(true);
}
} catch (err) {
console.error("Search error:", err);
}
}, 300);
setIsSearchResult(false);
});
const handleSearchPaste = (e) => {
setTimeout(() => {
handleSearchChange({ target: { value: e.target.value } });
}, 0);
};
useEffect(() => {
return () => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
};
}, []);
const getReportData = withSessionValidation(
async (id, skipUserRecord = 0, limit = 20, term = searchTerm) => {
setIsLoader(true);
const json = reportJson(id);
if (json) {
setActions(json.actions);
setReportName(json.reportName);
setHeading(json.heading);
const currentUser = Parse.User.current().id;
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
sessiontoken: localStorage.getItem("accesstoken")
};
try {
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
if (term) {
params.searchTerm = term;
}
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
const res = await axios.post(url, params, {
headers: headers,
signal: abortController.signal // is used to cancel fetch query
});
if (id === "5Go51Q7T8r") {
const listData = res.data?.result.filter(
(x) => x.Signers.length > 0
);
let arr = [];
for (const obj of listData) {
const isSigner = obj.Signers?.some(
(item) => item.UserId.objectId === currentUser
);
if (isSigner) {
let isRecord;
if (obj?.AuditTrail && obj?.AuditTrail.length > 0) {
isRecord = obj?.AuditTrail.some(
(item) =>
item?.UserPtr?.UserId?.objectId === currentUser &&
item.Activity === "Signed"
);
} else {
isRecord = false;
}
if (isRecord === false) {
arr.push(obj);
}
}
}
if (arr.length === docPerPage) {
setIsMoreDocs(true);
} else {
setIsMoreDocs(false);
}
setList((prevRecord) =>
prevRecord.length > 0 ? [...prevRecord, ...arr] : arr
);
} else {
if (res.data.result.length >= docPerPage) {
setIsMoreDocs(true);
} else {
setIsMoreDocs(false);
}
setIsNextRecord(false);
if (!res.data.result.error) {
setList((prevRecord) =>
prevRecord.length > 0
? [...prevRecord, ...res.data.result]
: res.data.result
);
}
}
setIsLoader(false);
} catch (err) {
const isCancel = axios.isCancel(err);
if (!isCancel) {
console.log("err ", err);
setIsLoader(false);
}
setIsLoader(false);
}
} else {
setIsLoader(false);
}
}
);
return (
<>
{isLoader ? (
<div className="h-[250px] flex justify-center items-center">
<Loader />
</div>
) : (
<>
{reportName ? (
<DocumentsReport
ReportName={reportName}
List={List}
setList={setList}
actions={actions}
heading={heading}
setIsNextRecord={setIsNextRecord}
isMoreDocs={isMoreDocs}
docPerPage={docPerPage}
mobileSearchOpen={mobileSearchOpen}
setMobileSearchOpen={setMobileSearchOpen}
searchTerm={searchTerm}
handleSearchChange={handleSearchChange}
handleSearchPaste={handleSearchPaste}
isSearchResult={isSearchResult}
/>
) : (
<div className="flex items-center justify-center h-[100px] w-full bg-white rounded-box">
<div className="text-center text-xl text-base-content">
{t("report-not-found")}
</div>
</div>
)}
</>
)}
</>
);
}
export default DashboardReport;
@@ -1,8 +1,9 @@
import React, { Suspense, lazy } from "react";
import { useState, useEffect, Suspense } from "react";
import { lazyWithRetry } from "../../utils";
import { useTranslation } from "react-i18next";
const DashboardButton = lazy(() => import("./DashboardButton"));
const DashboardCard = lazy(() => import("./DashboardCard"));
const DashboardReport = lazy(() => import("./DashboardReport"));
const DashboardButton = lazyWithRetry(() => import("./DashboardButton"));
const DashboardCard = lazyWithRetry(() => import("./DashboardCard"));
const DashboardReport = lazyWithRetry(() => import("./DashboardReport"));
const buttonList = [
{
label: "Sign yourself",
@@ -19,6 +20,7 @@ const buttonList = [
];
const GetDashboard = (props) => {
const { t } = useTranslation();
const Button = ({ label, redirectId, redirectType, icon }) => (
<DashboardButton
Icon={icon}
@@ -58,7 +60,9 @@ const GetDashboard = (props) => {
<div data-tut={col.widget.data.tourSection}>
<Suspense fallback={<div>please wait</div>}>
<div className="mb-3 md:mb-0">
<DashboardReport Record={col.widget} />
<DashboardReport
Record={col.widget}
/>
</div>
</Suspense>
</div>
@@ -92,7 +96,9 @@ const GetDashboard = (props) => {
return (
<Suspense fallback={<div>please wait</div>}>
<div className="mb-3 md:mb-0">
<DashboardReport Record={col.widget} />
<DashboardReport
Record={col.widget}
/>
</div>
</Suspense>
);
@@ -1,15 +1,16 @@
import React, { useState, useEffect, useRef } from "react";
import "../../styles/opensigndrive.css";
import axios from "axios";
import * as ContextMenu from "@radix-ui/react-context-menu";
import { ContextMenu } from "radix-ui";
import { useNavigate } from "react-router";
import Table from "react-bootstrap/Table";
import * as HoverCard from "@radix-ui/react-hover-card";
import { HoverCard } from "radix-ui";
import ModalUi from "../../primitives/ModalUi";
import FolderModal from "../shared/fields/FolderModal";
import { useTranslation } from "react-i18next";
import { handleDownloadPdf, isMobile } from "../../constant/Utils";
import Parse from "parse";
import { withSessionValidation } from "../../utils";
function DriveBody(props) {
const { t } = useTranslation();
@@ -52,13 +53,11 @@ function DriveBody(props) {
props.setSkip(0);
};
//function for change doc name and update doc name in _document class
const handledRenameDoc = async (data) => {
const handledRenameDoc = withSessionValidation(async (data) => {
setRename("");
const trimmedValue = renameValue.trim();
if (trimmedValue.length > 0) {
const updateName = {
Name: renameValue
};
const updateName = { Name: renameValue };
const docId = data.objectId;
const docData = props.pdfData;
const updatedData = docData.map((item) => {
@@ -71,11 +70,9 @@ function DriveBody(props) {
});
props.setPdfData(updatedData);
props.sortingData(null, null, updatedData);
await axios
.put(
`${localStorage.getItem(
"baseUrl"
)}classes/contracts_Document/${docId}`,
try {
await axios.put(
`${localStorage.getItem("baseUrl")}classes/contracts_Document/${docId}`,
updateName,
{
headers: {
@@ -84,20 +81,16 @@ function DriveBody(props) {
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
}
}
)
.then(() => {
// const res = result.data;
// console.log("res", res);
})
.catch((err) => {
console.log("Err ", err);
props.setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
});
);
} catch (err) {
console.error("Error in rename doc", err);
props.setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
});
}
}
};
});
//function for navigate user to microapp-signature component
const checkPdfStatus = async (data) => {
@@ -163,12 +156,10 @@ function DriveBody(props) {
}
};
//function for delete document
const handleDeleteDocument = async (docData) => {
const handleDeleteDocument = withSessionValidation(async (docData) => {
setIsDeleteDoc({});
const docId = docData.objectId;
const data = {
IsArchive: true
};
const data = { IsArchive: true };
await axios
.put(
@@ -190,19 +181,19 @@ function DriveBody(props) {
}
})
.catch((err) => {
console.log("Err ", err);
console.error("Err in delete doc", err);
props.setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
});
});
};
});
const handleMoveDocument = async (docData) => {
setIsOpenMoveModal(true);
setSelectDoc(docData);
};
//function for move document from one folder to another folder
const handleMoveFolder = async (selectFolderData) => {
const handleMoveFolder = withSessionValidation(async (selectFolderData) => {
const selecFolderId = selectDoc?.Folder?.objectId;
const moveFolderId = selectFolderData?.ObjectId;
let updateDocId = selectDoc?.objectId;
@@ -224,16 +215,12 @@ function DriveBody(props) {
}
};
} else {
updateData = {
Folder: { __op: "Delete" }
};
updateData = { Folder: { __op: "Delete" } };
}
await axios
.put(
`${localStorage.getItem(
"baseUrl"
)}classes/contracts_Document/${updateDocId}`,
`${localStorage.getItem("baseUrl")}classes/contracts_Document/${updateDocId}`,
updateData,
{
headers: {
@@ -243,9 +230,7 @@ function DriveBody(props) {
}
}
)
.then((Listdata) => {
// console.log("Listdata ", Listdata);
const res = Listdata.data;
if (res) {
const updatedData = props.pdfData.filter(
@@ -255,7 +240,7 @@ function DriveBody(props) {
}
})
.catch((err) => {
console.log("err", err);
console.error("err in move folder", err);
});
setIsOpenMoveModal(false);
@@ -263,7 +248,7 @@ function DriveBody(props) {
alert(t("folder-already-exist!"));
setIsOpenMoveModal(false);
}
};
});
const handleEnterPress = (e, data) => {
if (e.key === "Enter") {
handledRenameDoc(data);
@@ -303,7 +288,7 @@ function DriveBody(props) {
props.setPdfData(updatedData);
}
} catch (err) {
console.log("Err ", err);
console.error("Err in delete folder", err);
props.setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
@@ -347,12 +332,14 @@ function DriveBody(props) {
}
const signersName = () => {
const getSignersName = signerExist.map((data) => data.Name);
const signerName = getSignersName.join(", ");
const getSignersName =
signerExist?.length > 0 && signerExist?.map((data) => data?.Name || "");
const signerName =
getSignersName?.length > 0 ? getSignersName?.join(", ") : "";
return (
<span className="text-[12px] font-medium w-[90%] break-words">
{signerName}
{signerName && signerName}
</span>
);
};
@@ -364,14 +351,14 @@ function DriveBody(props) {
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
className="w-[26px] h-[26px] fill-current op-text-secondary"
className="w-[26px] h-[26px] fill-current"
>
<path d="M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z" />
</svg>
<span className="text-[12px] font-medium">{data.Name}</span>
</td>
<td>_</td>
<td>Folder</td>
<td>{t("folder")}</td>
<td>_</td>
<td>_</td>
</tr>
@@ -388,7 +375,7 @@ function DriveBody(props) {
<span className="text-[12px] font-medium">{data.Name}</span>
</td>
<td>{createddate}</td>
<td>Pdf</td>
<td>{t("pdf")}</td>
<td>{t(`drive-document-status.${status}`)}</td>
<td>
<i
@@ -403,7 +390,7 @@ function DriveBody(props) {
</tr>
)
) : listType === "list" && data.Type === "Folder" ? (
<div key={ind} className="relative w-[100px] h-[100px] mx-2 my-3">
<div className="relative w-[100px] h-[100px] mx-2 my-3">
<ContextMenu.Root>
<ContextMenu.Trigger className="flex flex-col justify-center items-center select-none-cls">
{/* folder */}
@@ -419,7 +406,7 @@ function DriveBody(props) {
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
className="w-[100px] h-[100px] fill-current op-text-secondary"
className="w-[100px] h-[100px] fill-current"
>
<path d="M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z" />
</svg>
@@ -445,7 +432,6 @@ function DriveBody(props) {
)}
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content
className="ContextMenuContent"
@@ -457,14 +443,14 @@ function DriveBody(props) {
className="ContextMenuItem"
>
<i className="fa-light fa-font mr-[8px]"></i>
<span>Rename</span>
<span>{t(`context-menu.Rename`)}</span>
</ContextMenu.Item>
<ContextMenu.Item
onClick={() => handleMenuItemClick("Delete", data, data.Type)}
className="ContextMenuItem"
>
<i className="fa-light fa-trash mr-[8px]"></i>
<span>Delete</span>
<span>{t(`context-menu.Delete`)}</span>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
@@ -549,7 +535,6 @@ function DriveBody(props) {
)
)}
</div>
<ContextMenu.Portal>
<ContextMenu.Content
className="ContextMenuContent"
@@ -602,7 +587,6 @@ function DriveBody(props) {
{signersName()}
</>
)}
<HoverCard.Arrow className="HoverCardArrow" />
</HoverCard.Content>
</HoverCard.Portal>
@@ -626,21 +610,21 @@ function DriveBody(props) {
</tr>
</thead>
<tbody>
{props.pdfData.map((data, ind) => {
return (
<React.Fragment key={ind}>
{handleFolderData(data, ind, "table")}
</React.Fragment>
);
})}
{props?.pdfData?.map((data, ind) => (
<React.Fragment key={ind}>
{handleFolderData(data, ind, "table")}
</React.Fragment>
))}
</tbody>
</Table>
</div>
) : (
<div className="flex flex-row flex-wrap items-center mt-1 pb-[20px] mx-[5px]">
{props.pdfData.map((data, ind) => {
return <div key={ind}>{handleFolderData(data, ind, "list")}</div>;
})}
{props?.pdfData?.map((data, ind) => (
<React.Fragment key={ind}>
{handleFolderData(data, ind, "list")}
</React.Fragment>
))}
</div>
)}
@@ -10,15 +10,15 @@ const AddRoleModal = (props) => {
isOpen={props.isModalRole}
handleClose={props.handleCloseRoleModal}
>
<div className="h-full py-[10px] px-[20px]">
<div className="text-base-content h-full py-[10px] px-[20px]">
<form className="flex flex-col" onSubmit={props.handleAddRole}>
<input
value={props.roleName}
onChange={(e) => props.setRoleName(e.target.value)}
placeholder={
props.signersdata.length > 0
? "User " + (props.signersdata.length + 1)
: "User 1"
? "Role " + (props.signersdata.length + 1)
: "Role 1"
}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs mt-1"
/>
@@ -33,7 +33,7 @@ const AddRoleModal = (props) => {
<button
onClick={props.handleCloseRoleModal}
type="button"
className="op-btn op-btn-ghost ml-2"
className="op-btn op-btn-ghost text-base-content ml-2"
>
{t("close")}
</button>
@@ -4,10 +4,11 @@ import ModalUi from "../../primitives/ModalUi";
function AgreementContent(props) {
const { t } = useTranslation();
const appName =
"OpenSign™";
const h2Style = "text-base-content font-medium text-lg";
const ulStyle = "list-disc px-4 py-3";
const handleOnclick = () => {
props.setIsAgreeTour(false);
props.setIsAgree(true);
props.setIsShowAgreeTerms(false);
props.showFirstWidget();
@@ -21,78 +22,94 @@ function AgreementContent(props) {
>
<div className="h-[100%] p-[20px]">
<h2 className={h2Style}>{t("term-cond-h")}</h2>
<span className="mt-2">{t("term-cond-p1")}</span>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<span className="mt-2">
{t("term-cond-p1", { appName: appName })}
</span>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<h2 className={h2Style}>{t("term-cond-h1")}</h2>
<span className="mt-2">{t("term-cond-p2")}</span>
<span className="mt-2">
{t("term-cond-p2", { appName: appName })}
</span>
<ul className={ulStyle}>
<li>{t("term-cond-p3")}</li>
<li>{t("term-cond-p3", { appName: appName })}</li>
<li>{t("term-cond-p4")}</li>
</ul>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<h2 className={h2Style}>{t("term-cond-h2")}</h2>
<span className="mt-2">{t("term-cond-p5")}</span>
<ul className={ulStyle}>
<li>{t("term-cond-p6")}</li>
<li>{t("term-cond-p7")}</li>
<li>{t("term-cond-p6", { appName: appName })}</li>
<li>{t("term-cond-p7", { appName: appName })}</li>
<li>{t("term-cond-p8")}</li>
</ul>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<h2 className={h2Style}>{t("term-cond-h3")}</h2>
<span className="mt-2">{t("term-cond-p9")}</span>
<ul className={ulStyle}>
<li>{t("term-cond-p10")}</li>
<li>{t("term-cond-p11")}</li>
</ul>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<h2 className={h2Style}>{t("term-cond-h4")}</h2>
<span className="mt-2">{t("term-cond-p12")}</span>
<span className="mt-2">
{t("term-cond-p12", { appName: appName })}
</span>
<ul className={ulStyle}>
<li>{t("term-cond-p13")}</li>
<li>{t("term-cond-p14")}</li>
<li>{t("term-cond-p15")}</li>
</ul>
<span>{t("term-cond-p16")}</span>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<span>{t("term-cond-p16", { appName: appName })}</span>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<h2 className={h2Style}>{t("term-cond-h5")}</h2>
<p>{t("term-cond-p17")}</p>
<p>{t("term-cond-p17", { appName: appName })}</p>
<p className="mt-2">{t("term-cond-p18")}</p>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<h2 className={h2Style}>{t("term-cond-h6")}</h2>
<span className="mt-2">{t("term-cond-p19")}</span>
<ul className={ulStyle}>
<li>{t("term-cond-p20")}</li>
<li>{t("term-cond-p21")}</li>
<li>{t("term-cond-p22")}</li>
<li>{t("term-cond-p22", { appName: appName })}</li>
</ul>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<h2 className={h2Style}>{t("term-cond-h7")}</h2>
<span className="mt-2">{t("term-cond-p23")}</span>
<span className="mt-2">
{t("term-cond-p23", { appName: appName })}
</span>
<ul className={ulStyle}>
<li>{t("term-cond-p24")}</li>
<li>{t("term-cond-p25")}</li>
</ul>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<h2 className={h2Style}>{t("term-cond-h8")}</h2>
<span className="mt-2">{t("term-cond-p26")}</span>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<span className="mt-2">
{t("term-cond-p26", { appName: appName })}
</span>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<h2 className={h2Style}>{t("term-cond-h9")}</h2>
<span className="mt-2">{t("term-cond-p27")}</span>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<span className="mt-2 font-medium">{t("term-cond-p28")}</span>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<span className="mt-2">{t("term-cond-p29")}</span>
<a
href="www.opensignlabs.com"
target="_blank"
className="text-blue-700 cursor-pointer"
>
{" "}
www.opensignlabs.com{" "}
</a>
<span>{t("term-cond-p30")}</span>
<span className="font-medium"> support@opensignlabs.com </span>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<span className="mt-2">
{t("term-cond-p27", { appName: appName })}
</span>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<span className="mt-2 font-medium">
{t("term-cond-p28", { appName: appName })}
</span>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<span className="mt-2">
{t("term-cond-p29", { appName: appName })}
</span>
<a
href="www.opensignlabs.com"
target="_blank"
className="text-blue-700 cursor-pointer"
>
www.opensignlabs.com
</a>
<span>{t("term-cond-p30")}</span>
<span className="font-medium"> support@opensignlabs.com </span>
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
<div className="mt-6 flex justify-start gap-2">
<button
onClick={() => handleOnclick()}
@@ -101,7 +118,7 @@ function AgreementContent(props) {
{t("agrre-button")}
</button>
<button
className="op-btn op-btn-ghost"
className="op-btn op-btn-ghost text-base-content"
onClick={() => props.setIsShowAgreeTerms(false)}
>
{t("close")}
@@ -4,7 +4,6 @@ import AgreementContent from "./AgreementContent";
function AgreementSign(props) {
const { t } = useTranslation();
const [isChecked, setIsChecked] = useState(false);
const [isShowAgreeTerms, setIsShowAgreeTerms] = useState(false);
return (
@@ -12,26 +11,12 @@ function AgreementSign(props) {
<div className="op-modal op-modal-open absolute z-[448]">
<div className="w-[95%] md:w-[60%] lg:w-[40%] op-modal-box overflow-y-auto hide-scrollbar text-sm p-4">
<div className="flex flex-row items-center">
<input
data-tut="IsAgree"
className="mr-3 op-checkbox op-checkbox-m"
type="checkbox"
value={isChecked}
onChange={(e) => {
setIsChecked(e.target.checked);
if (e.target.checked) {
props.setIsAgreeTour(false);
}
props.showFirstWidget();
}}
/>
<div className="text-[11px] md:text-base">
<div className="text-[11px] md:text-base text-base-content">
<span>{t("agree-p1")}</span>
<span
className="font-bold text-blue-600 cursor-pointer"
onClick={() => {
setIsShowAgreeTerms(true);
props.setIsAgreeTour(false);
}}
>
{t("agree-p2")}
@@ -39,26 +24,24 @@ function AgreementSign(props) {
<span> {t("agree-p3")}</span>
</div>
</div>
<div className="flex ml-[35px] mt-3">
<div className="flex mt-3">
<button
onClick={() => {
if (isChecked) {
props.setIsAgreeTour(false);
props.setIsAgree(true);
} else {
props.setIsAgreeTour(true);
}
props.setIsAgree(true);
props.showFirstWidget();
}}
className="op-btn op-btn-primary op-btn-sm w-full md:w-auto"
>
{t("agrre-button")}
</button>
</div>
<div className="mt-2 text-base-content">
<span className="text-[11px]">{t("agreement-note")}</span>
</div>
</div>
</div>
{isShowAgreeTerms && (
<AgreementContent
setIsAgreeTour={props.setIsAgreeTour}
setIsAgree={props.setIsAgree}
setIsShowAgreeTerms={setIsShowAgreeTerms}
showFirstWidget={props.showFirstWidget}
@@ -12,12 +12,9 @@ function BorderResize(props) {
return (
<div
style={{
width: getHeight() || "14px",
height: getHeight() || "14px"
}}
style={{ width: getHeight() || "14px", height: getHeight() || "14px" }}
className={`${props.right ? `-right-[12px]` : "-right-[2px]"} ${
props.top ? `-bottom-[12px]` : "-bottom-[2px] "
props.top ? `-bottom-[12px]` : "-bottom-[2px]"
} absolute inline-block hover:cursor-sw-resize border-r-[3px] border-b-[3px] border-[#188ae2]`}
></div>
);
@@ -0,0 +1,110 @@
import { useState, useEffect } from "react";
import ModalUi from "../../primitives/ModalUi";
import { fontsizeArr, fontColorArr } from "../../constant/Utils";
import { useTranslation } from "react-i18next";
export default function CellsSettingModal({
isOpen,
handleClose,
defaultData,
handleSave,
}) {
const { t } = useTranslation();
const [name, setName] = useState("");
const [cellCount, setCellCount] = useState(5);
const [fontSize, setFontSize] = useState(12);
const [fontColor, setFontColor] = useState("black");
useEffect(() => {
if (defaultData) {
setName(defaultData.options?.name || "Cells");
setCellCount(defaultData.options?.cellCount || 5);
setFontSize(defaultData.options?.fontSize || 12);
setFontColor(defaultData.options?.fontColor || "black");
}
}, [defaultData]);
const onSubmit = (e) => {
e.preventDefault();
handleSave &&
handleSave({
name,
cellCount: parseInt(cellCount, 10),
fontSize,
fontColor,
});
};
return (
<ModalUi isOpen={isOpen} handleClose={handleClose} title={t("widget-info")}>
<form
onSubmit={onSubmit}
className="p-[20px] text-base-content flex flex-col gap-3"
>
<div>
<label htmlFor="name" className="text-[13px]">
{t("name")} <span className="text-[red]">*</span>
</label>
<input
required
name="name"
className="op-input op-input-bordered op-input-sm w-full text-xs focus:outline-none hover:border-base-content"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<label htmlFor="cellCount" className="text-[13px]">
{t("cell-count")}
</label>
<input
type="number"
min="1"
name="cellCount"
className="op-input op-input-bordered op-input-sm w-full text-xs focus:outline-none hover:border-base-content"
value={cellCount}
onChange={(e) => setCellCount(e.target.value)}
/>
</div>
<div className="flex flex-col md:flex-row md:items-center gap-3">
<div className="flex items-center gap-2">
<span className="whitespace-nowrap">{t("font-size")}: </span>
<select
className="ml-[7px] w-[60%] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
value={fontSize}
onChange={(e) => setFontSize(parseInt(e.target.value))}
>
{fontsizeArr.map((size, ind) => (
<option className="text-[13px]" value={size} key={ind}>
{size}
</option>
))}
</select>
</div>
<div className="flex items-center">
<span>{t("color")}: </span>
<select
className="ml-[33px] md:ml-4 w-[65%] md:w-full op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
value={fontColor}
onChange={(e) => setFontColor(e.target.value)}
>
{fontColorArr.map((color, ind) => (
<option value={color} key={ind}>
{t(`color-type.${color}`)}
</option>
))}
</select>
<span
className="w-5 h-[19px] ml-1"
style={{ background: fontColor }}
/>
</div>
</div>
<div className="h-[1px] w-full bg-[#b7b3b3] my-[16px]"></div>
<button type="submit" className="op-btn op-btn-primary">
{t("save")}
</button>
</form>
</ModalUi>
);
}
@@ -0,0 +1,143 @@
import { useState, useEffect, useRef } from "react";
const Cell = ({
isEnabled,
count,
h,
value,
editable,
onChange,
onKeyDown,
onBlur,
inputRef,
index,
fontSize,
fontColor,
hint
}) => (
<div
className={`${isEnabled ? "bg-white border-gray-400" : "select-none-cls pointer-events-none border-gray-500"} flex items-center justify-center border-[1px]`}
style={{ flex: `0 0 ${100 / count}%`, height: h }}
>
<input
disabled={!isEnabled}
maxLength={1}
value={value}
readOnly={!editable}
ref={inputRef}
onChange={editable ? (e) => onChange && onChange(e, index) : undefined}
onKeyDown={editable ? (e) => onKeyDown && onKeyDown(e, index) : undefined}
// trigger validation when leaving a cell
onBlur={editable ? (e) => onBlur && onBlur(e, index) : undefined}
className={`${isEnabled ? "placeholder-gray-300" : "placeholder-gray-500"} w-full text-center focus:outline-none bg-transparent`}
placeholder={hint}
style={{ fontFamily: "Arial, sans-serif", fontSize, color: fontColor }}
/>
</div>
);
export default function CellsWidget({
isEnabled,
count = 8,
height = 40,
value = "",
editable = false,
onChange,
onKeyDown,
onBlur,
onCellCountChange,
inputRefs,
resizable = false,
fontSize = "12px",
fontColor = "black",
hint = ""
}) {
const [cellCount, setCellCount] = useState(count);
// keep internal state in sync with prop updates
useEffect(() => setCellCount(count), [count]);
const startX = useRef(0);
const startCount = useRef(cellCount);
const capture = (downEv, onMove, onUp = () => {}) => {
const id = downEv.pointerId;
const move = (ev) => id === ev.pointerId && onMove(ev);
const up = (ev) => {
if (id !== ev.pointerId) return;
onUp(ev);
downEv.target.releasePointerCapture(id);
window.removeEventListener("pointermove", move);
window.removeEventListener("pointerup", up);
};
window.addEventListener("pointermove", move);
window.addEventListener("pointerup", up);
downEv.target.setPointerCapture(id);
};
const onTopHandlePointerDown = (ev) => {
// Prevent triggering the widget drag logic
ev.stopPropagation();
ev.preventDefault();
startX.current = ev.clientX;
startCount.current = cellCount;
capture(ev, (moveEv) => {
const dx = moveEv.clientX - startX.current;
const delta = Math.floor(-dx / 5);
let newCount = Math.max(1, startCount.current + delta);
if (newCount !== cellCount) {
setCellCount(newCount);
onCellCountChange?.(newCount);
}
});
};
const cells = Array.from({ length: cellCount }).map((_, i) => value[i] || "");
const hints = Array.from({ length: cellCount }).map((_, i) => hint[i] || "");
return (
<div
className="relative flex w-full h-full overflow-visible"
style={{ height }}
>
{resizable && (
<div
className="cell-size-handle absolute left-1/2 -translate-x-1/2 -bottom-4 rotate-180 cursor-ew-resize touch-none"
onPointerDown={onTopHandlePointerDown}
>
<svg
className="w-4 h-4 text-blue-600"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="m9.69 18.933.003.001C9.89 19.02 10 19 10 19s.11.02.308-.066l.002-.001.006-.003.018-.008a5.741 5.741 0 0 0 .281-.14c.186-.096.446-.24.757-.433.62-.384 1.445-.966 2.274-1.765C15.302 14.988 17 12.493 17 9A7 7 0 1 0 3 9c0 3.492 1.698 5.988 3.355 7.584a13.731 13.731 0 0 0 2.273 1.765 11.842 11.842 0 0 0 .976.544l.062.029.018.008.006.003ZM10 11.25a2.25 2.25 0 1 0 0-4.5 2.25 2.25 0 0 0 0 4.5Z"
clipRule="evenodd"
/>
</svg>
</div>
)}
{cells.map((val, i) => (
<Cell
key={i}
isEnabled={isEnabled}
count={cellCount}
h={height}
value={val}
editable={editable}
onChange={onChange}
onKeyDown={onKeyDown}
onBlur={onBlur}
inputRef={inputRefs ? (el) => (inputRefs.current[i] = el) : undefined}
index={i}
fontSize={fontSize}
fontColor={fontColor}
hint={hints[i]}
/>
))}
</div>
);
}
@@ -1,276 +0,0 @@
import React, { useEffect, useState } from "react";
import opensignLogo from "../../assets/images/logo.png";
import {
Page,
Text,
View,
Document,
StyleSheet,
Image
} from "@react-pdf/renderer";
function Certificate({ pdfData }) {
const [isMultiSigners, setIsMultiSigners] = useState();
const [multiSigner, setMultiSigners] = useState([]);
const [isLoad, setIsLoad] = useState(false);
useEffect(() => {
handleSignerData();
// eslint-disable-next-line
}, []);
const handleSignerData = () => {
const checkSigners = pdfData.filter((data) => data.Signers);
if (checkSigners && checkSigners.length > 0) {
setIsMultiSigners(true);
const checkSignSigners =
pdfData[0].AuditTrail &&
pdfData[0].AuditTrail.length > 0 &&
pdfData[0].AuditTrail.filter((data) => data.Activity === "Signed");
setMultiSigners(checkSignSigners);
} else {
setIsMultiSigners(false);
}
setIsLoad(true);
};
const styles = StyleSheet.create({
page: {
borderRadius: "5px",
padding: "10px",
backgroundColor: "white"
},
section1: {
border: "1px solid rgb(177, 174, 174)",
padding: "20px"
},
textStyle: {
fontWeight: "bold",
fontSize: "11px",
marginBottom: "10px"
},
textStyle2: {
fontWeight: "600",
fontSize: "11px",
marginBottom: "10px",
color: "gray"
},
image: {
width: "71px",
height: "17px"
}
});
const generatedDate = () => {
const newDate = new Date();
const utcTime = newDate.toUTCString();
return (
<Text
style={{
color: "gray",
fontSize: "10px"
}}
>
Generated On {utcTime}
</Text>
);
};
const changeCompletedDate = () => {
const completedOn = pdfData[0].updatedAt;
const newDate = new Date(completedOn);
const utcTime = newDate.toUTCString();
return <Text style={styles.textStyle2}>{utcTime}</Text>;
};
const signerName = (data) => {
const getSignerName = pdfData[0].Signers.filter(
(sign) => sign.objectId === data.UserPtr.objectId
);
return (
getSignerName[0] &&
getSignerName.length > 0 && (
<>
<Text style={styles.textStyle}>
Name : &nbsp;
<Text style={styles.textStyle2}>{getSignerName[0].Name}</Text>
</Text>
<Text style={styles.textStyle}>
Email : &nbsp;
<Text style={styles.textStyle2}>{getSignerName[0].Email}</Text>
</Text>
</>
)
);
};
return (
isLoad && (
<Document>
{/** Page defines a single page of content. */}
<Page size="A4" style={styles.page}>
<View style={styles.section1}>
<View
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "30px"
}}
>
<Image src={opensignLogo} style={styles.image} />
{generatedDate()}
</View>
<View style={{ justifyContent: "center" }}>
<Text
style={{
textAlign: "center",
fontSize: "20px",
fontWeight: "bold",
color: "#31bceb",
marginBottom: "10px"
}}
>
{" "}
Certificate of Completion
</Text>
<View style={{ border: "1px solid #bdbbbb" }}></View>
<View>
<View>
<Text
style={{
fontSize: "16px",
fontWeight: "bold",
color: "#31bceb",
margin: "10px 0px 10px 0px"
}}
>
Summary
</Text>
</View>
<View style={{ display: "flex", flexDirection: "column" }}>
<Text style={styles.textStyle}>
Document ID : &nbsp;
<Text style={styles.textStyle2}>{pdfData[0].objectId}</Text>
</Text>
<Text style={styles.textStyle}>
Document Name : &nbsp;
<Text style={styles.textStyle2}>{pdfData[0].Name}</Text>
</Text>
<Text style={styles.textStyle}>
Organization : &nbsp;
<Text style={styles.textStyle2}>
{pdfData[0].ExtUserPtr.Company}
</Text>
</Text>
<Text style={styles.textStyle}>
Completed on : &nbsp;{changeCompletedDate()}
</Text>
{multiSigner && multiSigner.length > 0 && (
<Text style={styles.textStyle}>
Signers : &nbsp;
<Text style={styles.textStyle2}>
{multiSigner.length}
</Text>
</Text>
)}
</View>
{isMultiSigners ? (
<View style={{ display: "flex", flexDirection: "column" }}>
<Text
style={{
fontSize: "16px",
fontWeight: "bold",
color: "#31bceb",
margin: "10px 0px 10px 0px"
}}
>
Recipients
</Text>
<View>
{multiSigner &&
multiSigner.map((data, ind) => {
return (
<View
key={ind}
style={{
display: "flex",
flexDirection: "column"
}}
>
<View
style={{
border: "0.4px solid #bdbbbb",
marginBottom: "10px"
}}
></View>
{signerName(data)}
<Text style={styles.textStyle}>
Accessed from : &nbsp;
<Text style={styles.textStyle2}>
{data.ipAddress}
</Text>
</Text>
</View>
);
})}
</View>
</View>
) : (
<View style={{ display: "flex", flexDirection: "column" }}>
<Text
style={{
fontSize: "16px",
fontWeight: "bold",
color: "#31bceb",
margin: "10px 0px 10px 0px"
}}
>
Recipients
</Text>
<Text style={styles.textStyle}>
Signers : &nbsp;<Text style={styles.textStyle2}>1</Text>
</Text>
<Text style={styles.textStyle}>
Name : &nbsp;
<Text style={styles.textStyle2}>
{pdfData[0].ExtUserPtr.Name}
</Text>
</Text>
<Text style={styles.textStyle}>
Email : &nbsp;
<Text style={styles.textStyle2}>
{pdfData[0].ExtUserPtr.Email}
</Text>
</Text>
<Text style={styles.textStyle}>
Accessed from : &nbsp;
<Text style={styles.textStyle2}>
{pdfData[0].AuditTrail &&
pdfData[0].AuditTrail[0].ipAddress}
</Text>
</Text>
<Text style={styles.textStyle}>
Signed on : &nbsp;{changeCompletedDate()}
</Text>
</View>
)}
</View>
</View>
</View>
</Page>
</Document>
)
);
}
export default Certificate;
@@ -0,0 +1,155 @@
import { useRef, useState } from "react";
import ModalUi from "../../primitives/ModalUi";
import { EmailBody } from "./EmailBody";
import {
contractDocument,
defaultMailBody,
defaultMailSubject,
sendEmailToSigners
} from "../../constant/Utils";
import { useTranslation } from "react-i18next";
import Loader from "../../primitives/Loader";
import { useNavigate } from "react-router";
function CustomizeMail(props) {
const { t } = useTranslation();
const navigate = useNavigate();
const copyUrlRef = useRef(null);
const [isCustomize, setIsCustomize] = useState(false);
const [isLoader, setIsLoader] = useState(false);
const handleCloseSendmailModal = () => {
if (props?.handleClose) {
props?.handleClose();
return;
}
props?.setIsMailModal(false);
navigate("/report/1MwEuxLEkF");
};
const handleOnchangeRequest = (value) => {
props?.setCustomizeMail((prev) => ({
...prev,
body: value
}));
};
const handleEmailSendToSigners = async () => {
setIsLoader(true);
const documentData = await contractDocument(props?.documentId);
if (documentData && documentData?.length > 0) {
props?.setDocumentDetails && props?.setDocumentDetails(documentData[0]);
if (
documentData?.[0]?.SendinOrder &&
documentData?.[0]?.SendinOrder === true
) {
const ownerEmail = documentData[0].ExtUserPtr.Email;
const ownerDetails = documentData[0].Signers.find(
(x) => x.Email === ownerEmail
);
props?.setCurrUserId && props?.setCurrUserId(ownerDetails?.objectId);
}
//function is used to send email to signers for sign the document
const mailRes = await sendEmailToSigners(
documentData,
props?.signerList,
props?.customizeMail,
props?.defaultMail,
isCustomize,
);
props?.setIsMailModal(false);
props?.setIsSend(true);
setIsLoader(false);
if (mailRes?.status === "success") {
props?.setMailStatus("success");
} else if (mailRes?.status === "quota-reached") {
props?.setMailStatus("quotareached");
} else if (mailRes?.status === "daily-quota-reached") {
props?.setMailStatus("dailyquotareached");
} else {
props?.setMailStatus("failed");
}
// setMailStatus(mail_status);
} else {
alert("something-went-wrong-mssg");
}
};
return (
<>
{isLoader ? (
<div className="absolute w-full h-full flex justify-center items-center bg-black/30 rounded-box z-30">
<Loader />
</div>
) : (
<ModalUi
isOpen={props?.isMailModal}
title={t("send-mail")}
handleClose={() => handleCloseSendmailModal()}
>
<div className="max-h-96 overflow-y-scroll scroll-hide p-[20px] text-base-content">
{!isCustomize && <span>{t("placeholder-alert-3")}</span>}
{
isCustomize && (
<>
<EmailBody
requestBody={props?.customizeMail?.body}
requestSubject={props?.customizeMail?.subject}
handleOnchangeRequest={handleOnchangeRequest}
setCustomizeMail={props?.setCustomizeMail}
/>
<div
className="flex justify-end items-center gap-1 mt-2 op-link op-link-primary"
onClick={() => {
props?.setCustomizeMail({
subject: defaultMailSubject,
body: defaultMailBody
});
}}
>
<span>{t("reset-to-default")}</span>
</div>
</>
)
}
<div className="flex flex-row items-center gap-2 md:gap-6 mt-2">
<div className="flex flex-row gap-2">
<button
onClick={() => handleEmailSendToSigners()}
className="op-btn op-btn-primary font-[500] text-sm shadow"
>
{t("send")}
</button>
{isCustomize && (
<button
onClick={() => setIsCustomize(false)}
className="op-btn op-btn-ghost font-[500] text-sm"
>
{t("close")}
</button>
)}
</div>
{
!isCustomize && (
<span
className="op-link op-link-accent text-sm"
onClick={() => setIsCustomize(true)}
>
{t("customize-email")}
</span>
)
}
</div>
<div className="flex justify-center items-center mt-3">
<span className="h-[1px] w-[20%] bg-[#ccc]"></span>
<span className="ml-[5px] mr-[5px]">{t("or")}</span>
<span className="h-[1px] w-[20%] bg-[#ccc]"></span>
</div>
<div className="my-3">{props?.handleShareList()}</div>
<p id="copyUrl" ref={copyUrlRef} className="hidden"></p>
</div>
</ModalUi>
)}
</>
);
}
export default CustomizeMail;
@@ -1,42 +1,41 @@
import React, { useState } from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
function DefaultSignature(props) {
const { t } = useTranslation();
const defaultSignImg = useSelector((state) => state.widget.defaultSignImg);
const myInitial = useSelector((state) => state.widget.myInitial);
const tabName = ["my-signature", "my-initials"];
const [activeTab, setActiveTab] = useState(0);
const confirmToaddDefaultSign = (type) => {
if (!props.isAgree) {
props.setIsAgreeTour(true);
} else {
if (props?.xyPosition.length > 0) {
//check signature or initial widgets exist or not for auto signing
const getCurrentSignerXY = props?.xyPosition.filter(
(data) => data.Id === props.uniqueId
);
const checkIsSignInitialExist = getCurrentSignerXY?.every(
(placeholderObj) =>
placeholderObj?.placeHolder?.some((placeholder) =>
placeholder?.pos?.some((posItem) => posItem?.type === type)
)
);
if (checkIsSignInitialExist) {
props?.setDefaultSignAlert({
isShow: true,
alertMessage: t("default-sign-alert", { widgetsType: type }),
type: type
});
} else {
props?.setDefaultSignAlert({
isShow: true,
alertMessage: t("defaultSign-alert", { widgetsType: type })
});
}
if (props?.xyPosition.length > 0) {
//check signature or initial widgets exist or not for auto signing
const getCurrentSignerXY = props?.xyPosition.filter(
(data) => data.Id === props.uniqueId
);
const checkIsSignInitialExist = getCurrentSignerXY?.every(
(placeholderObj) =>
placeholderObj?.placeHolder?.some((placeholder) =>
placeholder?.pos?.some((posItem) => posItem?.type === type)
)
);
if (checkIsSignInitialExist) {
props?.setDefaultSignAlert({
isShow: true,
alertMessage: t("default-sign-alert", { widgetsType: type }),
type: type
});
} else {
props?.setDefaultSignAlert({
isShow: true,
alertMessage: t("please-select-position!")
alertMessage: t("defaultSign-alert", { widgetsType: type })
});
}
} else {
props?.setDefaultSignAlert({
isShow: true,
alertMessage: t("please-select-position!")
});
}
};
@@ -69,15 +68,15 @@ function DefaultSignature(props) {
<img
alt="signature"
className="w-full h-full object-contain"
src={props?.defaultSignImg}
src={defaultSignImg}
/>
) : (
activeTab === 1 &&
(props?.myInitial ? (
(myInitial ? (
<img
alt="signature"
className="w-full h-full object-contain"
src={props?.myInitial}
src={myInitial}
/>
) : (
<div className="flex justify-center items-center h-full">
@@ -97,7 +96,7 @@ function DefaultSignature(props) {
disabled={
activeTab === 0 && !props?.isDefault
? true
: activeTab === 1 && !props.myInitial
: activeTab === 1 && !myInitial
? true
: false
}
@@ -106,9 +105,7 @@ function DefaultSignature(props) {
</button>
{!props.isDefault && (
<div className="absolute bg-black/70 text-white w-full h-full flex items-center justify-center text-[11px] cursor-default">
<span className="-rotate-45">
Document creator has disabled this option
</span>
<span className="-rotate-45">{t("option-disabled-by-owner")}</span>
</div>
)}
</div>
@@ -30,15 +30,19 @@ function DraftDocument() {
documentData === "Error: Something went wrong!" ||
(documentData.result && documentData.result.error)
) {
setIsLoading({
isLoader: false,
message: "Error: Something went wrong!"
});
if (documentData?.result?.error?.includes("deleted")) {
setIsLoading({
isLoader: false,
message: t("document-deleted")
});
} else {
setIsLoading({
isLoader: false,
message: t("something-went-wrong-mssg")
});
}
} else {
setIsLoading({
isLoader: false,
message: "No data found!"
});
setIsLoading({ isLoader: false, message: t("no-data") });
}
};
@@ -1,5 +1,5 @@
import React from "react";
import { getWidgetType, widgets } from "../../constant/Utils";
import { widgets } from "../../constant/Utils";
import getWidgetType from "./getWidgetType";
function DragElement(item) {
const getWidgets = widgets;
@@ -0,0 +1,77 @@
import { useDragLayer } from "react-dnd";
import { defaultWidthHeight } from "../../constant/Utils";
import { useEffect, useRef } from "react";
function DragGuideLinesLayer(props) {
const { showGuidelines } = props;
const prevDragState = useRef({ isDragging: false, x: 0, y: 0 });
const { isDraggingWidget, offset, itemType } = useDragLayer((monitor) => ({
isDraggingWidget: monitor.isDragging(),
offset: monitor.getClientOffset(),
itemType: monitor.getItem()
}));
useEffect(() => {
// Hide guidelines when dragging stops
if (!isDraggingWidget) {
showGuidelines(false);
return;
}
if (!offset) return;
//getting container by id
const container = document.getElementById("container");
if (!container) return;
if (itemType?.text) {
// Calculate the position of the dragged element relative to the container
// Get the container's bounding rectangle (position and size in the viewport)
const containerRect = container.getBoundingClientRect();
// Compute the X and Y coordinates of the dragged item inside the container
// by subtracting the container's top-left offset from the current drag offset
const x = offset.x - containerRect.left;
const y = offset.y - containerRect.top;
// Check if theres any change in the dragging state or the dragged elements position
// Compare the current drag state with the previously stored one
const hasStateChanged =
prevDragState.current.isDragging !== isDraggingWidget || // Drag started or stopped
prevDragState.current.x !== x || // X position changed
prevDragState.current.y !== y; // Y position changed
if (hasStateChanged) {
// Update the previous drag state with the latest values
prevDragState.current = { isDragging: isDraggingWidget, x, y };
if (isDraggingWidget) {
// Adjust the current drag coordinates relative to the initial widgets button position
const getXPosition = props?.signBtnPosition?.[0]
? x - props.signBtnPosition[0].xPos
: x;
const getYPosition = props?.signBtnPosition?.[0]
? y - props.signBtnPosition[0].yPos
: y;
// Get the default width and height of the dragged checkbox/radio widget type
const el = document.getElementById("checkbox&radio-preview");
let rect;
if (el) {
rect = el.getBoundingClientRect();
}
const widget = {
type: itemType?.text,
width: defaultWidthHeight(itemType?.text).width,
height: defaultWidthHeight(itemType?.text).height
};
const getWidth =
rect?.width || props?.posWidth(widget, props?.isSignYourself);
const getHeight =
rect?.height || props?.posHeight(widget, props?.isSignYourself);
// Display the alignment guidelines based on the current drag position and widget size
showGuidelines(true, getXPosition, getYPosition, getWidth, getHeight);
}
}
}
}, [isDraggingWidget, offset, itemType, showGuidelines]);
}
export default DragGuideLinesLayer;
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import ModalUi from "../../primitives/ModalUi";
import { radioButtonWidget } from "../../constant/Utils";
import { useTranslation } from "react-i18next";
@@ -7,34 +7,37 @@ import { fontColorArr, fontsizeArr } from "../../constant/Utils";
function DropdownWidgetOption(props) {
const { t } = useTranslation();
const [dropdownOptionList, setDropdownOptionList] = useState([
"option-1",
"option-2"
"Option-1",
"Option-2"
]);
const [minCount, setMinCount] = useState(0);
const [maxCount, setMaxCount] = useState(0);
const [dropdownName, setDropdownName] = useState(props.type);
const [dropdownName, setDropdownName] = useState();
const [isReadOnly, setIsReadOnly] = useState(false);
const [isHideLabel, setIsHideLabel] = useState(false);
const [status, setStatus] = useState("required");
const [defaultValue, setDefaultValue] = useState("");
const statusArr = ["required", "optional"];
const [defaultCheckbox, setDefaultCheckbox] = useState([]);
const [layout, setLayout] = useState("vertical");
const statusArr = ["required", "optional"];
const layoutArr = ["vertical", "horizontal"];
const isPrefillExist = props?.roleName === "prefill";
const resetState = () => {
setDropdownOptionList(["option-1", "option-2"]);
setDropdownName(props.type);
setDropdownOptionList(["Option-1", "Option-2"]);
setDropdownName(props.currWidgetsDetails?.options?.name || props.type);
setIsReadOnly(false);
setIsHideLabel(false);
setMinCount(0);
setMaxCount(0);
setDefaultCheckbox([]);
setDefaultValue("");
setLayout("vertical");
};
useEffect(() => {
if (
props.currWidgetsDetails?.options?.name &&
props.currWidgetsDetails?.options?.values
props.currWidgetsDetails?.options?.values?.length > 0
) {
setDropdownName(props.currWidgetsDetails?.options?.name);
setDropdownOptionList(props.currWidgetsDetails?.options?.values);
@@ -49,6 +52,7 @@ function DropdownWidgetOption(props) {
setStatus(props.currWidgetsDetails?.options?.status || "required");
setDefaultValue(props.currWidgetsDetails?.options?.defaultValue || "");
setDefaultCheckbox(props.currWidgetsDetails?.options?.defaultValue || []);
setLayout(props.currWidgetsDetails?.options?.layout || "vertical");
} else {
setStatus("required");
resetState();
@@ -84,7 +88,7 @@ function DropdownWidgetOption(props) {
const deleteOption = true;
const addOption = false;
const getUpdatedOptions = dropdownOptionList.filter(
(data, index) => index !== ind
(_, index) => index !== ind
);
setDropdownOptionList(getUpdatedOptions);
props.handleSaveWidgetsOptions(
@@ -99,11 +103,42 @@ function DropdownWidgetOption(props) {
};
const handleSaveOption = () => {
const defaultData =
defaultCheckbox && defaultCheckbox.length > 0
? defaultCheckbox
: defaultValue;
if (["checkbox", radioButtonWidget, "dropdown"].includes(props.type)) {
const allUnique =
new Set(dropdownOptionList).size === dropdownOptionList.length;
if (!allUnique) {
alert("Please remove duplicate option");
return;
}
}
const isDropdownOrRadio =
props?.type === "dropdown" || props?.type === radioButtonWidget;
const isCheckbox = props?.type === "checkbox";
const defaultData = isCheckbox ? defaultCheckbox : defaultValue;
const readOnlyWithoutValue =
isReadOnly && !defaultValue && status !== "optional";
const WidgetLayout = ["checkbox", radioButtonWidget].includes(props.type)
? layout
: null;
// If its a dropdown and its read-only without a value (nor marked optional), stop here.
if (isDropdownOrRadio && readOnlyWithoutValue) {
alert(t("readonly-error", { widgetName: props?.type }));
return;
} else if (
isCheckbox &&
isReadOnly &&
minCount > 0 &&
defaultCheckbox?.length === 0
) {
alert(t("readonly-error", { widgetName: props?.type }));
return;
}
// Otherwise (either not a dropdown, or a valid dropdown), do the save + reset exactly once.
props.handleSaveWidgetsOptions(
dropdownName,
dropdownOptionList,
@@ -114,30 +149,23 @@ function DropdownWidgetOption(props) {
null,
status,
defaultData,
isHideLabel
isHideLabel,
WidgetLayout
);
// props.setShowDropdown(false);
setDropdownOptionList(["option-1", "option-2"]);
setDropdownName(props.type);
// props.setCurrWidgetsDetails({});
setIsReadOnly(false);
setIsHideLabel(false);
setMinCount(0);
setMaxCount(0);
setDefaultCheckbox([]);
setDefaultValue("");
resetState();
};
const handleDefaultCheck = (index) => {
const getDefaultCheck = defaultCheckbox.includes(index);
if (getDefaultCheck) {
return true;
} else {
return false;
}
const handleSelectDefaultCheckbox = (e, index) => {
const checked = e.target.checked;
setDefaultCheckbox((prev) =>
checked
? prev.includes(index)
? prev
: [...prev, index]
: prev.filter((i) => i !== index)
);
};
return (
<ModalUi isOpen={props.showDropdown} title={props.title} showClose={false}>
<div className="h-full p-[15px] text-base-content">
@@ -148,25 +176,25 @@ function DropdownWidgetOption(props) {
}}
>
<div>
<label className="text-[13px] font-semibold">
<label htmlFor="title" className="text-[13px] font-semibold">
{t("name")}
<span className="text-[red] text-[13px]"> *</span>
</label>
<input
id="title"
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
required
defaultValue={dropdownName}
value={dropdownName}
onChange={(e) => setDropdownName(e.target.value)}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
required
/>
<label className="text-[13px] font-semibold mt-[5px]">
{t("options")}
</label>
<div className="flex flex-col">
{dropdownOptionList.map((option, index) => (
{dropdownOptionList?.map((option, index) => (
<div
key={index}
className="flex flex-row mb-[5px] items-center"
@@ -174,21 +202,8 @@ function DropdownWidgetOption(props) {
{props.type === "checkbox" && props.isShowAdvanceFeature && (
<input
type="checkbox"
checked={handleDefaultCheck(index)}
onChange={(e) => {
if (e.target.checked) {
const getDefaultCheck =
defaultCheckbox?.includes(index);
if (!getDefaultCheck) {
setDefaultCheckbox((prev) => [...prev, index]);
}
} else {
const removeOption = defaultCheckbox.filter(
(data) => data !== index
);
setDefaultCheckbox(removeOption);
}
}}
checked={defaultCheckbox?.includes(index)}
onChange={(e) => handleSelectDefaultCheckbox(e, index)}
className="op-checkbox focus:outline-none hover:border-base-content mr-[5px]"
/>
)}
@@ -210,58 +225,60 @@ function DropdownWidgetOption(props) {
></i>
</div>
))}
<i
onClick={handleAddInput}
className="fa-light fa-square-plus text-[25px] ml-[10px] op-text-primary cursor-pointer"
></i>
<div>
<i
className="fa-light fa-square-plus text-[25px] ml-[10px] op-text-primary cursor-pointer"
aria-label="Add option"
onClick={handleAddInput}
></i>
</div>
</div>
{["dropdown", radioButtonWidget].includes(props.type) && (
<>
<label className="text-[13px] font-semibold mt-[5px]">
{t("default-value")}
</label>
<select
onChange={(e) => setDefaultValue(e.target.value)}
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content w-full text-xs"
name="defaultvalue"
value={defaultValue}
placeholder="select default value"
>
<option value="" disabled hidden className="text-[13px]">
{t("select")}...
</option>
{dropdownOptionList.map((data, ind) => {
return (
<option className="text-[13px]" key={ind} value={data}>
{data}
</option>
);
})}
</select>
</>
)}
{props.type !== "checkbox" && props.type !== radioButtonWidget && (
<>
<div className="flex flex-row gap-[10px] mt-[0.5rem]">
{statusArr.map((data, ind) => {
return (
<div
key={ind}
className="flex flex-row gap-[5px] items-center"
>
<input
className="op-radio op-radio-xs my-1"
type="radio"
name="status"
onChange={() => setStatus(data.toLowerCase())}
checked={status.toLowerCase() === data.toLowerCase()}
/>
<div className="text-[13px] font-500">{data}</div>
</div>
);
})}
</div>
</>
{["dropdown", radioButtonWidget].includes(props.type) &&
!isPrefillExist && (
<>
<label className="text-[13px] font-semibold mt-[5px]">
{t("default-value")}
</label>
<select
value={defaultValue}
onChange={(e) => setDefaultValue(e.target.value)}
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content w-full text-xs"
name="defaultvalue"
>
<option value="" disabled hidden className="text-[13px]">
{t("select")}...
</option>
{dropdownOptionList.map((data, ind) => {
return (
<option className="text-[13px]" key={ind} value={data}>
{data}
</option>
);
})}
</select>
</>
)}
{((props.type !== "checkbox" && !isPrefillExist) ||
isPrefillExist) && (
<div className="flex flex-row gap-[10px] mt-[0.5rem]">
{statusArr.map((data, ind) => (
<div
key={ind}
className="flex flex-row gap-[5px] items-center"
>
<input
className="op-radio op-radio-xs my-1"
type="radio"
name="status"
onChange={() => setStatus(data.toLowerCase())}
checked={status.toLowerCase() === data.toLowerCase()}
/>
<div className="text-[13px] font-500 capitalize">
{data}
</div>
</div>
))}
</div>
)}
<div className="flex items-center mt-3 mb-3">
<span>{t("font-size")} :</span>
@@ -282,8 +299,8 @@ function DropdownWidgetOption(props) {
);
})}
</select>
<div className="flex flex-row gap-1 items-center ml-4 ">
<span>{t("color")} : </span>
<div className="flex flex-row gap-1 items-center ml-4">
<span className="capitalize">{t("color")} : </span>
<select
value={
props.fontColor ||
@@ -316,7 +333,7 @@ function DropdownWidgetOption(props) {
props.type
) && (
<div className="flex flex-row gap-5 my-2 items-center text-center">
{props.isShowAdvanceFeature && (
{props.isShowAdvanceFeature && !isPrefillExist && (
<div className="flex items-center">
<input
id="isreadonly"
@@ -325,7 +342,10 @@ function DropdownWidgetOption(props) {
className="op-checkbox op-checkbox-sm"
onChange={(e) => setIsReadOnly(e.target.checked)}
/>
<label className="ml-1 mb-0" htmlFor="isreadonly">
<label
className="ml-2 mb-0 capitalize"
htmlFor="isreadonly"
>
{t("read-only")}
</label>
</div>
@@ -340,15 +360,45 @@ function DropdownWidgetOption(props) {
onChange={(e) => setIsHideLabel(e.target.checked)}
/>
<label className="ml-1 mb-0" htmlFor="ishidelabel">
<label
className="ml-2 mb-0 capitalize"
htmlFor="ishidelabel"
>
{t("hide-labels")}
</label>
</div>
)}
</div>
)}
{["checkbox", radioButtonWidget].includes(props.type) && (
<>
<div className="text-[13px] font-semibold mt-[5px] capitalize">
{t("layout")}
</div>
<div
className={`${props.type === "checkbox" ? "mb-[10px]" : ""} flex flex-row gap-[10px] mt-[0.5rem]`}
>
{layoutArr.map((data, ind) => (
<div
key={ind}
className="flex flex-row gap-[5px] items-center"
>
<input
className="op-radio op-radio-xs my-1"
type="radio"
name="layout"
checked={layout.toLowerCase() === data.toLowerCase()}
onChange={() => setLayout(data.toLowerCase())}
/>
<label className="text-[13px] font-500 mb-0">
{t(data)}
</label>
</div>
))}
</div>
</>
)}
</div>
<div
className={`${
props.type === "checkbox" && props.isShowAdvanceFeature
@@ -367,7 +417,7 @@ function DropdownWidgetOption(props) {
{props.currWidgetsDetails?.options?.values?.length > 0 && (
<button
type="submit"
className="op-btn op-btn-ghost ml-1"
className="op-btn op-btn-ghost text-base-content ml-1"
onClick={() => {
props.handleClose && props.handleClose();
resetState();
@@ -1,324 +0,0 @@
import React, {
useState,
} from "react";
import {
getFileName
} from "../../constant/Utils";
import { useTranslation } from "react-i18next";
import { Tooltip } from "react-tooltip";
import SignersInput from "../shared/fields/SignersInput";
const EditTemplate = ({
template,
onSuccess,
}) => {
const { t } = useTranslation();
const [formData, setFormData] = useState({
Name: template?.Name || "",
Note: template?.Note || "",
Description: template?.Description || "",
SendinOrder: template?.SendinOrder ? `${template?.SendinOrder}` : "false",
AutomaticReminders: template?.AutomaticReminders || false,
RemindOnceInEvery: template?.RemindOnceInEvery || 5,
IsEnableOTP: template?.IsEnableOTP ? `${template?.IsEnableOTP}` : "false",
IsTourEnabled: template?.IsTourEnabled
? `${template?.IsTourEnabled}`
: "false",
NotifyOnSignatures:
template?.NotifyOnSignatures !== undefined
? template?.NotifyOnSignatures
: false,
Bcc: template?.Bcc,
RedirectUrl: template?.RedirectUrl || "",
AllowModifications: template?.AllowModifications || false,
TimeToCompleteDays: template?.TimeToCompleteDays || 15
});
// `isValidURL` is used to check valid webhook url
function isValidURL(value) {
try {
const url = new URL(value);
return url.protocol === "https:" || url.protocol === "http:";
} catch (error) {
return false;
}
}
const handleStrInput = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
// Define a function to handle form submission
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
if (formData.RedirectUrl && !isValidURL(formData?.RedirectUrl)) {
alert(t("invalid-redirect-url"));
return;
}
const isChecked = formData.SendinOrder === "true" ? true : false;
const isTourEnabled = formData?.IsTourEnabled === "false" ? false : true;
const AutoReminder = formData?.AutomaticReminders || false;
const IsEnableOTP = formData.IsEnableOTP === "true" ? true : false;
const allowModify = formData?.AllowModifications || false;
let reminderDate = {};
if (AutoReminder) {
const RemindOnceInEvery = parseInt(formData?.RemindOnceInEvery);
const ReminderDate = new Date(template?.createdAt);
ReminderDate.setDate(ReminderDate.getDate() + RemindOnceInEvery);
reminderDate = { NextReminderDate: ReminderDate };
}
const data = {
...formData,
SendinOrder: isChecked,
IsEnableOTP: IsEnableOTP,
IsTourEnabled: isTourEnabled,
AllowModifications: allowModify,
...reminderDate
};
onSuccess(data);
};
// `handleNotifySignChange` is trigger when user change radio of notify on signatures
const handleNotifySignChange = (value) => {
setFormData((obj) => ({ ...obj, NotifyOnSignatures: value }));
};
const handleBcc = (data) => {
if (data && data.length > 0) {
const trimEmail = data.map((item) => ({
objectId: item?.value,
Name: item?.label,
Email: item?.email
}));
setFormData((prev) => ({ ...prev, Bcc: trimEmail }));
}
};
return (
<div className="max-h-[300px] md:max-h-[400px] overflow-y-scroll p-[10px]">
<div className="text-base-content">
<form onSubmit={handleSubmit}>
<div className="mb-[0.35rem]">
<label htmlFor="name" className="text-[13px]">
{t("report-heading.File")}
</label>
<div className="op-input op-input-bordered op-input-sm focus:outline-none py-2 font-semibold w-full text-xs">
{getFileName(template.URL)}
</div>
</div>
<div className="mb-[0.35rem]">
<label htmlFor="name" className="text-[13px]">
{t("Title")}
<span className="text-[13px] text-[red]"> *</span>
</label>
<input
type="text"
name="Name"
value={formData.Name}
onChange={(e) => handleStrInput(e)}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
required
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-[0.35rem]">
<label htmlFor="Note" className="text-[13px]">
{t("report-heading.Note")}
</label>
<input
type="text"
name="Note"
id="Note"
value={formData.Note}
onChange={(e) => handleStrInput(e)}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-[0.35rem]">
<label htmlFor="Description" className="text-[13px]">
{t("description")}
</label>
<input
type="text"
name="Description"
id="Description"
value={formData.Description}
onChange={(e) => handleStrInput(e)}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-[0.35rem]">
<label className="text-[13px]">{t("send-in-order")}</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
<input
type="radio"
value={"true"}
className="op-radio op-radio-xs"
name="SendinOrder"
checked={formData.SendinOrder === "true"}
onChange={handleStrInput}
/>
<div className="text-[12px]">{t("yes")}</div>
</div>
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
<input
type="radio"
value={"false"}
name="SendinOrder"
className="op-radio op-radio-xs"
checked={formData.SendinOrder === "false"}
onChange={handleStrInput}
/>
<div className="text-[12px]">{t("no")}</div>
</div>
</div>
</div>
<div className="text-xs mt-3">
<label className="block">
<span>
{t("enable-tour")}
<a data-tooltip-id="istourenabled-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>{" "}
</span>
<Tooltip id="istourenabled-tooltip" className="z-50">
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">{t("enable-tour")}</p>
<p className="p-[5px]">
<ol className="list-disc">
<li>
<span className="font-bold">{t("yes")}: </span>
<span>{t("istourenabled-help.p1")}</span>
</li>
<li>
<span className="font-bold">{t("no")}: </span>
<span>{t("istourenabled-help.p2")}</span>
</li>
</ol>
</p>
<p>{t("istourenabled-help.p3")}</p>
</div>
</Tooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={"true"}
className="op-radio op-radio-xs"
name="IsTourEnabled"
checked={formData.IsTourEnabled === "true"}
onChange={handleStrInput}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={"false"}
name="IsTourEnabled"
className="op-radio op-radio-xs"
checked={formData.IsTourEnabled === "false"}
onChange={handleStrInput}
/>
<div className="text-center">{t("no")}</div>
</div>
</div>
</div>
<div className="text-xs mt-3">
<label>
{t("notify-on-signatures")}
<a data-tooltip-id="nos-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>{" "}
<Tooltip id="nos-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px] text-[11px]">
<p className="font-bold">{t("notify-on-signatures")}</p>
<p>{t("notify-on-signatures-help.p1")}</p>
<p>{t("notify-on-signatures-help.note")}</p>
</div>
</Tooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div
className={
`flex items-center gap-2 ml-2 mb-1`
}
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(true)}
checked={formData.NotifyOnSignatures === true}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div
className={
`flex items-center gap-2 ml-2 mb-1`
}
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(false)}
checked={formData.NotifyOnSignatures === false}
/>
<div className="text-center">{t("no")}</div>
</div>
</div>
</div>
<div className="text-xs mt-3">
<SignersInput
label={t("Bcc")}
initialData={template?.Bcc}
onChange={handleBcc}
helptextZindex={50}
helpText={t("bcc-help")}
isCaptureAllData
/>
</div>
<div className="text-xs mt-2">
<label className="block">Redirect Url</label>
<input
name="RedirectUrl"
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
value={formData.RedirectUrl}
onChange={handleStrInput}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
/>
</div>
<div className="text-xs mt-2">
<label className="block">
{t("time-to-complete")}
<span className="text-red-500 text-[13px]">*</span>
</label>
<input
type="number"
name="TimeToCompleteDays"
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
value={formData.TimeToCompleteDays}
onChange={(e) => handleStrInput(e)}
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
required
/>
</div>
<div className="mt-[1rem] flex justify-start">
<button type="submit" className="op-btn op-btn-primary">
{t("submit")}
</button>
</div>
</form>
</div>
</div>
);
};
export default EditTemplate;
@@ -0,0 +1,596 @@
import {
useState,
useRef,
} from "react";
import {
base64ToArrayBuffer,
convertBase64ToFile,
generatePdfName,
getFileName
} from "../../constant/Utils";
import {
maxDescriptionLength,
maxNoteLength,
maxTitleLength
} from "../../constant/const";
import { useTranslation } from "react-i18next";
import { Tooltip } from "react-tooltip";
import SignersInput from "../shared/fields/SignersInput";
import { PDFDocument } from "pdf-lib";
import ModalUi from "../../primitives/ModalUi";
import { SaveFileSize } from "../../constant/saveFileSize";
const EditTemplate = ({
title,
handleClose,
pdfbase64,
template,
onSuccess,
setPdfArrayBuffer,
setPdfBase64Url,
isAddYourSelfCheckbox,
}) => {
const appName =
"OpenSign™";
const { t } = useTranslation();
const inputFileRef = useRef(null);
const [formData, setFormData] = useState({
Name: template?.Name || "",
Note: template?.Note || "",
Description: template?.Description || "",
SendinOrder: template?.SendinOrder ? `${template?.SendinOrder}` : "false",
AutomaticReminders: template?.AutomaticReminders || false,
RemindOnceInEvery: template?.RemindOnceInEvery || 5,
IsEnableOTP: template?.IsEnableOTP ? `${template?.IsEnableOTP}` : "false",
IsTourEnabled: template?.IsTourEnabled
? `${template?.IsTourEnabled}`
: "false",
NotifyOnSignatures:
template?.NotifyOnSignatures !== undefined
? template?.NotifyOnSignatures
: false,
Bcc: template?.Bcc,
RedirectUrl: template?.RedirectUrl || "",
AllowModifications: template?.AllowModifications || false,
TimeToCompleteDays: template?.TimeToCompleteDays || 15,
});
const pensList = ["blue", "red", "black"];
const [selectedColors, setSelectedColors] = useState(
template?.PenColors || pensList
);
const [isUpdate, setIsUpdate] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [uploadPdf, setUploadPdf] = useState({
name: "",
base64: "",
url: ""
});
const handleDrop = (e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
handleFile(file);
};
const handleFile = (file) => {
if (file && file.type === "application/pdf") {
handleReplaceFileValdition(file);
// You can handle the file here
} else {
alert(t("only-pdf-allowed"));
if (inputFileRef.current) inputFileRef.current.value = "";
}
};
const handleDragOver = (e) => {
e.preventDefault();
};
// `isValidURL` is used to check valid webhook url
function isValidURL(value) {
try {
const url = new URL(value);
return url.protocol === "https:" || url.protocol === "http:";
} catch (error) {
return false;
}
}
const handleStrInput = (e) => {
setIsUpdate(true);
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const getPdfMetadataHash = async (pdfBytes) => {
const pdfDoc = await PDFDocument.load(pdfBytes);
const pages = pdfDoc.getPages();
const metaString = pages
.map((page, index) => {
const { width, height } = page.getSize();
return `${index + 1}:${Math.round(width)}x${Math.round(height)}`;
})
.join("|");
const encoder = new TextEncoder();
const data = encoder.encode(metaString);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
return Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};
const handleFileInput = async (e) => {
const file = e.target.files[0];
if (!file) return;
handleReplaceFileValdition(file);
};
const handleReplaceFileValdition = async (file) => {
try {
const basePdfBytes = base64ToArrayBuffer(pdfbase64);
const expectedHash = await getPdfMetadataHash(basePdfBytes);
const fileReader = new FileReader();
fileReader.onload = async (event) => {
const uploadedPdfBytes = event.target.result;
const uploadedHash = await getPdfMetadataHash(uploadedPdfBytes);
if (expectedHash === uploadedHash) {
const arrayBuffer = uploadedPdfBytes;
const uint8Array = new Uint8Array(arrayBuffer);
const binaryString = Array.from(uint8Array)
.map((b) => String.fromCharCode(b))
.join("");
const base64 = btoa(binaryString);
const pdfName = generatePdfName(16);
setIsUpdate(true);
setUploadPdf((prev) => ({ ...prev, name: pdfName, base64: base64 }));
// alert("✅ PDFs match (based on page number, width, height)");
} else {
alert("❌ PDF do NOT match based on page number, width, height");
if (inputFileRef.current) inputFileRef.current.value = "";
}
};
fileReader.readAsArrayBuffer(file);
} catch (err) {
alert("Error: " + err.message);
if (inputFileRef.current) inputFileRef.current.value = "";
}
};
// Define a function to handle form submission
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
if (formData.RedirectUrl && !isValidURL(formData?.RedirectUrl)) {
alert(t("invalid-redirect-url"));
return;
}
if (formData?.Name?.length > maxTitleLength) {
alert(t("title-length-alert"));
return;
}
if (formData?.Note?.length > maxNoteLength) {
alert(t("note-length-alert"));
return;
}
if (formData?.Description?.length > maxDescriptionLength) {
alert(t("description-length-alert"));
return;
}
let pdfUrl;
if (uploadPdf?.base64) {
pdfUrl = await convertBase64ToFile(
uploadPdf.name,
uploadPdf.base64,
);
setUploadPdf((prev) => ({ ...prev, url: pdfUrl }));
const pdfBuffer = base64ToArrayBuffer(uploadPdf.base64);
setPdfArrayBuffer && setPdfArrayBuffer(pdfBuffer);
setPdfBase64Url && setPdfBase64Url(uploadPdf.base64);
const tenantId =
localStorage.getItem("TenantId") ||
template?.ExtUserPtr?.TenantId?.objectId;
const buffer = atob(uploadPdf.base64);
const userId = template?.ExtUserPtr?.UserId?.objectId;
SaveFileSize(buffer.length, pdfUrl, tenantId, userId);
}
const isChecked = formData.SendinOrder === "true" ? true : false;
const isTourEnabled = formData?.IsTourEnabled === "false" ? false : true;
const AutoReminder = formData?.AutomaticReminders || false;
const IsEnableOTP = formData.IsEnableOTP === "true" ? true : false;
const allowModify = formData?.AllowModifications || false;
let reminderDate = {};
const remindOnceInEvery = formData?.RemindOnceInEvery;
const TimeToCompleteDays = parseInt(formData?.TimeToCompleteDays);
const reminderCount = TimeToCompleteDays / remindOnceInEvery;
if (AutoReminder && reminderCount > 15) {
alert(t("only-15-reminder-allowed"));
return;
}
if (AutoReminder) {
const RemindOnceInEvery = parseInt(formData?.RemindOnceInEvery);
const ReminderDate = new Date(template?.createdAt);
ReminderDate.setDate(ReminderDate.getDate() + RemindOnceInEvery);
reminderDate = { NextReminderDate: ReminderDate };
}
const data = {
...formData,
...(pdfUrl ? { URL: pdfUrl } : {}),
SendinOrder: isChecked,
IsEnableOTP: IsEnableOTP,
IsTourEnabled: isTourEnabled,
AllowModifications: allowModify,
PenColors: selectedColors,
...reminderDate
};
onSuccess(data);
};
// `handleNotifySignChange` is trigger when user change radio of notify on signatures
const handleNotifySignChange = (value) => {
setIsUpdate(true);
setFormData((obj) => ({ ...obj, NotifyOnSignatures: value }));
};
const handleBcc = (data) => {
if (data && data.length > 0) {
const trimEmail = data.map((item) => ({
objectId: item?.value,
Name: item?.label,
Email: item?.email
}));
setIsUpdate(true);
setFormData((prev) => ({ ...prev, Bcc: trimEmail }));
}
};
const handleEditTemplateClose = () => {
if (isUpdate) {
setShowConfirm(true);
} else {
handleClose();
}
};
const discardChanges = () => {
setShowConfirm(false);
handleClose();
};
const handleColorsChange = (color) => {
setSelectedColors((prev) => {
// If user tries to uncheck the last remaining color → block it
if (prev.length === 1 && prev.includes(color)) {
return prev;
}
// Normal toggle behavior
return prev.includes(color)
? prev.filter((c) => c !== color) // remove
: [...prev, color]; // add
});
};
return (
<ModalUi
isOpen
title={isUpdate ? `${title} (unsaved)` : title}
handleClose={handleEditTemplateClose}
>
<ModalUi isOpen={showConfirm} showClose={false}>
<div className="p-[20px]">
<p className="text-base font-normal text-base-content py-[5px] md:py-[6px] px-[5px]">
{t("unsaved-changes-discard-them?")}
</p>
<div className="flex items-center mt-2.5 gap-2 md:gap-3 text-white">
<button
className="op-btn op-btn-primary px-6"
onClick={discardChanges}
>
{t("yes-discard")}
</button>
<button
className="op-btn op-btn-secondary px-4 md:px-6"
onClick={() => setShowConfirm(false)}
>
{t("cancel")}
</button>
</div>
</div>
</ModalUi>
<div className="max-h-[300px] md:max-h-[400px] overflow-y-scroll p-[10px]">
<div className="text-base-content">
<form onSubmit={handleSubmit}>
<div className="mb-[0.35rem]">
<label htmlFor="name" className="text-[13px]">
{t("report-heading.File")}
</label>
<div
className="border-[1.5px] border-dashed border-gray-300 rounded-lg px-4 py-6 text-center text-gray-500 bg-white cursor-pointer hover:border-base-content transition"
onDrop={handleDrop}
onDragOver={handleDragOver}
onClick={() => inputFileRef?.current?.click()}
>
<label
htmlFor="fileUpload"
className="cursor-pointer text-center mb-0"
>
{t("browse-or-drag-to-replace-existing-file")}
</label>
</div>
<input
ref={inputFileRef}
type="file"
className="hidden"
accept="application/pdf"
onChange={(e) => handleFileInput(e)}
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
onInput={(e) => e.target.setCustomValidity("")}
/>
{uploadPdf?.name && (
<div
onClick={() => inputFileRef?.current?.click()}
className="mt-2 cursor-pointer op-input op-input-bordered op-input-sm focus:outline-none py-2 font-semibold w-full text-xs"
>
selected:{" "}
{uploadPdf?.url
? `${uploadPdf?.name}.pdf`
: getFileName(template.URL)}
</div>
)}
</div>
<div className="mb-[0.35rem]">
<label htmlFor="name" className="text-[13px]">
{t("Title")}
<span className="text-[13px] text-[red]"> *</span>
</label>
<input
type="text"
name="Name"
value={formData.Name}
onChange={(e) => handleStrInput(e)}
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
onInput={(e) => e.target.setCustomValidity("")}
required
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-[0.35rem]">
<label htmlFor="Note" className="text-[13px]">
{t("report-heading.Note")}
</label>
<input
type="text"
name="Note"
id="Note"
value={formData.Note}
onChange={(e) => handleStrInput(e)}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-[0.35rem]">
<label htmlFor="Description" className="text-[13px]">
{t("description")}
</label>
<input
type="text"
name="Description"
id="Description"
value={formData.Description}
onChange={(e) => handleStrInput(e)}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
</div>
<div className="mb-[0.35rem]">
<label className="text-[13px]">{t("send-in-order")}</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
<input
type="radio"
value={"true"}
className="op-radio op-radio-xs"
name="SendinOrder"
checked={formData.SendinOrder === "true"}
onChange={handleStrInput}
/>
<div className="text-[12px]">{t("yes")}</div>
</div>
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
<input
type="radio"
value={"false"}
name="SendinOrder"
className="op-radio op-radio-xs"
checked={formData.SendinOrder === "false"}
onChange={handleStrInput}
/>
<div className="text-[12px]">{t("no")}</div>
</div>
</div>
</div>
<div className="text-xs mt-3">
<label className="block">
<span>
{t("enable-tour")}
<a data-tooltip-id="istourenabled-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>{" "}
</span>
<Tooltip id="istourenabled-tooltip" className="z-50">
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">{t("enable-tour")}</p>
<div className="p-[5px]">
<ol className="list-disc">
<li>
<span className="font-bold">{t("yes")}: </span>
<span>{t("istourenabled-help.p1")}</span>
</li>
<li>
<span className="font-bold">{t("no")}: </span>
<span>{t("istourenabled-help.p2")}</span>
</li>
</ol>
</div>
<p>{t("istourenabled-help.p3", { appName: appName })}</p>
</div>
</Tooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={"true"}
className="op-radio op-radio-xs"
name="IsTourEnabled"
checked={formData.IsTourEnabled === "true"}
onChange={handleStrInput}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div className="flex items-center gap-2 ml-2 mb-1">
<input
type="radio"
value={"false"}
name="IsTourEnabled"
className="op-radio op-radio-xs"
checked={formData.IsTourEnabled === "false"}
onChange={handleStrInput}
/>
<div className="text-center">{t("no")}</div>
</div>
</div>
</div>
<div className="text-xs mt-3">
<label>
{t("notify-on-signatures")}
<a data-tooltip-id="nos-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>{" "}
<Tooltip id="nos-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px] text-[11px]">
<p className="font-bold">{t("notify-on-signatures")}</p>
<p>{t("notify-on-signatures-help.p1")}</p>
<p>{t("notify-on-signatures-help.note")}</p>
</div>
</Tooltip>
</label>
<div className="flex flex-col md:flex-row md:gap-4">
<div
className={
`flex items-center gap-2 ml-2 mb-1`
}
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(true)}
checked={formData.NotifyOnSignatures === true}
/>
<div className="text-center">{t("yes")}</div>
</div>
<div
className={
`flex items-center gap-2 ml-2 mb-1`
}
>
<input
className="mr-[2px] op-radio op-radio-xs"
type="radio"
onChange={() => handleNotifySignChange(false)}
checked={formData.NotifyOnSignatures === false}
/>
<div className="text-center">{t("no")}</div>
</div>
</div>
</div>
<div className="text-xs mt-3 mb-4">
<label htmlFor="penColors">
{t("pen-colors")}
<a data-tooltip-id="pen-colors-tooltip" className="ml-1">
<sup>
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
</sup>
</a>
<Tooltip id="pen-colors-tooltip" className="z-[999]">
<div className="max-w-[200px] md:max-w-[450px]">
<p className="font-bold">{t("pen-colors")}</p>
<div>{t("pen-colors-help")}</div>
</div>
</Tooltip>
</label>
<div className="ml-[7px] flex flex-col md:flex-row gap-[10px] mb-[0.7rem]">
{pensList.map((color) => (
<div
key={color}
className="flex flex-row gap-[5px] items-center"
>
<input
className="mr-[2px] op-checkbox op-checkbox-xs"
type="checkbox"
name="penColors"
checked={selectedColors.includes(color)}
onChange={() => handleColorsChange(color)}
/>
<div className="hover:underline underline-offset-2 cursor-default capitalize">
{color}
</div>
</div>
))}
</div>
</div>
<div className="text-xs mt-3">
<SignersInput
label={t("Bcc")}
initialData={template?.Bcc}
onChange={handleBcc}
helptextZindex={50}
helpText={t("bcc-help")}
isCaptureAllData
isAddYourSelfCheckbox={isAddYourSelfCheckbox}
/>
</div>
<div className="text-xs mt-2">
<label className="block">{t("redirect-url")}</label>
<input
name="RedirectUrl"
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
value={formData.RedirectUrl}
onChange={handleStrInput}
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
onInput={(e) => e.target.setCustomValidity("")}
/>
</div>
<div className="text-xs mt-2">
<label className="block">
{t("time-to-complete")}
<span className="text-red-500 text-[13px]">*</span>
</label>
<input
type="number"
name="TimeToCompleteDays"
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
value={formData.TimeToCompleteDays}
onChange={(e) => handleStrInput(e)}
onInvalid={(e) =>
e.target.setCustomValidity(t("input-required"))
}
onInput={(e) => e.target.setCustomValidity("")}
min={1}
required
/>
</div>
<div className="mt-[1rem] flex justify-start">
<button type="submit" className="op-btn op-btn-primary">
{t("submit")}
</button>
</div>
</form>
</div>
</div>
</ModalUi>
);
};
export default EditTemplate;
@@ -1,5 +1,9 @@
import React from "react";
import { Quill } from "react-quill-new";
// Use a third-party plugin so editors can toggle raw HTML view.
import htmlEditButton from "quill-html-edit-button";
// Register the module which adds a "<>" button for HTML editing.
Quill.register("modules/htmlEditButton", htmlEditButton);
// Custom Undo button icon component for Quill editor. You can import it directly
// from 'quill/assets/icons/undo.svg' but I found that a number of loaders do not
@@ -56,7 +60,9 @@ export const module1 = {
container: "#toolbar1",
handlers: { undo: undoChange, redo: redoChange }
},
history: { delay: 500, maxStack: 100, userOnly: true }
history: { delay: 500, maxStack: 100, userOnly: true },
// Enable the "<>" button registered above
htmlEditButton: {}
};
// Modules object for setting up the Quill editor
@@ -65,7 +71,9 @@ export const module2 = {
container: "#toolbar2",
handlers: { undo: undoChange, redo: redoChange }
},
history: { delay: 500, maxStack: 100, userOnly: true }
history: { delay: 500, maxStack: 100, userOnly: true },
// Enable the "<>" button registered above
htmlEditButton: {}
};
// Formats objects for setting up the Quill editor
@@ -1,4 +1,3 @@
import React from "react";
import Tooltip from "../../primitives/Tooltip";
import ReactQuill from "react-quill-new";
import "../../styles/quill.css";
@@ -18,7 +17,12 @@ export function EmailBody(props) {
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
onInput={(e) => e.target.setCustomValidity("")}
value={props.requestSubject}
onChange={(e) => props.setRequestSubject(e.target.value)}
onChange={(e) => {
props?.setCustomizeMail((prev) => ({
...prev,
subject: e.target.value
}));
}}
placeholder='${senderName} has requested you to sign "${documentName}"'
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
@@ -31,7 +35,6 @@ export function EmailBody(props) {
theme="snow"
value={props.requestBody}
placeholder={t("email-placeholder")}
ref={props.editorRef}
modules={module1}
formats={formats}
onChange={props.handleOnchangeRequest}
@@ -1,20 +1,21 @@
import React, { useState } from "react";
import axios from "axios";
import { handleToPrint } from "../../constant/Utils";
import { themeColor, emailRegex } from "../../constant/const";
import { useState } from "react";
import {
handleToPrint,
} from "../../constant/Utils";
import {
emailRegex,
} from "../../constant/const";
import Loader from "../../primitives/Loader";
import ModalUi from "../../primitives/ModalUi";
import { useTranslation } from "react-i18next";
import Parse from "parse";
function EmailComponent({
isEmail,
pdfUrl,
setIsEmail,
setSuccessEmail,
pdfDetails,
sender,
setIsAlert,
extUserId,
setIsDownloadModal
}) {
const { t } = useTranslation();
@@ -24,81 +25,13 @@ function EmailComponent({
const [emailErr, setEmailErr] = useState(false);
const [isDownloading, setIsDownloading] = useState("");
const isAndroid = /Android/i.test(navigator.userAgent);
//function for send email
const sendEmail = async () => {
const pdfName = pdfDetails[0]?.Name;
setIsLoading(true);
let sendMail;
const docId = pdfDetails?.[0]?.objectId || "";
let presignedUrl = pdfUrl;
try {
const axiosRes = await axios.post(
`${localStorage.getItem("baseUrl")}/functions/getsignedurl`,
{
url: pdfUrl,
docId: docId,
},
{
headers: {
"content-type": "Application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
}
}
);
presignedUrl = axiosRes.data.result;
} catch (err) {
console.log("err in getsignedurl", err);
}
for (let i = 0; i < emailList.length; i++) {
try {
const imgPng =
"https://qikinnovation.ams3.digitaloceanspaces.com/logo.png";
let url = `${localStorage.getItem("baseUrl")}functions/sendmailv3`;
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
sessionToken: localStorage.getItem("accesstoken")
};
const openSignUrl = "https://www.opensignlabs.com/contact-us";
const themeBGcolor = themeColor;
let params = {
extUserId: extUserId,
pdfName: pdfName,
url: presignedUrl,
recipient: emailList[i],
subject: `${sender.name} has signed the doc - ${pdfName}`,
replyto:
pdfDetails?.[0]?.ExtUserPtr?.Email ||
"",
from:
sender.email,
html:
"<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body> <div style='background-color:#f5f5f5;padding:20px'> <div style='box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;background-color:white;'> <div><img src=" +
imgPng +
" height='50' style='padding:20px,width:170px,height:40px'/> </div><div style='padding:2px;font-family:system-ui; background-color:" +
themeBGcolor +
";'> <p style='font-size:20px;font-weight:400;color:white;padding-left:20px',> Document Copy</p></div><div><p style='padding:20px;font-family:system-ui;font-size:14px'>A copy of the document <strong>" +
pdfName +
" </strong>is attached to this email. Kindly download the document from the attachment.</p></div> </div><div><p>This is an automated email from OpenSign™. For any queries regarding this email, please contact the sender " +
sender.email +
" directly. If you think this email is inappropriate or spam, you may file a complaint with OpenSign™ <a href= " +
openSignUrl +
" target=_blank>here</a> </p></div></div></body></html>"
};
sendMail = await axios.post(url, params, { headers: headers });
} catch (error) {
console.log("error", error);
setIsLoading(false);
setIsEmail(false);
setIsAlert({
isShow: true,
alertMessage: t("something-went-wrong-mssg")
});
}
}
if (sendMail?.data?.result?.status === "success") {
const params = { docId: pdfDetails?.[0]?.objectId, recipients: emailList };
const sendmail = await Parse.Cloud.run("forwarddoc", params);
if (sendmail?.status === "success") {
setSuccessEmail(true);
setIsEmail(false);
setTimeout(() => {
@@ -107,7 +40,8 @@ function EmailComponent({
setEmailList([]);
}, 1500);
setIsLoading(false);
} else {
}
else {
setIsLoading(false);
setIsEmail(false);
setIsAlert({
@@ -177,7 +111,7 @@ function EmailComponent({
</div>
)}
<div className="flex justify-between items-center py-[10px] px-[20px] border-b-[1px] border-base-content">
<span className="text-base-content font-semibold">
<span className="text-base-content font-bold text-sm md:text-lg">
{t("successfully-signed")}
</span>
<div className="flex flex-row">
@@ -186,14 +120,14 @@ function EmailComponent({
onClick={(e) =>
handleToPrint(e, setIsDownloading, pdfDetails)
}
className="op-btn op-btn-neutral op-btn-sm text-[15px]"
className="op-btn op-btn-neutral op-btn-sm text-xs md:text-[15px]"
>
<i className="fa-light fa-print" aria-hidden="true"></i>
{t("print")}
</button>
)}
<button
className="op-btn op-btn-primary op-btn-sm text-[15px] ml-2"
className="op-btn op-btn-primary op-btn-sm text-xs md:text-[15px] ml-2"
onClick={() => {
handleClose();
setIsDownloadModal(true);
@@ -209,12 +143,12 @@ function EmailComponent({
{t("email-mssg")}
</p>
{emailList.length > 0 ? (
<div className="p-0 border-[1.5px] op-border-primary rounded w-full text-[15px]">
<div className="p-0 border-[1px] op-border-primary w-full rounded-md text-[15px] overflow-hidden">
<div className="flex flex-row flex-wrap">
{emailList.map((data, ind) => {
return (
<div
className="flex flex-row items-center op-bg-primary m-[4px] rounded-md py-[5px] px-[10px]"
className="flex flex-row items-center op-bg-primary mx-[2px] mt-[2px] rounded-md py-[5px] px-[10px]"
key={ind}
>
<span className="text-base-100 text-[13px]">
@@ -234,7 +168,7 @@ function EmailComponent({
<input
type="email"
value={emailValue}
className="p-[10px] pb-[20px] rounded w-full text-[15px] bg-transparent outline-none"
className="p-[10px] rounded-md w-full text-[15px] bg-transparent outline-none"
onChange={handleEmailValue}
onKeyDown={handleEnterPress}
onBlur={() => emailValue && handleEnterPress("add")}
@@ -251,7 +185,7 @@ function EmailComponent({
<input
type="email"
value={emailValue}
className="p-[10px] pb-[20px] rounded w-full text-[15px] outline-none bg-transparent border-[1.5px] op-border-primary"
className="p-[10px] pb-[20px] text-base-content rounded-md w-full text-[15px] outline-none bg-transparent border-[1px] op-border-primary"
onChange={handleEmailValue}
onKeyDown={handleEnterPress}
placeholder={t("enter-email-plaholder")}
@@ -269,34 +203,22 @@ function EmailComponent({
{t("email-error-1")}
</p>
)}
<button
className={`${
emailValue ? "cursor-pointer" : "cursor-default"
} op-btn op-btn-primary op-btn-sm m-2 shadow-md`}
onClick={() => emailValue && handleEnterPress("add")}
>
<i className="fa-light fa-plus" aria-hidden="true"></i>
</button>
<div className="bg-[#e3e2e1] mt-[10px] p-[5px] rounded">
<span className="font-bold">{t("report-heading.Note")}: </span>
<span className="text-[15px]">{t("email-error-2")}</span>
<div className="mt-2">
<button
type="button"
className="op-btn op-btn-secondary"
onClick={() => emailList.length > 0 && sendEmail()}
>
{t("send")}
</button>
<button
type="button"
className="op-btn op-btn-ghost text-base-content ml-2"
onClick={() => handleClose()}
>
{t("close")}
</button>
</div>
<hr className="w-full my-[15px] bg-base-content" />
<button
type="button"
className="op-btn op-btn-secondary"
onClick={() => emailList.length > 0 && sendEmail()}
>
{t("send")}
</button>
<button
type="button"
className="op-btn op-btn-ghost ml-2"
onClick={() => handleClose()}
>
{t("close")}
</button>
</div>
</ModalUi>
)}
@@ -1,305 +0,0 @@
import React, { useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { tomorrow } from "react-syntax-highlighter/dist/esm/styles/prism";
import { copytoData } from "../../constant/Utils";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
function EmbedTab(props) {
const { t } = useTranslation();
const navigate = useNavigate();
const tabName = [
{ title: "React/Next.js", icon: "fa-brands fa-react", color: "#61dafb" },
{ title: "JavaScript", icon: "fa-brands fa-js", color: "#ffd43b" },
{ title: "Angular", icon: "fa-brands fa-angular", color: "#ff5733" }
];
const [activeTab, setActiveTab] = useState(0);
// State to track if the code has been copied
const [isCopied, setIsCopied] = useState(false);
const reactCode = [
{
id: 0,
title: "Installation",
codeString: `npm install @opensign/react`
},
{
id: 1,
title: "Usage",
codeString: `
import React from "react";
import Opensign from "@opensign/react";
export default function App() {
return (
<div className="app">
<Opensign
onLoad={() => console.log("success")}
onLoadError={(error) => console.log(error)}
templateId= "${props.templateId ? props.templateId : "#templateId"}"
/>
</div>
);
}
`
}
];
const angularCode = [
{
id: 0,
title: "Installation",
codeString: `npm install @opensign/angular`
},
{
id: 1,
title: "Usage",
codeString: `
import { Component } from '@angular/core';
import { OpensignComponent } from "@opensign/angular"
@Component({
selector:'app-root',
standalone: true,
imports: [OpensignComponent],
template:\`<opensign templateId="${props.templateId ? props.templateId : "#templateId"}"
(onLoad)="handleLoad()"
(onLoadError)="handleError($event)"
></opensign>\`,
})
export class AppComponent {
handleLoad() {
console.log("success");
}
handleError(error: string) {
console.log(error);
}
}
`
}
];
const jsCodeString = `
<script
src= "${window.location.origin}/static/js/public-template.bundle.js"
id="opensign-script"
templateId=${props.templateId ? props.templateId : "#templateId"}
></script>
`;
const handleCopy = (code, ind) => {
copytoData(code);
setIsCopied({ ...isCopied, [ind]: true });
setTimeout(() => setIsCopied(false), 3000);
};
return (
<div className={`${props.templateId && "border-t-[1px] mt-4"}`}>
{props.templateId && (
<h3 className="text-base-content font-bold text-lg pt-[15px] pb-[5px]">
{t("embed-template")}
</h3>
)}
<div className="flex justify-center items-center mt-2">
<div role="tablist" className="op-tabs op-tabs-bordered">
{tabName.map((tabData, ind) => (
<div
onClick={() => setActiveTab(ind)}
key={ind}
role="tab"
className={`${
activeTab === ind ? "op-tab-active" : ""
} op-tab flex items-center pb-10 md:pb-0`}
>
<i
className={`${tabData.icon}`}
style={{ color: tabData.color }}
></i>
<span className="ml-1 text-[10px] font-medium md:font-normal md:text-[15px]">
{tabData.title}
</span>
</div>
))}
</div>
</div>
<div>
{activeTab === 0 ? (
<div className="mt-4">
{reactCode.map((data, ind) => {
return (
<div key={ind}>
<p className="font-medium text-[18px]">
{t(`${data.title}`)}
</p>
{ind === 0 && (
<p className="text-[15px] mt-2">
{t("public-template-mssg-1")}
</p>
)}
<div className="relative p-1">
<div
onClick={() => handleCopy(data.codeString, ind)}
className="absolute top-[20px] right-[20px] cursor-pointer"
>
<i className="fa-light fa-copy text-white mr-[2px]" />
<span className=" text-white">
{isCopied[ind] ? t("copied-code") : t("copy-code")}
</span>
</div>
<SyntaxHighlighter
customStyle={{
borderRadius: "15px"
}}
language="javascript"
style={tomorrow}
>
{data.codeString}
</SyntaxHighlighter>
</div>
</div>
);
})}
{props.isEmbedPage && (
<p className="text-[15px] my-2">
{t("js-snippet-msg-1")}
<span
className="text-blue-600 cursor-pointer px-1"
onClick={() => navigate("/report/6TeaPr321t")}
>
{t("js-snippet-msg-2")}
</span>
<span>{t("js-snippet-msg-3")}</span>
</p>
)}
<p className="font-medium mt-3 text-[15px]">
{t("public-template-mssg-3")}
</p>
<p className="my-[6px]">
{" "}
{t("public-template-mssg-4")}
<a
href="https://www.npmjs.com/package/@opensign/react"
target="_blank"
rel="noreferrer"
className="cursor-pointer text-blue-700 "
>
{" "}
OpenSign React package{" "}
</a>
{t("public-template-mssg-5")}
</p>
</div>
) : activeTab === 1 ? (
<div className="mt-4">
<div>
<p className="font-medium text-[18px]">{t(`Usage`)}</p>
<p className="text-[15px] my-2">{t("js-snippet-msg")}</p>
<div className="relative p-1">
<div
onClick={() => handleCopy(jsCodeString, 0)}
className="absolute top-[20px] right-[20px] cursor-pointer"
>
<i className="fa-light fa-copy text-white mr-[2px]" />
<span className=" text-white">
{isCopied[0] ? t("copied-code") : t("copy-code")}
</span>
</div>
<SyntaxHighlighter
customStyle={{
borderRadius: "15px"
}}
language="javascript"
style={tomorrow}
>
{jsCodeString}
</SyntaxHighlighter>
</div>
{props.isEmbedPage && (
<p className="text-[15px] my-2">
{t("js-snippet-msg-1")}
<span
className="text-blue-600 cursor-pointer px-1"
onClick={() => navigate("/report/6TeaPr321t")}
>
{t("js-snippet-msg-2")}
</span>
<span>{t("js-snippet-msg-3")}</span>
</p>
)}
</div>
</div>
) : (
activeTab === 2 && (
<div className="mt-4">
{angularCode.map((data, ind) => {
return (
<div key={ind}>
<p className="font-medium text-[18px]">
{t(`${data.title}`)}
</p>
{ind === 0 && (
<p className="text-[15px] mt-2">
{t("angular-npm-mssg-1")}
</p>
)}
<div className="relative p-1">
<div
onClick={() => handleCopy(data.codeString, ind)}
className="absolute top-[20px] right-[20px] cursor-pointer"
>
<i className="fa-light fa-copy text-white mr-[2px]" />
<span className=" text-white">
{isCopied[ind] ? t("copied-code") : t("copy-code")}
</span>
</div>
<SyntaxHighlighter
customStyle={{
borderRadius: "15px"
}}
language="javascript"
style={tomorrow}
>
{data.codeString}
</SyntaxHighlighter>
</div>
</div>
);
})}
{props.isEmbedPage && (
<p className="text-[15px] my-2">
{t("js-snippet-msg-1")}
<span
className="text-blue-600 cursor-pointer px-1"
onClick={() => navigate("/report/6TeaPr321t")}
>
{t("js-snippet-msg-2")}
</span>
<span>{t("js-snippet-msg-3")}</span>
</p>
)}
<p className="font-medium mt-3 text-[15px]">
{t("public-template-mssg-3")}
</p>
<p className="my-[6px]">
{" "}
{t("public-template-mssg-4")}
<a
href="https://www.npmjs.com/package/@opensign/angular"
target="_blank"
rel="noreferrer"
className="cursor-pointer text-blue-700 "
>
{" "}
OpenSign Angular package{" "}
</a>
{t("public-template-mssg-5")}
</p>
</div>
)
)}
</div>
</div>
);
}
export default EmbedTab;
@@ -0,0 +1,30 @@
const Guidelines = ({ x1, x2, y1, y2 }) => {
return (
<>
{/* Horizontal guidelines */}
{/* top guide */}{" "}
<div
className="absolute pointer-events-none z-[1000] left-0 w-full border-t-[1px] border-dashed border-[#3b82f6]"
style={{ top: y1 }}
/>
{/* bottom guide */}
<div
className="absolute pointer-events-none z-[1000] left-0 w-full border-t-[1px] border-dashed border-[#3b82f6]"
style={{ top: y2 }}
/>
{/* Vertical guidelines */}
{/* left guide */}
<div
className="absolute pointer-events-none z-[1000] top-0 h-full border-l-[1px] border-dashed border-[#3b82f6]"
style={{ left: x1 }}
/>
{/* right guide */}
<div
className="absolute pointer-events-none z-[1000] top-0 h-full border-l-[1px] border-dashed border-[#3b82f6]"
style={{ left: x2 }}
/>
</>
);
};
export default Guidelines;
@@ -0,0 +1,94 @@
import React, { useEffect, useState, useRef } from "react";
import ModalUi from "../../primitives/ModalUi";
import { useTranslation } from "react-i18next";
export default function PageReorderModal({
isOpen,
handleClose,
totalPages = 0,
onSave
}) {
const { t } = useTranslation();
const [order, setOrder] = useState([]);
// Keeps track of the page order relative to the original PDF
const orderRef = useRef([]);
// Captures the order when the modal opens
const initialOrderRef = useRef([]);
// Initialize orderRef when total pages change (e.g. after upload)
useEffect(() => {
if (orderRef.current.length !== totalPages) {
orderRef.current = Array.from({ length: totalPages }, (_, i) => i + 1);
}
}, [totalPages]);
// When modal opens, display the last saved order and store it as initial
useEffect(() => {
if (isOpen) {
setOrder(orderRef.current);
initialOrderRef.current = [...orderRef.current];
}
}, [isOpen]);
const move = (index, dir) => {
const swapIndex = index + dir;
if (swapIndex < 0 || swapIndex >= order.length) return;
const newOrder = [...order];
[newOrder[index], newOrder[swapIndex]] = [newOrder[swapIndex], newOrder[index]];
setOrder(newOrder);
};
const handleSave = () => {
const saveOrder = order.map((num) =>
initialOrderRef.current.indexOf(num) + 1
);
// Persist the new display order for next time
orderRef.current = [...order];
onSave && onSave(saveOrder);
};
const isUnchanged =
order.length === initialOrderRef.current.length &&
order.every((n, i) => n === initialOrderRef.current[i]);
return (
<ModalUi isOpen={isOpen} handleClose={handleClose} title={t("reorder-pages")}>
<div className="p-[20px] flex flex-col gap-2 text-base-content">
{order.map((num, i) => (
<div key={num} className="flex items-center justify-between">
<span>
{t("page")} {num}
</span>
<div className="flex gap-1">
<button
className="op-btn op-btn-xs op-btn-ghost text-base-content"
disabled={i === 0}
onClick={() => move(i, -1)}
>
<i className="fa-light fa-arrow-up"></i>
</button>
<button
className="op-btn op-btn-xs op-btn-ghost text-base-content"
disabled={i === order.length - 1}
onClick={() => move(i, 1)}
>
<i className="fa-light fa-arrow-down"></i>
</button>
</div>
</div>
))}
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<button onClick={handleSave} type="button" className="op-btn op-btn-primary" disabled={isUnchanged}>
{t("save")}
</button>
<button
onClick={handleClose}
type="button"
className="op-btn op-btn-ghost text-base-content ml-1"
>
{t("close")}
</button>
</div>
</ModalUi>
);
}
@@ -2,33 +2,41 @@ import React, { useRef, useState } from "react";
import PrevNext from "./PrevNext";
import {
base64ToArrayBuffer,
decryptPdf,
deletePdfPage,
flattenPdf,
getFileAsArrayBuffer,
handleDownloadCertificate,
handleDownloadPdf,
handleRemoveWidgets,
handleToPrint
handleToPrint,
reorderPdfPages
} from "../../constant/Utils";
import "../../styles/signature.css";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { DropdownMenu } from "radix-ui";
import ModalUi from "../../primitives/ModalUi";
import Loader from "../../primitives/Loader";
import PageReorderModal from "./PageReorderModal";
import { useTranslation } from "react-i18next";
import { PDFDocument } from "pdf-lib";
import { maxFileSize } from "../../constant/const";
function Header(props) {
const { t } = useTranslation();
const filterPrefill =
props?.signerPos &&
props?.signerPos?.filter((data) => data.Role !== "prefill");
const isMobile = window.innerWidth < 767;
const [isDownloading, setIsDownloading] = useState("");
const [isDeletePage, setIsDeletePage] = useState(false);
const [isReorderModal, setIsReorderModal] = useState(false);
const mergePdfInputRef = useRef(null);
const enabledBackBtn = props?.disabledBackBtn === true ? false : true;
//function for show decline alert
const handleDeclinePdfAlert = async () => {
const currentDecline = { currnt: "Sure", isDeclined: true };
props?.setIsDecline(currentDecline);
if (props?.handleDecline) {
props.handleDecline();
} else {
const currentDecline = { currnt: "Sure", isDeclined: true };
props?.setIsDecline(currentDecline);
}
};
const handleDetelePage = async () => {
props?.setIsUploadPdf && props?.setIsUploadPdf(true);
@@ -50,18 +58,69 @@ function Header(props) {
}
};
// `removeFile` is used to remove file if exists
const removeFile = (e) => {
if (e) {
e.target.value = "";
}
};
const handleFileUpload = async (e) => {
const file = e.target.files[0];
if (!file) {
alert("Please upload a valid PDF file.");
alert(t("please-select-pdf"));
return;
}
if (!file.type.includes("pdf")) {
alert("Only PDF files are allowed.");
alert(t("only-pdf-allowed"));
return;
}
const fileSize =
maxFileSize;
const pdfsize = file?.size;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e);
return;
}
try {
const uploadedPdfBytes = await file.arrayBuffer();
let uploadedPdfBytes = await file.arrayBuffer();
try {
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
const pdfFile = await decryptPdf(file, "");
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
} catch (err) {
if (err?.response?.status === 401) {
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
if (password) {
try {
const pdfFile = await decryptPdf(file, password);
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
// Upload the file to Parse Server
} catch (err) {
console.error("Incorrect password or decryption failed", err);
alert(t("incorrect-password-or-decryption-failed"));
}
} else {
alert(t("provide-password"));
}
} else {
console.log("Err ", err);
alert(t("error-uploading-pdf"));
}
}
} else {
alert(t("error-uploading-pdf"));
}
}
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true
});
@@ -78,6 +137,13 @@ function Header(props) {
useObjectStreams: false
});
const pdfBuffer = base64ToArrayBuffer(pdfBase64);
const pdfsize = pdfBuffer?.byteLength;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e);
return;
}
props.setPdfArrayBuffer(pdfBuffer);
props.setPdfBase64Url(pdfBase64);
props.setIsUploadPdf && props.setIsUploadPdf(true);
@@ -88,12 +154,43 @@ function Header(props) {
}
};
const handleReorderSave = async (order) => {
try {
const pdfupdatedData = await reorderPdfPages(props.pdfArrayBuffer, order);
if (pdfupdatedData) {
props.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
props.setPdfBase64Url(pdfupdatedData.base64);
props.setAllPages(pdfupdatedData.totalPages);
props.setPageNumber(1);
}
} catch (e) {
console.log("error in reorder pdf pages", e);
}
setIsReorderModal(false);
};
const handleDownloadDoc = async () => {
await handleDownloadPdf(
props?.pdfDetails,
setIsDownloading,
props.pdfBase64
);
};
const handleDownloadBtn = async () => {
if (
props?.isCompleted
) {
props?.setIsDownloadModal(true);
} else {
await handleDownloadDoc();
}
};
return (
<div className="flex py-[5px]">
{isMobile && props?.isShowHeader ? (
<div
id="navbar"
className="stickyHead"
className="stickyHead touch-none"
style={{
width: window.innerWidth + "px"
}}
@@ -127,17 +224,7 @@ function Header(props) {
>
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() => {
if (props?.isCompleted) {
props?.setIsDownloadModal(true);
} else {
handleDownloadPdf(
props?.pdfDetails,
setIsDownloading,
props.pdfBase64
);
}
}}
onClick={() => handleDownloadBtn()}
>
<div className="flex flex-row">
<i
@@ -147,25 +234,27 @@ function Header(props) {
{t("download")}
</div>
</DropdownMenu.Item>
{props?.isCompleted && (
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() =>
handleDownloadCertificate(
props?.pdfDetails,
setIsDownloading
)
}
>
<div className="border-none bg-[#fff]">
<i
className="fa-light fa-award mr-[3px]"
aria-hidden="true"
></i>
{t("certificate")}
</div>
</DropdownMenu.Item>
)}
{
props?.isCompleted && (
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() =>
handleDownloadCertificate(
props?.pdfDetails,
setIsDownloading
)
}
>
<div className="border-none bg-[#fff]">
<i
className="fa-light fa-award mr-[3px]"
aria-hidden="true"
></i>
{t("certificate")}
</div>
</DropdownMenu.Item>
)
}
{props?.isSignYourself && (
<DropdownMenu.Item
className="DropdownMenuItem"
@@ -216,7 +305,7 @@ function Header(props) {
<div
onClick={() => {
if (!props?.isMailSend) {
props?.alertSendEmail();
props?.handleSaveDoc();
}
}}
className={`${
@@ -258,15 +347,23 @@ function Header(props) {
className="bg-white shadow-md rounded-md px-3 py-2"
sideOffset={5}
>
{props?.setIsEditTemplate && (
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() => props?.setIsEditTemplate(true)}
>
<div className="flex flex-row">
<i
className="fa-light fa-gear mr-[3px]"
aria-hidden="true"
></i>
<span className="font-[500]">{t("Edit")}</span>
</div>
</DropdownMenu.Item>
)}
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() =>
handleDownloadPdf(
props?.pdfDetails,
setIsDownloading,
props.pdfBase64
)
}
onClick={() => handleDownloadDoc()}
>
<div className="flex flex-row">
<i
@@ -302,6 +399,17 @@ function Header(props) {
</span>
</div>
</DropdownMenu.Item>
<DropdownMenu.Item
className="DropdownMenuItem"
onClick={() => setIsReorderModal(true)}
>
<div className="flex flex-row">
<i className="fa-light fa-list-ol text-gray-500 2xl:text-[30px] mr-[3px]"></i>
<span className="font-[500]">
{t("reorder-pages")}
</span>
</div>
</DropdownMenu.Item>
<DropdownMenu.Item
className="DropdownMenuItem"
@@ -375,42 +483,21 @@ function Header(props) {
/>
{props?.isPlaceholder ? (
<>
<div className="flex mx-[100px] lg:mx-0 order-last lg:order-none">
{!props?.isMailSend &&
props?.signersdata.length > 0 &&
props?.signersdata.length !== filterPrefill.length && (
<div>
{filterPrefill.length === 0 ? (
<span className="text-[13px] text-[#f5405e]">
{t("add")}{" "}
{props?.signersdata.length - filterPrefill.length}{" "}
{t("recipients")} {t("widgets-name.signature")}
</span>
) : (
<span className="text-[13px] text-[#f5405e]">
{t("add")}{" "}
{props?.signersdata.length - filterPrefill.length}{" "}
{t("more")}
{t("recipients")} {t("widgets-name.signature")}
</span>
)}
</div>
)}
</div>
<div className="flex mx-[100px] lg:mx-0 order-last lg:order-none"></div>
<div className="flex">
{props?.setIsEditTemplate && (
<button
onClick={() => props?.setIsEditTemplate(true)}
className="outline-none border-none text-center mr-[3px]"
>
<i className="fa-light fa-gear fa-lg"></i>
<i className="fa-light fa-gear fa-lg text-base-content"></i>
</button>
)}
{enabledBackBtn && (
<button
onClick={() => window.history.go(-2)}
type="button"
className="op-btn op-btn-ghost op-btn-sm mr-[3px]"
className="op-btn op-btn-ghost text-base-content op-btn-sm mr-[3px]"
>
{t("back")}
</button>
@@ -419,7 +506,7 @@ function Header(props) {
disabled={props?.isMailSend && true}
data-tut="headerArea"
className="op-btn op-btn-primary op-btn-sm mr-[3px]"
onClick={() => props?.alertSendEmail()}
onClick={() => props?.handleSaveDoc()}
>
{props?.completeBtnTitle
? props?.completeBtnTitle
@@ -445,38 +532,32 @@ function Header(props) {
></i>
<span className="hidden lg:block">{t("print")}</span>
</button>
{props?.isCompleted && (
<button
type="button"
onClick={() =>
handleDownloadCertificate(
props?.pdfDetails,
setIsDownloading
)
}
className="op-btn op-btn-secondary op-btn-sm mr-[3px] shadow"
>
<i
className="fa-light fa-award py-[3px]"
aria-hidden="true"
></i>
<span className="hidden lg:block">{t("certificate")}</span>
</button>
)}
{
props?.isCompleted && (
<button
type="button"
onClick={() =>
handleDownloadCertificate(
props?.pdfDetails,
setIsDownloading
)
}
className="op-btn op-btn-secondary op-btn-sm mr-[3px] shadow"
>
<i
className="fa-light fa-award py-[3px]"
aria-hidden="true"
></i>
<span className="hidden lg:block">
{t("certificate")}
</span>
</button>
)
}
<button
type="button"
className="op-btn op-btn-primary op-btn-sm mr-[3px] shadow"
onClick={() => {
if (props?.isCompleted) {
props?.setIsDownloadModal(true);
} else {
handleDownloadPdf(
props?.pdfDetails,
setIsDownloading,
props.pdfBase64
);
}
}}
onClick={() => handleDownloadBtn()}
>
<i
className="fa-light fa-download py-[3px]"
@@ -487,29 +568,13 @@ function Header(props) {
</div>
) : (
<div className="flex" data-tut="reactourFifth">
{(!props?.templateId && !props?.isSelfSign) ||
(!props.isGuestSignFlow && (
<button
onClick={() => window.history.go(-2)}
type="button"
className="op-btn op-btn-ghost op-btn-sm mr-[3px]"
>
{t("back")}
</button>
))}
{props?.currentSigner && (
<>
{props?.templateId && (
<button
onClick={() =>
handleDownloadPdf(
props?.pdfDetails,
setIsDownloading,
props.pdfBase64
)
}
onClick={() => handleDownloadDoc()}
type="button"
className="op-btn op-btn-ghost op-btn-sm mr-[3px]"
className="op-btn op-btn-ghost text-base-content op-btn-sm mr-[3px]"
>
<span className="hidden lg:block">{t("download")}</span>
</button>
@@ -525,14 +590,8 @@ function Header(props) {
{!props?.templateId && (
<button
type="button"
className="op-btn op-btn-ghost op-btn-sm mr-[3px]"
onClick={() =>
handleDownloadPdf(
props?.pdfDetails,
setIsDownloading,
props.pdfBase64
)
}
className="op-btn op-btn-ghost text-base-content op-btn-sm mr-[3px]"
onClick={() => handleDownloadDoc()}
>
<i className="fa-light fa-arrow-down font-semibold lg:hidden"></i>
<span className="hidden lg:block">{t("download")}</span>
@@ -551,23 +610,25 @@ function Header(props) {
)
) : props?.isCompleted ? (
<div className="flex flex-row">
{props?.isCompleted && (
<button
type="button"
onClick={() =>
handleDownloadCertificate(
props?.pdfDetails,
setIsDownloading
)
}
className="op-btn op-btn-secondary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
>
<i className="fa-light fa-award" aria-hidden="true"></i>
<span className="hidden lg:block ml-1">
{t("certificate")}
</span>
</button>
)}
{
props?.isCompleted && (
<button
type="button"
onClick={() =>
handleDownloadCertificate(
props?.pdfDetails,
setIsDownloading
)
}
className="op-btn op-btn-secondary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
>
<i className="fa-light fa-award" aria-hidden="true"></i>
<span className="hidden lg:block ml-1">
{t("certificate")}
</span>
</button>
)
}
<button
onClick={(e) =>
handleToPrint(e, setIsDownloading, props?.pdfDetails)
@@ -581,7 +642,8 @@ function Header(props) {
<button
type="button"
className="op-btn op-btn-primary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
onClick={() => props?.setIsDownloadModal(true)}
// onClick={() => props?.setIsDownloadModal(true)}
onClick={() => handleDownloadBtn()}
>
<i className="fa-light fa-download" aria-hidden="true"></i>
<span className="hidden lg:block ml-1">{t("download")}</span>
@@ -610,7 +672,7 @@ function Header(props) {
<button
onClick={() => window.history.go(-2)}
type="button"
className="op-btn op-btn-ghost op-btn-sm mr-[3px]"
className="op-btn op-btn-ghost text-base-content op-btn-sm mr-[3px]"
>
{t("back")}
</button>
@@ -655,8 +717,8 @@ function Header(props) {
handleClose={() => setIsDeletePage(false)}
>
<div className="h-[100%] p-[20px]">
<p className="font-medium">{t("delete-alert-2")}</p>
<p className="pt-3">{t("delete-note")}</p>
<p className="font-medium text-base-content">{t("delete-alert-2")}</p>
<p className="pt-3 text-base-content">{t("delete-note")}</p>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<button
onClick={() => handleDetelePage()}
@@ -668,12 +730,18 @@ function Header(props) {
<button
onClick={() => setIsDeletePage(false)}
type="button"
className="op-btn op-btn-ghost"
className="op-btn op-btn-ghost text-base-content"
>
{t("no")}
</button>
</div>
</ModalUi>
<PageReorderModal
isOpen={isReorderModal}
handleClose={() => setIsReorderModal(false)}
totalPages={props.allPages}
onSave={handleReorderSave}
/>
</div>
);
}
@@ -0,0 +1,298 @@
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import {
base64ToArrayBuffer,
decryptPdf,
deletePdfPage,
flattenPdf,
getFileAsArrayBuffer,
handleRemoveWidgets,
reorderPdfPages
} from "../../constant/Utils";
import ModalUi from "../../primitives/ModalUi";
import { PDFDocument } from "pdf-lib";
import { maxFileSize } from "../../constant/const";
import PageReorderModal from "./PageReorderModal";
function PdfTools(props) {
const { t } = useTranslation();
const mergePdfInputRef = useRef(null);
const [isDeletePage, setIsDeletePage] = useState(false);
const [isReorderModal, setIsReorderModal] = useState(false);
const handleDetelePage = async () => {
props.setIsUploadPdf && props.setIsUploadPdf(true);
try {
const pdfupdatedData = await deletePdfPage(
props.pdfArrayBuffer,
props.pageNumber
);
if (pdfupdatedData?.totalPages === 1) {
alert(t("delete-alert"));
} else {
props.setPdfBase64Url(pdfupdatedData.base64);
props.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
setIsDeletePage(false);
handleRemoveWidgets(
props.setSignerPos,
props.signerPos,
props.pageNumber
);
props.setAllPages(pdfupdatedData.remainingPages || 1);
if (props.allPages === props.pageNumber) {
props.setPageNumber(props.pageNumber - 1);
} else if (props.allPages > 2) {
props.setPageNumber(props.pageNumber);
}
}
} catch (e) {
console.log("error in delete pdf page", e);
}
};
// `removeFile` is used to remove file if exists
const removeFile = (e) => {
if (e) {
e.target.value = "";
}
};
const handleFileUpload = async (e) => {
props.setIsTour && props.setIsTour(false);
const file = e.target.files[0];
if (!file) {
alert(t("please-select-pdf"));
return;
}
if (!file.type.includes("pdf")) {
alert(t("only-pdf-allowed"));
return;
}
const fileSize =
maxFileSize;
const pdfsize = file?.size;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e);
return;
}
try {
let uploadedPdfBytes = await file.arrayBuffer();
try {
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
const pdfFile = await decryptPdf(file, "");
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
} catch (err) {
if (err?.response?.status === 401) {
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
if (password) {
try {
const pdfFile = await decryptPdf(file, password);
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
// Upload the file to Parse Server
} catch (err) {
console.error("Incorrect password or decryption failed", err);
alert(t("incorrect-password-or-decryption-failed"));
}
} else {
alert(t("provide-password"));
}
} else {
console.log("Err ", err);
alert(t("error-uploading-pdf"));
}
}
} else {
alert(t("error-uploading-pdf"));
}
}
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true
});
const basePdfDoc = await PDFDocument.load(props.pdfArrayBuffer);
// Copy pages from the uploaded PDF to the base PDF
const uploadedPdfPages = await basePdfDoc.copyPages(
uploadedPdfDoc,
uploadedPdfDoc.getPageIndices()
);
uploadedPdfPages.forEach((page) => basePdfDoc.addPage(page));
// Save the updated PDF
const pdfBase64 = await basePdfDoc.saveAsBase64({
useObjectStreams: false
});
const pdfBuffer = base64ToArrayBuffer(pdfBase64);
const pdfsize = pdfBuffer?.byteLength;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e);
return;
}
props.setPdfArrayBuffer(pdfBuffer);
props.setPdfBase64Url(pdfBase64);
props.setIsUploadPdf && props.setIsUploadPdf(true);
mergePdfInputRef.current.value = "";
} catch (error) {
mergePdfInputRef.current.value = "";
console.error("Error merging PDF:", error);
}
};
const handleReorderSave = async (order) => {
try {
const pdfupdatedData = await reorderPdfPages(props.pdfArrayBuffer, order);
if (pdfupdatedData) {
props.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
props.setPdfBase64Url(pdfupdatedData.base64);
props.setAllPages(pdfupdatedData.totalPages);
props.setPageNumber(1);
}
} catch (e) {
console.log("error in reorder pdf pages", e);
}
setIsReorderModal(false);
};
const handleDeletePage = () => {
setIsDeletePage(true);
props.setIsTour && props.setIsTour(false);
};
const handleReorderPages = () => {
setIsReorderModal(true);
props.setIsTour && props.setIsTour(false);
};
const handleZoomIn = () => {
props.clickOnZoomIn();
props.setIsTour && props.setIsTour(false);
};
const handleZoomOut = () => {
props.clickOnZoomOut();
props.setIsTour && props.setIsTour(false);
};
const handleRotate = () => {
props.handleRotationFun(90);
props.setIsTour && props.setIsTour(false);
};
const handleAntiRotate = () => {
props.handleRotationFun(-90);
props.setIsTour && props.setIsTour(false);
};
return (
<>
<span
data-tut="pdftools"
className="hidden h-max md:flex flex-col gap-1 text-center md:w-[5%] mt-[42px]"
>
{!props.isDisableEditTools && (
<>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={() => mergePdfInputRef.current.click()}
title={t("add-pages")}
>
<input
type="file"
className="hidden"
accept="application/pdf"
ref={mergePdfInputRef}
onChange={handleFileUpload}
/>
<i className="fa-light fa-plus text-gray-500 2xl:text-[25px]"></i>
</span>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={handleDeletePage}
title={t("delete-page")}
>
<i className="fa-light fa-trash text-gray-500 2xl:text-[25px]"></i>
</span>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={handleReorderPages}
title={t("reorder-pages")}
>
<i className="fa-light fa-list-ol text-gray-500 2xl:text-[25px]"></i>
</span>
</>
)}
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={handleZoomIn}
title={t("zoom-in")}
>
<i className="fa-light fa-magnifying-glass-plus text-gray-500 2xl:text-[25px]"></i>
</span>
{!props.isDisableEditTools && (
<>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={handleRotate}
title={t("rotate-right")}
>
<i className="fa-light fa-rotate-right text-gray-500 2xl:text-[25px]"></i>
</span>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
title={t("rotate-left")}
onClick={handleAntiRotate}
>
<i className="fa-light fa-rotate-left text-gray-500 2xl:text-[25px]"></i>
</span>
</>
)}
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={handleZoomOut}
title={t("zoom-out")}
>
<i className="fa-light fa-magnifying-glass-minus text-gray-500 2xl:text-[30px]"></i>
</span>
</span>
<ModalUi
isOpen={isDeletePage}
title={t("delete-page")}
handleClose={() => setIsDeletePage(false)}
>
<div className="h-[100%] p-[20px]">
<p className="font-medium text-base-content">{t("delete-alert-2")}</p>
<p className="pt-3 text-base-content">{t("delete-note")}</p>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<button
onClick={() => handleDetelePage()}
type="button"
className="op-btn op-btn-primary"
>
{t("yes")}
</button>
<button
onClick={() => setIsDeletePage(false)}
type="button"
className="op-btn op-btn-ghost text-base-content ml-1"
>
{t("no")}
</button>
</div>
</ModalUi>
<PageReorderModal
isOpen={isReorderModal}
handleClose={() => setIsReorderModal(false)}
totalPages={props.allPages}
onSave={handleReorderSave}
/>
</>
);
}
export default PdfTools;
-177
View File
@@ -1,177 +0,0 @@
import React, { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import {
base64ToArrayBuffer,
deletePdfPage,
handleRemoveWidgets
} from "../../constant/Utils";
import ModalUi from "../../primitives/ModalUi";
import { PDFDocument } from "pdf-lib";
function PdfZoom(props) {
const { t } = useTranslation();
const mergePdfInputRef = useRef(null);
const [isDeletePage, setIsDeletePage] = useState(false);
const handleDetelePage = async () => {
props.setIsUploadPdf && props.setIsUploadPdf(true);
try {
const pdfupdatedData = await deletePdfPage(
props.pdfArrayBuffer,
props.pageNumber
);
if (pdfupdatedData?.totalPages === 1) {
alert(t("delete-alert"));
} else {
props.setPdfBase64Url(pdfupdatedData.base64);
props.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
setIsDeletePage(false);
handleRemoveWidgets(
props.setSignerPos,
props.signerPos,
props.pageNumber
);
props.setAllPages(pdfupdatedData.remainingPages || 1);
if (props.allPages === props.pageNumber) {
props.setPageNumber(props.pageNumber - 1);
} else if (props.allPages > 2) {
props.setPageNumber(props.pageNumber);
}
}
} catch (e) {
console.log("error in delete pdf page", e);
}
};
const handleFileUpload = async (e) => {
const file = e.target.files[0];
if (!file) {
alert("Please upload a valid PDF file.");
return;
}
if (!file.type.includes("pdf")) {
alert("Only PDF files are allowed.");
return;
}
try {
const uploadedPdfBytes = await file.arrayBuffer();
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true
});
const basePdfDoc = await PDFDocument.load(props.pdfArrayBuffer);
// Copy pages from the uploaded PDF to the base PDF
const uploadedPdfPages = await basePdfDoc.copyPages(
uploadedPdfDoc,
uploadedPdfDoc.getPageIndices()
);
uploadedPdfPages.forEach((page) => basePdfDoc.addPage(page));
// Save the updated PDF
const pdfBase64 = await basePdfDoc.saveAsBase64({
useObjectStreams: false
});
const pdfBuffer = base64ToArrayBuffer(pdfBase64);
props.setPdfArrayBuffer(pdfBuffer);
props.setPdfBase64Url(pdfBase64);
props.setIsUploadPdf && props.setIsUploadPdf(true);
mergePdfInputRef.current.value = "";
} catch (error) {
mergePdfInputRef.current.value = "";
console.error("Error merging PDF:", error);
}
};
return (
<>
<span className="hidden md:flex flex-col gap-1 text-center md:w-[5%] mt-[42px]">
{!props.isDisableEditTools && (
<>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={() => mergePdfInputRef.current.click()}
title={t("add-pages")}
>
<input
type="file"
className="hidden"
accept="application/pdf"
ref={mergePdfInputRef}
onChange={handleFileUpload}
/>
<i className="fa-light fa-plus text-gray-500 2xl:text-[25px]"></i>
</span>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={() => setIsDeletePage(true)}
title={t("delete-page")}
>
<i className="fa-light fa-trash text-gray-500 2xl:text-[25px]"></i>
</span>
</>
)}
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={() => props.clickOnZoomIn()}
title={t("zoom-in")}
>
<i className="fa-light fa-magnifying-glass-plus text-gray-500 2xl:text-[25px]"></i>
</span>
{!props.isDisableEditTools && (
<>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
onClick={() => props.handleRotationFun(90)}
title={t("rotate-right")}
>
<i className="fa-light fa-rotate-right text-gray-500 2xl:text-[25px]"></i>
</span>
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
title={t("rotate-left")}
onClick={() => props.handleRotationFun(-90)}
>
<i className="fa-light fa-rotate-left text-gray-500 2xl:text-[25px]"></i>
</span>
</>
)}
<span
className="bg-gray-50 px-[4px] 2xl:py-[10px]"
onClick={() => props.clickOnZoomOut()}
style={{
cursor: props.zoomPercent > 0 ? "pointer" : "default"
}}
title={t("zoom-out")}
>
<i className="fa-light fa-magnifying-glass-minus text-gray-500 2xl:text-[30px]"></i>
</span>
</span>
<ModalUi
isOpen={isDeletePage}
title={t("delete-page")}
handleClose={() => setIsDeletePage(false)}
>
<div className="h-[100%] p-[20px]">
<p className="font-medium">{t("delete-alert-2")}</p>
<p className="pt-3">{t("delete-note")}</p>
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
<button
onClick={() => handleDetelePage()}
type="button"
className="op-btn op-btn-primary"
>
{t("yes")}
</button>
<button
onClick={() => setIsDeletePage(false)}
type="button"
className="op-btn op-btn-ghost ml-1"
>
{t("no")}
</button>
</div>
</ModalUi>
</>
);
}
export default PdfZoom;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,60 +0,0 @@
import React from "react";
import { themeColor } from "../../constant/const";
import {
defaultWidthHeight,
isMobile,
radioButtonWidget,
resizeBorderExtraWidth,
textWidget
} from "../../constant/Utils";
function PlaceholderBorder(props) {
const getResizeBorderExtraWidth = resizeBorderExtraWidth();
const defaultWidth = defaultWidthHeight(props.pos.type).width;
const defaultHeight = defaultWidthHeight(props.pos.type).height;
const width = () => {
const getWidth =
props.placeholderBorder.w || props.pos.Width || defaultWidth;
return (
getWidth * props.scale * props.containerScale + getResizeBorderExtraWidth
);
};
const height = () => {
const getHeight =
props.placeholderBorder.h || props.pos.Height || defaultHeight;
return (
getHeight * props.scale * props.containerScale + getResizeBorderExtraWidth
);
};
const handleMinWidth = () => {
if (props.pos.type === "checkbox" || props.pos.type === radioButtonWidget) {
return props.getCheckboxRenderWidth.width + getResizeBorderExtraWidth;
} else {
return width();
}
};
const handleMinHeight = () => {
if (props.pos.type === "checkbox" || props.pos.type === radioButtonWidget) {
return props.getCheckboxRenderWidth.height + getResizeBorderExtraWidth;
} else {
return height();
}
};
return (
<div
onMouseEnter={() => !isMobile && props?.setDraggingEnabled(false)}
onTouchEnd={() =>
props.pos.type === textWidget && props?.setDraggingEnabled(false)
}
className="absolute inline-block w-[14px] h-[14px] border-[0.2px] overflow-hidden border-dashed"
style={{
borderColor: themeColor,
minWidth: handleMinWidth() || 0,
minHeight: handleMinHeight() || 0
}}
></div>
);
}
export default PlaceholderBorder;
@@ -1,14 +1,18 @@
import React, { useState } from "react";
import { useState } from "react";
import ModalUi from "../../primitives/ModalUi";
import {
drawWidget,
handleCopyNextToWidget,
randomId,
textWidget
randomId
} from "../../constant/Utils";
import { useTranslation } from "react-i18next";
import { setPrefillImg } from "../../redux/reducers/widgetSlice";
import { useDispatch, useSelector } from "react-redux";
function PlaceholderCopy(props) {
const { prefillImg } = useSelector((state) => state.widget);
const { t } = useTranslation();
const dispatch = useDispatch();
const copyType = [
{ id: 1, type: "All pages" },
{ id: 2, type: "All pages but last" },
@@ -125,14 +129,19 @@ function PlaceholderCopy(props) {
);
for (let i = 0; i < props.allPages; i++) {
const newId = randomId();
const newPlaceholder = { ...currentPlaceholder, key: newId };
const nameId = randomId(2);
const widgetName = `${currentPlaceholder?.options?.name}${nameId}`;
const newPlaceholder = {
...currentPlaceholder,
key: newId,
options: { ...currentPlaceholder?.options, name: widgetName }
};
//get exist placeholder position for particular page
const existPlaceholder = filterSignerPosition[0].placeHolder.filter(
(data) => data.pageNumber === newPageNumber
);
const existPlaceholderPosition =
existPlaceholder[0] && existPlaceholder[0].pos;
//function for get copy to requested location of placeholder position
const getPlaceholderObj = getCopyPlaceholderPosition(
type,
@@ -144,6 +153,23 @@ function PlaceholderCopy(props) {
newPlaceholderPosition.push(getPlaceholderObj);
}
newPageNumber++;
if (
filterSignerPosition[0]?.Role === "prefill" &&
currentPlaceholder?.options?.response &&
(currentPlaceholder?.type === "image" ||
currentPlaceholder?.type === drawWidget)
) {
const getPrefillImg = prefillImg?.find(
(x) => x.id === currentPlaceholder?.key
);
const base64Img = getPrefillImg?.base64;
dispatch(
setPrefillImg({
id: newId,
base64: base64Img
})
);
}
}
let updatedSignerPlaceholder;
if (props?.signerObjId) {
@@ -213,6 +239,7 @@ function PlaceholderCopy(props) {
//function for getting selected type placeholder copy
const handleApplyCopy = () => {
const newId = randomId();
if (selectCopyType === 4) {
const signerPosition = props.xyPosition;
let currentXYposition;
@@ -230,13 +257,30 @@ function PlaceholderCopy(props) {
);
//function to create new widget next to just widget
handleCopyNextToWidget(
newId,
currentXYposition,
props.widgetType,
props.xyPosition,
props.pageNumber,
props.setXyPosition,
props?.Id
);
if (
filterSignerPosition[0]?.Role === "prefill" &&
currentXYposition?.options?.response &&
(currentXYposition?.type === "image" ||
currentXYposition?.type === drawWidget)
) {
const getPrefillImg = prefillImg?.find(
(x) => x.id === currentXYposition?.key
);
const base64Img = getPrefillImg?.base64;
dispatch(
setPrefillImg({
id: newId,
base64: base64Img
})
);
}
} else {
const getIndex = props?.xyPosition.findIndex(
(data) => data.pageNumber === props.pageNumber
@@ -248,8 +292,8 @@ function PlaceholderCopy(props) {
);
//function to create new widget next to just widget
handleCopyNextToWidget(
newId,
currentXYposition,
props.widgetType,
props.xyPosition,
getIndex,
props.setXyPosition
@@ -260,11 +304,6 @@ function PlaceholderCopy(props) {
}
};
const handleUniqueId = () => {
const signerId = props.signerObjId ? props.signerObjId : props.Id;
if (signerId && props.widgetType === textWidget && props.setTempSignerId) {
props.setUniqueId(props?.tempSignerId);
props.setTempSignerId("");
}
props.setIsPageCopy(false);
setSelectCopyType(1);
};
@@ -306,7 +345,7 @@ function PlaceholderCopy(props) {
</button>
<button
type="button"
className="op-btn op-btn-ghost ml-2"
className="op-btn op-btn-ghost text-base-content ml-2"
onClick={() => handleUniqueId()}
>
{t("cancel")}
@@ -1,864 +0,0 @@
import React, { useEffect, useState, forwardRef, useRef } from "react";
import {
getMonth,
getYear,
onChangeHeightOfTextArea,
onChangeInput,
radioButtonWidget,
range,
textInputWidget,
textWidget
} from "../../constant/Utils";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import "../../styles/signature.css";
import RegexParser from "regex-parser";
import { emailRegex } from "../../constant/const";
import { useTranslation } from "react-i18next";
const textWidgetCls =
"w-full h-full md:min-w-full md:min-h-full z-[999] text-[12px] rounded-[2px] border-[1px] border-[#007bff] overflow-hidden resize-none outline-none text-base-content item-center whitespace-pre-wrap bg-white";
const selectWidgetCls =
"w-full h-full absolute left-0 top-0 border-[1px] border-[#007bff] rounded-[2px] focus:outline-none text-base-content";
const widgetCls =
"select-none-cls overflow-hidden w-full h-full text-black flex flex-col justify-center items-center";
function PlaceholderType(props) {
const { t } = useTranslation();
const type = props?.pos?.type;
const widgetTypeTraslation = t(`widgets-name.${props?.pos?.type}`);
const [selectOption, setSelectOption] = useState("");
const [validatePlaceholder, setValidatePlaceholder] = useState("");
const inputRef = useRef(null);
const [textValue, setTextValue] = useState();
const [selectedCheckbox, setSelectedCheckbox] = useState([]);
const years = range(1950, getYear(new Date()) + 16, 1);
const fontSize = props.calculateFont(props.pos.options?.fontSize);
const fontColor = props.pos.options?.fontColor || "black";
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
];
const textWidgetStyle = {
fontSize: fontSize,
color: fontColor,
fontFamily: "Arial, sans-serif",
overflow: "hidden",
textAlign: "start",
width: "100%",
display: "flex",
height: "100%"
};
const validateExpression = (regexValidation) => {
if (textValue && regexValidation) {
let regexObject = regexValidation;
if (props.pos?.options?.validation?.type === "regex") {
regexObject = RegexParser(regexValidation);
}
// new RegExp(regexValidation);
let isValidate = regexObject.test(textValue);
if (!isValidate) {
props?.setValidateAlert(true);
inputRef.current.focus();
}
}
};
const handleInputBlur = () => {
const validateType = props.pos?.options?.validation?.type;
let regexValidation;
if (validateType && validateType !== "text") {
switch (validateType) {
case "email":
regexValidation = emailRegex;
validateExpression(regexValidation);
break;
case "number":
regexValidation = /^[0-9\s]*$/;
validateExpression(regexValidation);
break;
default:
regexValidation = props.pos?.options?.validation?.pattern || "";
validateExpression(regexValidation);
}
}
};
const handleTextValid = (e) => {
const textInput = e.target.value;
setTextValue(textInput);
};
function checkRegularExpress(validateType) {
switch (validateType) {
case "email":
setValidatePlaceholder("demo@gmail.com");
break;
case "number":
setValidatePlaceholder("12345");
break;
case "text":
setValidatePlaceholder("please enter text");
break;
default:
setValidatePlaceholder("please enter value");
}
}
useEffect(() => {
if (type && type === "checkbox" && props.isNeedSign) {
const isDefaultValue = props.pos.options?.defaultValue;
if (isDefaultValue) {
setSelectedCheckbox(isDefaultValue);
}
} else if (props.pos?.options?.hint) {
setValidatePlaceholder(props.pos?.options.hint);
} else if (props.pos?.options?.validation?.type) {
checkRegularExpress(props.pos?.options?.validation?.type);
}
setTextValue(
props.pos?.options?.response
? props.pos?.options?.response
: props.pos?.options?.defaultValue
? props.pos?.options?.defaultValue
: ""
);
setSelectOption(
props.pos?.options?.response
? props.pos?.options?.response
: props.pos?.options?.defaultValue
? props.pos?.options?.defaultValue
: ""
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (
["name", "email", "job title", "company"].includes(props.pos?.type) &&
props.isNeedSign &&
props.data?.signerObjId === props?.signerObjId
) {
const defaultData = props.pos?.options?.defaultValue;
if (defaultData) {
setTextValue(defaultData);
}
} else if ([textInputWidget].includes(props.pos?.type)) {
const defaultData = props.pos?.options?.defaultValue;
if (defaultData) {
setTextValue(defaultData);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.pos?.options?.defaultValue]);
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
<div
style={{
fontSize: fontSize,
color: fontColor,
fontFamily: "Arial, sans-serif"
}}
className={`${selectWidgetCls} overflow-hidden`}
onClick={onClick}
ref={ref}
>
{value}
<i className="fa-light fa-calendar ml-[5px]"></i>
</div>
));
ExampleCustomInput.displayName = "ExampleCustomInput";
useEffect(() => {
if (
["name", "email", "job title", "company"].includes(type) &&
props.isNeedSign &&
props.data?.signerObjId === props.signerObjId
) {
const isDefault = true;
const senderUser = localStorage.getItem(`Extand_Class`);
const jsonSender = JSON.parse(senderUser);
onChangeInput(
jsonSender && jsonSender[0],
null,
props.xyPosition,
null,
props.setXyPosition,
props.data.Id,
isDefault
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [type]);
//function for show checked checkbox
const selectCheckbox = (ind) => {
const res = props.pos.options?.response;
const defaultCheck = props.pos.options?.defaultValue;
if (res && res?.length > 0) {
const isSelectIndex = res.indexOf(ind);
if (isSelectIndex > -1) {
return true;
} else {
return false;
}
// }
} else if (defaultCheck) {
const isSelectIndex = defaultCheck.indexOf(ind);
if (isSelectIndex > -1) {
return true;
} else {
return false;
}
} else {
return false;
}
};
const handleRadioCheck = (data) => {
const defaultData = props.pos.options?.defaultValue;
if (textValue === data) {
return true;
} else if (defaultData === data) {
return true;
} else {
return false;
}
};
//function for set checked and unchecked value of checkbox
const handleCheckboxValue = (isChecked, ind) => {
let updateSelectedCheckbox = [],
checkedList;
let isDefaultValue, isDefaultEmpty;
if (type === "checkbox") {
updateSelectedCheckbox = selectedCheckbox ? selectedCheckbox : [];
if (isChecked) {
updateSelectedCheckbox.push(ind);
setSelectedCheckbox(updateSelectedCheckbox);
} else {
checkedList = selectedCheckbox.filter((data) => data !== ind);
setSelectedCheckbox(checkedList);
}
if (props.isNeedSign) {
isDefaultValue = props.pos.options?.defaultValue;
}
if (isDefaultValue && isDefaultValue.length > 0) {
isDefaultEmpty = true;
}
onChangeInput(
checkedList ? checkedList : updateSelectedCheckbox,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data.Id,
false,
null,
isDefaultEmpty
);
}
};
//function to handle select radio widget and set value seletced by user
const handleCheckRadio = (isChecked, data) => {
let isDefaultValue,
isDefaultEmpty,
isRadio = true;
if (props.isNeedSign) {
isDefaultValue = props.pos.options?.defaultValue;
}
if (isDefaultValue) {
isDefaultEmpty = true;
}
if (isChecked) {
setTextValue(data);
} else {
setTextValue("");
}
onChangeInput(
data,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data.Id,
false,
null,
isDefaultEmpty,
isRadio
);
};
//function to set onchange date
const handleOnDateChange = (date) => {
props.setStartDate(date);
};
//handle height on enter press in text area
const handleEnterPress = (e) => {
const height = 18;
if (e.key === "Enter") {
//function to save height of text area
onChangeHeightOfTextArea(
height,
props.pos.type,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id
);
}
};
switch (type) {
case "signature":
return props.pos.SignUrl ? (
<img
alt="signature"
draggable="false"
src={props.pos.SignUrl}
className="w-full h-full select-none-cls "
/>
) : (
<div className={widgetCls}>
{props?.handleUserName &&
props?.handleUserName(
props?.data?.Id,
props?.data?.Role,
widgetTypeTraslation,
props.pos
)}
</div>
);
case "stamp":
return props.pos.SignUrl ? (
<img
alt="stamp"
draggable="false"
src={props.pos.SignUrl}
className="w-full h-full select-none-cls"
/>
) : (
<div className={widgetCls}>
{props?.handleUserName &&
props?.handleUserName(
props?.data?.Id,
props?.data?.Role,
widgetTypeTraslation,
props.pos
)}
</div>
);
case "checkbox":
return (
<div style={{ zIndex: props.isSignYourself && "99" }}>
{props.pos.options?.values?.map((data, ind) => {
return (
<div
key={ind}
className="select-none-cls flex items-center text-center gap-0.5"
>
<input
id={`checkbox-${props.pos.key + ind}`}
style={{ width: fontSize, height: fontSize }}
className={`${
ind === 0 ? "mt-0" : "mt-[5px]"
} flex justify-center op-checkbox rounded-[1px] `}
onBlur={handleInputBlur}
disabled={
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
}
type="checkbox"
checked={selectCheckbox(ind)}
onChange={(e) => {
if (e.target.checked) {
if (!props.isPlaceholder) {
const maxRequired =
props.pos.options?.validation?.maxRequiredCount;
const maxCountInt =
maxRequired && parseInt(maxRequired);
if (maxCountInt > 0) {
if (
selectedCheckbox &&
selectedCheckbox?.length <= maxCountInt - 1
) {
handleCheckboxValue(e.target.checked, ind);
}
} else {
handleCheckboxValue(e.target.checked, ind);
}
}
} else {
handleCheckboxValue(e.target.checked, ind);
}
}}
/>
{!props.pos.options?.isHideLabel && (
<label
htmlFor={`checkbox-${props.pos.key + ind}`}
style={{ fontSize: fontSize, color: fontColor }}
className="text-xs mb-0 text-center"
>
{data}
</label>
)}
</div>
);
})}
</div>
);
case textInputWidget:
return props.isSignYourself ||
(props.isSelfSign && props.data?.signerObjId === props.signerObjId) ||
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
<textarea
ref={inputRef}
placeholder={validatePlaceholder || t("widgets-name.text")}
rows={1}
onKeyDown={handleEnterPress}
value={textValue}
onBlur={handleInputBlur}
onChange={(e) => {
setTextValue(e.target.value);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
false
);
}}
className={`${
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
? " bg-black/68 select-none "
: "" + textWidgetCls
}`}
style={{ fontSize: fontSize, color: fontColor }}
disabled={
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
}
cols="50"
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{textValue || widgetTypeTraslation}</span>
</div>
);
case "dropdown":
return props.data?.signerObjId === props.signerObjId ? (
<select
style={{ fontSize: fontSize, color: fontColor }}
className={`${
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
? " disabled:bg-inherit select-none "
: "" + `${selectWidgetCls} text-[12px] bg-inherit`
}`}
id="myDropdown"
value={selectOption}
onChange={(e) => {
setSelectOption(e.target.value);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
false
);
}}
disabled={
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
}
>
{/* Default/Title option */}
<option
style={{ fontSize: fontSize, color: fontColor }}
value=""
disabled
hidden
>
{props?.pos?.options?.name}
</option>
{props.pos?.options?.values?.map((data, ind) => {
return (
<option
style={{ fontSize: fontSize, color: fontColor }}
key={ind}
value={data}
>
{data}
</option>
);
})}
</select>
) : (
<div style={textWidgetStyle} className="select-none-cls">
{props.pos?.options?.name
? props.pos.options.name
: widgetTypeTraslation}
</div>
);
case "initials":
return props.pos.SignUrl ? (
<img
alt="initials"
draggable="false"
src={props.pos.SignUrl}
className="w-full h-full select-none-cls"
/>
) : (
<div className={widgetCls}>
{props?.handleUserName &&
props?.handleUserName(
props?.data?.Id,
props?.data?.Role,
widgetTypeTraslation,
props.pos
)}
</div>
);
case "name":
return props.isSignYourself ||
(props.isSelfSign && props.data?.signerObjId === props.signerObjId) ||
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
<textarea
ref={inputRef}
placeholder={t("widgets-name.name")}
rows={1}
onKeyDown={handleEnterPress}
value={textValue}
onChange={(e) => {
const isDefault = false;
handleTextValid(e);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
isDefault
);
}}
className={textWidgetCls}
style={{ fontSize: fontSize, color: fontColor }}
cols="50"
/>
) : (
<div className="flex h-full select-none-cls" style={textWidgetStyle}>
<span>{widgetTypeTraslation}</span>
</div>
);
case "company":
return props.isSignYourself ||
(props.isSelfSign && props.data?.signerObjId === props.signerObjId) ||
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
<textarea
ref={inputRef}
placeholder={t("widgets-name.company")}
rows={1}
onKeyDown={handleEnterPress}
value={textValue}
onChange={(e) => {
handleTextValid(e);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
false
);
}}
className={textWidgetCls}
style={{ fontSize: fontSize, color: fontColor }}
cols="50"
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{widgetTypeTraslation}</span>
</div>
);
case "job title":
return props.isSignYourself ||
(props.isSelfSign && props.data?.signerObjId === props.signerObjId) ||
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
<textarea
ref={inputRef}
placeholder={t("widgets-name.job title")}
rows={1}
onKeyDown={handleEnterPress}
value={textValue}
onChange={(e) => {
handleTextValid(e);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
false
);
}}
className={textWidgetCls}
style={{ fontSize: fontSize, color: fontColor }}
cols="50"
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{widgetTypeTraslation}</span>
</div>
);
case "date":
return props.isSignYourself ||
(props.isSelfSign && props.data?.signerObjId === props.signerObjId) ||
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
<DatePicker
renderCustomHeader={({ date, changeYear, changeMonth }) => (
<div className="flex justify-start ml-2 ">
<select
className="bg-transparent outline-none"
value={months[getMonth(date)]}
onChange={({ target: { value } }) =>
changeMonth(months.indexOf(value))
}
>
{months.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<select
className="bg-transparent outline-none"
value={getYear(date)}
onChange={({ target: { value } }) => changeYear(value)}
>
{years.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
)}
disabled={
props.isPlaceholder ||
(props.isNeedSign && props.data?.signerObjId !== props.signerObjId)
}
onBlur={handleInputBlur}
closeOnScroll={true}
className={`${selectWidgetCls} outline-[#007bff]`}
selected={
props?.startDate
? props?.startDate
: props.pos.options?.response &&
new Date(props.pos.options.response)
}
onChange={(date) => handleOnDateChange(date)}
popperPlacement="top-end"
customInput={<ExampleCustomInput />}
dateFormat={
props.selectDate
? props.selectDate?.format
: props.pos?.options?.validation?.format
? props.pos?.options?.validation?.format
: "MM/dd/yyyy"
}
/>
) : (
<div
style={textWidgetStyle}
className="select-none-cls overflow-hidden"
>
<span>
{props.selectDate
? props.selectDate?.format
: props.pos?.options?.validation?.format
? props.pos?.options?.validation?.format
: "MM/dd/yyyy"}
</span>
</div>
);
case "image":
return props.pos.SignUrl ? (
<img
alt="image"
draggable="false"
src={props.pos.SignUrl}
className="w-full h-full select-none-cls"
/>
) : (
<div className={widgetCls}>
{props?.handleUserName &&
props?.handleUserName(
props?.data?.Id,
props?.data?.Role,
widgetTypeTraslation,
props.pos
)}
</div>
);
case "email":
return props.isSignYourself ||
(props.isSelfSign && props.data?.signerObjId === props.signerObjId) ||
(props.isNeedSign && props.data?.signerObjId === props.signerObjId) ? (
<textarea
ref={inputRef}
placeholder={t("widgets-name.email")}
rows={1}
onKeyDown={(e) => {
// Prevent new line on Enter key press
if (e.key === "Enter") {
e.preventDefault();
}
}}
value={textValue}
onBlur={handleInputBlur}
onChange={(e) => {
handleTextValid(e);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
false
);
}}
className={textWidgetCls}
style={{
fontSize: fontSize,
color: fontColor,
fontFamily: "Arial, sans-serif"
}}
cols="1"
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span>{widgetTypeTraslation}</span>
</div>
);
case radioButtonWidget:
return (
<div>
{props.pos.options?.values.map((data, ind) => {
return (
<div
key={ind}
className="select-none-cls flex items-center text-center gap-0.5"
>
<input
id={`radio-${props.pos.key + ind}`}
style={{
width: fontSize,
height: fontSize,
marginTop: ind > 0 ? "10px" : "0px"
}}
className={`flex justify-center op-radio`}
type="radio"
disabled={
props.isNeedSign &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId)
}
checked={handleRadioCheck(data)}
onChange={(e) => {
if (!props.isPlaceholder) {
handleCheckRadio(e.target.checked, data);
}
}}
/>
{!props.pos.options?.isHideLabel && (
<label
htmlFor={`radio-${props.pos.key + ind}`}
style={{ fontSize: fontSize, color: fontColor }}
className="text-xs mb-0"
>
{data}
</label>
)}
</div>
);
})}
</div>
);
case textWidget:
return (
<textarea
placeholder={t("widgets-name.text")}
rows={1}
onKeyDown={handleEnterPress}
value={textValue}
onBlur={handleInputBlur}
onChange={(e) => {
setTextValue(e.target.value);
onChangeInput(
e.target.value,
props.pos.key,
props.xyPosition,
props.index,
props.setXyPosition,
props.data && props.data?.Id,
false
);
}}
className={textWidgetCls}
style={{
fontFamily: "Arial, sans-serif",
fontSize: fontSize,
color: fontColor
}}
cols="50"
/>
);
default:
return props.pos.SignUrl ? (
<div className="pointer-events-none">
<img
alt="image"
draggable="false"
src={props.pos.SignUrl}
className="w-full h-full "
/>
</div>
) : (
<div className={widgetCls}>
{props.pos.isStamp ? <div>stamp</div> : <div>signature</div>}
{props?.handleUserName &&
props?.handleUserName(
props?.data?.Id,
props?.data?.Role,
null,
props.pos
)}
</div>
);
}
}
export default PlaceholderType;
@@ -0,0 +1,637 @@
import {
useEffect,
useState,
forwardRef,
useRef
} from "react";
import {
onChangeInput,
getMonth,
getYear,
radioButtonWidget,
textInputWidget,
cellsWidget,
textWidget,
months,
years,
selectCheckbox,
isBase64,
drawWidget
} from "../../constant/Utils";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import "../../styles/signature.css";
import { useTranslation } from "react-i18next";
import CellsWidget from "./CellsWidget";
import { useSelector } from "react-redux";
import Loader from "../../primitives/Loader";
const textWidgetCls =
"w-full h-full md:min-w-full md:min-h-full z-[999] text-[12px] overflow-hidden resize-none outline-none text-base-content item-center whitespace-pre-wrap";
const widgetCls =
"select-none-cls overflow-hidden w-full h-full text-black flex flex-col justify-center items-center";
function PlaceholderType(props) {
const selectWidgetCls = `w-full h-full absolute left-0 top-0 focus:outline-none text-base-content`;
const { t } = useTranslation();
const textRef = useRef();
const prefillImg = useSelector((state) => state.widget.prefillImg);
const prefillImgLoad = useSelector((state) => state.widget.prefillImgLoad);
const type = props?.pos?.type;
const iswidgetEnable =
props.isSignYourself ||
((props.isSelfSign || props.isNeedSign) &&
props.data?.signerObjId === props.signerObjId);
const isReadOnly =
props?.data?.Role !== "prefill" &&
(props.pos.options?.isReadOnly ||
props.data?.signerObjId !== props.signerObjId);
// prefer the latest response value over any default value
const widgetData =
props.pos?.options?.response ?? props.pos?.options?.defaultValue ?? "";
const [widgetValue, setwidgetValue] = useState();
const [selectedCheckbox, setSelectedCheckbox] = useState([]);
const [imgUrl, setImgUrl] = useState("");
const fontSize = props.calculateFont(props.pos.options?.fontSize);
const fontColor = props.pos.options?.fontColor || "black";
const textWidgetStyle = {
fontSize: fontSize,
color: fontColor,
fontFamily: "Arial, sans-serif",
overflow: "hidden",
textAlign: "start",
width: "100%",
display: "flex",
height: "100%"
};
useEffect(() => {
if (type !== "date") {
if (type && type === "checkbox") {
setSelectedCheckbox(
props?.pos?.options?.response ||
props?.pos?.options?.defaultValue ||
[]
);
}
else {
// keep displayed value in sync with the stored response
setwidgetValue(widgetData);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.pos, widgetData, type]);
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
<div
style={{
fontSize: fontSize,
color: fontColor,
fontFamily: "Arial, sans-serif"
}}
className={`${isReadOnly ? `select-none opacity-25` : ``} ${selectWidgetCls} overflow-hidden`}
onClick={onClick}
ref={ref}
>
{value}
<i className="fa-light fa-calendar ml-[5px]"></i>
</div>
));
ExampleCustomInput.displayName = "ExampleCustomInput";
const handleRadioCheck = (data) => {
const defaultData = widgetValue || props.pos.options?.defaultValue;
return defaultData === data;
};
//function is used to get prefill image's signedUrl after expired
useEffect(() => {
const loadImage = async () => {
const isBase64Url = isBase64(props?.pos?.SignUrl);
if (
props.pos.SignUrl &&
props.pos.type === "image" &&
props?.data?.Role === "prefill" &&
!isBase64Url
) {
const getPrefillImg = prefillImg?.find((x) => x.id === props.pos.key);
if (getPrefillImg) {
setImgUrl(getPrefillImg?.base64);
}
} else {
setImgUrl(props.pos.SignUrl);
}
};
loadImage();
}, [props.pos.SignUrl]);
const formatWidgetName = () => {
const widgetName = props?.pos?.options?.name;
const name = widgetName ? widgetName?.split(`-`) : ["-", "-", "-"];
const lastWord = name.length > 1 ? `-${name[name.length - 1]}` : "";
const title =
props?.pos?.type === name[0] ? `${name[0]}${lastWord}` : widgetName;
return props?.pos?.options?.hint || title;
};
//useEffect is used to increase auto height of text/textInput widget when user using multiline option and enter value in next line
useEffect(() => {
if (!textRef.current) return;
if (
(type === "text" || type === textInputWidget) &&
textRef.current &&
widgetValue
) {
const el = textRef.current;
// Count actual number of lines
const lines = widgetValue?.split("\n")?.length || props.pos?.Height;
// Get line height from computed style
const lineHeight = parseInt(window.getComputedStyle(el).lineHeight);
const textWidgetHeight = lines * lineHeight;
// Set widget box height (your logic)
onChangeInput(
null,
props.pos,
props?.xyPosition,
props.index,
props?.setXyPosition,
props.data && props.data.Id,
null,
null,
null,
null,
null,
textWidgetHeight
);
}
}, [widgetValue]);
switch (type) {
case "signature":
return props.pos.SignUrl ? (
<img
alt="signature"
draggable="false"
src={props.pos.SignUrl}
className={`${props.pos.signatureType !== "type" ? "object-contain" : ""} w-full h-full select-none-cls`}
/>
) : (
<div className={widgetCls}>
{props.pos.type && (
<div
style={{
fontSize: props.pos
? props.calculateFontsize(props.pos)
: "11px"
}}
className="font-medium"
>
{formatWidgetName()}
</div>
)}
</div>
);
case "stamp":
return props.pos.SignUrl ? (
<img
alt="stamp"
draggable="false"
src={props.pos.SignUrl}
className="w-full h-full select-none-cls object-contain"
/>
) : (
<div className={widgetCls}>
{props.pos.type && (
<div
style={{
fontSize: props.pos
? props.calculateFontsize(props.pos)
: "11px"
}}
className="font-medium"
>
{formatWidgetName()}
</div>
)}
</div>
);
case "checkbox":
const checkBoxLayout = props.pos.options?.layout || "vertical";
const isMultipleCheckbox =
props.pos.options?.values?.length > 0 ? true : false;
const checkBoxWrapperClass = `flex items-start whitespace-pre-wrap ${
checkBoxLayout === "horizontal"
? `flex-row flex-wrap lg:py-[1.6px] ${isMultipleCheckbox ? "gap-x-2" : ""}`
: `flex-col ${isMultipleCheckbox ? "gap-y-[3px]" : ""}`
}`; // Using gap-y-1 for consistency, adjust if needed
return (
<div
className={checkBoxWrapperClass}
style={{ zIndex: props.isSignYourself && "99" }}
>
{props.pos.options?.values?.map((data, ind) => (
<div key={ind} className="select-none-cls pointer-events-none">
<label
htmlFor={`checkbox-${props.pos.key + ind}`}
style={{ fontSize: fontSize, color: fontColor }}
className={`mb-0 flex items-center gap-1`}
>
<input
id={`checkbox-${props.pos.key + ind}`}
style={{ width: fontSize, height: fontSize }}
className="op-checkbox rounded-[1px]"
disabled={props.isNeedSign && isReadOnly}
type="checkbox"
readOnly
checked={!!selectCheckbox(ind, selectedCheckbox)}
/>
{!props.pos.options?.isHideLabel && (
<span className="leading-none">{data}</span>
)}
</label>
</div>
))}
</div>
);
case textInputWidget:
return props.isSignYourself || iswidgetEnable ? (
<textarea
placeholder={formatWidgetName()}
ref={textRef}
rows={50}
value={
widgetValue
}
className={`w-full resize-none overflow-hidden text-base-content item-center outline-none ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
color: fontColor,
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none"
}}
name="text"
readOnly
disabled={props.isNeedSign && isReadOnly}
cols="50"
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span className="ml-0.5">{formatWidgetName()}</span>
</div>
);
case cellsWidget: {
const count = props.pos.options?.cellCount || 5;
const cells = (widgetValue || "").split("");
const height = "100%";
const fontSize = props.calculateFont(props.pos.options?.fontSize);
const fontColor = props.pos.options?.fontColor || "black";
const handleCellResize = (newCount) => {
if (props.setCellCount) props.setCellCount(props.pos.key, newCount);
};
const isEditable =
props.isPlaceholder || props.isSignYourself || props.isSelfSign;
return (
<CellsWidget
isEnabled={iswidgetEnable}
count={count}
height={height}
value={
cells.join("")
}
editable={isEditable}
resizable={props?.isAllowModify}
fontSize={fontSize}
fontColor={fontColor}
hint={formatWidgetName()}
onCellCountChange={handleCellResize}
/>
);
}
case "dropdown":
return (
<div
style={textWidgetStyle}
className="select-none-cls flex justify-between items-center"
>
{widgetData || t("choose-one")}
<i className="fa-light fa-circle-chevron-down mr-1 "></i>
</div>
);
case "initials":
return props.pos.SignUrl ? (
<img
alt="initials"
draggable="false"
src={props.pos.SignUrl}
className={`${props.pos.signatureType !== "type" ? "object-contain" : ""} w-full h-full select-none-cls`}
/>
) : (
<div className={widgetCls}>
{props.pos.type && (
<div
style={{
fontSize: props.pos
? props.calculateFontsize(props.pos)
: "11px"
}}
className="font-medium text-center"
>
{formatWidgetName()}
</div>
)}
</div>
);
case "name":
return iswidgetEnable ? (
<textarea
readOnly
placeholder={formatWidgetName()}
rows={1}
value={
widgetValue
}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
color: fontColor,
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none"
}}
cols="50"
disabled={props.isNeedSign && isReadOnly}
/>
) : (
<div className="flex h-full select-none-cls" style={textWidgetStyle}>
<span className="ml-0.5">{formatWidgetName()}</span>
</div>
);
case "company":
return iswidgetEnable ? (
<textarea
readOnly
placeholder={formatWidgetName()}
rows={1}
value={
widgetValue
}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
color: fontColor,
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none"
}}
cols="50"
disabled={props.isNeedSign && isReadOnly}
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span className="ml-0.5">{formatWidgetName()}</span>
</div>
);
case "job title":
return iswidgetEnable ? (
<textarea
readOnly
placeholder={formatWidgetName()}
rows={1}
value={
widgetValue
}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
color: fontColor,
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none"
}}
cols="50"
disabled={props.isNeedSign && isReadOnly}
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span className="ml-0.5">{formatWidgetName()}</span>
</div>
);
case "date":
return iswidgetEnable || props?.data?.Role === "prefill" ? (
<DatePicker
renderCustomHeader={({ date, changeYear, changeMonth }) => (
<div className="flex justify-start ml-2 ">
<select
className="bg-transparent outline-none"
value={months[getMonth(date)]}
onChange={({ target: { value } }) =>
changeMonth(months.indexOf(value))
}
>
{months.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<select
className="bg-transparent outline-none"
value={getYear(date)}
onChange={({ target: { value } }) => changeYear(value)}
>
{years.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
)}
disabled={true}
closeOnScroll={true}
className={`${selectWidgetCls} outline-[#007bff]`}
selected={props?.startDate}
popperPlacement="top-end"
customInput={<ExampleCustomInput />}
dateFormat={
props?.selectDate?.format ||
props.pos?.options?.validation?.format ||
"MM/dd/yyyy"
}
/>
) : (
<div
style={textWidgetStyle}
className="select-none-cls overflow-hidden uppercase"
>
<span>
{props?.selectDate?.format ||
props.pos?.options?.validation?.format ||
"MM/dd/yyyy"}
</span>
</div>
);
case "image":
return prefillImgLoad[props.pos?.key] ? (
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-white/30 z-50">
<Loader />
</div>
) : imgUrl ? (
<img
alt="image"
draggable="false"
src={imgUrl}
className="w-full h-full select-none-cls object-contain"
/>
) : (
<div className={widgetCls}>
{props.pos.type && (
<div
style={{
fontSize: props.pos
? props.calculateFontsize(props.pos)
: "11px"
}}
className="font-medium text-center"
>
{formatWidgetName()}
</div>
)}
</div>
);
case "email":
return iswidgetEnable ? (
<textarea
readOnly
placeholder={formatWidgetName()}
rows={1}
value={
widgetValue
}
className={`${textWidgetCls} ${isReadOnly ? "select-none" : ""}`}
style={{
fontSize: fontSize,
color: fontColor,
background: isReadOnly ? props.data?.blockColor : "white",
pointerEvents: "none"
}}
cols="1"
disabled={props.isNeedSign && isReadOnly}
/>
) : (
<div style={textWidgetStyle} className="select-none-cls">
<span className="ml-0.5">{formatWidgetName()}</span>
</div>
);
case radioButtonWidget:
const radioLayout = props.pos.options?.layout || "vertical";
const isOnlyOneBtn = props.pos.options?.values?.length > 0 ? true : false;
const radioWrapperClass = `flex items-start whitespace-pre-wrap ${
radioLayout === "horizontal"
? `flex-row flex-wrap lg:py-[1.6px] ${isOnlyOneBtn ? "gap-x-[10px]" : ""}`
: `flex-col ${isOnlyOneBtn ? "gap-y-[5px]" : ""}`
}`; // Using gap-y-1 for consistency, adjust if needed
return (
<div className={radioWrapperClass}>
{props.pos.options?.values.map((data, ind) => (
<div key={ind} className="select-none-cls pointer-events-none">
<label
htmlFor={`radio-${props.pos.key + ind}`}
style={{ fontSize: fontSize, color: fontColor }}
className="text-xs mb-0 flex items-center gap-1"
>
<input
readOnly
id={`radio-${props.pos.key + ind}`}
style={{ width: fontSize, height: fontSize, lineHeight: 2 }}
className={`op-radio rounded-full border-black appearance-none bg-white inline-block align-middle relative ${
handleRadioCheck(data) ? "checked-radio" : ""
}`}
type="radio"
disabled={props.isNeedSign && isReadOnly}
checked={handleRadioCheck(data)}
/>
{!props.pos.options?.isHideLabel && (
<span className="leading-none">{data}</span>
)}
</label>
</div>
))}
</div>
);
case textWidget:
return (
<textarea
name="text"
ref={textRef}
readOnly
placeholder={t("widgets-name.text")}
rows={50}
cols={50}
defaultValue={
widgetValue
}
style={{
fontFamily: "Arial, sans-serif",
fontSize: fontSize,
color: fontColor,
background: "white"
}}
className="w-full resize-none overflow-hidden text-base-content item-center outline-none"
/>
);
case drawWidget:
return prefillImgLoad[props.pos?.key] ? (
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-white/30 z-50">
<Loader />
</div>
) : widgetValue ? (
<img
alt="signature"
draggable="false"
src={widgetValue}
className={`${props.pos.signatureType !== "type" ? "object-contain" : ""} w-full h-full select-none-cls`}
/>
) : (
<div className={widgetCls}>
{props.pos.type && (
<div
style={{
fontSize: props.pos
? props.calculateFontsize(props.pos)
: "11px"
}}
className="font-medium"
>
{formatWidgetName()}
</div>
)}
</div>
);
default:
return props.pos.SignUrl ? (
<div className="pointer-events-none">
<img
alt="image"
draggable="false"
src={props.pos.SignUrl}
className="w-full h-full "
/>
</div>
) : (
<div className={widgetCls}>
{props.pos.isStamp ? <div>stamp</div> : <div>signature</div>}
{props.pos.type && (
<div
style={{
fontSize: props.pos
? props.calculateFontsize(props.pos)
: "11px"
}}
className="font-medium"
>
{formatWidgetName()}
</div>
)}
</div>
);
}
}
export default PlaceholderType;
@@ -0,0 +1,876 @@
import React, { forwardRef, useEffect, useMemo, useRef, useState } from "react";
import ModalUi from "../../primitives/ModalUi";
import {
getMonth,
getYear,
radioButtonWidget,
compressedFileSize,
textWidget,
months,
changeDateToMomentFormat,
convertBase64ToFile,
generatePdfName,
drawWidget,
getBase64MimeType,
isBase64
} from "../../constant/Utils";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import { range } from "pdf-lib";
import { useTranslation } from "react-i18next";
import moment from "moment";
import AsyncSelect from "react-select/async";
import axios from "axios";
import AddContact from "../../primitives/AddContact";
import Loader from "../../primitives/Loader";
import { useDispatch, useSelector } from "react-redux";
import {
resetWidgetState,
setPrefillImg
} from "../../redux/reducers/widgetSlice";
import * as utils from "../../utils";
import Draw from "./tab/Draw";
import PenColorComponent from "./tab/PenColorComponent";
const widgetTitle = "font-medium";
const widgetLabelCss = (isRequired = false) => {
return `${isRequired ? "after:content-['_*'] after:text-red-500" : ""} block text-xs font-semibold`;
};
const ShowTextWidget = ({ position, handleWidgetDetails }) => {
const inputRef = useRef(null);
const [inputValue, setInputValue] = useState(position.options.response || "");
return (
<input
ref={inputRef}
rows={1}
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
handleWidgetDetails(position, e.target.value);
}}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
/>
);
};
const ImageComponent = (props) => {
const { t } = useTranslation();
const prefillImg = useSelector((state) => state.widget.prefillImg);
const imageRefs = useRef({});
let imgUrl = "";
const isBase64Url = isBase64(props?.position?.SignUrl);
if (isBase64Url) {
imgUrl = props?.position?.SignUrl;
} else {
const getPrefillImg = prefillImg?.find(
(x) => x.id === props?.position?.key
);
imgUrl = getPrefillImg?.base64;
}
return (
<>
<span
className={`${widgetLabelCss(props?.position?.options?.status === "required")} ${widgetTitle}`}
>
{props?.position.options?.name}
</span>
{imgUrl ? (
<>
<div className="cursor-pointer op-card border-[1px] border-gray-400 flex flex-col w-full h-full justify-center items-center ">
<img
alt="print img"
ref={(el) => (imageRefs.current[props?.id] = el)} // Assign ref dynamicallys
src={imgUrl}
draggable="false"
className="object-contain h-full w-full aspect-[5/2]"
onLoad={() => props?.handleImageLoaded?.(props?.position.key)}
onError={() => props?.handleImageLoaded?.(props?.position.key)}
/>
</div>
<span
onClick={() => props?.handleClearImage(props?.position)}
className="flex justify-start text-blue-500 underline cursor-pointer"
>
{t("clear")}
</span>
</>
) : (
<div
className="cursor-pointer op-card border-[1px] op-border-hover flex flex-col overflow-hidden w-full h-full aspect-[5/2] justify-center items-center"
onClick={() => imageRefs.current[props?.id]?.click()}
>
<input
type="file"
onChange={(e) => props?.onImageChange?.(e, props?.position)}
className="filetype"
accept="image/png,image/jpeg"
ref={(el) => (imageRefs.current[props?.id] = el)} // Assign ref dynamically
hidden
/>
<i className="fa-light text-base-content fa-cloud-upload-alt text-[25px]"></i>
<div className="text-[10px] text-base-content">{t("upload")}</div>
</div>
)}
</>
);
};
function PrefillWidgetModal(props) {
const { t } = useTranslation();
const dispatch = useDispatch();
const canvasRefs = useRef({});
const [penColor, setPenColor] = useState("blue");
// Track already loaded image keys so they don't increment multiple times
const loadedSet = useRef(new Set());
const initializedRef = useRef(false); // prevent rerun on state updates
const [image, setImage] = useState(null);
const [currentWidget, setCurrentWidget] = useState("");
const [userList, setUserList] = useState([]);
const [totalImages, setTotalImages] = useState(0);
const [loadedImages, setLoadedImages] = useState(0);
const [loading, setLoading] = useState(false);
const years = range(1950, getYear(new Date()) + 16, 1);
// useMemo to memoize the calculation of unique widgets
const uniqueWidget = useMemo(() => {
//functions to used remove duplicate name values across all pages
if (!props.prefillData) return [];
//This will help us track which name values have already been encountered across all pages.
const uniqueNames = new Set();
//Filter and flatten placeholder widgets while keeping unique names
const filteredArray = props.prefillData?.placeHolder?.map((item) => ({
...item,
pos: item.pos.filter((curr) => {
if (uniqueNames.has(curr?.options?.name)) return false; //Duplicate name found, remove it
uniqueNames.add(curr?.options?.name); //First time seen, add to set
return true;
})
}));
//latten the filtered array and exclude read-only widgets
const flatArray = filteredArray?.flatMap((page) =>
page.pos
.filter((widget) => !widget.options?.isReadOnly)
.map((widget) => ({
widget,
pageNumber: page.pageNumber
}))
);
return flatArray || [];
}, [props.prefillData]);
useEffect(() => {
dispatch(resetWidgetState([]));
}, []);
// Reset loader state when modal closes
useEffect(() => {
if (!props?.isPrefillModal) {
initializedRef.current = false;
setTotalImages(0);
setLoadedImages(0);
setLoading(false);
}
}, [props?.isPrefillModal]);
useEffect(() => {
//function is used to save all image base64 in redux state to display prefill images
const savePrefillImg = async () => {
const prefillImg = await utils?.savePrefillImg(props.xyPosition);
if (Array.isArray(prefillImg)) {
setLoading(true);
prefillImg.forEach((img) => dispatch(setPrefillImg(img)));
}
setLoading(false);
};
savePrefillImg();
}, [props.xyPosition]);
useEffect(() => {
if (totalImages > 0 && loadedImages === totalImages) {
setLoading(false);
}
}, [loadedImages, totalImages]);
useEffect(() => {
if (image?.src) {
handleWidgetDetails(currentWidget);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [image]);
// Run only once per modal open
useEffect(() => {
if (props?.isPrefillModal && !initializedRef.current) {
const getImgWidgets = uniqueWidget?.filter(
(w) => w.widget?.type === "image" && w.widget?.options?.response
);
if (getImgWidgets?.length > 0) {
const imgCount = getImgWidgets.length;
setTotalImages(imgCount);
setLoadedImages(0);
setLoading(true);
initializedRef.current = true; // mark as initialized
}
}
}, [props?.isPrefillModal, uniqueWidget]);
const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => (
<div
style={{ fontFamily: "Arial, sans-serif" }}
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full"
onClick={onClick}
ref={ref}
>
{value}
<i className="fa-light fa-calendar ml-[5px]"></i>
</div>
));
ExampleCustomInput.displayName = "ExampleCustomInput";
const handleDate = (position) => {
// The getDatePickerDate function retrieves the date in the correct format supported by the DatePicker.
const getDate = utils.getDatePickerDate(
position?.options.response,
position?.options?.validation?.format
);
return getDate;
};
//function to set date with required date format onchange date
const handleOnDateChange = (date, position) => {
const format = position?.options?.validation?.format || "MM/dd/yyyy";
let updateDate = date;
let newDate;
const isSpecialDateFormat =
format && ["dd-MM-yyyy", "dd.MM.yyyy", "dd/MM/yyyy"].includes(format);
if (isSpecialDateFormat) {
newDate = moment(updateDate).format(changeDateToMomentFormat(format));
} else {
//using moment package is used to change date as per the format provided in selectDate obj e.g. - MM/dd/yyyy -> 03/12/2024
newDate = new Date(updateDate);
newDate = moment(newDate.getTime()).format(
changeDateToMomentFormat(format)
);
}
handleWidgetDetails(position, newDate);
};
const handleSavePrefillImg = async (base64) => {
setLoading(true);
try {
const imageName = generatePdfName(16);
const imgType = image?.imgType || getBase64MimeType(base64);
const imageUrl = await convertBase64ToFile(
imageName,
base64 || image.src,
imgType
);
if (imageUrl) {
return imageUrl;
}
} catch (e) {
console.log("error in handleSavePrefillImg function ", e);
}
};
//function is used to handle prefill widgets details and check if there are any duplicate widget name field exist then update all duplicate value
const handleWidgetDetails = async (widgetDetails, response) => {
const widgetName = widgetDetails?.options?.name;
const getPrefill = props.xyPosition.find((x) => x?.Role === "prefill");
const getPlaceholder = getPrefill?.placeHolder;
let imgUrl;
if (widgetDetails?.type === "image") {
imgUrl = await handleSavePrefillImg(response);
}
const updatedData = getPlaceholder.map((page) => ({
...page,
pos: page.pos.map((item) => {
if (item.options.name === widgetName) {
if (widgetDetails?.type === "image") {
return {
...item,
SignUrl: imgUrl,
ImageType: image.imgType,
options: { ...item.options, response: imgUrl }
};
} else {
return {
...item,
options: { ...item.options, response: response }
};
}
} else {
return item;
}
})
}));
const newUpdateSigner = props.xyPosition.map((obj) => {
if (obj.Role === "prefill") {
return { ...obj, placeHolder: updatedData };
}
return obj;
});
props.setXyPosition(newUpdateSigner);
};
//function for set checked and unchecked value of checkbox
const handleCheckboxValue = (isChecked, ind, position) => {
let updateSelectedCheckbox = [];
updateSelectedCheckbox =
position.options?.defaultValue || position.options?.response || [];
if (isChecked) {
updateSelectedCheckbox.push(ind);
} else {
updateSelectedCheckbox = updateSelectedCheckbox.filter(
(data) => data !== ind
);
}
handleWidgetDetails(position, updateSelectedCheckbox);
};
//function for image upload or update
const onImageChange = (event, position) => {
if (event.target.files && event.target.files[0]) {
const file = event.target.files[0];
compressedFileSize(file, setImage);
setCurrentWidget(position);
}
};
const handleRadioCheck = (data, position) => {
const res = position?.options?.response;
const defaultCheck = position?.options?.defaultValue;
if (res === data || defaultCheck === data) {
return true;
} else {
return false;
}
};
//function for show checked checkbox
const selectCheckbox = (ind, position) => {
const res = position?.options?.response;
const defaultCheck = position?.options?.defaultValue;
if (res && res?.length > 0) {
const isSelectIndex = res.indexOf(ind);
if (isSelectIndex > -1) {
return true;
} else {
return false;
}
} else if (defaultCheck) {
const isSelectIndex = defaultCheck.indexOf(ind);
if (isSelectIndex > -1) {
return true;
} else {
return false;
}
} else {
return false;
}
};
const handleClearImage = (position) => {
let prefillPlaceholder = props?.xyPosition.filter(
(data) => data?.Role === "prefill"
);
const updatedArray = prefillPlaceholder[0]?.placeHolder?.map((page) => ({
...page,
pos: page.pos.map((item) => {
if (item.options.name === position.options.name) {
return {
...item,
...(item.SignUrl !== undefined && { SignUrl: "" }),
options: {
...position.options,
response: ""
}
};
}
return item;
})
}));
const newUpdateSigner = props.xyPosition.map((obj) => {
if (obj.Role === "prefill") {
return { ...obj, placeHolder: updatedArray };
}
return obj;
});
props.setXyPosition(newUpdateSigner);
};
const handleImageLoaded = (key) => {
// Prevent counting the same image multiple times.
// If this image (key) has not already been marked as loaded...
if (!loadedSet.current.has(key)) {
// Mark this image as loaded by adding its key to the Set
loadedSet.current.add(key);
// Increment the loadedImages state by 1
// (tracks how many images have finished loading)
setLoadedImages((prev) => prev + 1);
}
setLoading(false);
};
//function for set signature url
const handleSignatureChange = (drawImage, position) => {
handleWidgetDetails(position, drawImage);
};
const getCanvasRef = (widgetId) => {
if (!canvasRefs.current[widgetId]) {
canvasRefs.current[widgetId] = React.createRef();
}
return canvasRefs.current[widgetId];
};
const clearCanvasById = (widgetId) => {
const canvasRef = canvasRefs.current?.[widgetId];
if (canvasRef?.current) {
canvasRef.current.clear();
}
};
const handleWidgetType = (position, id) => {
switch (position?.type) {
case "checkbox":
return (
<>
<span
className={`${widgetLabelCss(position?.options?.status === "required")} ${widgetTitle}`}
>
{position.options?.name}
</span>
<div className="flex flex-col gap-y-1">
{position.options?.values?.map((data, ind) => (
<div
key={ind}
className="select-none-cls flex items-center text-center gap-0.5"
>
<input
id={`checkbox-${position.key + ind}`}
className="mt-[2px] op-checkbox op-checkbox-xs"
type="checkbox"
checked={selectCheckbox(ind, position)}
onChange={(e) =>
handleCheckboxValue(e.target.checked, ind, position)
}
/>
<label
htmlFor={`checkbox-${position.key + ind}`}
className="text-xs mb-0 text-center ml-[3px] cursor-pointer"
>
{data}
</label>
</div>
))}
</div>
</>
);
case textWidget:
return (
<>
<span
className={`${widgetLabelCss(position?.options?.status === "required")} ${widgetTitle}`}
>
{position.options?.name}
</span>
<ShowTextWidget
position={position}
handleWidgetDetails={handleWidgetDetails}
/>
</>
);
case "dropdown":
return (
<>
<span
className={`${widgetLabelCss(position?.options?.status === "required")} ${widgetTitle}`}
>
{position.options?.name}
</span>
<select
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-base-content w-full"
id="myDropdown"
value={
position?.options?.response || position?.options?.defaultValue
}
onChange={(e) => handleWidgetDetails(position, e.target.value)}
>
{/* Default/Title option */}
<option value="" disabled hidden>
{t("choose-one")}
</option>
{position?.options?.values?.map((data, ind) => (
<option key={ind} value={data}>
{data}
</option>
))}
</select>
</>
);
case "date":
return (
<>
<span
className={`${widgetLabelCss(position?.options?.status === "required")} ${widgetTitle}`}
>
{position.options?.name}
</span>
<DatePicker
portalId="datepicker-portal-root"
renderCustomHeader={({ date, changeYear, changeMonth }) => (
<div className="flex justify-start ml-2">
<select
className="bg-transparent outline-none"
value={months[getMonth(date)]}
onChange={({ target: { value } }) =>
changeMonth(months.indexOf(value))
}
>
{months.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<select
className="bg-transparent outline-none"
value={getYear(date)}
onChange={({ target: { value } }) => changeYear(value)}
>
{years.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
)}
closeOnScroll={true}
selected={handleDate(position)}
onChange={(date) => handleOnDateChange(date, position)}
customInput={<ExampleCustomInput />}
dateFormat={position?.options?.validation?.format || "MM/dd/yyyy"}
/>
</>
);
case "image":
return (
<ImageComponent
position={position}
onImageChange={onImageChange}
handleImageLoaded={handleImageLoaded}
handleClearImage={handleClearImage}
id={id}
/>
);
case radioButtonWidget:
return (
<>
<span
className={`${widgetLabelCss(position?.options?.status === "required")} ${widgetTitle}`}
>
{position.options?.name}
</span>
<div className="flex flex-col gap-y-1">
{position.options?.values.map((data, ind) => (
<div
key={ind}
className="select-none-cls flex items-center text-center gap-0.5"
>
<input
id={`radio-${position.key + ind}`}
className="mt-[2px] op-radio op-radio-xs"
type="radio"
checked={handleRadioCheck(data, position)}
onChange={() => handleWidgetDetails(position, data)}
/>
<label
htmlFor={`radio-${position.key + ind}`}
className="text-xs mb-0 ml-[2px] cursor-pointer"
>
{data}
</label>
</div>
))}
</div>
</>
);
case drawWidget:
return (
<div>
<span
className={`${widgetLabelCss(position?.options?.status === "required")} ${widgetTitle}`}
>
{position.options?.name}
</span>
<Draw
penColor={penColor}
canvasRef={getCanvasRef(position.key)}
currWidgetsDetails={position}
handleSignatureChange={handleSignatureChange}
prefillCls={"prefillCanvas"}
/>
<div className="flex flex-row justify-between mt-[10px]">
<PenColorComponent
penColor={penColor}
setPenColor={setPenColor}
/>
<span
onClick={() => {
clearCanvasById(position?.key);
handleClearImage(position);
}}
className="flex justify-start text-blue-500 underline cursor-pointer"
>
{t("clear")}
</span>
</div>
</div>
);
default:
return position?.SignUrl ? (
<div className="pointer-events-none">
<img
alt="image"
draggable="false"
src={position?.SignUrl}
className="w-full h-full"
/>
</div>
) : (
<div className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full">
No widget
</div>
);
}
};
const handleEmbedPrefill = async (item) => {
//checking is there any draw widget response have base64 url then generate that base64 to url and save it
const prefillWidgets = props.xyPosition?.find((x) => x.Role === "prefill");
let updatedXyPosition = [];
if (prefillWidgets) {
const isDrawWidget = (prefillWidgets.placeHolder ?? []).some((ph) =>
(ph?.pos ?? []).some((p) => p?.type === drawWidget)
);
if (isDrawWidget) {
const getPrefill = props.xyPosition.find((x) => x?.Role === "prefill");
const getPlaceholder = getPrefill?.placeHolder ?? [];
const updatedData = await Promise.all(
getPlaceholder.map(async (page) => ({
...page,
pos: await Promise.all(
page.pos.map(async (item) => {
if (item?.type === drawWidget) {
const widgetResponse = item?.options?.response;
// Skip if no response
if (!widgetResponse) return item;
// If already URL, do not re-upload
if (
typeof widgetResponse === "string" &&
widgetResponse.startsWith("http")
) {
return item;
}
// Convert THIS widget's base64 → URL
const drawUrl = await handleSavePrefillImg(widgetResponse);
return {
...item,
options: {
...item.options,
response: drawUrl
}
};
}
return item;
})
)
}))
);
updatedXyPosition = props.xyPosition.map((obj) => {
if (obj.Role === "prefill") {
return {
...obj,
placeHolder: updatedData
};
}
return obj;
});
props.setXyPosition(updatedXyPosition);
}
}
await props.handleCreateDocument();
};
//`loadOptions` function to use show all list of signer in dropdown
const loadOptions = async (inputValue) => {
try {
const baseURL = localStorage.getItem("baseUrl");
const url = `${baseURL}functions/getsigners`;
const token =
{ "X-Parse-Session-Token": localStorage.getItem("accesstoken") };
const headers = {
"Content-Type": "application/json",
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
...token
};
const search = inputValue;
const axiosRes = await axios.post(url, { search }, { headers });
const contactRes = axiosRes?.data?.result || [];
if (contactRes) {
const res = JSON.parse(JSON.stringify(contactRes));
const result = res;
setUserList(result);
return await result.map((item) => ({
label: `${item.Name}<${item.Email}>`,
value: item.objectId
}));
}
} catch (error) {
console.log("err", error);
}
};
//`handleInputChange` function to get signers list from dropdown
const handleInputChange = (item, id) => {
const signerExist = props.forms.some((x) => x.label === item.label);
if (signerExist) {
alert(t("already-exist-signer"));
} else {
let newForm = [...props.forms];
let signerId = newForm[id].value;
newForm[id].label = item?.label;
// newForm[id].value = item?.value;
props.setForms(newForm);
const getSigner = userList.find((x) => x.objectId === item.value);
props.handleAddUser(getSigner, signerId);
}
};
//show modal to create new contact
const handleCreateNew = (e, id) => {
e.preventDefault();
props.setIsNewContact({ status: true, id: id });
};
const closePopup = () => {
props.setIsNewContact({ status: false, id: "" });
};
return (
<>
<ModalUi
title={uniqueWidget?.length > 0 ? t("prefill-widget") : "Recipients"}
isOpen={true}
handleClose={props.handleClosePrefillModal}
>
<div className="relative">
{(props?.isSubmit || loading) && (
<div className="absolute inset-0 flex items-center justify-center bg-white/70 z-[9999]">
<Loader />
</div>
)}
{uniqueWidget?.length > 0 && (
<div className="py-3 px-[10px] op-card border-[1px] border-gray-400 m-3 md:m-6 text-base-content flex flex-col relative">
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-10 gap-y-4 w-full">
{uniqueWidget.map((x, id) => (
<div key={id} className="flex flex-col gap-2 w-full">
{handleWidgetType(x.widget, id)}
</div>
))}
</div>
</div>
)}
<div>
{props.forms.length > 0 && (
<div className="overflow-y-auto m-3">
{uniqueWidget?.length > 0 && (
<h1 className="font-medium text-[15px] mb-2">
{t("recipients")}
</h1>
)}
<div className="py-3 px-[10px] op-card border-[1px] border-gray-400 text-base-content flex flex-col relative">
{props.forms?.map((field, id) => {
return (
<div className="flex flex-col" key={field?.value}>
<label>{field?.role}</label>
<div className="flex justify-between items-center gap-1">
<div className="flex-1">
<AsyncSelect
cacheOptions
defaultOptions
value={field}
loadingMessage={() => t("loading")}
noOptionsMessage={() => t("contact-not-found")}
loadOptions={loadOptions}
onChange={(item) => handleInputChange(item, id)}
unstyled
onFocus={() => loadOptions()}
classNames={{
control: () =>
"op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full h-full text-[11px]",
valueContainer: () =>
"flex flex-row gap-x-[2px] gap-y-[2px] md:gap-y-0 w-full my-[2px]",
multiValue: () =>
"op-badge op-badge-primary h-full text-[11px]",
multiValueLabel: () => "mb-[2px]",
menu: () =>
"mt-1 shadow-md rounded-lg bg-base-200 text-base-content absolute z-9999",
menuList: () => "shadow-md rounded-lg",
option: () =>
"bg-base-200 text-base-content rounded-lg m-1 hover:bg-base-300 p-2",
noOptionsMessage: () =>
"p-2 bg-base-200 rounded-lg m-1 p-2"
}}
menuPortalTarget={document.getElementById(
"selectSignerModal"
)}
/>
</div>
<button
onClick={(e) => handleCreateNew(e, field.value)}
className="op-btn op-btn-accent op-btn-outline op-btn-sm"
>
<i className="fa-light fa-plus"></i>
</button>
</div>
</div>
);
})}
</div>
</div>
)}
</div>
<div className="flex gap-2 mx-4 mb-3">
<button
disabled={props?.isSubmit}
className="op-btn op-btn-primary op-btn-sm w-[80px]"
onClick={() => handleEmbedPrefill(props?.item)}
>
<span>{t("next")}</span>
</button>
<button
className="op-btn op-btn-ghost op-btn-sm"
onClick={() => props.navigatePageToDoc()}
>
<span>{t("edit-draft")}</span>
</button>
</div>
</div>
</ModalUi>
<ModalUi
title={t("add-contact")}
isOpen={props.isNewContact.status}
handleClose={closePopup}
>
<AddContact
isDisableTitle
isAddYourSelfCheckbox
details={props.handleAddUser}
closePopup={closePopup}
newContactId={props.isNewContact.id}
/>
</ModalUi>
</>
);
}
export default PrefillWidgetModal;
@@ -20,7 +20,7 @@ function PrevNext({ pageNumber, allPages, changePage }) {
onClick={previousPage}
>
<span className="block">
<i className="fa-light fa-backward" aria-hidden="true"></i>
<i className="fa-light fa-chevron-up" aria-hidden="true"></i>
</span>
</button>
<span className="text-xs text-base-content font-medium mx-2 2xl:text-[20px]">
@@ -32,7 +32,7 @@ function PrevNext({ pageNumber, allPages, changePage }) {
onClick={nextPage}
>
<span className="block">
<i className="fa-light fa-forward" aria-hidden="true"></i>
<i className="fa-light fa-chevron-down" aria-hidden="true"></i>
</span>
</button>
</div>
@@ -1,4 +1,4 @@
import React, { useRef, useState } from "react";
import { useRef, useState } from "react";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import {
color,
@@ -12,6 +12,7 @@ const cursor =
const RecipientList = (props) => {
const [animationParent] = useAutoAnimate();
const [isHover, setIsHover] = useState();
const [isPrefill, setIsPrefill] = useState(false);
const [isEdit, setIsEdit] = useState(false);
//function for onhover signer name change background color
const inputRef = useRef(null);
@@ -31,7 +32,7 @@ const RecipientList = (props) => {
e.preventDefault();
};
//handle draggable element drop and also used in mobile view on up and down key to chnage sequence of recipient's list
//handle draggable element drop and also used in mobile view on up and down key to change sequence of recipient's list
const handleChangeSequence = (e, ind, isUp, isDown, obj) => {
e.preventDefault();
let draggedItemId;
@@ -80,8 +81,42 @@ const RecipientList = (props) => {
});
props?.setSignerPos(changeOrderSignerList);
};
const handleSelectRecipient = (e, index, obj, prefill) => {
e.preventDefault();
props?.setIsSelectId(index);
props?.setUniqueId(obj.Id);
props?.setRoleName(obj.Role);
props?.setBlockColor(obj?.blockColor);
props?.handleModal && props?.handleModal();
setIsPrefill(prefill ?? false);
props.setIsTour && props?.setIsTour(false);
};
const isSelected = (ind) => {
const isUserSelected =
(!isMobile && isHover === ind) ||
(!isPrefill && props.isSelectListId === ind);
return isUserSelected;
};
return (
<>
{props?.prefillSigner?.length > 0 &&
props?.prefillSigner?.map((obj, ind) => (
<div
key={ind}
data-tut="prefillTour"
className={`${
props.uniqueId === obj.Id
? "op-bg-primary text-white"
: "transparent text-base-content"
} cursor-pointer px-2 py-1 m-1 mb-2 border-[1px] gap-1 rounded-xl flex justify-center items-center op-border-primary text-[12px] font-bold whitespace-nowrap text-ellipsis`}
onClick={(e) => handleSelectRecipient(e, ind, obj, true)}
>
<i
className={`${props.uniqueId === obj.Id ? "bg-white op-text-primary" : "op-bg-primary text-white"} w-[20px] h-[20px] flex justify-center items-center text-[10px] fa-light fa-signature rounded-full`}
></i>
<span>{obj.Name}</span>
</div>
))}
{props.signersdata.length > 0 &&
props.signersdata.map((obj, ind) => {
return (
@@ -118,20 +153,11 @@ const RecipientList = (props) => {
: color[ind % color.length]
: "transparent"
}}
onClick={(e) => {
e.preventDefault();
props.setIsSelectId(ind);
props.setUniqueId(obj.Id);
props.setRoleName(obj.Role);
props.setBlockColor(obj?.blockColor);
if (props.handleModal) {
props.handleModal();
}
}}
onClick={(e) => handleSelectRecipient(e, ind, obj)}
>
<div className="flex flex-row items-center w-full">
<div className="flex flex-row items-center w-full overflow-hidden pr-2">
<div
className="flex w-[30px] h-[30px] rounded-full items-center justify-center mr-2"
className="flex flex-shrink-0 w-[30px] h-[30px] rounded-full items-center justify-center mr-2"
style={{
background: obj?.blockColor
? darkenColor(obj?.blockColor, 0.4)
@@ -142,38 +168,22 @@ const RecipientList = (props) => {
{isWidgetExist(obj.Id) ? (
<i className="fa-light fa-check"></i>
) : (
<>
{obj.Name
? getFirstLetter(obj.Name)
: getFirstLetter(obj.Role)}
</>
<>{getFirstLetter(obj?.Name ?? obj?.Role)}</>
)}
</span>
</div>
<div
className={`${
obj.Name ? "flex-col" : "flex-row"
} flex items-center`}
className={`${obj.Name ? "flex-col" : "flex-row"} ${
isSelected(ind) ? "text-[#424242]" : "text-base-content"
} flex overflow-hidden flex-grow-0`}
>
{obj.Name ? (
<span
className={`${
(!isMobile && isHover === ind) ||
props.isSelectListId === ind
? "text-[#424242]"
: "text-base-content"
} text-[12px] font-bold w-[100px] whitespace-nowrap overflow-hidden text-ellipsis`}
>
<span className="text-[12px] font-bold truncate whitespace-nowrap">
{obj.Name}
</span>
) : (
<span
className={`${
(!isMobile && isHover === ind) ||
props.isSelectListId === ind
? "text-[#424242]"
: "text-base-content"
} text-[12px] font-bold w-[100px] whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer`}
className="text-[12px] font-bold truncate whitespace-nowrap cursor-pointer"
onClick={() => {
setIsEdit({ [obj.Id]: true });
props.setRoleName(obj.Role);
@@ -182,7 +192,7 @@ const RecipientList = (props) => {
{isEdit?.[obj.Id] && props.handleRoleChange ? (
<input
ref={inputRef}
className="bg-transparent p-[3px]"
className="bg-transparent p-[3px] w-full"
value={obj.Role}
onChange={(e) => props.handleRoleChange(e, obj.Id)}
onBlur={() => {
@@ -204,14 +214,7 @@ const RecipientList = (props) => {
</span>
)}
{obj.Name && (
<span
className={` ${
(!isMobile && isHover === ind) ||
props.isSelectListId === ind
? "text-[#424242]"
: "text-base-content"
} text-[10px] font-medium w-[100px] whitespace-nowrap overflow-hidden text-ellipsis`}
>
<span className="text-[10px] font-medium truncate whitespace-nowrap">
{obj?.Role || obj?.Email}
</span>
)}
@@ -247,18 +250,13 @@ const RecipientList = (props) => {
</div>
</div>
)}
{props.handleDeleteUser && (
{props.handleDeleteUser && obj?.Role !== "prefill" && (
<div
onClick={(e) => {
e.stopPropagation();
props.handleDeleteUser(obj.Id);
}}
className={`${
(!isMobile && isHover === ind) ||
props.isSelectListId === ind
? "text-[#424242]"
: "text-base-content"
} cursor-pointer`}
className={`${isSelected(ind) ? "text-[#424242]" : "text-base-content"} cursor-pointer`}
>
<i className="fa-light fa-trash-can 2xl:text-[22px]"></i>
</div>
@@ -3,7 +3,13 @@ import { useTranslation } from "react-i18next";
import { Document, Page } from "react-pdf";
import { useSelector } from "react-redux";
import { PDFDocument } from "pdf-lib";
import { base64ToArrayBuffer } from "../../constant/Utils";
import {
base64ToArrayBuffer,
decryptPdf,
flattenPdf,
getFileAsArrayBuffer
} from "../../constant/Utils";
import { maxFileSize } from "../../constant/const";
function RenderAllPdfPage(props) {
const { t } = useTranslation();
@@ -11,7 +17,7 @@ function RenderAllPdfPage(props) {
const mergePdfInputRef = useRef(null);
const [signPageNumber, setSignPageNumber] = useState([]);
const [bookmarkColor, setBookmarkColor] = useState("");
const isHeader = useSelector((state) => state.showHeader);
const isSidebar = useSelector((state) => state.sidebar.isOpen);
const [pageWidth, setPageWidth] = useState("");
//set all number of pages after load pdf
@@ -42,10 +48,10 @@ function RenderAllPdfPage(props) {
};
// Use setTimeout to wait for the transition to complete
const timer = setTimeout(updateSize, 100); // match the transition duration
const timer = setTimeout(updateSize, 150); // match the transition duration
return () => clearTimeout(timer);
}, [isHeader, pageContainer, props?.containerWH]);
}, [isSidebar, pageContainer, props?.containerWH]);
//'function `addSignatureBookmark` is used to display the page where the user's signature is located.
const addSignatureBookmark = (index) => {
const ispageNumber = signPageNumber.includes(index + 1);
@@ -62,19 +68,69 @@ function RenderAllPdfPage(props) {
};
const pdfDataBase64 = `data:application/pdf;base64,${props?.pdfBase64Url}`;
// `removeFile` is used to remove file if exists
const removeFile = (e) => {
if (e) {
e.target.value = "";
}
};
// `handleFileUpload` is trigger when user click on add pages btn and is used to merge multiple pdf
const handleFileUpload = async (e) => {
const file = e.target.files[0];
if (!file) {
alert("Please upload a valid PDF file.");
alert(t("please-select-pdf"));
return;
}
if (!file.type.includes("pdf")) {
alert("Only PDF files are allowed.");
alert(t("only-pdf-allowed"));
return;
}
const fileSize =
maxFileSize;
const pdfsize = file?.size;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e);
return;
}
try {
const uploadedPdfBytes = await file.arrayBuffer();
let uploadedPdfBytes = await file.arrayBuffer();
try {
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
} catch (err) {
if (err?.message?.includes("is encrypted")) {
try {
const pdfFile = await decryptPdf(file, "");
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
} catch (err) {
if (err?.response?.status === 401) {
const password = prompt(
`PDF "${file.name}" is password-protected. Enter password:`
);
if (password) {
try {
const pdfFile = await decryptPdf(file, password);
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
// Upload the file to Parse Server
} catch (err) {
console.error("Incorrect password or decryption failed", err);
alert(t("incorrect-password-or-decryption-failed"));
}
} else {
alert(t("provide-password"));
}
} else {
console.log("Err ", err);
alert(t("error-uploading-pdf"));
}
}
} else {
alert(t("error-uploading-pdf"));
}
}
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
ignoreEncryption: true
});
@@ -91,6 +147,13 @@ function RenderAllPdfPage(props) {
useObjectStreams: false
});
const pdfBuffer = base64ToArrayBuffer(pdfBase64);
const pdfsize = pdfBuffer?.byteLength;
const fileSizeBytes = fileSize * 1024 * 1024;
if (pdfsize > fileSizeBytes) {
alert(`${t("file-alert-1")} ${fileSize} MB`);
removeFile(e);
return;
}
props.setPdfArrayBuffer(pdfBuffer);
props.setPdfBase64Url(pdfBase64);
props.setIsUploadPdf && props.setIsUploadPdf(true);
@@ -110,6 +173,7 @@ function RenderAllPdfPage(props) {
autoSignScroll hide-scrollbar max-h-[100vh] `}
>
<Document
error=""
loading={t("loading-doc")}
onLoadSuccess={onDocumentLoad}
file={pdfDataBase64}
@@ -155,8 +219,8 @@ function RenderAllPdfPage(props) {
onChange={handleFileUpload}
/>
<i className="fa-light fa-plus text-gray-500"></i>
<span className="text-xs lg:text-sm text-base-content ">
Add pages
<span className="text-xs lg:text-sm text-base-content">
{t("add-pages")}
</span>
</button>
)}
@@ -1,729 +0,0 @@
import React, { useState } from "react";
import RSC from "react-scrollbars-custom";
import { Document, Page } from "react-pdf";
import {
defaultWidthHeight,
getContainerScale,
handleImageResize,
handleSignYourselfImageResize,
isMobile
} from "../../constant/Utils";
import Placeholder from "./Placeholder";
import Alert from "../../primitives/Alert";
import { useTranslation } from "react-i18next";
function RenderPdf(props) {
const { t } = useTranslation();
const [scaledHeight, setScaledHeight] = useState();
//check isGuestSigner is present in local if yes than handle login flow header in mobile view
const isGuestSigner = localStorage.getItem("isGuestSigner");
// handle signature block width and height according to screen
const posWidth = (pos, signYourself) => {
const containerScale = getContainerScale(
props.pdfOriginalWH,
props.pageNumber,
props.containerWH
);
const defaultWidth = defaultWidthHeight(pos.type).width;
const posWidth = pos.Width ? pos.Width : defaultWidth;
if (signYourself) {
return posWidth * props.scale * containerScale;
} else {
if (pos.isMobile && pos.scale) {
if (pos.IsResize) {
if (props.scale > 1) {
return posWidth * pos.scale * containerScale * props.scale;
} else {
return posWidth * containerScale;
}
} else {
if (props.scale > 1) {
return posWidth * pos.scale * containerScale * props.scale;
} else {
return posWidth * pos.scale * containerScale;
}
}
} else {
return posWidth * props.scale * containerScale;
}
}
};
const posHeight = (pos, signYourself) => {
const containerScale = getContainerScale(
props.pdfOriginalWH,
props.pageNumber,
props.containerWH
);
const posHeight = pos.Height || defaultWidthHeight(pos.type).height;
if (signYourself) {
return posHeight * props.scale * containerScale;
} else {
if (pos.isMobile && pos.scale) {
if (pos.IsResize) {
if (props.scale > 1) {
return posHeight * pos.scale * containerScale * props.scale;
} else {
return posHeight * containerScale;
}
} else {
if (props.scale > 1) {
return posHeight * pos.scale * containerScale * props.scale;
} else {
return posHeight * pos.scale * containerScale;
}
}
} else {
return posHeight * props.scale * containerScale;
}
}
};
//function for render placeholder block over pdf document
const checkSignedSigners = (data) => {
let checkSign = [];
//condition to handle quick send flow and using normal request sign flow
checkSign = props.signedSigners
? props.signedSigners?.filter(
(sign) =>
sign?.Id === data?.Id || sign?.objectId === data?.signerObjId
)
: [];
const handleAllUserName = (Id, Role, type, pos) => {
return (
<React.Fragment>
<div className="text-black text-[8px] font-bold">
{
props.pdfDetails[0].Signers?.find(
(signer) => signer.objectId === data.signerObjId
)?.Name
}
</div>
{type && (
<div
style={{ fontSize: pos ? calculateFontsize(pos) : "11px" }}
className="font-bold"
>
{type}
</div>
)}
{Role && (
<div
style={{ fontSize: pos ? calculateFontsize(pos) : "8px" }}
className="text-black font-medium"
>
{`(${Role})`}
</div>
)}
</React.Fragment>
);
};
return (
checkSign.length === 0 &&
data?.placeHolder?.map((placeData, key) => {
return (
<React.Fragment key={key}>
{placeData.pageNumber === props.pageNumber &&
placeData.pos.map((pos) => {
return (
pos && (
<React.Fragment key={pos.key}>
<Placeholder
pos={pos}
setSignKey={props.setSignKey}
setIsSignPad={props.setIsSignPad}
setIsStamp={props.setIsStamp}
handleSignYourselfImageResize={handleImageResize}
index={props.pageNumber}
xyPosition={props.signerPos}
setXyPosition={props.setSignerPos}
data={data}
setIsResize={props.setIsResize}
isShowBorder={props.isSelfSign}
isAlllowModify={props.isAlllowModify}
signerObjId={props.signerObjectId}
isShowDropdown={true}
isNeedSign={props.pdfRequest}
isSelfSign={true}
isSignYourself={false}
posWidth={posWidth}
posHeight={posHeight}
handleUserName={handleAllUserName}
isDragging={props.isDragging}
pdfDetails={props.pdfDetails}
setIsInitial={props.setIsInitial}
setValidateAlert={props.setValidateAlert}
unSignedWidgetId={props.unSignedWidgetId}
setSelectWidgetId={props.setSelectWidgetId}
selectWidgetId={props.selectWidgetId}
setCurrWidgetsDetails={props.setCurrWidgetsDetails}
uniqueId={props.uniqueId}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
ispublicTemplate={props.ispublicTemplate}
handleUserDetails={props.handleUserDetails}
isResize={props.isResize}
setIsAgreeTour={props.setIsAgreeTour}
isAgree={props.isAgree}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
setWidgetType={props.setWidgetType}
setUniqueId={props.setUniqueId}
setIsSelectId={props.setIsSelectId}
handleDeleteSign={props.handleDeleteSign}
setIsPageCopy={props.setIsPageCopy}
handleTextSettingModal={props.handleTextSettingModal}
setIsCheckbox={props.setIsCheckbox}
isFreeResize={false}
isOpenSignPad={true}
assignedWidgetId={props.assignedWidgetId}
isApplyAll={true}
setFontSize={props.setFontSize}
fontSize={props.fontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
setRequestSignTour={props.setRequestSignTour}
/>
</React.Fragment>
)
);
})}
</React.Fragment>
);
})
);
};
const calculateFontsize = (pos) => {
const width = posWidth(pos);
const height = posHeight(pos);
if (height === width || height < width) {
return `${height / 5}px`;
} else if (width < height) {
return `${width / 10}px`;
}
};
//function for render placeholder block over pdf document
const handleUserName = (Id, Role, type, pos) => {
if (Id) {
const checkSign = props.signersdata.find((sign) => sign.Id === Id);
if (checkSign?.Name) {
return (
<>
<div
style={{
fontSize: pos ? calculateFontsize(pos) : "8px"
}}
className="text-black font-medium"
>
{checkSign?.Name}
</div>
{type && (
<div
style={{
fontSize: pos ? calculateFontsize(pos) : "11px"
}}
className=" font-bold"
>
{type}
</div>
)}
{Role && (
<div
style={{
fontSize: pos ? calculateFontsize(pos) : "8px"
}}
className="text-black text-[8px] font-medium"
>
{`(${Role})`}
</div>
)}
</>
);
} else {
return (
<>
{type && (
<div
style={{ fontSize: pos ? calculateFontsize(pos) : "11px" }}
className=" font-bold"
>
{type}
</div>
)}
<div
style={{ fontSize: pos ? calculateFontsize(pos) : "8px" }}
className="text-black font-medium"
>
{Role}
</div>
</>
);
}
} else {
return (
<>
{type && <div className="text-[11px] font-bold">{type}</div>}
<div className="text-black text-[8px] font-medium">{Role}</div>
</>
);
}
};
const pdfDataBase64 = `data:application/pdf;base64,${props.pdfBase64Url}`;
//calculate render height of pdf in mobile view
const handlePageLoadSuccess = (page) => {
const containerWidth = props.divRef.current.offsetWidth; // Get container width
const viewport = page.getViewport({ scale: 1 });
const scale = containerWidth / viewport.width; // Scale to fit container width
const scaleHeight = viewport.height * scale;
setScaledHeight(scaleHeight);
};
return (
<>
{props.successEmail && (
<Alert type={"success"}>{t("success-email-alert")}</Alert>
)}
{isMobile ? (
<RSC
style={{
position: "relative",
boxShadow: "rgba(17, 12, 46, 0.15) 0px 48px 100px 0px",
//49 is height of header
height: isGuestSigner ? window.innerHeight - 49 : scaledHeight,
zIndex: 0
}}
noScrollY={props.scale === 1 ? true : false}
noScrollX={props.scale === 1 ? true : false}
>
<div
data-tut="reactourForth"
className={`${
isGuestSigner ? "30px" : ""
} border-[0.1px] border-[#ebe8e8] overflow-x-auto`}
style={{
width:
props.containerWH?.width &&
props.containerWH?.width * props.scale
}}
ref={props.drop}
id="container"
>
{props.containerWH?.width &&
props.pdfOriginalWH.length > 0 &&
(props.pdfRequest || props.isSelfSign
? props.signerPos?.map((data, key) => {
return (
<React.Fragment key={key}>
{checkSignedSigners(data)}
</React.Fragment>
);
})
: props.placeholder // placeholder mobile
? props.signerPos?.map((data, ind) => {
return (
<React.Fragment key={ind}>
{data?.placeHolder &&
data?.placeHolder.map((placeData, index) => {
return (
<React.Fragment key={index}>
{placeData.pageNumber === props.pageNumber &&
placeData.pos.map((pos) => {
return (
<React.Fragment key={pos.key}>
<Placeholder
pos={pos}
setIsPageCopy={props.setIsPageCopy}
setSignKey={props.setSignKey}
handleDeleteSign={
props.handleDeleteSign
}
setIsStamp={props.setIsStamp}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
handleSignYourselfImageResize={
handleImageResize
}
index={props.pageNumber}
xyPosition={props.signerPos}
setXyPosition={props.setSignerPos}
data={data}
setIsResize={props.setIsResize}
setShowDropdown={
props.setShowDropdown
}
isShowBorder={true}
isPlaceholder={true}
setUniqueId={props.setUniqueId}
handleLinkUser={
props.handleLinkUser
}
handleUserName={handleUserName}
isSignYourself={false}
posWidth={posWidth}
posHeight={posHeight}
isDragging={props.isDragging}
setIsValidate={props.setIsValidate}
setWidgetType={props.setWidgetType}
setIsRadio={props.setIsRadio}
setIsCheckbox={props.setIsCheckbox}
setCurrWidgetsDetails={
props.setCurrWidgetsDetails
}
setSelectWidgetId={
props.setSelectWidgetId
}
selectWidgetId={
props.selectWidgetId
}
handleNameModal={
props.handleNameModal
}
setTempSignerId={
props.setTempSignerId
}
uniqueId={props.uniqueId}
handleTextSettingModal={
props.handleTextSettingModal
}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
setIsSelectId={props.setIsSelectId}
fontSize={props.fontSize}
setFontSize={props.setFontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
isResize={props.isResize}
unSignedWidgetId={
props.unSignedWidgetId
}
isFreeResize={true}
/>
</React.Fragment>
);
})}
</React.Fragment>
);
})}
</React.Fragment>
);
})
: !props.pdfDetails?.[0]?.IsCompleted &&
props.xyPosition?.map((data, ind) => {
return (
<React.Fragment key={ind}>
{data.pageNumber === props.pageNumber &&
data.pos.map((pos, id) => {
return (
pos && (
<Placeholder
key={id}
pos={pos}
setIsPageCopy={props.setIsPageCopy}
setSignKey={props.setSignKey}
handleDeleteSign={props.handleDeleteSign}
setIsStamp={props.setIsStamp}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
handleSignYourselfImageResize={
handleSignYourselfImageResize
}
index={props.index}
xyPosition={props.xyPosition}
setXyPosition={props.setXyPosition}
containerWH={props.containerWH}
setIsSignPad={props.setIsSignPad}
isShowBorder={true}
isSignYourself={true}
posWidth={posWidth}
posHeight={posHeight}
pdfDetails={props.pdfDetails[0]}
isDragging={props.isDragging}
setIsInitial={props.setIsInitial}
setWidgetType={props.setWidgetType}
setSelectWidgetId={props.setSelectWidgetId}
selectWidgetId={props.selectWidgetId}
handleUserName={handleUserName}
setIsCheckbox={props.setIsCheckbox}
setValidateAlert={props.setValidateAlert}
setCurrWidgetsDetails={
props.setCurrWidgetsDetails
}
handleTextSettingModal={
props.handleTextSettingModal
}
scale={props.scale}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
fontSize={props.fontSize}
setFontSize={props.setFontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
isResize={props.isResize}
setIsResize={props.setIsResize}
isFreeResize={false}
isOpenSignPad={true}
/>
)
);
})}
</React.Fragment>
);
}))}
<Document
onLoadError={() => props.setPdfLoad(false)}
loading={t("loading-doc")}
onLoadSuccess={props.pageDetails}
// ref={pdfRef}'
onClick={() => {
if (props.setSelectWidgetId) {
props.setSelectWidgetId("");
}
}}
file={pdfDataBase64}
>
<Page
onLoadSuccess={handlePageLoadSuccess}
scale={props.scale || 1}
key={props.index}
pageNumber={props.pageNumber}
width={props.containerWH.width}
renderAnnotationLayer={false}
renderTextLayer={false}
onGetAnnotationsError={(error) => {
console.log("annotation error", error);
}}
/>
</Document>
</div>
</RSC>
) : (
<RSC
style={{
position: "relative",
boxShadow: "rgba(17, 12, 46, 0.15) 0px 48px 100px 0px",
height: window.innerHeight + "px",
zIndex: 0
}}
noScrollY={false}
noScrollX={props.scale === 1 ? true : false}
>
<div
style={{
width:
props.containerWH?.width &&
props.containerWH?.width * props.scale
}}
ref={props.drop}
id="container"
>
{props.pdfLoad &&
props.containerWH?.width &&
props.pdfOriginalWH.length > 0 &&
(props.pdfRequest || props.isSelfSign //pdf request sign flow
? props.signerPos?.map((data, key) => {
return (
<React.Fragment key={key}>
{checkSignedSigners(data)}
</React.Fragment>
);
})
: props.placeholder //placeholder and template flow
? props.signerPos.map((data, ind) => {
return (
<React.Fragment key={ind}>
{data?.placeHolder &&
data?.placeHolder.map((placeData, index) => {
return (
<React.Fragment key={index}>
{placeData.pageNumber === props.pageNumber &&
placeData.pos.map((pos) => {
return (
<React.Fragment key={pos.key}>
<Placeholder
pos={pos}
setIsPageCopy={props.setIsPageCopy}
setSignKey={props.setSignKey}
handleDeleteSign={
props.handleDeleteSign
}
setIsStamp={props.setIsStamp}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
handleSignYourselfImageResize={
handleImageResize
}
index={props.pageNumber}
xyPosition={props.signerPos}
setXyPosition={props.setSignerPos}
data={data}
setIsResize={props.setIsResize}
setShowDropdown={
props.setShowDropdown
}
isShowBorder={true}
isPlaceholder={true}
setUniqueId={props.setUniqueId}
handleLinkUser={
props.handleLinkUser
}
handleUserName={handleUserName}
isSignYourself={false}
posWidth={posWidth}
posHeight={posHeight}
isDragging={props.isDragging}
setIsValidate={props.setIsValidate}
setWidgetType={props.setWidgetType}
setIsRadio={props.setIsRadio}
setIsCheckbox={props.setIsCheckbox}
setCurrWidgetsDetails={
props.setCurrWidgetsDetails
}
setSelectWidgetId={
props.setSelectWidgetId
}
selectWidgetId={
props.selectWidgetId
}
handleNameModal={
props.handleNameModal
}
setTempSignerId={
props.setTempSignerId
}
uniqueId={props.uniqueId}
handleTextSettingModal={
props.handleTextSettingModal
}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
setIsSelectId={props.setIsSelectId}
fontSize={props.fontSize}
setFontSize={props.setFontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
isResize={props.isResize}
unSignedWidgetId={
props.unSignedWidgetId
}
isFreeResize={true}
/>
</React.Fragment>
);
})}
</React.Fragment>
);
})}
</React.Fragment>
);
})
: !props.pdfDetails?.[0]?.IsCompleted &&
props.xyPosition?.map((data, ind) => {
// signyourself flow
return (
<React.Fragment key={ind}>
{data.pageNumber === props.pageNumber &&
data.pos.map((pos) => {
return (
<React.Fragment key={pos.key}>
<Placeholder
pos={pos}
setIsPageCopy={props.setIsPageCopy}
setSignKey={props.setSignKey}
handleDeleteSign={props.handleDeleteSign}
setIsStamp={props.setIsStamp}
handleTabDrag={props.handleTabDrag}
handleStop={(event, dragElement) =>
props.handleStop(
event,
dragElement,
pos.type
)
}
handleSignYourselfImageResize={
handleSignYourselfImageResize
}
index={props.index}
xyPosition={props.xyPosition}
setXyPosition={props.setXyPosition}
setIsSignPad={props.setIsSignPad}
isShowBorder={true}
isSignYourself={true}
posWidth={posWidth}
posHeight={posHeight}
pdfDetails={props.pdfDetails[0]}
isDragging={props.isDragging}
setIsInitial={props.setIsInitial}
setWidgetType={props.setWidgetType}
setSelectWidgetId={props.setSelectWidgetId}
selectWidgetId={props.selectWidgetId}
handleUserName={handleUserName}
setIsCheckbox={props.setIsCheckbox}
setValidateAlert={props.setValidateAlert}
setCurrWidgetsDetails={
props.setCurrWidgetsDetails
}
handleTextSettingModal={
props.handleTextSettingModal
}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
fontSize={props.fontSize}
setFontSize={props.setFontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
isResize={props.isResize}
setIsResize={props.setIsResize}
isFreeResize={false}
isOpenSignPad={true}
/>
</React.Fragment>
);
})}
</React.Fragment>
);
}))}
{/* this component for render pdf document is in middle of the component */}
<Document
onLoadError={() => props.setPdfLoad(false)}
loading={t("loading-doc")}
onLoadSuccess={props.pageDetails}
onClick={() => {
if (props.setSelectWidgetId) {
props.setSelectWidgetId("");
}
}}
file={pdfDataBase64}
>
<Page
key={props.index}
width={props.containerWH.width}
scale={props.scale || 1}
className={"-z-[1]"} // when user zoom-in in tablet widgets move backward that's why pass -z-[1]
pageNumber={props.pageNumber}
renderAnnotationLayer={false}
renderTextLayer={false}
onGetAnnotationsError={(error) => {
console.log("annotation error", error);
}}
/>
</Document>
</div>
</RSC>
)}
</>
);
}
export default RenderPdf;
@@ -0,0 +1,570 @@
import React, {
useState,
useRef,
useCallback,
useLayoutEffect,
useEffect
} from "react";
import RSC from "react-scrollbars-custom";
import { Document, Page } from "react-pdf";
import {
defaultWidthHeight,
getContainerScale,
handleImageResize,
handleSignYourselfImageResize,
isMobile
} from "../../constant/Utils";
import Placeholder from "./Placeholder";
import Alert from "../../primitives/Alert";
import { useTranslation } from "react-i18next";
import usePdfPinchZoom from "../../hook/usePdfPinchZoom";
import { useDispatch, useSelector } from "react-redux";
import Guidelines from "./Guidelines";
import { useGuidelinesContext } from "../../context/GuidelinesContext";
import { toggleSidebar } from "../../redux/reducers/sidebarReducer";
import DragGuidelinesLayer from "./DragGridLinesLayer";
import { useDrop } from "react-dnd";
import WidgetsDragPreview from "./WidgetsDragPreview";
function RenderPdf(props) {
const { t } = useTranslation();
const dispatch = useDispatch();
const { guideline, showGuidelines } = useGuidelinesContext();
const [, drop] = useDrop({
accept: "BOX",
drop: (item, monitor) => {
props?.addPositionOfSignature(item, monitor);
showGuidelines(false);
}
});
const [scaledHeight, setScaledHeight] = useState();
//check isGuestSigner is present in local if yes than handle login flow header in mobile view
const isGuestSigner = localStorage.getItem("isGuestSigner");
const scrollTriggerId = useSelector((state) => state.widget.scrollTriggerId);
const isOpen = useSelector((state) => state.sidebar.isOpen);
const scrollRef = useRef(null);
const pdfContainerRef = useRef(null);
useEffect(() => {
dispatch(toggleSidebar(false));
return () => {
dispatch(toggleSidebar(true));
};
}, []);
// enable pinch to zoom only on actual pdf wrapper
usePdfPinchZoom(
pdfContainerRef,
props.scale,
props.setScale,
props.setZoomPercent
);
// handle signature block width and height according to screen
const posWidth = (pos, signYourself) => {
const containerScale = getContainerScale(
props.pdfOriginalWH,
props.pageNumber,
props.containerWH
);
const defaultWidth = defaultWidthHeight(pos.type).width;
const posWidth = pos.Width ? pos.Width : defaultWidth;
if (signYourself) {
return posWidth * props.scale * containerScale;
} else {
if (pos.isMobile && pos.scale) {
if (pos.IsResize) {
if (props.scale > 1) {
return posWidth * pos.scale * containerScale * props.scale;
} else {
return posWidth * containerScale;
}
} else {
if (props.scale > 1) {
return posWidth * pos.scale * containerScale * props.scale;
} else {
return posWidth * pos.scale * containerScale;
}
}
} else {
return posWidth * props.scale * containerScale;
}
}
};
const posHeight = (pos, signYourself) => {
const containerScale = getContainerScale(
props.pdfOriginalWH,
props.pageNumber,
props.containerWH
);
const posHeight = pos.Height || defaultWidthHeight(pos.type).height;
if (signYourself) {
return posHeight * props.scale * containerScale;
} else {
if (pos.isMobile && pos.scale) {
if (pos.IsResize) {
if (props.scale > 1) {
return posHeight * pos.scale * containerScale * props.scale;
} else {
return posHeight * containerScale;
}
} else {
if (props.scale > 1) {
return posHeight * pos.scale * containerScale * props.scale;
} else {
return posHeight * pos.scale * containerScale;
}
}
} else {
return posHeight * props.scale * containerScale;
}
}
};
// `smoothScrollTo` is used to provide smooth scrolling while focus on widget
const smoothScrollTo = (targetY, duration = 500) => {
const sb = scrollRef.current;
if (!sb) return;
const start = sb.scrollTop;
const change = targetY - start;
const startTime = performance.now();
const animate = (now) => {
const elapsed = now - startTime;
const t = Math.min(1, elapsed / duration);
// easeInOutQuad
const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
sb.scrollTo(sb.scrollLeft, start + change * ease);
if (t < 1) requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
};
// `scrollToTarget` is used to focus on widget and scroll to top
const scrollToTarget = useCallback(() => {
// Get the scrollbar container ref
const sb = scrollRef.current;
// If there's no content element or no widget details, bail out
if (!sb?.contentElement || !props.currWidgetsDetails) return;
// The absolute Y position (relative to the document) where we want to scroll
const yPosition = props?.currWidgetsDetails?.yPosition || 0;
const containerScale = getContainerScale(
props.pdfOriginalWH,
props.pageNumber,
props.containerWH
);
const targetTop = yPosition * containerScale * props.scale;
// The Y offset of the scrollable content container itself
const { offsetTop } = sb.contentElement;
// Account for the header height + a little extra padding
// Different header height if user is a guest signer
const headerOffset = isGuestSigner ? 10 : 79;
// Compute the scroll position inside the container
const positionTop = targetTop - offsetTop - headerOffset;
const pageNumber = props.pageNumber > 0 ? props.pageNumber - 1 : 0;
const ogH = props.pdfOriginalWH[pageNumber]?.height;
// If the modal for this widget is open, we may need to expand the PDF container
if (props.isShowModal[scrollTriggerId]) {
// Original PDF height for this page
if (pdfContainerRef?.current) {
// Increase container height to include the area up to the target
pdfContainerRef.current.style.height = `${ogH + (targetTop - offsetTop)}px`;
}
} else {
// Otherwise, reset any inline height override
if (pdfContainerRef?.current) {
pdfContainerRef.current.style.height = "";
}
}
// Actually perform the scroll: keep the same horizontal scroll, scroll vertically
if (targetTop > ogH * 0.75 && !isGuestSigner && isOpen) {
smoothScrollTo(positionTop - 300);
} else if (targetTop > ogH * 0.75 && !isGuestSigner && !isOpen) {
smoothScrollTo(positionTop - 100);
} else {
smoothScrollTo(positionTop);
}
// Highlight only the target widget; reset others to default border
document.querySelectorAll(".signYourselfBlock").forEach((w) => {
w.style.border =
w.id === String(scrollTriggerId)
? "1.5px solid red" // active widget in red
: "1.5px solid #007bff"; // others in blue
});
}, [
scrollTriggerId,
props.currWidgetsDetails?.yPosition,
props.isShowModal,
props.pdfOriginalWH,
props.pageNumber
]);
useLayoutEffect(() => {
// Whenever scrollTriggerId changes, fire off the scroll in the next rAF
// to ensure the DOM has painted/layout is stable before scrolling
if (scrollTriggerId) {
scrollToTarget();
}
}, [scrollTriggerId, scrollToTarget]);
// function for render placeholder block over pdf document (all signing flow)
const checkSignedSigners = (data) => {
let checkSign = [];
//condition to handle quick send flow and using normal request sign flow
checkSign = props.signedSigners
? props.signedSigners?.filter(
(sign) =>
sign?.Id === data?.Id || sign?.objectId === data?.signerObjId
)
: [];
return (
checkSign.length === 0 &&
data?.placeHolder?.map((placeData, key) => (
<React.Fragment key={key}>
{placeData.pageNumber === props.pageNumber &&
placeData.pos.map(
(pos, ind) =>
pos && (
<React.Fragment key={ind}>
<Placeholder
pos={pos}
handleSignYourselfImageResize={handleImageResize}
index={props.pageNumber}
xyPosition={props.signerPos}
setXyPosition={props.setSignerPos}
data={data}
setIsResize={props.setIsResize}
isShowBorder={props.isSelfSign}
isAlllowModify={props.isAlllowModify}
signerObjId={props.signerObjectId}
isShowDropdown={true}
isNeedSign={props.pdfRequest}
isSelfSign={true}
isSignYourself={false}
posWidth={posWidth}
posHeight={posHeight}
isDragging={props.isDragging}
pdfDetails={props.pdfDetails}
unSignedWidgetId={props.unSignedWidgetId}
setCurrWidgetsDetails={props.setCurrWidgetsDetails}
uniqueId={props.uniqueId}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
ispublicTemplate={props.ispublicTemplate}
handleUserDetails={props.handleUserDetails}
isResize={props.isResize}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
setUniqueId={props.setUniqueId}
setIsSelectId={props.setIsSelectId}
handleDeleteWidget={props.handleDeleteWidget}
setIsPageCopy={props.setIsPageCopy}
handleTextSettingModal={props.handleTextSettingModal}
handleCellSettingModal={props.handleCellSettingModal}
setIsCheckbox={props.setIsCheckbox}
isFreeResize={props.isSelfSign ? true : false}
isOpenSignPad={true}
assignedWidgetId={props.assignedWidgetId}
setCellCount={props.setCellCount}
setFontSize={props.setFontSize}
fontSize={props.fontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
setIsReqSignTourDisabled={props.setIsReqSignTourDisabled}
calculateFontsize={calculateFontsize}
currWidgetsDetails={props?.currWidgetsDetails}
handleNameModal={props?.handleNameModal}
/>
</React.Fragment>
)
)}
</React.Fragment>
))
);
};
const calculateFontsize = (pos) => {
const width = posWidth(pos);
const height = posHeight(pos);
if (height === width || height < width) {
return `${height / 5}px`;
} else if (width < height) {
return `${width / 10}px`;
}
};
const pdfDataBase64 = `data:application/pdf;base64,${props.pdfBase64Url}`;
// calculate render height of pdf in mobile view
const handlePageLoadSuccess = (page) => {
if (isMobile) {
const containerWidth = props.divRef.current.offsetWidth; // Get container width
const viewport = page.getViewport({ scale: 1 });
const scale = containerWidth / viewport.width; // Scale to fit container width
const scaleHeight = viewport.height * scale;
setScaledHeight(scaleHeight);
}
};
return (
<>
{props.successEmail && (
<Alert type={"success"}>{t("success-email-alert")}</Alert>
)}
<RSC
ref={scrollRef}
style={{
position: "relative",
boxShadow: "rgba(17, 12, 46, 0.15) 0px 48px 100px 0px",
height: isMobile
? isGuestSigner
? window.innerHeight - 49 // 49 is height of header
: scaledHeight
: `${window.innerHeight}px`,
zIndex: 0
}}
noScrollY={isMobile ? props.scale === 1 : false}
noScrollX={props.scale === 1}
>
<div
data-tut={isMobile ? "reactourForth" : undefined}
className={
isMobile
? `${isGuestSigner ? "30px" : ""} border-[0.1px] border-[#ebe8e8] overflow-x-auto relative`
: "relative"
}
style={{
width:
props.containerWH?.width && props.containerWH?.width * props.scale
}}
ref={(node) => {
pdfContainerRef.current = node;
drop && drop(node);
}}
id="container"
>
{props.pdfLoad !== false &&
props.containerWH?.width &&
props.pdfOriginalWH.length > 0 && (
<>
{props.pdfRequest || props.isSelfSign
? // request sign, guest sign,
props.signerPos?.map((data, key) => (
<React.Fragment key={key}>
{checkSignedSigners(data)}
</React.Fragment>
))
: props.placeholder // placeholdersign document, draft document, create template, draft template
? props.signerPos?.map((data, ind) => (
<React.Fragment key={ind}>
{data?.placeHolder &&
data?.placeHolder.map((placeData, index) => (
<React.Fragment key={index}>
{placeData.pageNumber === props.pageNumber &&
placeData.pos.map((pos) => (
<React.Fragment key={pos.key}>
<Placeholder
pos={pos}
setIsPageCopy={props.setIsPageCopy}
handleDeleteWidget={
props.handleDeleteWidget
}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
handleSignYourselfImageResize={
handleImageResize
}
index={props.pageNumber}
xyPosition={props.signerPos}
setXyPosition={props.setSignerPos}
data={data}
setIsResize={props.setIsResize}
setShowDropdown={props.setShowDropdown}
isShowBorder={true}
isPlaceholder={true}
setUniqueId={props.setUniqueId}
handleLinkUser={props.handleLinkUser}
isSignYourself={false}
posWidth={posWidth}
posHeight={posHeight}
isDragging={props.isDragging}
setIsValidate={props.setIsValidate}
setIsRadio={props.setIsRadio}
setIsCheckbox={props.setIsCheckbox}
setCurrWidgetsDetails={
props.setCurrWidgetsDetails
}
handleNameModal={props.handleNameModal}
uniqueId={props.uniqueId}
handleTextSettingModal={
props.handleTextSettingModal
}
handleCellSettingModal={
props.handleCellSettingModal
}
scale={props.scale}
containerWH={props.containerWH}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
setIsSelectId={props.setIsSelectId}
fontSize={props.fontSize}
setFontSize={props.setFontSize}
setCellCount={props.setCellCount}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
isResize={props.isResize}
unSignedWidgetId={
props.unSignedWidgetId
}
isFreeResize={true}
calculateFontsize={calculateFontsize}
currWidgetsDetails={
props?.currWidgetsDetails
}
setRoleName={props?.setRoleName}
pdfDetails={props.pdfDetails}
setIsReqSignTourDisabled={
props.setIsReqSignTourDisabled
}
/>
</React.Fragment>
))}
</React.Fragment>
))}
</React.Fragment>
))
: !props.pdfDetails?.[0]?.IsCompleted && // signyourself flow
props.xyPosition?.map((data, ind) => (
<React.Fragment key={ind}>
{data.pageNumber === props.pageNumber &&
data.pos.map(
(pos, id) =>
pos && (
<Placeholder
key={id}
pos={pos}
setIsPageCopy={props.setIsPageCopy}
handleDeleteWidget={
props.handleDeleteWidget
}
handleTabDrag={props.handleTabDrag}
handleStop={props.handleStop}
handleSignYourselfImageResize={
handleSignYourselfImageResize
}
index={props.index}
xyPosition={props.xyPosition}
setXyPosition={props.setXyPosition}
containerWH={props.containerWH}
isShowBorder={true}
isSignYourself={true}
posWidth={posWidth}
posHeight={posHeight}
isDragging={props.isDragging}
setIsCheckbox={props.setIsCheckbox}
setCurrWidgetsDetails={
props.setCurrWidgetsDetails
}
handleTextSettingModal={
props.handleTextSettingModal
}
handleCellSettingModal={
props.handleCellSettingModal
}
scale={props.scale}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
fontSize={props.fontSize}
setFontSize={props.setFontSize}
fontColor={props.fontColor}
setFontColor={props.setFontColor}
isResize={props.isResize}
setIsResize={props.setIsResize}
isFreeResize={true}
isOpenSignPad={true}
calculateFontsize={calculateFontsize}
currWidgetsDetails={
props?.currWidgetsDetails
}
setIsReqSignTourDisabled={
props.setIsReqSignTourDisabled
}
unSignedWidgetId={props.unSignedWidgetId}
handleNameModal={props.handleNameModal}
/>
)
)}
</React.Fragment>
))}
</>
)}
<Document
error={<p className="mx-2">{t("failed-to-load-refresh-page")}</p>}
onLoadError={(e) => {
console.log("PDF load error", e);
props.setPdfLoad(false);
}}
loading={t("loading-doc")}
onLoadSuccess={(pdf) => {
props.setPdfLoad(true);
props.pageDetails(pdf);
}}
onClick={() => {
props.setCurrWidgetsDetails && props.setCurrWidgetsDetails({});
showGuidelines(false);
}}
file={pdfDataBase64}
>
<Page
key={props.index}
onLoadSuccess={handlePageLoadSuccess}
width={props.containerWH.width}
scale={props.scale || 1}
className={isMobile ? "select-none touch-callout-none" : "-z-[1]"}
pageNumber={props.pageNumber}
renderAnnotationLayer={false}
renderTextLayer={false}
onGetAnnotationsError={(error) => {
console.log("annotation error", error);
}}
/>
</Document>
{guideline.show && (
<Guidelines
x1={guideline.x1}
x2={guideline.x2}
y1={guideline.y1}
y2={guideline.y2}
/>
)}
<DragGuidelinesLayer
signBtnPosition={props.signBtnPosition}
showGuidelines={showGuidelines}
posWidth={posWidth}
posHeight={posHeight}
isSignYourself={props.signerPos?.length > 0 ? true : false}
/>
<WidgetsDragPreview
posWidth={posWidth}
posHeight={posHeight}
isSignYourself={props.signerPos?.length > 0 ? true : false}
uniqueId={props?.uniqueId}
data={props?.signerPos?.find((x) => x.Id === props?.uniqueId)}
isNeedSign={props?.pdfRequest}
pdfOriginalWH={props.pdfOriginalWH}
pageNumber={props.pageNumber}
containerWH={props.containerWH}
scale={props?.scale}
/>
</div>
</RSC>
</>
);
}
export default RenderPdf;
@@ -9,7 +9,9 @@ function SelectLanguage(props) {
{ value: "es", text: "Española" }, //spanish
{ value: "fr", text: "Français" }, //french
{ value: "it", text: "Italiano" }, //italian
{ value: "de", text: "Deutsch" } //german
{ value: "de", text: "Deutsch" }, //german
{ value: "hi", text: "हिन्दी" }, //hindi
{ value: "kr", text: "한국어" } //korean
];
const defaultLanguage = i18next.language || "en";
const [lang, setLang] = useState(defaultLanguage);
@@ -23,14 +25,14 @@ function SelectLanguage(props) {
<div
className={`${
!props.isProfile && " mt-[9px] pb-2 md:pb-0 "
} flex justify-center items-center `}
} flex justify-center items-center text-base-content`}
>
<select
value={lang}
onChange={handleChangeLang}
className={`${
!props.isProfile ? " md:w-[15%] w-[50%]" : "w-[180px]"
} op-select op-select-bordered bg-white op-select-sm `}
} op-select op-select-bordered op-select-sm `}
>
<option disabled>select</option>
{languages.map((item) => {

Some files were not shown because too many files have changed in this diff Show More