Compare commits

...
Author SHA1 Message Date
RaktimaandGitHub e5f2c5c0a9 Merge pull request #2247 from OpenSignLabs/updates-32480718420
Merge pull request #2737 from nxglabs/sync-to-public_repo-32459158600
2026-08-21 17:50:18 +05:30
raktima-opensignlabs 7e0c2517f2 Merge pull request #2737 from nxglabs/sync-to-public_repo-32459158600
Merge pull request #2736 from nxglabs/raktima-main-patch-security
2026-08-21 12:11:59 +00:00
RaktimaandGitHub a5456dadcf Merge pull request #2245 from OpenSignLabs/updates-32235783890
Merge pull request #2730 from nxglabs/sync-to-public_repo-32233884118
2026-08-19 15:14:23 +05:30
raktima-opensignlabs 0158b0e176 Merge pull request #2730 from nxglabs/sync-to-public_repo-32233884118
Merge pull request #2729 from nxglabs/raktima-main-patch-security
2026-08-19 09:05:08 +00:00
RaktimaNXG 5d385a0ea3 Merge branch 'main' of https://github.com/OpenSignLabs/OpenSign into staging 2026-08-17 13:29:58 +05:30
RaktimaandGitHub 82584d6be9 Merge pull request #2243 from OpenSignLabs/raktima-patch-24
fix: update lockfile
2026-08-17 13:15:27 +05:30
RaktimaNXG cba774a336 fix: update lockfile 2026-08-17 13:14:44 +05:30
RaktimaNXG c44351ea67 fix: update lockfile 2026-08-17 12:48:00 +05:30
RaktimaandGitHub 3a861f3e0f v2.41.2 (#2241) 2026-08-14 23:15:48 +05:30
RaktimaNXG 348ff6de70 Merge branch 'main' of https://github.com/OpenSignLabs/OpenSign into staging 2026-08-14 22:24:09 +05:30
AmolandGitHub a773c50774 Merge pull request #2240 from OpenSignLabs/updates-31819970805 2026-08-14 22:08:53 +05:30
raktima-opensignlabs 7c414157dd Merge pull request #2717 from nxglabs/sync-to-public_repo-31818759561
Merge pull request #2716 from nxglabs/raktima-main-patch-security
2026-08-14 16:34:38 +00:00
RaktimaandGitHub 7b4785461c v2.41.1 (#2233) 2026-08-11 17:09:48 +05:30
AmolandGitHub 7f114dc124 Merge pull request #2234 from OpenSignLabs/updates-31487056035 2026-08-11 17:06:37 +05:30
raktima-opensignlabs 50cc654166 Merge pull request #2709 from nxglabs/sync-to-public_repo-31486925834
Merge pull request #2708 from nxglabs/raktima-patch-main-7
2026-08-11 11:32:14 +00:00
RaktimaNXG dc8555dfec Merge branch 'main' of https://github.com/OpenSignLabs/OpenSign into staging 2026-08-11 16:09:43 +05:30
AmolandGitHub 43e911dfdc Merge pull request #2232 from OpenSignLabs/updates-31406529761 2026-08-11 12:40:51 +05:30
raktima-opensignlabs 05f472ace8 Merge pull request #2707 from nxglabs/sync-to-public_repo-31405962035
Merge pull request #2706 from nxglabs/raktima-patch-main-7
2026-08-10 15:59:21 +00:00
AmolandGitHub 3729564878 Merge pull request #2705 from nxglabs/sync-to-public_repo-31391986146 (#2231) 2026-08-10 20:21:00 +05:30
ZathielandGitHub f72624fa26 Merge pull request #2150 from OpenSignLabs/dependabot/npm_and_yarn/npm_and_yarn-66413a1f6e
build(deps-dev): bump picomatch from 2.3.1 to 2.3.2 in the npm_and_yarn group across 1 directory
2026-07-06 23:16:24 +02:00
raktima-opensignlabsandAmol 8140588898 Merge pull request #2611 from nxglabs/sync-to-public_repo-28355409725
Merge pull request #2610 from nxglabs/staging
2026-06-30 16:28:06 +05:30
RaktimaandGitHub f2ada47302 Merge pull request #2199 from OpenSignLabs/updates-28428983168
Merge pull request #2611 from nxglabs/sync-to-public_repo-28355409725
2026-06-30 13:49:11 +05:30
raktima-opensignlabs b63d609a70 Merge pull request #2611 from nxglabs/sync-to-public_repo-28355409725
Merge pull request #2610 from nxglabs/staging
2026-06-30 07:52:10 +00:00
RaktimaandGitHub d97b353a81 Merge pull request #2194 from OpenSignLabs/staging
Merge pull request #2192 from OpenSignLabs/updates-27678195317
2026-06-22 10:43:43 +05:30
RaktimaNXG 80ccd5d022 update package-lock.json file 2026-06-22 10:21:08 +05:30
RaktimaandGitHub e948fdc60b Merge branch 'main' into staging 2026-06-22 09:29:24 +05:30
RaktimaandGitHub 4bdfe9d725 Merge pull request #2192 from OpenSignLabs/updates-27678195317
Merge pull request #2576 from nxglabs/sync-to-public_repo-27677371306
2026-06-17 14:54:02 +05:30
raktima-opensignlabs be0c4a2cbf Merge pull request #2576 from nxglabs/sync-to-public_repo-27677371306
Merge pull request #2575 from nxglabs/staging
2026-06-17 09:08:15 +00:00
b417754a0c fix: handle server.cjs file (#2184)
Co-authored-by: raktima-opensignlabs <raktima-opensignlabs@users.noreply.github.com>
2026-05-26 10:50:21 +05:30
RaktimaandGitHub 6271fedcbf Merge pull request #2183 from OpenSignLabs/updates-26432416990
Merge pull request #2474 from nxglabs/sync-to-public_repo-26403170706
2026-05-26 10:03:53 +05:30
raktima-opensignlabs d3cb8d575c Merge pull request #2474 from nxglabs/sync-to-public_repo-26403170706
Merge pull request #2473 from nxglabs/raktima-patch-main-6
2026-05-26 04:33:08 +00:00
RaktimaandGitHub 1e228c86c3 v2.39.0 (#2180) 2026-05-22 21:03:28 +05:30
RaktimaandGitHub 0c295961c3 Merge pull request #2179 from OpenSignLabs/updates-26234947661
Merge pull request #2467 from nxglabs/sync-to-public_repo-26233821722
2026-05-21 20:46:42 +05:30
raktima-opensignlabs 5fbd1fc6f2 Merge pull request #2467 from nxglabs/sync-to-public_repo-26233821722
Merge pull request #2466 from nxglabs/raktima-main-patch-5
2026-05-21 15:14:25 +00:00
RaktimaandGitHub 3f4007d564 Merge pull request #2177 from OpenSignLabs/updates-26033200443
Merge pull request #2463 from nxglabs/sync-to-public_repo-26032162086
2026-05-18 17:57:04 +05:30
raktima-opensignlabs b49dc6bc6a Merge pull request #2463 from nxglabs/sync-to-public_repo-26032162086
Merge pull request #2462 from nxglabs/staging
2026-05-18 12:22:49 +00:00
RaktimaandGitHub b325095d60 Merge pull request #2175 from OpenSignLabs/updates-25926972891
Merge pull request #2460 from nxglabs/sync-to-public_repo-25861367352
2026-05-18 10:11:45 +05:30
raktima-opensignlabs 56a119a901 Merge pull request #2460 from nxglabs/sync-to-public_repo-25861367352
Merge pull request #2459 from nxglabs/staging
2026-05-15 15:44:29 +00:00
dependabot[bot]andGitHub bea891691b build(deps-dev): bump picomatch
Bumps the npm_and_yarn group with 1 update in the / directory: [picomatch](https://github.com/micromatch/picomatch).


Updates `picomatch` from 2.3.1 to 2.3.2
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-31 13:17:37 +00:00
AmolandGitHub b9a309fa36 Merge pull request #2143 from OpenSignLabs/staging
v2.38.0
2026-03-16 17:47:26 +05:30
prafull-opensignlabsandGitHub 197c00dd79 Merge pull request #2142 from OpenSignLabs/updates-23143134478
v2.38.0
2026-03-16 17:46:23 +05:30
prafull-opensignlabs ef81a0409b v2.38.0 2026-03-16 12:14:55 +00:00
AmolandGitHub dd499ba0d0 Merge pull request #2130 from OpenSignLabs/staging 2026-03-04 13:13:57 +05:30
prafull-opensignlabsandGitHub a56b87a702 Merge pull request #2128 from OpenSignLabs/updates-22659538479
v2.37.0
2026-03-04 13:05:13 +05:30
prafull-opensignlabs acb0f3083c Merge pull request 2026-03-04 07:31:19 +00:00
prafull-opensignlabsandGitHub 606eb6a93b Merge pull request #2102 from OpenSignLabs/staging
v2.36.0
2026-02-06 18:21:57 +05:30
prafull-opensignlabsandGitHub 2e2e535b70 Merge pull request #2101 from OpenSignLabs/updates-21751052809
v2.36.0
2026-02-06 18:20:23 +05:30
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 fe8ab56d47 Merge pull request #2084 from OpenSignLabs/staging
fix: handle overlapping of date modal
2026-01-09 14:47:18 +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 4c5a3509cd Merge pull request #2081 from OpenSignLabs/staging
refactor: spelling correction
2026-01-07 19:26:30 +05:30
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
AmolandGitHub f751a97f24 Merge pull request #2077 from OpenSignLabs/staging 2026-01-07 15:16:25 +05:30
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
AmolandGitHub e4ecb5dbfd Merge pull request #2060 from OpenSignLabs/staging 2025-12-22 15:29:21 +05:30
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 2297d6fc22 Merge pull request #2051 from OpenSignLabs/staging
v2.33.0
2025-12-08 17:28:27 +05:30
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
AmolandGitHub 8922b61ecd Merge pull request #2043 from OpenSignLabs/staging
v2.32.0
2025-11-28 18:56:30 +05:30
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 db4b2d58d6 Merge pull request #2030 from OpenSignLabs/staging
fix: remove type parameter from parse.save method
2025-11-20 20:18:58 +05:30
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
AmolandGitHub 7017930709 Merge pull request #2028 from OpenSignLabs/staging
fix: add password validation in change password page
2025-11-20 18:27:50 +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
AmolandGitHub 810037e3c8 Merge pull request #2026 from OpenSignLabs/staging
build(deps): update dependencies
2025-11-20 16:24:43 +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 f8987a57d9 Merge pull request #2024 from OpenSignLabs/staging
v2.31.0
2025-11-20 12:20:55 +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 f3d20391fa Merge pull request #1995 from OpenSignLabs/staging
v2.30.0
2025-10-16 14:24:55 +05:30
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
AndrewandGitHub 6594ba03a3 Merge pull request #1974 from OpenSignLabs/staging
v2.29.2
2025-10-01 19:39:25 +05:30
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
AmolandGitHub 9c2a388f66 Merge pull request #1970 from OpenSignLabs/staging
v2.29.1
2025-09-30 17:59:48 +05:30
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
AndrewandGitHub 8c49e1ac18 Merge pull request #1967 from OpenSignLabs/staging
v2.29.0
2025-09-29 10:56:48 +05:30
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
AmolandGitHub e1bf2ed94b Merge pull request #1946 from OpenSignLabs/staging
refactor: remove unnecessary code
2025-09-17 19:11:11 +05:30
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
AmolandGitHub 797fcbd624 Merge pull request #1944 from OpenSignLabs/staging
build(deps): update dependencies and dark mode ui
2025-09-17 14:38:38 +05:30
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
AmolandGitHub 847657dc76 Merge pull request #1942 from OpenSignLabs/staging
v2.28.0
2025-09-15 20:31:08 +05:30
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 3a361cc21e Merge pull request #1932 from OpenSignLabs/staging
fix: getting error 'user not found' while deleting user
2025-09-08 16:24:29 +05:30
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 1496c1b1da Merge pull request #1930 from OpenSignLabs/staging
v2.27.2
2025-09-08 10:38:37 +05:30
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 d87fa38976 Merge pull request #1836 from OpenSignLabs/staging
v2.26.0
2025-07-07 15:16:19 +05:30
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 df4630aa31 Merge pull request #1805 from OpenSignLabs/staging
v2.25.0
2025-06-19 17:37:33 +05:30
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 d8a0d9137e Merge pull request #1790 from OpenSignLabs/staging
v2.24.1
2025-06-06 18:17:54 +05:30
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 1b2528ac1d Merge pull request #1765 from OpenSignLabs/staging
v2.23.0
2025-05-30 20:32:35 +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 9b357acbdd Merge pull request #1757 from OpenSignLabs/staging
v2.22.0
2025-05-28 11:27:44 +05:30
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 ff4fa3a15f v2.21.1
handle undefined placeholders
2025-05-15 16:52:45 +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 f104b0ca86 Merge pull request #1736 from OpenSignLabs/staging
v2.21.0
2025-05-15 12:23:07 +05:30
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 31affb53f1 Merge pull request #1718 from OpenSignLabs/staging
v2.20.0
2025-05-05 18:31:52 +05:30
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 338b76029f Merge pull request #1710 from OpenSignLabs/staging
fix: quick send not working
2025-04-29 12:17:44 +05:30
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 646e202b91 Merge pull request #1705 from OpenSignLabs/staging
refactor: remove extra comment from migration
2025-04-27 12:21:49 +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 8a34917343 Merge pull request #1703 from OpenSignLabs/staging
fix: update migration script
2025-04-27 12:03:23 +05:30
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 9b8c9403d1 Merge pull request #1701 from OpenSignLabs/staging
v2.19.0
2025-04-25 19:18:06 +05:30
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 40198e1bd8 Merge pull request #1693 from OpenSignLabs/staging
fix: correct public URL, normalize emails, migrate user/orgAdmin logic to cloud function
2025-04-22 14:10:35 +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 0e6220e16c Merge pull request #1690 from OpenSignLabs/staging
v2.18.0
2025-04-17 12:07:30 +05:30
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 fc3a5ad1bf Merge pull request #1682 from OpenSignLabs/staging
v2.17.0
2025-04-11 13:21:32 +05:30
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 4919d6c3d3 Merge pull request #1673 from OpenSignLabs/staging
v2.16.0
2025-04-05 11:37:25 +05:30
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 91e89bb0e2 Merge pull request #1664 from OpenSignLabs/staging
v2.15.0
2025-03-28 11:38:46 +05:30
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 f942b2571a Merge pull request #1662 from OpenSignLabs/staging
feat: changes to implement one click deployment
2025-03-24 13:48:24 +05:30
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 ae2c15660d Merge pull request #1655 from OpenSignLabs/staging
refactor: remove unnecessary code
2025-03-23 13:12:52 +05:30
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 721d52d539 Merge pull request #1649 from OpenSignLabs/staging
v2.14.0
2025-03-21 20:10:43 +05:30
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 b03e27cf70 Merge pull request #1647 from OpenSignLabs/staging
v2.13.0
2025-03-20 17:38:11 +05:30
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
prafull-opensignlabsandGitHub 4eae44c9b4 Merge pull request #1604 from OpenSignLabs/staging
v2.13.1
2025-02-21 13:02:46 +05:30
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 79234441db Merge pull request #1600 from OpenSignLabs/staging
v2.13.0
2025-02-21 12:47:30 +05:30
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
prafull-opensignlabsandGitHub 38f8561d18 Merge pull request #1592 from OpenSignLabs/updates-13391279852
build(dev-deps): update dependencies
2025-02-18 18:38:49 +05:30
prafull-opensignlabs 10e6ab58a5 Merge pull request
revert: description parameter
2025-02-18 13:07:15 +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
ZathielandGitHub 39fc2daa19 Merge pull request #1569 from OpenSignLabs/dependabot/npm_and_yarn/apps/OpenSign/npm_and_yarn-fef486fdf1
build(deps-dev): bump postcss from 8.5.1 to 8.5.2 in /apps/OpenSign in the npm_and_yarn group
2025-02-15 23:19:15 +01:00
prafull-opensignlabsandGitHub c38f59642f Merge pull request #1573 from OpenSignLabs/updates-13306452314
v2.13.0
2025-02-13 16:54:14 +05:30
prafull-opensignlabs e9d0af7abb v2.13.0
refactor: remove unnecessary code
2025-02-13 11:15:39 +00:00
dependabot[bot]andGitHub b39d845cd6 build(deps-dev): bump postcss
Bumps the npm_and_yarn group in /apps/OpenSign with 1 update: [postcss](https://github.com/postcss/postcss).


Updates `postcss` from 8.5.1 to 8.5.2
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.1...8.5.2)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-11 04:54:06 +00:00
455 changed files with 72277 additions and 53923 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>
+8117 -18174
View File
File diff suppressed because it is too large Load Diff
+107 -72
View File
@@ -1,73 +1,93 @@
{
"name": "open_sign",
"version": "0.1.0",
"version": "2.37.0",
"private": true,
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
"@lottiefiles/dotlottie-react": "^0.12.3",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@formkit/auto-animate": "^0.9.0",
"@imgly/background-removal": "^1.7.0",
"@lottiefiles/dotlottie-react": "^0.19.0",
"@mui/icons-material": "^5.15.10",
"@mui/material": "^5.15.10",
"@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",
"@radix-ui/themes": "^3.3.0",
"@reactour/tour": "^3.8.0",
"@reduxjs/toolkit": "^2.11.2",
"@usewaypoint/block-avatar": "^0.0.3",
"@usewaypoint/block-button": "^0.0.3",
"@usewaypoint/block-columns-container": "^0.0.3",
"@usewaypoint/block-container": "^0.0.2",
"@usewaypoint/block-divider": "^0.0.4",
"@usewaypoint/block-heading": "^0.0.3",
"@usewaypoint/block-html": "^0.0.3",
"@usewaypoint/block-image": "^0.0.5",
"@usewaypoint/block-spacer": "^0.0.3",
"@usewaypoint/block-text": "^0.0.7",
"@usewaypoint/document-core": "^0.0.6",
"@usewaypoint/email-builder": "^0.0.9",
"axios": "^1.18.0",
"date-fns-tz": "^3.2.0",
"dompurify": "^3.4.1",
"file-saver": "^2.0.5",
"i18next": "^23.16.8",
"i18next-browser-languagedetector": "^8.0.2",
"i18next-http-backend": "^3.0.1",
"highlight.js": "^11.11.1",
"i18next": "^26.0.8",
"i18next-browser-languagedetector": "^8.2.1",
"i18next-http-backend": "^3.0.6",
"jszip": "^3.10.1",
"juice": "^11.1.1",
"jwt-decode": "^4.0.0",
"moment": "^2.30.1",
"nth-check": "^2.1.1",
"parse": "^5.3.0",
"parse": "^8.1.0",
"pdf-lib": "^1.17.1",
"pkijs": "^3.4.0",
"prettier": "^3.8.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",
"process": "^0.11.10",
"quill-html-edit-button": "^3.0.0",
"radix-ui": "^1.4.3",
"react": "^19.2.5",
"react-bootstrap": "^2.10.10",
"react-colorful": "^5.6.1",
"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.5",
"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": "^17.0.6",
"react-konva": "^19.2.3",
"react-pdf": "^10.4.1",
"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.3",
"react-router": "^7.18.2",
"react-scrollbars-custom": "^4.1.1",
"react-select": "^5.10.0",
"react-signature-canvas": "^1.0.7",
"react-syntax-highlighter": "^15.6.1",
"react-timezone-select": "^3.2.8",
"react-tooltip": "^5.28.0",
"react-web-share": "^2.0.2",
"reactour": "^1.19.4",
"react-select": "^5.10.2",
"react-signature-canvas": "^1.1.0-alpha.2",
"react-timezone-select": "^3.3.3",
"react-to-print": "^3.3.0",
"react-tooltip": "^5.30.1",
"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",
"web-vitals": "^5.2.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"zod": "^3.22.4",
"zustand": "^4.5.1"
},
"scripts": {
"build": "npm run version && react-scripts build",
"start-dev": "react-scripts start",
"start": "serve -s build",
"build": "npm run version && NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
"start-dev": "vite",
"dev": "vite",
"preview": "vite preview",
"start": "node server.cjs",
"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"
},
@@ -95,34 +115,49 @@
}
},
"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/core": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@babel/preset-react": "^7.28.5",
"@babel/runtime-corejs2": "^7.29.2",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.2.14",
"@vitejs/plugin-react": "^6.0.1",
"@vitest/ui": "^4.1.5",
"autoprefixer": "^10.5.0",
"babel-loader": "^9.2.1",
"clean-webpack-plugin": "^4.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": "^10.0.4",
"css-loader": "^7.1.4",
"daisyui": "^4.12.24",
"eslint": "^9.39.4",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react": "^7.37.5",
"jsdom": "^29.0.2",
"lint-staged": "^16.4.0",
"postcss": "^8.5.23",
"pretty-quick": "^4.2.2",
"rollup-plugin-node-polyfills": "^0.2.1",
"tailwindcss": "^3.4.19",
"tmp": "^0.2.7",
"vite": ">=8.0.16",
"vite-plugin-svgr": "^5.2.0",
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.1.5"
},
"overrides": {
"nth-check": "$nth-check",
"ws": "$ws"
"brace-expansion": ">=2.1.2",
"tmp": "$tmp",
"react": "$react",
"react-dom": "$react-dom",
"form-data": ">=4.0.6",
"js-yaml": "^3.15.0",
"undici": "^7.29.0",
"fast-uri": "^3.1.5",
"nanoid": ">=3.3.16",
"ws": ">=8.21.0",
"protobufjs": ">=7.6.5"
},
"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
+5
View File
@@ -0,0 +1,5 @@
{
"rewrites": [
{ "source": "/:path((?!\\.well-known/).*)", "destination": "/index.html" }
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.
+160
View File
@@ -0,0 +1,160 @@
#!/usr/bin/env node
/* eslint-env node */
/* eslint-disable no-console */
// Tiny static file server for the production build.
// - Serves real files from ./build (including dotfile dirs like .well-known)
// - Falls back to /index.html only when the requested path does not exist
// (SPA client-side routing).
const http = require("node:http");
const fs = require("node:fs");
const path = require("node:path");
const root = path.join(__dirname, "build");
const port = Number(process.env.PORT) || 3000;
const host = process.env.HOST || "0.0.0.0";
const mime = {
".html": "text/html; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".mjs": "application/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json; charset=utf-8",
".map": "application/json; charset=utf-8",
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".ico": "image/x-icon",
".webp": "image/webp",
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".otf": "font/otf",
".txt": "text/plain; charset=utf-8",
".pdf": "application/pdf",
".csv": "text/csv; charset=utf-8",
".wasm": "application/wasm",
".xml": "application/xml; charset=utf-8"
};
function contentType(filePath) {
return mime[path.extname(filePath).toLowerCase()] || "application/octet-stream";
}
function safeJoin(reqPath) {
let decoded;
try {
decoded = decodeURIComponent(reqPath.split("?")[0].split("#")[0]);
} catch {
return null;
}
const resolved = path.normalize(path.join(root, decoded));
if (resolved !== root && !resolved.startsWith(root + path.sep)) return null;
return resolved;
}
function cacheControl(filePath) {
const ext = path.extname(filePath).toLowerCase();
const relativePath = path.relative(root, filePath);
const isBuildAsset =
relativePath === "assets" ||
relativePath.startsWith(`assets${path.sep}`);
if (ext === ".html" || ext === "") {
return "no-cache";
}
return isBuildAsset
? "public, max-age=31536000, immutable"
: "no-cache";
}
function streamFile(req, res, filePath, stats) {
const headers = {
"Content-Type": contentType(filePath),
"Content-Length": stats.size,
"Last-Modified": stats.mtime.toUTCString(),
"Cache-Control": cacheControl(filePath)
};
if (req.method === "HEAD") {
res.writeHead(200, headers);
return res.end();
}
const stream = fs.createReadStream(filePath);
stream.on("error", (err) => {
console.error("Stream error:", err);
if (!res.headersSent) {
res.writeHead(500, { "Content-Type": "text/plain" });
res.end("Internal Server Error");
} else {
res.destroy();
}
});
res.on("close", () => {
if (!stream.destroyed) stream.destroy();
});
res.on("error", (err) => {
console.error("Response error:", err);
if (!stream.destroyed) stream.destroy();
});
res.writeHead(200, headers);
stream.pipe(res);
}
function sendIndex(req, res) {
const indexPath = path.join(root, "index.html");
fs.stat(indexPath, (err, stats) => {
if (err || !stats || !stats.isFile()) {
res.writeHead(500, { "Content-Type": "text/plain" });
return res.end("index.html not found");
}
streamFile(req, res, indexPath, stats);
});
}
const server = http.createServer((req, res) => {
if (req.method === "OPTIONS") {
const filePath = safeJoin(req.url || "/");
res.writeHead(204);
return res.end();
}
if (req.method !== "GET" && req.method !== "HEAD") {
res.writeHead(405, { Allow: "GET, HEAD, OPTIONS" });
return res.end();
}
const reqUrl = req.url || "/";
const filePath = safeJoin(reqUrl);
if (!filePath) {
res.writeHead(400);
return res.end("Bad Request");
}
fs.stat(filePath, (err, stats) => {
if (err) {
// No file at this path → SPA fallback to index.html
return sendIndex(req, res);
}
if (stats.isFile()) {
return streamFile(req, res, filePath, stats);
}
if (stats.isDirectory()) {
const indexInDir = path.join(filePath, "index.html");
return fs.stat(indexInDir, (dirErr, dirStats) => {
if (!dirErr && dirStats && dirStats.isFile()) {
return streamFile(req, res, indexInDir, dirStats);
}
return sendIndex(req, res);
});
}
// Path exists but is neither file nor directory → SPA fallback
return sendIndex(req, res);
});
});
server.listen(port, host, () => {
console.log(`Serving ${root} on http://${host}:${port}`);
});
+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;
+151
View File
@@ -0,0 +1,151 @@
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"));
const EmailBuilder = lazyWithRetry(() => import("./pages/EmailBuilder"));
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="/emailbuilder" element={<EmailBuilder />} />
<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]">
@@ -326,16 +218,15 @@ const AddUser = (props) => {
></i>
</div>
<div className="text-[12px] ml-2 mb-0 text-[red] select-none">
{t("password-generateed")}
{t("password-generated")}
</div>
</div>
<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 [isBulkAvailable, setIsBulkAvailable] = 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 () => {
setIsBulkAvailable(true);
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>
)}
{isBulkAvailable ? (
<>
{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,95 @@
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,
label,
showLabel = true,
showClear = true,
dateClassName = ""
}) => {
const { t } = useTranslation();
const CustomInput = forwardRef(({ value, onClick }, ref) => (
<div
className="w-full border-gray-400 rounded-[50px] border-[1px] px-3 text-xs py-2 focus:outline-none hover:border-base-content flex items-center justify-between"
onClick={onClick}
ref={ref}
>
<span className={`${dateClassName} truncate`}>{value}</span>
<i className={`fa-light fa-calendar`}></i>
</div>
));
CustomInput.displayName = "CustomInput";
return (
<>
{showLabel && (
<span className="flex-shrink-0">{label || 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>
)}
wrapperClassName="w-full"
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"
/>
{showClear && handleClear && (
<span
onClick={() => handleClear()}
className="underline text-blue-500 cursor-pointer ml-2"
>
{t("clear")}
</span>
)}
</>
);
};
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>
);
}
@@ -0,0 +1,130 @@
import { useState, useEffect } from "react";
import DOMPurify from "dompurify";
import juice from "juice";
import { Trans, useTranslation } from "react-i18next";
const EmailBodyEditor = ({
value,
onChange,
smallscreen = false,
bodyName,
isReset = false,
isTemplateLoaded
}) => {
const { t } = useTranslation();
const [inputHtml, setInputHtml] = useState("");
const [cleanPreview, setCleanPreview] = useState("");
useEffect(() => {
initProcessContent();
}, [isTemplateLoaded]);
useEffect(() => {
if (isReset) {
initProcessContent(value);
}
}, [isReset]);
const initProcessContent = () => {
// 1. Sanitize immediately to strip scripts/malicious tags
const sanitized = DOMPurify.sanitize(value, {
USE_PROFILES: { html: true }, // Ensures basic HTML structure
ADD_ATTR: ["target"] // Allow links to open in new tabs
});
// 2. Inline CSS for email compatibility
// Note: Juice is fast, but for huge files, you might debounce this
try {
const inlined = juice(sanitized);
setInputHtml(inlined);
onChange?.(inlined);
setCleanPreview(inlined);
} catch (err) {
onChange?.(sanitized);
setInputHtml(sanitized); // Fallback if juice fails
setCleanPreview(sanitized);
}
};
const processContent = (value) => {
// 1. Sanitize immediately to strip scripts/malicious tags
const sanitized = DOMPurify.sanitize(value, {
USE_PROFILES: { html: true }, // Ensures basic HTML structure
ADD_ATTR: ["target"] // Allow links to open in new tabs
});
// 2. Inline CSS for email compatibility
// Note: Juice is fast, but for huge files, you might debounce this
try {
const inlined = juice(sanitized);
setCleanPreview(inlined);
onChange?.(inlined);
} catch (err) {
setCleanPreview(sanitized); // Fallback if juice fails
onChange?.(sanitized);
}
};
const handleChange = (e) => {
const value = e.target.value;
setInputHtml(value);
processContent(value);
};
const screen = smallscreen ? "flex-col" : "flex-col md:flex-row ";
const template =
bodyName === "request"
? "#sample/requestemail"
: bodyName === "completion"
? "#sample/completionemail"
: "#";
return (
<>
<p className="text-sm ">
<Trans i18nKey={"open-email-builder"}>
{"You can create email template using "}
<a
href={`/emailbuilder${template}`}
target="_blank"
referrerPolicy="no-referrer"
className="op-link op-link-primary font-medium"
>
email builder
</a>
{" platform and copy html code."}
</Trans>
</p>
<div className={`flex ${screen} gap-5`}>
{/* Editor Pane */}
<div className="flex flex-1 flex-col mt-2">
<label>{t("paste-html-here")}:</label>
<div className="flex-1 mt-1 p-3 text-xs op-textarea op-textarea-bordered min-h-[70vh]">
<textarea
className="w-full min-h-[70vh] focus:outline-none"
value={inputHtml}
onChange={(e) => handleChange(e)}
placeholder="<html><body><h1>Hello!</h1></body></html>"
/>
</div>
</div>
{/* Live Preview Pane */}
<div className="flex flex-1 flex-col mt-2">
<label>{t("preview")}</label>
<div className="flex-1 mt-1 bg-white border-[1px] op-textarea border-[#ccc] min-h-[70vh]">
{cleanPreview && (
<iframe
title="Safe Preview"
srcDoc={cleanPreview}
// SECURITY: sandbox prevents scripts from running even if they slip through
sandbox="allow-popups allow-popups-to-escape-sandbox"
className="w-full min-h-[70vh]"
/>
)}
</div>
</div>
</div>
</>
);
};
export default EmailBodyEditor;
@@ -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,650 @@
import {
useState,
useEffect,
useMemo
} from "react";
import axios from "axios";
import Loader from "../../primitives/Loader";
import { useTranslation } from "react-i18next";
import {
emailRegex,
} from "../../constant/const";
import {
getSignedUrl,
isBase64,
getBase64FromUrl,
generateId,
} from "../../constant/Utils";
import {
handleEmbedPrefillToDoc,
isWidgetResponseCompatible,
formatCSVDate,
normalizeKey,
loadPdfOnce,
hasSignatureWidget,
} from "../../utils";
import { useDispatch, useSelector } from "react-redux";
import {
setBulkLoader,
} from "../../redux/reducers/widgetSlice";
import Table from "./components/Table";
import PrefillWidgets from "./components/PrefillWidgets";
import WizardHeader from "./components/WizardHeader";
import ResponseTab from "./components/ResponseTab";
import { steps } from "../../json/BulkSendSteps";
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 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 [prefillWidgets, setPrefillWidgets] = useState([]);
const [step, setStep] = useState(0);
const [stepsList, setStepsList] = useState(steps(t));
const [stepKey, setStepKey] = useState(steps(t)?.[0]?.key || "");
const [message, setMessage] = useState({ status: "", message: "" });
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 reservedEmails = (
props?.Placeholders?.filter((p) => !!p?.signerObjId) || []
)
.map(
(role) =>
normalizeKey(role?.email) || normalizeKey(role?.signerPtr?.Email)
)
.filter(Boolean);
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;
if (widget?.options?.formula) 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,
options: { ...widget.options, response: response },
label: label,
response,
pageNumber: page.pageNumber
};
})
)
);
return {
fieldId: role.Id,
email: "",
label: role.Role,
signer: {},
widgets: widgets
};
})
);
const prefills = users.find((r) => r?.label?.toLowerCase() === "prefill");
const signers = users.filter(
(r) => r?.label?.toLowerCase() !== "prefill"
);
setFields(signers);
if (prefills && prefills?.widgets?.length) {
setPrefillWidgets(prefills?.widgets);
} else {
setStepsList((s) => s.filter((step) => step.key !== "prefill"));
setStepKey("recipients");
setStep(0);
}
setEmails(reservedEmails);
// Build headers with required ordering:
// 1) all role emails first
// 2) then all widgets
const emailHeaders = [];
const widgetHeaders = [];
for (const role of signers) {
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: generateId(8), fields: signers }]);
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");
// Viewer/approver roles do not require a signature widget; only
// signer-role placeholders must have one.
const isSignExist =
filtered?.length > 0 && filtered.every((p) => hasSignatureWidget(p));
setIsSignatureExist(isSignExist);
setIsVacantRoles(filtered?.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) => {
setForms((prev) => {
const next = [...prev];
const form = next[formIndex];
if (!form) return prev;
const fields = [...(form.fields ?? [])];
const fieldId = fields.findIndex((f) => f.label === role);
if (fieldId < 0) return prev;
const email = signer?.Email ? signer.Email : signer || "";
fields[fieldId] = {
...fields[fieldId],
email: normalizeKey(email),
signer: signer?.objectId ? signer : ""
};
next[formIndex] = { ...form, fields };
return next;
});
};
const handleWidgetDetails = (
value = "",
formIndex,
roleIndex = 0,
widgetIndex
) => {
setForms((prev) => {
const next = [...prev];
const form = next[formIndex];
if (!form) return prev;
const fields = [...(form.fields ?? [])];
const field = fields[roleIndex];
if (!field) return prev;
const widgets = [...(field.widgets ?? [])];
const w = widgets[widgetIndex];
if (!w) return prev;
widgets[widgetIndex] = {
...w,
options: { ...w?.options, response: value },
response: value
};
fields[roleIndex] = { ...field, widgets };
next[formIndex] = { ...form, fields };
return next;
});
};
function validateEmails(data) {
for (const [rowIndex, item] of data.entries()) {
const rowEmails = new Set();
const rowNumber = rowIndex + 1;
const filterFields = item?.fields || [];
for (const field of filterFields) {
// skip prefill
if (field?.label === "prefill") continue;
const normalizedEmail = normalizeKey(field?.email);
const params = { email: field.email, row: rowNumber };
if (!normalizedEmail) {
alert(t("email-not-found-in-row", { ...params }));
return false;
} else if (!emailRegex.test(normalizedEmail)) {
alert(t("invalid-email-found-in-row", { ...params }));
return false;
} else if (
rowEmails.has(normalizedEmail) ||
emails?.includes(normalizedEmail)
) {
alert(t("duplicate-email-found-in-row", { ...params }));
return false;
} else {
rowEmails.add(normalizedEmail);
}
}
}
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) || "";
const isValidResponse = isWidgetResponseCompatible(
widgetDetails,
newResponse
);
if (!isValidResponse) return widgetDetails;
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];
let pdfUrl = props.item?.URL || props.item?.SignedUrl;
const prefillExist = Placeholders?.some(
(data) => data.Role === "prefill"
);
if (prefillExist) {
const updatedPrefills = updateWidgetValues(
prefillWidgets,
Placeholders
);
const prefillDetails = updatedPrefills?.find(
(data) => data.Role === "prefill"
);
const pdfSignedUrl = await getSignedUrl(
pdfUrl,
"", //docId
props.item.objectId, // templateId
);
const pdfArrayBuffer = await loadPdfOnce(pdfSignedUrl);
if (pdfArrayBuffer === "Error") {
const error = t("something-went-wrong-mssg");
alert(error);
dispatch(setBulkLoader(false));
return;
}
pdfUrl = await handleEmbedPrefillToDoc(
prefillDetails,
1, // scale
pdfArrayBuffer,
[], // prefillImg,
props.item?.ExtUserPtr?.UserId?.objectId // userId
);
if (pdfUrl?.error) {
const error = pdfUrl?.error?.includes("not compatible")
? t("pdf-uncompatible", { appName: appName })
: pdfUrl?.error;
alert(error);
dispatch(setBulkLoader(false));
return;
}
}
// 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
);
let signedUrl = pdfUrl;
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) {
setStep((s) => Math.min(2, s + 1));
setStepKey("response");
setMessage({
status: "success",
message: "Documents sent successfully."
});
// props.handleClose("success", Documents?.length);
}
} catch (err) {
const message =
err?.response?.data?.error || err?.message || "something went wrong.";
console.error("Error sending documents:", message);
setMessage({ status: "failed", 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">
{props.Placeholders?.length > 0 ? (
isSignatureExist ? (
isVacantRoles ? (
<>
<div className="border-t mt-3" />
{/* Wizard header */}
<div className="px-6 py-3">
<WizardHeader
steps={stepsList}
step={step}
// onStepClick={(i) => i <= step && setStep(i)}
/>
</div>
{stepKey === "prefill" && (
<PrefillWidgets
prefills={prefillWidgets}
setPrefills={setPrefillWidgets}
onNext={() => {
setStep((s) => Math.min(2, s + 1));
setStepKey("recipients");
}}
/>
)}
{stepKey === "recipients" && (
<>
<form onSubmit={handleSubmit}>
<div className="min-h-max max-h-[250px] overflow-auto">
{
forms?.length > 0 && (
<div className="mx-4">
<Table
headers={headers}
rowData={forms}
handleInputChange={handleInputChange}
handleWidgetDetails={handleWidgetDetails}
/>
</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>
</>
)}
{stepKey === "response" && (
<ResponseTab
prefillCount={prefillWidgets?.length}
documentCount={forms?.length}
message={message}
/>
)}
</>
) : (
<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,68 @@
import RenderWidgets from "./RenderWidgets";
import { useTranslation } from "react-i18next";
const PrefillWidgets = ({ prefills = [], setPrefills, onNext }) => {
const { t } = useTranslation();
const handleWidgetDetails = (value, widgetLabel) => {
setPrefills((prev) => {
const widgets = [...(prev ?? [])];
const index = widgets.findIndex((w) => w?.label === widgetLabel);
if (index === -1) return prev;
widgets[index] = {
...widgets[index],
options: { ...widgets[index].options, response: value },
response: value
};
return widgets;
});
};
const handleNext = (e) => {
e?.preventDefault();
e?.stopPropagation();
onNext?.();
};
return (
<>
{prefills?.length > 0 && (
<form
onSubmit={handleNext}
className="m-3 md:m-6 text-base-content flex flex-col relative"
>
<div className="py-3 px-[10px] op-card border-[1px] border-gray-400">
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-10 gap-y-4 w-full">
{[...prefills]
.sort((a, b) =>
a.pageNumber !== b.pageNumber
? a.pageNumber - b.pageNumber
: (a.yPosition ?? 0) - (b.yPosition ?? 0)
)
.map((widget) => (
<RenderWidgets
key={widget.key}
showLabel
widget={widget}
handleWidgetDetails={(value) =>
handleWidgetDetails(value, widget.label)
}
/>
))}
</div>
</div>
<div className="flex flex-row flex-wrap mt-3 gap-3 justify-center">
<button
type="submit"
className="op-btn op-btn-primary w-[150px] focus:outline-none"
>
{t("next")}
</button>
</div>
</form>
)}
</>
);
};
export default PrefillWidgets;
@@ -0,0 +1,630 @@
import { forwardRef, memo, 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,
generateId,
getMonth,
getYear,
months,
radioButtonWidget,
textInputWidget,
textWidget,
years
} from "../../../constant/Utils";
import PenColorComponent from "../../pdf/tab/PenColorComponent";
import { getDatePickerDate, toHtmlPattern } from "../../../utils";
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 widgetLabelCss = (isRequired = false) => {
return `${isRequired ? "after:content-['_*'] after:text-red-500" : ""} block text-xs font-semibold`;
};
const DateWidget = ({ widget, isRequired, onChange, showLabel }) => {
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">
{showLabel && (
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
{widget?.options?.name}
</div>
)}
<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>
)}
wrapperClassName="w-full"
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,
showLabel,
}) => {
const { t } = useTranslation();
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]);
const [value, setValue] = useState(() => {
const response =
widget?.options?.response ?? widget?.options?.defaultValue ?? "";
return inputType === "number" && response ? Number(response) : response;
});
const handleInputChange = (e) => {
const text = e.target.value || "";
setValue(text);
onChange(text);
};
return (
<div className="flex flex-col">
{showLabel && (
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
{widget?.options?.name}
</div>
)}
<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",
].join(" ")}
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, showLabel }) => {
const { t } = useTranslation();
const [selectedCheckbox, setSelectedCheckbox] = useState(
() => widget?.options?.response || widget?.options?.defaultValue || []
);
const groupName = `checkbox-group-${widget.key}`;
const noneSelected = (selectedCheckbox?.length ?? 0) === 0;
const handleCheckboxValue = (isChecked, ind) => {
// ✅ clear hidden input error immediately
const hidden = document.querySelector(`input[name="${groupName}"]`);
hidden?.setCustomValidity("");
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);
};
return (
<div className="flex flex-col">
{showLabel && (
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
{widget?.options?.name}
</div>
)}
<div className="flex flex-col gap-y-1 min-w-max relative">
{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)}
onChange={(e) => 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>
))}
{/* ✅ the validator */}
{isRequired && (
<input
tabIndex={-1}
aria-hidden="true"
className="absolute opacity-0 pointer-events-none w-0.5 h-0.5 left-[0.46rem] top-4"
name={groupName}
value={noneSelected ? "" : "selected"} // empty => invalid, non-empty => valid
required
onInvalid={(e) =>
e.target.setCustomValidity(t("select-at-least-option"))
}
onChange={(e) => e.target.setCustomValidity("")}
/>
)}
</div>
</div>
);
};
const DropdownWidget = ({ widget, isRequired, onChange, showLabel }) => {
const { t } = useTranslation();
const [selected, setSelected] = useState(
() =>
widget?.options?.response?.trim() ||
widget?.options?.defaultValue?.trim() ||
""
);
const handleDropdownChange = (value) => {
const select = value?.trim();
setSelected(select);
onChange(select);
};
return (
<div className="flex flex-col">
{showLabel && (
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
{widget?.options?.name}
</div>
)}
<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?.trim()}>
{option}
</option>
))}
</select>
</div>
</div>
);
};
const RadioButtonWidget = ({ widget, isRequired, onChange, showLabel }) => {
const [selected, setSelected] = useState(
() =>
widget?.options?.response?.trim() ||
widget?.options?.defaultValue?.trim() ||
""
);
const id = generateId(4);
const handleRadioChange = (value) => {
const select = value?.trim();
setSelected(select);
onChange(select);
};
return (
<div className="flex flex-col">
{showLabel && (
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
{widget?.options?.name}
</div>
)}
<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}-${id}`} // 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?.trim()}
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, showLabel }) => {
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">
{showLabel && (
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
{widget?.options?.name}
</div>
)}
<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>
) : (
<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>
{(defaultImg?.src || img?.src) && (
<span
onClick={handleClearImage}
className="flex justify-start text-blue-500 underline cursor-pointer ml-1"
>
{t("clear")}
</span>
)}
</div>
);
};
const DrawWidget = ({ widget, isRequired, onChange, showLabel }) => {
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">
{showLabel && (
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
{widget?.options?.name}
</div>
)}
<div className="relative w-fit">
{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, showLabel }) => {
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 hint = widget?.options?.hint;
const regExpression = inputValidation(serverRegex, inputType);
const pattern = useMemo(() => toHtmlPattern(regExpression), [regExpression]);
useEffect(() => {
const response =
widget?.options?.response ?? widget?.options?.defaultValue ?? "";
setWord(String(response || ""));
}, []);
const handleChange = (e) => {
const value = e.target.value;
setWord(value);
onChange(value);
};
return (
<div className="flex flex-col">
{showLabel && (
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
{widget?.options?.name}
</div>
)}
<div className="relative">
<input
type="text"
placeholder={
hint || t("enter-value", { value: widget?.options?.name })
}
value={word ?? ""}
onChange={(e) => handleChange(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("");
}}
className="op-input op-input-bordered op-input-sm focus:outline-none text-base-content hover:border-base-content w-full text-xs"
maxLength={count}
/>
</div>
</div>
);
};
const RenderWidgets = ({
widget,
handleWidgetDetails,
showLabel = false,
}) => {
const label = widget?.label?.toLowerCase() ?? "";
const isPrefill = label.includes(PREFILL_PREFIX);
const commonProps = {
widget,
isRequired: widget.options?.status === "required" && isPrefill,
showLabel: showLabel,
onChange: handleWidgetDetails,
};
switch (widget.type) {
case "date":
return <DateWidget {...commonProps} />;
case textWidget:
case textInputWidget:
case "name":
case "email":
case "company":
case "job title":
case "number":
return <TextWidget {...commonProps} />;
case "checkbox":
return <CheckboxWidget {...commonProps} />;
case radioButtonWidget:
return <RadioButtonWidget {...commonProps} />;
case "dropdown":
return <DropdownWidget {...commonProps} />;
case "image":
case "stamp":
return <ImageWidget {...commonProps} />;
case "initials":
case "draw":
return <DrawWidget {...commonProps} />;
case "cells":
return <CellsWidget {...commonProps} />;
default:
return null;
}
};
export default memo(RenderWidgets);
@@ -0,0 +1,33 @@
import { useTranslation } from "react-i18next";
function ResponseTab({
prefillCount = 0,
documentCount = 0,
message = { status: "", message: "" }
}) {
const { t } = useTranslation();
const { status, message: msg } = message;
return (
<div className="rounded-box border p-4 m-3 md:m-6 ">
<div className="font-medium">{t("summary")}</div>
<ul className="mt-2 list-disc pl-5 text-base-content/80">
<li>
{t("prefill-fields")}: {prefillCount}
</li>
<li>
{t("documents")}: {documentCount}
</li>
<li>
{t("status")}: {status}
</li>
{msg && (
<li>
{t("message")}: {msg}
</li>
)}
</ul>
</div>
);
}
export default ResponseTab;
@@ -0,0 +1,104 @@
import {
useState
} from "react";
import { useTranslation } from "react-i18next";
import SuggestionInput from "../../shared/fields/SuggestionInput";
import RenderWidgets from "./RenderWidgets";
const requiredAsteriskCls = (isRequired = false) => {
return isRequired ? "after:content-['_*'] after:text-red-500" : "";
};
const Table = ({
headers = [],
rowData = [],
handleInputChange,
handleWidgetDetails,
initialPerPage = 25,
}) => {
const { t } = useTranslation();
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(initialPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentData = rowData.slice(startIndex, endIndex);
return (
<div className="w-full">
{/* Table */}
<div
className="overflow-x-auto border border-gray-300 rounded-lg"
>
<table className="op-table border-collapse w-full">
<thead className="text-[13px] text-center">
<tr className="border-y-[1px]">
{headers?.map((header) => (
<th
key={header.label}
className={`${requiredAsteriskCls(header?.isRequired)} p-2`}
>
{header.label}
</th>
))}
{rowData.length > 1 && <th className="p-2">{t("action")}</th>}
</tr>
</thead>
<tbody>
{currentData.length > 0 &&
currentData.map((form, formIndex) => {
const globalIndex = startIndex + 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} id={`table-row-${formIndex}`}>
{emailFields.map((field, fieldIndex) => (
<td key={field.fieldId} className="p-2">
<SuggestionInput
required
type="email"
value={field.email ?? ""}
index={fieldIndex}
onChange={(signer) =>
handleInputChange(globalIndex, signer, field.label)
}
/>
</td>
))}
{widgets.map(({ widget, fieldIndex, widgetIndex }) => (
<td key={`${form.Id}-${widget.key}`} className="p-2">
<RenderWidgets
widget={widget}
handleWidgetDetails={(value) =>
handleWidgetDetails(
value,
globalIndex, // formIndex,
fieldIndex,
widgetIndex
)
}
/>
</td>
))}
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
};
export default Table;
@@ -0,0 +1,74 @@
/* ---------------- Wizard Header ---------------- */
import React from "react";
function WizardHeader({ steps, step, onStepClick }) {
return (
<div className="flex flex-col md:flex-row flex-wrap items-center justify-center gap-0 md:gap-2">
{steps.map((s, i) => {
const isActive = i === step;
const isDone = i < step;
return (
<React.Fragment key={s.key}>
<button
type="button"
onClick={() => onStepClick?.(i)}
className={[
"inline-flex items-center gap-2 rounded-md px-3 py-2 text-xs lg:text-sm font-medium border focus:outline-none",
isActive
? "op-bg-primary text-primary-content border-primary"
: isDone
? "bg-base-100 text-base-content border-base-300 hover:bg-base-200"
: "bg-base-100 text-base-content/70 border-base-300"
].join(" ")}
title={s.label}
>
<span
className={[
"inline-flex h-5 w-5 items-center justify-center rounded-full text-xs",
isActive
? "op-bg-primary-content/15 text-primary-content"
: isDone
? "bg-base-200 text-base-content"
: "bg-base-200 text-base-content/70"
].join(" ")}
>
{i + 1}
</span>
<span className="whitespace-nowrap">{s.label}</span>
{/* info icon like screenshot */}
<a data-tooltip-id={`${s.key}-tooltip`} className="ml-1">
<span
className={[
"inline-flex h-[1.10rem] w-[1.10rem] items-center justify-center border-[1px] rounded-full",
!isActive
? "border-[#33bbff] text-[#33bbff]"
: "border border-base-300 text-base-300"
].join(" ")}
aria-hidden="true"
>
<i className="fa-light fa-question rounded-full text-[13px]"></i>
</span>
</a>
<span className="text-left">{s.help}</span>
</button>
{i < steps.length - 1 && (
<span
className="mx-1 select-none text-base-content/40 text-center text-2xl md:mb-2 rotate-90 md:rotate-0"
aria-hidden="true"
>
&#8250;
</span>
)}
</React.Fragment>
);
})}
</div>
);
}
export default WizardHeader;
@@ -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>
);
@@ -0,0 +1,70 @@
import React from 'react';
import { Box, Typography } from '@mui/material';
import { TEditorBlock } from '../../../documents/editor/core';
import { setDocument, useDocument, useSelectedBlockId } from '../../../documents/editor/EditorContext';
import AvatarSidebarPanel from './input-panels/AvatarSidebarPanel';
import ButtonSidebarPanel from './input-panels/ButtonSidebarPanel';
import ColumnsContainerSidebarPanel from './input-panels/ColumnsContainerSidebarPanel';
import ContainerSidebarPanel from './input-panels/ContainerSidebarPanel';
import DividerSidebarPanel from './input-panels/DividerSidebarPanel';
import EmailLayoutSidebarPanel from './input-panels/EmailLayoutSidebarPanel';
import HeadingSidebarPanel from './input-panels/HeadingSidebarPanel';
import HtmlSidebarPanel from './input-panels/HtmlSidebarPanel';
import ImageSidebarPanel from './input-panels/ImageSidebarPanel';
import SpacerSidebarPanel from './input-panels/SpacerSidebarPanel';
import TextSidebarPanel from './input-panels/TextSidebarPanel';
function renderMessage(val: string) {
return (
<Box sx={{ m: 3, p: 1, border: '1px dashed', borderColor: 'divider' }}>
<Typography color="text.secondary">{val}</Typography>
</Box>
);
}
export default function ConfigurationPanel() {
const document = useDocument();
const selectedBlockId = useSelectedBlockId();
if (!selectedBlockId) {
return renderMessage('Click on a block to inspect.');
}
const block = document[selectedBlockId];
if (!block) {
return renderMessage(`Block with id ${selectedBlockId} was not found. Click on a block to reset.`);
}
const setBlock = (conf: TEditorBlock) => setDocument({ [selectedBlockId]: conf });
const { data, type } = block;
switch (type) {
case 'Avatar':
return <AvatarSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
case 'Button':
return <ButtonSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
case 'ColumnsContainer':
return (
<ColumnsContainerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />
);
case 'Container':
return <ContainerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
case 'Divider':
return <DividerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
case 'Heading':
return <HeadingSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
case 'Html':
return <HtmlSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
case 'Image':
return <ImageSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
case 'EmailLayout':
return <EmailLayoutSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
case 'Spacer':
return <SpacerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
case 'Text':
return <TextSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
default:
return <pre>{JSON.stringify(block, null, ' ')}</pre>;
}
}
@@ -0,0 +1,89 @@
import React, { useState } from "react";
import { ZodError } from "zod";
import { AspectRatioOutlined } from "@mui/icons-material";
import { ToggleButton } from "@mui/material";
import {
AvatarProps,
AvatarPropsDefaults,
AvatarPropsSchema
} from "@usewaypoint/block-avatar";
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
import SliderInput from "./helpers/inputs/SliderInput";
import TextInput from "./helpers/inputs/TextInput";
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
type AvatarSidebarPanelProps = {
data: AvatarProps;
setData: (v: AvatarProps) => void;
};
export default function AvatarSidebarPanel({
data,
setData
}: AvatarSidebarPanelProps) {
const [, setErrors] = useState<ZodError | null>(null);
const updateData = (d: unknown) => {
const res = AvatarPropsSchema.safeParse(d);
if (res.success) {
setData(res.data);
setErrors(null);
} else {
setErrors(res.error);
}
};
const size = data.props?.size ?? AvatarPropsDefaults.size;
const imageUrl = data.props?.imageUrl ?? AvatarPropsDefaults.imageUrl;
const alt = data.props?.alt ?? AvatarPropsDefaults.alt;
const shape = data.props?.shape ?? AvatarPropsDefaults.shape;
return (
<BaseSidebarPanel title="Avatar block">
<SliderInput
label="Size"
iconLabel={<AspectRatioOutlined sx={{ color: "text.secondary" }} />}
units="px"
step={3}
min={32}
max={256}
defaultValue={size}
onChange={(size) => {
updateData({ ...data, props: { ...data.props, size } });
}}
/>
<RadioGroupInput
label="Shape"
defaultValue={shape}
onChange={(shape) => {
updateData({ ...data, props: { ...data.props, shape } });
}}
>
<ToggleButton value="circle">Circle</ToggleButton>
<ToggleButton value="square">Square</ToggleButton>
<ToggleButton value="rounded">Rounded</ToggleButton>
</RadioGroupInput>
<TextInput
label="Image URL"
defaultValue={imageUrl}
onChange={(imageUrl) => {
updateData({ ...data, props: { ...data.props, imageUrl } });
}}
/>
<TextInput
label="Alt text"
defaultValue={alt}
onChange={(alt) => {
updateData({ ...data, props: { ...data.props, alt } });
}}
/>
<MultiStylePropertyPanel
names={["textAlign", "padding"]}
value={data.style}
onChange={(style) => updateData({ ...data, style })}
/>
</BaseSidebarPanel>
);
}
@@ -0,0 +1,132 @@
import React, { useState } from "react";
import { ZodError } from "zod";
import { ToggleButton } from "@mui/material";
import {
ButtonProps,
ButtonPropsDefaults,
ButtonPropsSchema
} from "@usewaypoint/block-button";
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
import ColorInput from "./helpers/inputs/ColorInput";
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
import TextInput from "./helpers/inputs/TextInput";
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
type ButtonSidebarPanelProps = {
data: ButtonProps;
setData: (v: ButtonProps) => void;
};
export default function ButtonSidebarPanel({
data,
setData
}: ButtonSidebarPanelProps) {
const [, setErrors] = useState<ZodError | null>(null);
const updateData = (d: unknown) => {
const res = ButtonPropsSchema.safeParse(d);
if (res.success) {
setData(res.data);
setErrors(null);
} else {
setErrors(res.error);
}
};
const text = data.props?.text ?? ButtonPropsDefaults.text;
const url = data.props?.url ?? ButtonPropsDefaults.url;
const fullWidth = data.props?.fullWidth ?? ButtonPropsDefaults.fullWidth;
const size = data.props?.size ?? ButtonPropsDefaults.size;
const buttonStyle =
data.props?.buttonStyle ?? ButtonPropsDefaults.buttonStyle;
const buttonTextColor =
data.props?.buttonTextColor ?? ButtonPropsDefaults.buttonTextColor;
const buttonBackgroundColor =
data.props?.buttonBackgroundColor ??
ButtonPropsDefaults.buttonBackgroundColor;
return (
<BaseSidebarPanel title="Button block">
<TextInput
label="Text"
defaultValue={text}
onChange={(text) =>
updateData({ ...data, props: { ...data.props, text } })
}
/>
<TextInput
label="Url"
defaultValue={url}
onChange={(url) =>
updateData({ ...data, props: { ...data.props, url } })
}
/>
<RadioGroupInput
label="Width"
defaultValue={fullWidth ? "FULL_WIDTH" : "AUTO"}
onChange={(v) =>
updateData({
...data,
props: { ...data.props, fullWidth: v === "FULL_WIDTH" }
})
}
>
<ToggleButton value="FULL_WIDTH">Full</ToggleButton>
<ToggleButton value="AUTO">Auto</ToggleButton>
</RadioGroupInput>
<RadioGroupInput
label="Size"
defaultValue={size}
onChange={(size) =>
updateData({ ...data, props: { ...data.props, size } })
}
>
<ToggleButton value="x-small">Xs</ToggleButton>
<ToggleButton value="small">Sm</ToggleButton>
<ToggleButton value="medium">Md</ToggleButton>
<ToggleButton value="large">Lg</ToggleButton>
</RadioGroupInput>
<RadioGroupInput
label="Style"
defaultValue={buttonStyle}
onChange={(buttonStyle) =>
updateData({ ...data, props: { ...data.props, buttonStyle } })
}
>
<ToggleButton value="rectangle">Rectangle</ToggleButton>
<ToggleButton value="rounded">Rounded</ToggleButton>
<ToggleButton value="pill">Pill</ToggleButton>
</RadioGroupInput>
<ColorInput
label="Text color"
defaultValue={buttonTextColor}
onChange={(buttonTextColor) =>
updateData({ ...data, props: { ...data.props, buttonTextColor } })
}
/>
<ColorInput
label="Button color"
defaultValue={buttonBackgroundColor}
onChange={(buttonBackgroundColor) =>
updateData({
...data,
props: { ...data.props, buttonBackgroundColor }
})
}
/>
<MultiStylePropertyPanel
names={[
"backgroundColor",
"fontFamily",
"fontSize",
"fontWeight",
"textAlign",
"padding"
]}
value={data.style}
onChange={(style) => updateData({ ...data, style })}
/>
</BaseSidebarPanel>
);
}
@@ -0,0 +1,100 @@
import React, { useState } from "react";
import { ZodError } from "zod";
import {
SpaceBarOutlined,
VerticalAlignBottomOutlined,
VerticalAlignCenterOutlined,
VerticalAlignTopOutlined
} from "@mui/icons-material";
import { ToggleButton } from "@mui/material";
import ColumnsContainerPropsSchema, {
ColumnsContainerProps
} from "../../../../documents/blocks/ColumnsContainer/ColumnsContainerPropsSchema";
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
import ColumnWidthsInput from "./helpers/inputs/ColumnWidthsInput";
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
import SliderInput from "./helpers/inputs/SliderInput";
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
type ColumnsContainerPanelProps = {
data: ColumnsContainerProps;
setData: (v: ColumnsContainerProps) => void;
};
export default function ColumnsContainerPanel({
data,
setData
}: ColumnsContainerPanelProps) {
const [, setErrors] = useState<ZodError | null>(null);
const updateData = (d: unknown) => {
const res = ColumnsContainerPropsSchema.safeParse(d);
if (res.success) {
setData(res.data);
setErrors(null);
} else {
setErrors(res.error);
}
};
return (
<BaseSidebarPanel title="Columns block">
<RadioGroupInput
label="Number of columns"
defaultValue={data.props?.columnsCount === 2 ? "2" : "3"}
onChange={(v) => {
updateData({
...data,
props: { ...data.props, columnsCount: v === "2" ? 2 : 3 }
});
}}
>
<ToggleButton value="2">2</ToggleButton>
<ToggleButton value="3">3</ToggleButton>
</RadioGroupInput>
<ColumnWidthsInput
defaultValue={data.props?.fixedWidths}
onChange={(fixedWidths) => {
updateData({ ...data, props: { ...data.props, fixedWidths } });
}}
/>
<SliderInput
label="Columns gap"
iconLabel={<SpaceBarOutlined sx={{ color: "text.secondary" }} />}
units="px"
step={4}
marks
min={0}
max={80}
defaultValue={data.props?.columnsGap ?? 0}
onChange={(columnsGap) =>
updateData({ ...data, props: { ...data.props, columnsGap } })
}
/>
<RadioGroupInput
label="Alignment"
defaultValue={data.props?.contentAlignment ?? "middle"}
onChange={(contentAlignment) => {
updateData({ ...data, props: { ...data.props, contentAlignment } });
}}
>
<ToggleButton value="top">
<VerticalAlignTopOutlined fontSize="small" />
</ToggleButton>
<ToggleButton value="middle">
<VerticalAlignCenterOutlined fontSize="small" />
</ToggleButton>
<ToggleButton value="bottom">
<VerticalAlignBottomOutlined fontSize="small" />
</ToggleButton>
</RadioGroupInput>
<MultiStylePropertyPanel
names={["backgroundColor", "padding"]}
value={data.style}
onChange={(style) => updateData({ ...data, style })}
/>
</BaseSidebarPanel>
);
}
@@ -0,0 +1,39 @@
import React, { useState } from "react";
import type { ZodError } from "zod";
import ContainerPropsSchema, {
ContainerProps
} from "../../../../documents/blocks/Container/ContainerPropsSchema";
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
type ContainerSidebarPanelProps = {
data: ContainerProps;
setData: (v: ContainerProps) => void;
};
export default function ContainerSidebarPanel({
data,
setData
}: ContainerSidebarPanelProps) {
const [, setErrors] = useState<ZodError | null>(null);
const updateData = (d: unknown) => {
const res = ContainerPropsSchema.safeParse(d);
if (res.success) {
setData(res.data);
setErrors(null);
} else {
setErrors(res.error);
}
};
return (
<BaseSidebarPanel title="Container block">
<MultiStylePropertyPanel
names={["backgroundColor", "borderColor", "borderRadius", "padding"]}
value={data.style}
onChange={(style) => updateData({ ...data, style })}
/>
</BaseSidebarPanel>
);
}
@@ -0,0 +1,66 @@
import React, { useState } from "react";
import { ZodError } from "zod";
import { HeightOutlined } from "@mui/icons-material";
import {
DividerProps,
DividerPropsDefaults,
DividerPropsSchema
} from "@usewaypoint/block-divider";
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
import ColorInput from "./helpers/inputs/ColorInput";
import SliderInput from "./helpers/inputs/SliderInput";
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
type DividerSidebarPanelProps = {
data: DividerProps;
setData: (v: DividerProps) => void;
};
export default function DividerSidebarPanel({
data,
setData
}: DividerSidebarPanelProps) {
const [, setErrors] = useState<ZodError | null>(null);
const updateData = (d: unknown) => {
const res = DividerPropsSchema.safeParse(d);
if (res.success) {
setData(res.data);
setErrors(null);
} else {
setErrors(res.error);
}
};
const lineColor = data.props?.lineColor ?? DividerPropsDefaults.lineColor;
const lineHeight = data.props?.lineHeight ?? DividerPropsDefaults.lineHeight;
return (
<BaseSidebarPanel title="Divider block">
<ColorInput
label="Color"
defaultValue={lineColor}
onChange={(lineColor) =>
updateData({ ...data, props: { ...data.props, lineColor } })
}
/>
<SliderInput
label="Height"
iconLabel={<HeightOutlined sx={{ color: "text.secondary" }} />}
units="px"
step={1}
min={1}
max={24}
defaultValue={lineHeight}
onChange={(lineHeight) =>
updateData({ ...data, props: { ...data.props, lineHeight } })
}
/>
<MultiStylePropertyPanel
names={["backgroundColor", "padding"]}
value={data.style}
onChange={(style) => updateData({ ...data, style })}
/>
</BaseSidebarPanel>
);
}
@@ -0,0 +1,84 @@
import React, { useState } from "react";
import { ZodError } from "zod";
import { WidthWide, RoundedCornerOutlined } from "@mui/icons-material";
import EmailLayoutPropsSchema, {
EmailLayoutProps
} from "../../../../documents/blocks/EmailLayout/EmailLayoutPropsSchema";
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
import ColorInput, { NullableColorInput } from "./helpers/inputs/ColorInput";
import { NullableFontFamily } from "./helpers/inputs/FontFamily";
import SliderInput from "./helpers/inputs/SliderInput";
type EmailLayoutSidebarFieldsProps = {
data: EmailLayoutProps;
setData: (v: EmailLayoutProps) => void;
};
export default function EmailLayoutSidebarFields({
data,
setData
}: EmailLayoutSidebarFieldsProps) {
const [, setErrors] = useState<ZodError | null>(null);
const updateData = (d: unknown) => {
const res = EmailLayoutPropsSchema.safeParse(d);
if (res.success) {
setData(res.data);
setErrors(null);
} else {
setErrors(res.error);
}
};
return (
<BaseSidebarPanel title="Global">
<ColorInput
label="Backdrop color"
defaultValue={data.backdropColor ?? "#F5F5F5"}
onChange={(backdropColor) => updateData({ ...data, backdropColor })}
/>
<ColorInput
label="Canvas color"
defaultValue={data.canvasColor ?? "#FFFFFF"}
onChange={(canvasColor) => updateData({ ...data, canvasColor })}
/>
<NullableColorInput
label="Canvas border color"
defaultValue={data.borderColor ?? null}
onChange={(borderColor) => updateData({ ...data, borderColor })}
/>
<SliderInput
iconLabel={<WidthWide />}
units="px"
step={10}
min={320}
max={1200}
label="Canvas width"
defaultValue={data.canvasWidth ?? 600}
onChange={(canvasWidth) => updateData({ ...data, canvasWidth })}
/>
<SliderInput
iconLabel={<RoundedCornerOutlined />}
units="px"
step={4}
marks
min={0}
max={48}
label="Canvas border radius"
defaultValue={data.borderRadius ?? 0}
onChange={(borderRadius) => updateData({ ...data, borderRadius })}
/>
<NullableFontFamily
label="Font family"
defaultValue="MODERN_SANS"
onChange={(fontFamily) => updateData({ ...data, fontFamily })}
/>
<ColorInput
label="Text color"
defaultValue={data.textColor ?? "#262626"}
onChange={(textColor) => updateData({ ...data, textColor })}
/>
</BaseSidebarPanel>
);
}
@@ -0,0 +1,71 @@
import React, { useState } from "react";
import { ZodError } from "zod";
import { ToggleButton } from "@mui/material";
import {
HeadingProps,
HeadingPropsDefaults,
HeadingPropsSchema
} from "@usewaypoint/block-heading";
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
import TextInput from "./helpers/inputs/TextInput";
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
type HeadingSidebarPanelProps = {
data: HeadingProps;
setData: (v: HeadingProps) => void;
};
export default function HeadingSidebarPanel({
data,
setData
}: HeadingSidebarPanelProps) {
const [, setErrors] = useState<ZodError | null>(null);
const updateData = (d: unknown) => {
const res = HeadingPropsSchema.safeParse(d);
if (res.success) {
setData(res.data);
setErrors(null);
} else {
setErrors(res.error);
}
};
return (
<BaseSidebarPanel title="Heading block">
<TextInput
label="Content"
rows={3}
defaultValue={data.props?.text ?? HeadingPropsDefaults.text}
onChange={(text) => {
updateData({ ...data, props: { ...data.props, text } });
}}
/>
<RadioGroupInput
label="Level"
defaultValue={data.props?.level ?? HeadingPropsDefaults.level}
onChange={(level) => {
updateData({ ...data, props: { ...data.props, level } });
}}
>
<ToggleButton value="h1">H1</ToggleButton>
<ToggleButton value="h2">H2</ToggleButton>
<ToggleButton value="h3">H3</ToggleButton>
</RadioGroupInput>
<MultiStylePropertyPanel
names={[
"color",
"backgroundColor",
"fontFamily",
"fontWeight",
"textAlign",
"padding"
]}
value={data.style}
onChange={(style) => updateData({ ...data, style })}
/>
</BaseSidebarPanel>
);
}
@@ -0,0 +1,54 @@
import React, { useState } from "react";
import { ZodError } from "zod";
import { HtmlProps, HtmlPropsSchema } from "@usewaypoint/block-html";
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
import TextInput from "./helpers/inputs/TextInput";
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
type HtmlSidebarPanelProps = {
data: HtmlProps;
setData: (v: HtmlProps) => void;
};
export default function HtmlSidebarPanel({
data,
setData
}: HtmlSidebarPanelProps) {
const [, setErrors] = useState<ZodError | null>(null);
const updateData = (d: unknown) => {
const res = HtmlPropsSchema.safeParse(d);
if (res.success) {
setData(res.data);
setErrors(null);
} else {
setErrors(res.error);
}
};
return (
<BaseSidebarPanel title="Html block">
<TextInput
label="Content"
rows={5}
defaultValue={data.props?.contents ?? ""}
onChange={(contents) =>
updateData({ ...data, props: { ...data.props, contents } })
}
/>
<MultiStylePropertyPanel
names={[
"color",
"backgroundColor",
"fontFamily",
"fontSize",
"textAlign",
"padding"
]}
value={data.style}
onChange={(style) => updateData({ ...data, style })}
/>
</BaseSidebarPanel>
);
}
@@ -0,0 +1,106 @@
import React, { useState } from "react";
import { ZodError } from "zod";
import {
VerticalAlignBottomOutlined,
VerticalAlignCenterOutlined,
VerticalAlignTopOutlined
} from "@mui/icons-material";
import { Stack, ToggleButton } from "@mui/material";
import { ImageProps, ImagePropsSchema } from "@usewaypoint/block-image";
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
import TextDimensionInput from "./helpers/inputs/TextDimensionInput";
import TextInput from "./helpers/inputs/TextInput";
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
type ImageSidebarPanelProps = {
data: ImageProps;
setData: (v: ImageProps) => void;
};
export default function ImageSidebarPanel({
data,
setData
}: ImageSidebarPanelProps) {
const [, setErrors] = useState<ZodError | null>(null);
const updateData = (d: unknown) => {
const res = ImagePropsSchema.safeParse(d);
if (res.success) {
setData(res.data);
setErrors(null);
} else {
setErrors(res.error);
}
};
return (
<BaseSidebarPanel title="Image block">
<TextInput
label="Source URL"
defaultValue={data.props?.url ?? ""}
onChange={(v) => {
const url = v.trim().length === 0 ? null : v.trim();
updateData({ ...data, props: { ...data.props, url } });
}}
/>
<TextInput
label="Alt text"
defaultValue={data.props?.alt ?? ""}
onChange={(alt) =>
updateData({ ...data, props: { ...data.props, alt } })
}
/>
<TextInput
label="Click through URL"
defaultValue={data.props?.linkHref ?? ""}
onChange={(v) => {
const linkHref = v.trim().length === 0 ? null : v.trim();
updateData({ ...data, props: { ...data.props, linkHref } });
}}
/>
<Stack direction="row" spacing={2}>
<TextDimensionInput
label="Width"
defaultValue={data.props?.width}
onChange={(width) =>
updateData({ ...data, props: { ...data.props, width } })
}
/>
<TextDimensionInput
label="Height"
defaultValue={data.props?.height}
onChange={(height) =>
updateData({ ...data, props: { ...data.props, height } })
}
/>
</Stack>
<RadioGroupInput
label="Alignment"
defaultValue={data.props?.contentAlignment ?? "middle"}
onChange={(contentAlignment) =>
updateData({ ...data, props: { ...data.props, contentAlignment } })
}
>
<ToggleButton value="top">
<VerticalAlignTopOutlined fontSize="small" />
</ToggleButton>
<ToggleButton value="middle">
<VerticalAlignCenterOutlined fontSize="small" />
</ToggleButton>
<ToggleButton value="bottom">
<VerticalAlignBottomOutlined fontSize="small" />
</ToggleButton>
</RadioGroupInput>
<MultiStylePropertyPanel
names={["backgroundColor", "textAlign", "padding"]}
value={data.style}
onChange={(style) => updateData({ ...data, style })}
/>
</BaseSidebarPanel>
);
}
@@ -0,0 +1,50 @@
import React, { useState } from "react";
import { ZodError } from "zod";
import { HeightOutlined } from "@mui/icons-material";
import {
SpacerProps,
SpacerPropsDefaults,
SpacerPropsSchema
} from "@usewaypoint/block-spacer";
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
import SliderInput from "./helpers/inputs/SliderInput";
type SpacerSidebarPanelProps = {
data: SpacerProps;
setData: (v: SpacerProps) => void;
};
export default function SpacerSidebarPanel({
data,
setData
}: SpacerSidebarPanelProps) {
const [, setErrors] = useState<ZodError | null>(null);
const updateData = (d: unknown) => {
const res = SpacerPropsSchema.safeParse(d);
if (res.success) {
setData(res.data);
setErrors(null);
} else {
setErrors(res.error);
}
};
return (
<BaseSidebarPanel title="Spacer block">
<SliderInput
label="Height"
iconLabel={<HeightOutlined sx={{ color: "text.secondary" }} />}
units="px"
step={4}
min={4}
max={128}
defaultValue={data.props?.height ?? SpacerPropsDefaults.height}
onChange={(height) =>
updateData({ ...data, props: { ...data.props, height } })
}
/>
</BaseSidebarPanel>
);
}
@@ -0,0 +1,64 @@
import React, { useState } from "react";
import { ZodError } from "zod";
import { TextProps, TextPropsSchema } from "@usewaypoint/block-text";
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
import BooleanInput from "./helpers/inputs/BooleanInput";
import TextInput from "./helpers/inputs/TextInput";
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
type TextSidebarPanelProps = {
data: TextProps;
setData: (v: TextProps) => void;
};
export default function TextSidebarPanel({
data,
setData
}: TextSidebarPanelProps) {
const [, setErrors] = useState<ZodError | null>(null);
const updateData = (d: unknown) => {
const res = TextPropsSchema.safeParse(d);
if (res.success) {
setData(res.data);
setErrors(null);
} else {
setErrors(res.error);
}
};
return (
<BaseSidebarPanel title="Text block">
<TextInput
label="Content"
rows={5}
defaultValue={data.props?.text ?? ""}
onChange={(text) =>
updateData({ ...data, props: { ...data.props, text } })
}
/>
<BooleanInput
label="Markdown (GitHub flavored)"
defaultValue={data.props?.markdown ?? false}
onChange={(markdown) =>
updateData({ ...data, props: { ...data.props, markdown } })
}
/>
<MultiStylePropertyPanel
names={[
"color",
"backgroundColor",
"fontFamily",
"fontSize",
"fontWeight",
"textAlign",
"padding"
]}
value={data.style}
onChange={(style) => updateData({ ...data, style })}
/>
</BaseSidebarPanel>
);
}
@@ -0,0 +1,27 @@
import React from "react";
import { Box, Stack, Typography } from "@mui/material";
type SidebarPanelProps = {
title: string;
children: React.ReactNode;
};
export default function BaseSidebarPanel({
title,
children
}: SidebarPanelProps) {
return (
<Box p={2}>
<Typography
variant="overline"
color="text.secondary"
sx={{ display: "block", mb: 2 }}
>
{title}
</Typography>
<Stack spacing={5} mb={3}>
{children}
</Stack>
</Box>
);
}
@@ -0,0 +1,27 @@
import React, { useState } from 'react';
import { FormControlLabel, Switch } from '@mui/material';
type Props = {
label: string;
defaultValue: boolean;
onChange: (value: boolean) => void;
};
export default function BooleanInput({ label, defaultValue, onChange }: Props) {
const [value, setValue] = useState(defaultValue);
return (
<FormControlLabel
label={label}
control={
<Switch
checked={value}
onChange={(_, checked: boolean) => {
setValue(checked);
onChange(checked);
}}
/>
}
/>
);
}
@@ -0,0 +1,102 @@
import React, { useState } from "react";
import { AddOutlined, CloseOutlined } from "@mui/icons-material";
import { ButtonBase, InputLabel, Menu, Stack } from "@mui/material";
import Picker from "./Picker";
const BUTTON_SX = {
border: "1px solid",
borderColor: "cadet.400",
width: 32,
height: 32,
borderRadius: "4px",
bgcolor: "#FFFFFF"
};
type Props =
| {
nullable: true;
label: string;
onChange: (value: string | null) => void;
defaultValue: string | null;
}
| {
nullable: false;
label: string;
onChange: (value: string) => void;
defaultValue: string;
};
export default function ColorInput({
label,
defaultValue,
onChange,
nullable
}: Props) {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [value, setValue] = useState(defaultValue);
const handleClickOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const renderResetButton = () => {
if (!nullable) {
return null;
}
if (typeof value !== "string" || value.trim().length === 0) {
return null;
}
return (
<ButtonBase
onClick={() => {
setValue(null);
onChange(null);
}}
>
<CloseOutlined fontSize="small" sx={{ color: "grey.600" }} />
</ButtonBase>
);
};
const renderOpenButton = () => {
if (value) {
return (
<ButtonBase
onClick={handleClickOpen}
sx={{ ...BUTTON_SX, bgcolor: value }}
/>
);
}
return (
<ButtonBase onClick={handleClickOpen} sx={{ ...BUTTON_SX }}>
<AddOutlined fontSize="small" />
</ButtonBase>
);
};
return (
<Stack alignItems="flex-start">
<InputLabel sx={{ mb: 0.5 }}>{label}</InputLabel>
<Stack direction="row" spacing={1}>
{renderOpenButton()}
{renderResetButton()}
</Stack>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
MenuListProps={{
sx: { height: "auto", padding: 0 }
}}
>
<Picker
value={value || ""}
onChange={(v) => {
setValue(v);
onChange(v);
}}
/>
</Menu>
</Stack>
);
}
@@ -0,0 +1,98 @@
import React, { useState } from "react";
import { HexColorInput, HexColorPicker } from "react-colorful";
import { Box, Stack, SxProps } from "@mui/material";
import Swatch from "./Swatch";
const DEFAULT_PRESET_COLORS = [
"#E11D48",
"#DB2777",
"#C026D3",
"#9333EA",
"#7C3AED",
"#4F46E5",
"#2563EB",
"#0284C7",
"#0891B2",
"#0D9488",
"#059669",
"#16A34A",
"#65A30D",
"#CA8A04",
"#D97706",
"#EA580C",
"#DC2626",
"#FFFFFF",
"#FAFAFA",
"#F5F5F5",
"#E5E5E5",
"#D4D4D4",
"#A3A3A3",
"#737373",
"#525252",
"#404040",
"#262626",
"#171717",
"#0A0A0A",
"#000000"
];
const SX: SxProps = {
p: 1,
".react-colorful__pointer ": {
width: 16,
height: 16
},
".react-colorful__saturation": {
mb: 1,
borderRadius: "4px"
},
".react-colorful__last-control": {
borderRadius: "4px"
},
".react-colorful__hue-pointer": {
width: "4px",
borderRadius: "4px",
height: 24,
cursor: "col-resize"
},
".react-colorful__saturation-pointer": {
cursor: "all-scroll"
},
input: {
padding: 1,
border: "1px solid",
borderColor: "grey.300",
borderRadius: "4px",
width: "100%"
}
};
type Props = {
value: string;
onChange: (v: string) => void;
};
export default function Picker({ value, onChange }: Props) {
const [internalValue, setInternalValue] = useState(value);
const handleChange = (v: string) => {
setInternalValue(v);
if (/^#[0-9a-fA-F]{6}$/.test(v)) {
onChange(v);
}
};
return (
<Stack spacing={1} sx={SX}>
<HexColorPicker color={value} onChange={handleChange} />
<Swatch
paletteColors={DEFAULT_PRESET_COLORS}
value={value}
onChange={handleChange}
/>
<Box pt={1}>
<HexColorInput prefixed color={internalValue} onChange={handleChange} />
</Box>
</Stack>
);
}
@@ -0,0 +1,48 @@
import React from "react";
import { Box, Button, SxProps } from "@mui/material";
type Props = {
paletteColors: string[];
value: string;
onChange: (value: string) => void;
};
const TILE_BUTTON: SxProps = {
width: 24,
height: 24
};
export default function Swatch({ paletteColors, value, onChange }: Props) {
const renderButton = (colorValue: string) => {
return (
<Button
key={colorValue}
onClick={() => onChange(colorValue)}
sx={{
...TILE_BUTTON,
backgroundColor: colorValue,
border: "1px solid",
borderColor: value === colorValue ? "black" : "grey.200",
minWidth: 24,
display: "inline-flex",
"&:hover": {
backgroundColor: colorValue,
borderColor: "grey.500"
}
}}
/>
);
};
return (
<Box
width="100%"
sx={{
display: "grid",
gap: 1,
gridTemplateColumns: "1fr 1fr 1fr 1fr 1fr 1fr"
}}
>
{paletteColors.map((c) => renderButton(c))}
</Box>
);
}
@@ -0,0 +1,21 @@
import React from "react";
import BaseColorInput from "./BaseColorInput";
type Props = {
label: string;
onChange: (value: string) => void;
defaultValue: string;
};
export default function ColorInput(props: Props) {
return <BaseColorInput {...props} nullable={false} />;
}
type NullableProps = {
label: string;
onChange: (value: null | string) => void;
defaultValue: null | string;
};
export function NullableColorInput(props: NullableProps) {
return <BaseColorInput {...props} nullable />;
}
@@ -0,0 +1,68 @@
import React, { useState } from 'react';
import { Stack } from '@mui/material';
import TextDimensionInput from './TextDimensionInput';
export const DEFAULT_2_COLUMNS = [6] as [number];
export const DEFAULT_3_COLUMNS = [4, 8] as [number, number];
type TWidthValue = number | null | undefined;
type FixedWidths = [
//
number | null | undefined,
number | null | undefined,
number | null | undefined,
];
type ColumnsLayoutInputProps = {
defaultValue: FixedWidths | null | undefined;
onChange: (v: FixedWidths | null | undefined) => void;
};
export default function ColumnWidthsInput({ defaultValue, onChange }: ColumnsLayoutInputProps) {
const [currentValue, setCurrentValue] = useState<[TWidthValue, TWidthValue, TWidthValue]>(() => {
if (defaultValue) {
return defaultValue;
}
return [null, null, null];
});
const setIndexValue = (index: 0 | 1 | 2, value: number | null | undefined) => {
const nValue: FixedWidths = [...currentValue];
nValue[index] = value;
setCurrentValue(nValue);
onChange(nValue);
};
const columnsCountValue = 3;
let column3 = null;
if (columnsCountValue === 3) {
column3 = (
<TextDimensionInput
label="Column 3"
defaultValue={currentValue?.[2]}
onChange={(v) => {
setIndexValue(2, v);
}}
/>
);
}
return (
<Stack direction="row" spacing={1}>
<TextDimensionInput
label="Column 1"
defaultValue={currentValue?.[0]}
onChange={(v) => {
setIndexValue(0, v);
}}
/>
<TextDimensionInput
label="Column 2"
defaultValue={currentValue?.[1]}
onChange={(v) => {
setIndexValue(1, v);
}}
/>
{column3}
</Stack>
);
}
@@ -0,0 +1,36 @@
import React, { useState } from 'react';
import { MenuItem, TextField } from '@mui/material';
import { FONT_FAMILIES } from '../../../../../../documents/blocks/helpers/fontFamily';
const OPTIONS = FONT_FAMILIES.map((option) => (
<MenuItem key={option.key} value={option.key} sx={{ fontFamily: option.value }}>
{option.label}
</MenuItem>
));
type NullableProps = {
label: string;
onChange: (value: null | string) => void;
defaultValue: null | string;
};
export function NullableFontFamily({ label, onChange, defaultValue }: NullableProps) {
const [value, setValue] = useState(defaultValue ?? 'inherit');
return (
<TextField
select
variant="standard"
label={label}
value={value}
onChange={(ev) => {
const v = ev.target.value;
setValue(v);
onChange(v === null ? null : v);
}}
>
<MenuItem value="inherit">Match email settings</MenuItem>
{OPTIONS}
</TextField>
);
}
@@ -0,0 +1,33 @@
import React, { useState } from 'react';
import { TextFieldsOutlined } from '@mui/icons-material';
import { InputLabel, Stack } from '@mui/material';
import RawSliderInput from './raw/RawSliderInput';
type Props = {
label: string;
defaultValue: number;
onChange: (v: number) => void;
};
export default function FontSizeInput({ label, defaultValue, onChange }: Props) {
const [value, setValue] = useState(defaultValue);
const handleChange = (value: number) => {
setValue(value);
onChange(value);
};
return (
<Stack spacing={1} alignItems="flex-start">
<InputLabel shrink>{label}</InputLabel>
<RawSliderInput
iconLabel={<TextFieldsOutlined sx={{ fontSize: 16 }} />}
value={value}
setValue={handleChange}
units="px"
step={1}
min={10}
max={48}
/>
</Stack>
);
}
@@ -0,0 +1,27 @@
import React, { useState } from 'react';
import { ToggleButton } from '@mui/material';
import RadioGroupInput from './RadioGroupInput';
type Props = {
label: string;
defaultValue: string;
onChange: (value: string) => void;
};
export default function FontWeightInput({ label, defaultValue, onChange }: Props) {
const [value, setValue] = useState(defaultValue);
return (
<RadioGroupInput
label={label}
defaultValue={value}
onChange={(fontWeight) => {
setValue(fontWeight);
onChange(fontWeight);
}}
>
<ToggleButton value="normal">Regular</ToggleButton>
<ToggleButton value="bold">Bold</ToggleButton>
</RadioGroupInput>
);
}
@@ -0,0 +1,95 @@
import React, { useState } from 'react';
import {
AlignHorizontalLeftOutlined,
AlignHorizontalRightOutlined,
AlignVerticalBottomOutlined,
AlignVerticalTopOutlined,
} from '@mui/icons-material';
import { InputLabel, Stack } from '@mui/material';
import RawSliderInput from './raw/RawSliderInput';
type TPaddingValue = {
top: number;
bottom: number;
right: number;
left: number;
};
type Props = {
label: string;
defaultValue: TPaddingValue | null;
onChange: (value: TPaddingValue) => void;
};
export default function PaddingInput({ label, defaultValue, onChange }: Props) {
const [value, setValue] = useState(() => {
if (defaultValue) {
return defaultValue;
}
return {
top: 0,
left: 0,
bottom: 0,
right: 0,
};
});
function handleChange(internalName: keyof TPaddingValue, nValue: number) {
const v = {
...value,
[internalName]: nValue,
};
setValue(v);
onChange(v);
}
return (
<Stack spacing={2} alignItems="flex-start" pb={1}>
<InputLabel shrink>{label}</InputLabel>
<RawSliderInput
iconLabel={<AlignVerticalTopOutlined sx={{ fontSize: 16 }} />}
value={value.top}
setValue={(num) => handleChange('top', num)}
units="px"
step={4}
min={0}
max={80}
marks
/>
<RawSliderInput
iconLabel={<AlignVerticalBottomOutlined sx={{ fontSize: 16 }} />}
value={value.bottom}
setValue={(num) => handleChange('bottom', num)}
units="px"
step={4}
min={0}
max={80}
marks
/>
<RawSliderInput
iconLabel={<AlignHorizontalLeftOutlined sx={{ fontSize: 16 }} />}
value={value.left}
setValue={(num) => handleChange('left', num)}
units="px"
step={4}
min={0}
max={80}
marks
/>
<RawSliderInput
iconLabel={<AlignHorizontalRightOutlined sx={{ fontSize: 16 }} />}
value={value.right}
setValue={(num) => handleChange('right', num)}
units="px"
step={4}
min={0}
max={80}
marks
/>
</Stack>
);
}
@@ -0,0 +1,33 @@
import React, { useState } from 'react';
import { InputLabel, Stack, ToggleButtonGroup } from '@mui/material';
type Props = {
label: string | JSX.Element;
children: JSX.Element | JSX.Element[];
defaultValue: string;
onChange: (v: string) => void;
};
export default function RadioGroupInput({ label, children, defaultValue, onChange }: Props) {
const [value, setValue] = useState(defaultValue);
return (
<Stack alignItems="flex-start">
<InputLabel shrink>{label}</InputLabel>
<ToggleButtonGroup
exclusive
fullWidth
value={value}
size="small"
onChange={(_, v: unknown) => {
if (typeof v !== 'string') {
throw new Error('RadioGroupInput can only receive string values');
}
setValue(v);
onChange(v);
}}
>
{children}
</ToggleButtonGroup>
</Stack>
);
}
@@ -0,0 +1,36 @@
import React, { useState } from 'react';
import { InputLabel, Stack } from '@mui/material';
import RawSliderInput from './raw/RawSliderInput';
type SliderInputProps = {
label: string;
iconLabel: JSX.Element;
step?: number;
marks?: boolean;
units: string;
min?: number;
max?: number;
defaultValue: number;
onChange: (v: number) => void;
};
export default function SliderInput({ label, defaultValue, onChange, ...props }: SliderInputProps) {
const [value, setValue] = useState(defaultValue);
return (
<Stack spacing={1} alignItems="flex-start">
<InputLabel shrink>{label}</InputLabel>
<RawSliderInput
value={value}
setValue={(value: number) => {
setValue(value);
onChange(value);
}}
{...props}
/>
</Stack>
);
}
@@ -0,0 +1,36 @@
import React, { useState } from 'react';
import { FormatAlignCenterOutlined, FormatAlignLeftOutlined, FormatAlignRightOutlined } from '@mui/icons-material';
import { ToggleButton } from '@mui/material';
import RadioGroupInput from './RadioGroupInput';
type Props = {
label: string;
defaultValue: string | null;
onChange: (value: string | null) => void;
};
export default function TextAlignInput({ label, defaultValue, onChange }: Props) {
const [value, setValue] = useState(defaultValue ?? 'left');
return (
<RadioGroupInput
label={label}
defaultValue={value}
onChange={(value) => {
setValue(value);
onChange(value);
}}
>
<ToggleButton value="left">
<FormatAlignLeftOutlined fontSize="small" />
</ToggleButton>
<ToggleButton value="center">
<FormatAlignCenterOutlined fontSize="small" />
</ToggleButton>
<ToggleButton value="right">
<FormatAlignRightOutlined fontSize="small" />
</ToggleButton>
</RadioGroupInput>
);
}
@@ -0,0 +1,33 @@
import React from 'react';
import { TextField, Typography } from '@mui/material';
type TextDimensionInputProps = {
label: string;
defaultValue: number | null | undefined;
onChange: (v: number | null) => void;
};
export default function TextDimensionInput({ label, defaultValue, onChange }: TextDimensionInputProps) {
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (ev) => {
const value = parseInt(ev.target.value);
onChange(isNaN(value) ? null : value);
};
return (
<TextField
fullWidth
onChange={handleChange}
defaultValue={defaultValue}
label={label}
variant="standard"
placeholder="auto"
size="small"
InputProps={{
endAdornment: (
<Typography variant="body2" color="text.secondary">
px
</Typography>
),
}}
/>
);
}
@@ -0,0 +1,35 @@
import React, { useState } from 'react';
import { InputProps, TextField } from '@mui/material';
type Props = {
label: string;
rows?: number;
placeholder?: string;
helperText?: string | JSX.Element;
InputProps?: InputProps;
defaultValue: string;
onChange: (v: string) => void;
};
export default function TextInput({ helperText, label, placeholder, rows, InputProps, defaultValue, onChange }: Props) {
const [value, setValue] = useState(defaultValue);
const isMultiline = typeof rows === 'number' && rows > 1;
return (
<TextField
fullWidth
multiline={isMultiline}
minRows={rows}
variant={isMultiline ? 'outlined' : 'standard'}
label={label}
placeholder={placeholder}
helperText={helperText}
InputProps={InputProps}
value={value}
onChange={(ev) => {
const v = ev.target.value;
setValue(v);
onChange(v);
}}
/>
);
}
@@ -0,0 +1,58 @@
import React from "react";
import { Box, Slider, Stack, Typography } from "@mui/material";
type SliderInputProps = {
iconLabel: JSX.Element;
step?: number;
marks?: boolean;
units: string;
min?: number;
max?: number;
value: number;
setValue: (v: number) => void;
};
export default function RawSliderInput({
iconLabel,
value,
setValue,
units,
...props
}: SliderInputProps) {
return (
<Stack
direction="row"
alignItems="center"
spacing={2}
justifyContent="space-between"
width="100%"
>
<Box sx={{ minWidth: 24, lineHeight: 1, flexShrink: 0 }}>{iconLabel}</Box>
<Slider
{...props}
value={value}
onChange={(_, value: unknown) => {
if (typeof value !== "number") {
throw new Error(
"RawSliderInput values can only receive numeric values"
);
}
setValue(value);
}}
/>
<Box sx={{ minWidth: 32, textAlign: "right", flexShrink: 0 }}>
<Typography
variant="body2"
color="text.secondary"
sx={{ lineHeight: 1 }}
>
{value}
{units}
</Typography>
</Box>
</Stack>
);
}
@@ -0,0 +1,29 @@
import React from "react";
import { TStyle } from "../../../../../../documents/blocks/helpers/TStyle";
import SingleStylePropertyPanel from "./SingleStylePropertyPanel";
type MultiStylePropertyPanelProps = {
names: (keyof TStyle)[];
value: TStyle | undefined | null;
onChange: (style: TStyle) => void;
};
export default function MultiStylePropertyPanel({
names,
value,
onChange
}: MultiStylePropertyPanelProps) {
return (
<>
{names.map((name) => (
<SingleStylePropertyPanel
key={name}
name={name}
value={value || {}}
onChange={onChange}
/>
))}
</>
);
}
@@ -0,0 +1,110 @@
import React from "react";
import { RoundedCornerOutlined } from "@mui/icons-material";
import { TStyle } from "../../../../../../documents/blocks/helpers/TStyle";
import { NullableColorInput } from "../inputs/ColorInput";
import { NullableFontFamily } from "../inputs/FontFamily";
import FontSizeInput from "../inputs/FontSizeInput";
import FontWeightInput from "../inputs/FontWeightInput";
import PaddingInput from "../inputs/PaddingInput";
import SliderInput from "../inputs/SliderInput";
import TextAlignInput from "../inputs/TextAlignInput";
type StylePropertyPanelProps = {
name: keyof TStyle;
value: TStyle;
onChange: (style: TStyle) => void;
};
export default function SingleStylePropertyPanel({
name,
value,
onChange
}: StylePropertyPanelProps) {
const defaultValue = value[name] ?? null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleChange = (v: any) => {
onChange({ ...value, [name]: v });
};
switch (name) {
case "backgroundColor":
return (
<NullableColorInput
label="Background color"
defaultValue={defaultValue}
onChange={handleChange}
/>
);
case "borderColor":
return (
<NullableColorInput
label="Border color"
defaultValue={defaultValue}
onChange={handleChange}
/>
);
case "borderRadius":
return (
<SliderInput
iconLabel={<RoundedCornerOutlined />}
units="px"
step={4}
marks
min={0}
max={48}
label="Border radius"
defaultValue={defaultValue}
onChange={handleChange}
/>
);
case "color":
return (
<NullableColorInput
label="Text color"
defaultValue={defaultValue}
onChange={handleChange}
/>
);
case "fontFamily":
return (
<NullableFontFamily
label="Font family"
defaultValue={defaultValue}
onChange={handleChange}
/>
);
case "fontSize":
return (
<FontSizeInput
label="Font size"
defaultValue={defaultValue}
onChange={handleChange}
/>
);
case "fontWeight":
return (
<FontWeightInput
label="Font weight"
defaultValue={defaultValue}
onChange={handleChange}
/>
);
case "textAlign":
return (
<TextAlignInput
label="Alignment"
defaultValue={defaultValue}
onChange={handleChange}
/>
);
case "padding":
return (
<PaddingInput
label="Padding"
defaultValue={defaultValue}
onChange={handleChange}
/>
);
}
}

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