diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index c04cac0..cb425b7 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -17,6 +17,9 @@ "@types/react-dom": "^17.0.11", "@types/react-router-dom": "^5.3.2", "dayjs": "^1.10.7", + "draft-js": "^0.11.7", + "draft-js-import-html": "^1.4.1", + "draftjs-to-html": "^0.9.1", "esbuild": "^0.14.9", "focus-visible": "^5.2.0", "framer-motion": "^5.5.5", @@ -24,11 +27,16 @@ "lodash": "^4.17.21", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-draft-wysiwyg": "^1.15.0", "react-dropzone": "^12.0.4", "react-icons": "^4.3.1", "react-router-dom": "^6.2.1", "typescript": "^4.5.4", "urql": "^2.0.6" + }, + "devDependencies": { + "@types/draftjs-to-html": "^0.8.1", + "@types/react-draft-wysiwyg": "^1.13.4" } }, "node_modules/@babel/code-frame": { @@ -1145,6 +1153,25 @@ "react-dom": "^16.8.0 || 17.x" } }, + "node_modules/@types/draft-js": { + "version": "0.11.9", + "resolved": "https://registry.npmjs.org/@types/draft-js/-/draft-js-0.11.9.tgz", + "integrity": "sha512-cQJBZjjIlGaPA1tOY+wGz2KhlPtAAZOIXpUvGPxPRw5uzZ2tcj8m6Yu1QDV9YgP36+cqE3cUvgkARBzgUiuI/Q==", + "dev": true, + "dependencies": { + "@types/react": "*", + "immutable": "~3.7.4" + } + }, + "node_modules/@types/draftjs-to-html": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@types/draftjs-to-html/-/draftjs-to-html-0.8.1.tgz", + "integrity": "sha512-NBkphQs+qZ/sAz/j1pCUaxkPAOx00LTsE88aMSSfcvK+UfCpjHJDqIMCkm6wKotuJvY5w0BtdRazQ0sAaXzPdg==", + "dev": true, + "dependencies": { + "@types/draft-js": "*" + } + }, "node_modules/@types/history": { "version": "4.7.9", "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz", @@ -1191,6 +1218,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-draft-wysiwyg": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/@types/react-draft-wysiwyg/-/react-draft-wysiwyg-1.13.4.tgz", + "integrity": "sha512-wasD1t78JDmQvdPDRPf/mf5FSHMlncunW0F6KMOKB3awzi3Wi21yHMGsRAUOkfTr3R8F+yceG8fSLz0kYWu/QA==", + "dev": true, + "dependencies": { + "@types/draft-js": "*", + "@types/react": "*" + } + }, "node_modules/@types/react-router": { "version": "5.1.17", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.17.tgz", @@ -1259,6 +1296,11 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, "node_modules/attr-accept": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", @@ -1306,6 +1348,11 @@ "node": ">=0.8.0" } }, + "node_modules/classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -1340,6 +1387,16 @@ "toggle-selection": "^1.0.6" } }, + "node_modules/core-js": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz", + "integrity": "sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cosmiconfig": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", @@ -1355,6 +1412,14 @@ "node": ">=8" } }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dependencies": { + "node-fetch": "2.6.7" + } + }, "node_modules/css-box-model": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", @@ -1383,6 +1448,68 @@ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" }, + "node_modules/draft-js": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/draft-js/-/draft-js-0.11.7.tgz", + "integrity": "sha512-ne7yFfN4sEL82QPQEn80xnADR8/Q6ALVworbC5UOSzOvjffmYfFsr3xSZtxbIirti14R7Y33EZC5rivpLgIbsg==", + "dependencies": { + "fbjs": "^2.0.0", + "immutable": "~3.7.4", + "object-assign": "^4.1.1" + }, + "peerDependencies": { + "react": ">=0.14.0", + "react-dom": ">=0.14.0" + } + }, + "node_modules/draft-js-import-element": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/draft-js-import-element/-/draft-js-import-element-1.4.0.tgz", + "integrity": "sha512-WmYT5PrCm47lGL5FkH6sRO3TTAcn7qNHsD3igiPqLG/RXrqyKrqN4+wBgbcT2lhna/yfWTRtgzAbQsSJoS1Meg==", + "dependencies": { + "draft-js-utils": "^1.4.0", + "synthetic-dom": "^1.4.0" + }, + "peerDependencies": { + "draft-js": ">=0.10.0", + "immutable": "3.x.x" + } + }, + "node_modules/draft-js-import-html": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/draft-js-import-html/-/draft-js-import-html-1.4.1.tgz", + "integrity": "sha512-KOZmtgxZriCDgg5Smr3Y09TjubvXe7rHPy/2fuLSsL+aSzwUDwH/aHDA/k47U+WfpmL4qgyg4oZhqx9TYJV0tg==", + "dependencies": { + "draft-js-import-element": "^1.4.0" + }, + "peerDependencies": { + "draft-js": ">=0.10.0", + "immutable": "3.x.x" + } + }, + "node_modules/draft-js-utils": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/draft-js-utils/-/draft-js-utils-1.4.1.tgz", + "integrity": "sha512-xE81Y+z/muC5D5z9qWmKfxEW1XyXfsBzSbSBk2JRsoD0yzMGGHQm/0MtuqHl/EUDkaBJJLjJ2EACycoDMY/OOg==", + "peerDependencies": { + "draft-js": ">=0.10.0", + "immutable": "3.x.x" + } + }, + "node_modules/draftjs-to-html": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/draftjs-to-html/-/draftjs-to-html-0.9.1.tgz", + "integrity": "sha512-fFstE6+IayaVFBEvaFt/wN8vdj8FsTRzij7dy7LI9QIwf5LgfHFi9zSpvCg+feJ2tbYVqHxUkjcibwpsTpgFVQ==" + }, + "node_modules/draftjs-utils": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/draftjs-utils/-/draftjs-utils-0.10.2.tgz", + "integrity": "sha512-EstHqr3R3JVcilJrBaO/A+01GvwwKmC7e4TCjC7S94ZeMh4IVmf60OuQXtHHpwItK8C2JCi3iljgN5KHkJboUg==", + "peerDependencies": { + "draft-js": "^0.11.x", + "immutable": "3.x.x || 4.x.x" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -1647,6 +1774,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/fbjs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-2.0.0.tgz", + "integrity": "sha512-8XA8ny9ifxrAWlyhAbexXcs3rRMtxWcs3M0lctLfB49jRDHiaxj+Mo0XxbwE7nKZYzgCFoq64FS+WFd4IycPPQ==", + "dependencies": { + "core-js": "^3.6.4", + "cross-fetch": "^3.0.4", + "fbjs-css-vars": "^1.0.0", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.18" + } + }, + "node_modules/fbjs-css-vars": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", + "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==" + }, "node_modules/file-selector": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz", @@ -1802,6 +1949,23 @@ "react-is": "^16.7.0" } }, + "node_modules/html-to-draftjs": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/html-to-draftjs/-/html-to-draftjs-1.5.0.tgz", + "integrity": "sha512-kggLXBNciKDwKf+KYsuE+V5gw4dZ7nHyGMX9m0wy7urzWjKGWyNFetmArRLvRV0VrxKN70WylFsJvMTJx02OBQ==", + "peerDependencies": { + "draft-js": "^0.10.x || ^0.11.x", + "immutable": "3.x.x || 4.x.x" + } + }, + "node_modules/immutable": { + "version": "3.7.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz", + "integrity": "sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -1856,6 +2020,14 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/linkify-it": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", + "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -1877,6 +2049,25 @@ "loose-envify": "cli.js" } }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1945,6 +2136,14 @@ "tslib": "^2.1.0" } }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dependencies": { + "asap": "~2.0.3" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -1991,6 +2190,24 @@ "react": "17.0.2" } }, + "node_modules/react-draft-wysiwyg": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/react-draft-wysiwyg/-/react-draft-wysiwyg-1.15.0.tgz", + "integrity": "sha512-p1cYZcWc6/ALFBVksbFoCM3b29fGQDlZLIMrXng0TU/UElxIOF2/AWWo4L5auIYVhmqKTZ0NkNjnXOzGGuxyeA==", + "dependencies": { + "classnames": "^2.2.6", + "draftjs-utils": "^0.10.2", + "html-to-draftjs": "^1.5.0", + "linkify-it": "^2.2.0", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "draft-js": "^0.10.x || ^0.11.x", + "immutable": "3.x.x || 4.x.x", + "react": "0.13.x || 0.14.x || ^15.0.0-0 || 15.x.x || ^16.0.0-0 || ^16.x.x || ^17.x.x || ^18.x.x", + "react-dom": "0.13.x || 0.14.x || ^15.0.0-0 || 15.x.x || ^16.0.0-0 || ^16.x.x || ^17.x.x || ^18.x.x" + } + }, "node_modules/react-dropzone": { "version": "12.0.4", "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-12.0.4.tgz", @@ -2207,6 +2424,11 @@ "object-assign": "^4.1.1" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -2240,6 +2462,11 @@ "node": ">=4" } }, + "node_modules/synthetic-dom": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/synthetic-dom/-/synthetic-dom-1.4.0.tgz", + "integrity": "sha512-mHv51ZsmZ+ShT/4s5kg+MGUIhY7Ltq4v03xpN1c8T1Krb5pScsh/lzEjyhrVD0soVDbThbd2e+4dD9vnDG4rhg==" + }, "node_modules/tiny-invariant": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", @@ -2258,6 +2485,11 @@ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", @@ -2275,6 +2507,29 @@ "node": ">=4.2.0" } }, + "node_modules/ua-parser-js": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz", + "integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, "node_modules/urql": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/urql/-/urql-2.0.6.tgz", @@ -2333,6 +2588,20 @@ "loose-envify": "^1.0.0" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/wonka": { "version": "4.0.15", "resolved": "https://registry.npmjs.org/wonka/-/wonka-4.0.15.tgz", @@ -2529,8 +2798,7 @@ "@chakra-ui/css-reset": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@chakra-ui/css-reset/-/css-reset-1.1.1.tgz", - "integrity": "sha512-+KNNHL4OWqeKia5SL858K3Qbd8WxMij9mWIilBzLD4j2KFrl/+aWFw8syMKth3NmgIibrjsljo+PU3fy2o50dg==", - "requires": {} + "integrity": "sha512-+KNNHL4OWqeKia5SL858K3Qbd8WxMij9mWIilBzLD4j2KFrl/+aWFw8syMKth3NmgIibrjsljo+PU3fy2o50dg==" }, "@chakra-ui/descendant": { "version": "2.1.1", @@ -3134,8 +3402,7 @@ "@graphql-typed-document-node/core": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", - "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==", - "requires": {} + "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==" }, "@popperjs/core": { "version": "2.11.0", @@ -3172,6 +3439,25 @@ "tslib": "^2.1.0" } }, + "@types/draft-js": { + "version": "0.11.9", + "resolved": "https://registry.npmjs.org/@types/draft-js/-/draft-js-0.11.9.tgz", + "integrity": "sha512-cQJBZjjIlGaPA1tOY+wGz2KhlPtAAZOIXpUvGPxPRw5uzZ2tcj8m6Yu1QDV9YgP36+cqE3cUvgkARBzgUiuI/Q==", + "dev": true, + "requires": { + "@types/react": "*", + "immutable": "~3.7.4" + } + }, + "@types/draftjs-to-html": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@types/draftjs-to-html/-/draftjs-to-html-0.8.1.tgz", + "integrity": "sha512-NBkphQs+qZ/sAz/j1pCUaxkPAOx00LTsE88aMSSfcvK+UfCpjHJDqIMCkm6wKotuJvY5w0BtdRazQ0sAaXzPdg==", + "dev": true, + "requires": { + "@types/draft-js": "*" + } + }, "@types/history": { "version": "4.7.9", "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz", @@ -3218,6 +3504,16 @@ "@types/react": "*" } }, + "@types/react-draft-wysiwyg": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/@types/react-draft-wysiwyg/-/react-draft-wysiwyg-1.13.4.tgz", + "integrity": "sha512-wasD1t78JDmQvdPDRPf/mf5FSHMlncunW0F6KMOKB3awzi3Wi21yHMGsRAUOkfTr3R8F+yceG8fSLz0kYWu/QA==", + "dev": true, + "requires": { + "@types/draft-js": "*", + "@types/react": "*" + } + }, "@types/react-router": { "version": "5.1.17", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.17.tgz", @@ -3279,6 +3575,11 @@ } } }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, "attr-accept": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", @@ -3316,6 +3617,11 @@ } } }, + "classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -3350,6 +3656,11 @@ "toggle-selection": "^1.0.6" } }, + "core-js": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz", + "integrity": "sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg==" + }, "cosmiconfig": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", @@ -3362,6 +3673,14 @@ "yaml": "^1.7.2" } }, + "cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "requires": { + "node-fetch": "2.6.7" + } + }, "css-box-model": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", @@ -3390,6 +3709,48 @@ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" }, + "draft-js": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/draft-js/-/draft-js-0.11.7.tgz", + "integrity": "sha512-ne7yFfN4sEL82QPQEn80xnADR8/Q6ALVworbC5UOSzOvjffmYfFsr3xSZtxbIirti14R7Y33EZC5rivpLgIbsg==", + "requires": { + "fbjs": "^2.0.0", + "immutable": "~3.7.4", + "object-assign": "^4.1.1" + } + }, + "draft-js-import-element": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/draft-js-import-element/-/draft-js-import-element-1.4.0.tgz", + "integrity": "sha512-WmYT5PrCm47lGL5FkH6sRO3TTAcn7qNHsD3igiPqLG/RXrqyKrqN4+wBgbcT2lhna/yfWTRtgzAbQsSJoS1Meg==", + "requires": { + "draft-js-utils": "^1.4.0", + "synthetic-dom": "^1.4.0" + } + }, + "draft-js-import-html": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/draft-js-import-html/-/draft-js-import-html-1.4.1.tgz", + "integrity": "sha512-KOZmtgxZriCDgg5Smr3Y09TjubvXe7rHPy/2fuLSsL+aSzwUDwH/aHDA/k47U+WfpmL4qgyg4oZhqx9TYJV0tg==", + "requires": { + "draft-js-import-element": "^1.4.0" + } + }, + "draft-js-utils": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/draft-js-utils/-/draft-js-utils-1.4.1.tgz", + "integrity": "sha512-xE81Y+z/muC5D5z9qWmKfxEW1XyXfsBzSbSBk2JRsoD0yzMGGHQm/0MtuqHl/EUDkaBJJLjJ2EACycoDMY/OOg==" + }, + "draftjs-to-html": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/draftjs-to-html/-/draftjs-to-html-0.9.1.tgz", + "integrity": "sha512-fFstE6+IayaVFBEvaFt/wN8vdj8FsTRzij7dy7LI9QIwf5LgfHFi9zSpvCg+feJ2tbYVqHxUkjcibwpsTpgFVQ==" + }, + "draftjs-utils": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/draftjs-utils/-/draftjs-utils-0.10.2.tgz", + "integrity": "sha512-EstHqr3R3JVcilJrBaO/A+01GvwwKmC7e4TCjC7S94ZeMh4IVmf60OuQXtHHpwItK8C2JCi3iljgN5KHkJboUg==" + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -3536,6 +3897,26 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" }, + "fbjs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-2.0.0.tgz", + "integrity": "sha512-8XA8ny9ifxrAWlyhAbexXcs3rRMtxWcs3M0lctLfB49jRDHiaxj+Mo0XxbwE7nKZYzgCFoq64FS+WFd4IycPPQ==", + "requires": { + "core-js": "^3.6.4", + "cross-fetch": "^3.0.4", + "fbjs-css-vars": "^1.0.0", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.18" + } + }, + "fbjs-css-vars": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", + "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==" + }, "file-selector": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz", @@ -3659,6 +4040,16 @@ "react-is": "^16.7.0" } }, + "html-to-draftjs": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/html-to-draftjs/-/html-to-draftjs-1.5.0.tgz", + "integrity": "sha512-kggLXBNciKDwKf+KYsuE+V5gw4dZ7nHyGMX9m0wy7urzWjKGWyNFetmArRLvRV0VrxKN70WylFsJvMTJx02OBQ==" + }, + "immutable": { + "version": "3.7.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz", + "integrity": "sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==" + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -3704,6 +4095,14 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "linkify-it": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", + "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "requires": { + "uc.micro": "^1.0.1" + } + }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -3722,6 +4121,14 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3777,6 +4184,14 @@ } } }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "~2.0.3" + } + }, "prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -3814,6 +4229,18 @@ "scheduler": "^0.20.2" } }, + "react-draft-wysiwyg": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/react-draft-wysiwyg/-/react-draft-wysiwyg-1.15.0.tgz", + "integrity": "sha512-p1cYZcWc6/ALFBVksbFoCM3b29fGQDlZLIMrXng0TU/UElxIOF2/AWWo4L5auIYVhmqKTZ0NkNjnXOzGGuxyeA==", + "requires": { + "classnames": "^2.2.6", + "draftjs-utils": "^0.10.2", + "html-to-draftjs": "^1.5.0", + "linkify-it": "^2.2.0", + "prop-types": "^15.7.2" + } + }, "react-dropzone": { "version": "12.0.4", "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-12.0.4.tgz", @@ -3845,8 +4272,7 @@ "react-icons": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz", - "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==", - "requires": {} + "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==" }, "react-is": { "version": "16.13.1", @@ -3968,6 +4394,11 @@ "object-assign": "^4.1.1" } }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -3995,6 +4426,11 @@ "has-flag": "^3.0.0" } }, + "synthetic-dom": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/synthetic-dom/-/synthetic-dom-1.4.0.tgz", + "integrity": "sha512-mHv51ZsmZ+ShT/4s5kg+MGUIhY7Ltq4v03xpN1c8T1Krb5pScsh/lzEjyhrVD0soVDbThbd2e+4dD9vnDG4rhg==" + }, "tiny-invariant": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", @@ -4010,6 +4446,11 @@ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", @@ -4020,6 +4461,16 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==" }, + "ua-parser-js": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz", + "integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==" + }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, "urql": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/urql/-/urql-2.0.6.tgz", @@ -4032,8 +4483,7 @@ "use-callback-ref": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz", - "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==", - "requires": {} + "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==" }, "use-sidecar": { "version": "1.0.5", @@ -4059,6 +4509,20 @@ "loose-envify": "^1.0.0" } }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "wonka": { "version": "4.0.15", "resolved": "https://registry.npmjs.org/wonka/-/wonka-4.0.15.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index 0762191..bfb313e 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -19,6 +19,9 @@ "@types/react-dom": "^17.0.11", "@types/react-router-dom": "^5.3.2", "dayjs": "^1.10.7", + "draft-js": "^0.11.7", + "draft-js-import-html": "^1.4.1", + "draftjs-to-html": "^0.9.1", "esbuild": "^0.14.9", "focus-visible": "^5.2.0", "framer-motion": "^5.5.5", @@ -26,10 +29,15 @@ "lodash": "^4.17.21", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-draft-wysiwyg": "^1.15.0", "react-dropzone": "^12.0.4", "react-icons": "^4.3.1", "react-router-dom": "^6.2.1", "typescript": "^4.5.4", "urql": "^2.0.6" + }, + "devDependencies": { + "@types/draftjs-to-html": "^0.8.1", + "@types/react-draft-wysiwyg": "^1.13.4" } } diff --git a/dashboard/src/components/DeleteEmailTemplateModal.tsx b/dashboard/src/components/DeleteEmailTemplateModal.tsx new file mode 100644 index 0000000..01d429f --- /dev/null +++ b/dashboard/src/components/DeleteEmailTemplateModal.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { + Button, + Center, + Flex, + MenuItem, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + useDisclosure, + Text, + useToast, +} from '@chakra-ui/react'; +import { useClient } from 'urql'; +import { FaRegTrashAlt } from 'react-icons/fa'; +import { DeleteEmailTemplate } from '../graphql/mutation'; +import { capitalizeFirstLetter } from '../utils'; + +interface deleteEmailTemplateModalInputPropTypes { + emailTemplateId: string; + eventName: string; + fetchEmailTemplatesData: Function; +} + +const DeleteEmailTemplateModal = ({ + emailTemplateId, + eventName, + fetchEmailTemplatesData, +}: deleteEmailTemplateModalInputPropTypes) => { + const client = useClient(); + const toast = useToast(); + const { isOpen, onOpen, onClose } = useDisclosure(); + + const deleteHandler = async () => { + const res = await client + .mutation(DeleteEmailTemplate, { params: { id: emailTemplateId } }) + .toPromise(); + if (res.error) { + toast({ + title: capitalizeFirstLetter(res.error.message), + isClosable: true, + status: 'error', + position: 'bottom-right', + }); + + return; + } else if (res.data?._delete_email_template) { + toast({ + title: capitalizeFirstLetter(res.data?._delete_email_template.message), + isClosable: true, + status: 'success', + position: 'bottom-right', + }); + } + onClose(); + fetchEmailTemplatesData(); + }; + return ( + <> + Delete + + + + Delete Email Template + + + Are you sure? + + + Email template for event {eventName} will be deleted + permanently! + + + + + + + + + + + ); +}; + +export default DeleteEmailTemplateModal; diff --git a/dashboard/src/components/InputField.tsx b/dashboard/src/components/InputField.tsx index f4d373d..39d68ac 100644 --- a/dashboard/src/components/InputField.tsx +++ b/dashboard/src/components/InputField.tsx @@ -12,7 +12,6 @@ import { Select, Textarea, Switch, - Code, Text, } from '@chakra-ui/react'; import { diff --git a/dashboard/src/components/Menu.tsx b/dashboard/src/components/Menu.tsx index e424a0f..7e59043 100644 --- a/dashboard/src/components/Menu.tsx +++ b/dashboard/src/components/Menu.tsx @@ -31,6 +31,7 @@ import { FiUsers, FiChevronDown, FiLink, + FiFileText, } from 'react-icons/fi'; import { BiCustomize } from 'react-icons/bi'; import { AiOutlineKey } from 'react-icons/ai'; @@ -113,6 +114,7 @@ const LinkItems: Array = [ }, { name: 'Users', icon: FiUsers, route: '/users' }, { name: 'Webhooks', icon: FiLink, route: '/webhooks' }, + { name: 'Email Templates', icon: FiFileText, route: '/email-templates' }, ]; interface SidebarProps extends BoxProps { diff --git a/dashboard/src/components/UpdateEmailTemplateModal.tsx b/dashboard/src/components/UpdateEmailTemplateModal.tsx new file mode 100644 index 0000000..5deee15 --- /dev/null +++ b/dashboard/src/components/UpdateEmailTemplateModal.tsx @@ -0,0 +1,375 @@ +import React, { useEffect, useState } from 'react'; +import { + Button, + Center, + Flex, + Input, + InputGroup, + MenuItem, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Select, + Text, + useDisclosure, + useToast, +} from '@chakra-ui/react'; +import { FaPlus } from 'react-icons/fa'; +import { useClient } from 'urql'; +import { Editor } from 'react-draft-wysiwyg'; +import { EditorState, convertToRaw, Modifier } from 'draft-js'; +import draftToHtml from 'draftjs-to-html'; +import { stateFromHTML } from 'draft-js-import-html'; +import { + UpdateModalViews, + EmailTemplateInputDataFields, + emailTemplateEventNames, + emailTemplateVariables, +} from '../constants'; +import { capitalizeFirstLetter } from '../utils'; +import { AddEmailTemplate, EditEmailTemplate } from '../graphql/mutation'; + +interface selectedEmailTemplateDataTypes { + [EmailTemplateInputDataFields.ID]: string; + [EmailTemplateInputDataFields.EVENT_NAME]: string; + [EmailTemplateInputDataFields.SUBJECT]: string; + [EmailTemplateInputDataFields.CREATED_AT]: number; + [EmailTemplateInputDataFields.TEMPLATE]: string; +} + +interface UpdateEmailTemplateInputPropTypes { + view: UpdateModalViews; + selectedTemplate?: selectedEmailTemplateDataTypes; + fetchEmailTemplatesData: Function; +} + +interface templateVariableDataTypes { + text: string; + value: string; +} + +interface emailTemplateDataType { + [EmailTemplateInputDataFields.EVENT_NAME]: string; + [EmailTemplateInputDataFields.SUBJECT]: string; +} + +interface validatorDataType { + [EmailTemplateInputDataFields.SUBJECT]: boolean; +} + +const initTemplateData: emailTemplateDataType = { + [EmailTemplateInputDataFields.EVENT_NAME]: + emailTemplateEventNames.BASIC_AUTH_SIGNUP, + [EmailTemplateInputDataFields.SUBJECT]: '', +}; + +const initTemplateValidatorData: validatorDataType = { + [EmailTemplateInputDataFields.SUBJECT]: true, +}; + +const UpdateEmailTemplate = ({ + view, + selectedTemplate, + fetchEmailTemplatesData, +}: UpdateEmailTemplateInputPropTypes) => { + const client = useClient(); + const toast = useToast(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const [loading, setLoading] = useState(false); + const [editorState, setEditorState] = React.useState( + EditorState.createEmpty() + ); + const [templateVariables, setTemplateVariables] = useState< + templateVariableDataTypes[] + >([]); + const [templateData, setTemplateData] = useState({ + ...initTemplateData, + }); + const [validator, setValidator] = useState({ + ...initTemplateValidatorData, + }); + const onEditorStateChange = (editorState: EditorState) => { + setEditorState(editorState); + }; + const inputChangehandler = (inputType: string, value: any) => { + if (inputType !== EmailTemplateInputDataFields.EVENT_NAME) { + setValidator({ + ...validator, + [inputType]: value?.trim().length, + }); + } + setTemplateData({ ...templateData, [inputType]: value }); + }; + + const validateData = () => { + const rawData: string = draftToHtml( + convertToRaw(editorState.getCurrentContent()) + ).trim(); + return ( + !loading && + rawData && + rawData !== '

' && + rawData !== '

' && + templateData[EmailTemplateInputDataFields.EVENT_NAME].length > 0 && + templateData[EmailTemplateInputDataFields.SUBJECT].length > 0 && + validator[EmailTemplateInputDataFields.SUBJECT] + ); + }; + + const saveData = async () => { + if (!validateData()) return; + setLoading(true); + const params = { + [EmailTemplateInputDataFields.EVENT_NAME]: + templateData[EmailTemplateInputDataFields.EVENT_NAME], + [EmailTemplateInputDataFields.SUBJECT]: + templateData[EmailTemplateInputDataFields.SUBJECT], + [EmailTemplateInputDataFields.TEMPLATE]: draftToHtml( + convertToRaw(editorState.getCurrentContent()) + ).trim(), + }; + let res: any = {}; + if ( + view === UpdateModalViews.Edit && + selectedTemplate?.[EmailTemplateInputDataFields.ID] + ) { + res = await client + .mutation(EditEmailTemplate, { + params: { + ...params, + id: selectedTemplate[EmailTemplateInputDataFields.ID], + }, + }) + .toPromise(); + } else { + res = await client.mutation(AddEmailTemplate, { params }).toPromise(); + } + setLoading(false); + if (res.error) { + toast({ + title: capitalizeFirstLetter(res.error.message), + isClosable: true, + status: 'error', + position: 'bottom-right', + }); + } else if ( + res.data?._add_email_template || + res.data?._update_email_template + ) { + toast({ + title: capitalizeFirstLetter( + res.data?._add_email_template?.message || + res.data?._update_email_template?.message + ), + isClosable: true, + status: 'success', + position: 'bottom-right', + }); + setTemplateData({ + ...initTemplateData, + }); + setValidator({ ...initTemplateValidatorData }); + fetchEmailTemplatesData(); + } + view === UpdateModalViews.ADD && onClose(); + }; + const resetData = () => { + if (selectedTemplate) { + setTemplateData(selectedTemplate); + setEditorState( + EditorState.createWithContent(stateFromHTML(selectedTemplate.template)) + ); + } else { + setTemplateData({ ...initTemplateData }); + setEditorState(EditorState.createEmpty()); + } + }; + useEffect(() => { + if ( + isOpen && + view === UpdateModalViews.Edit && + selectedTemplate && + Object.keys(selectedTemplate || {}).length + ) { + const { id, created_at, template, ...rest } = selectedTemplate; + setTemplateData(rest); + setEditorState(EditorState.createWithContent(stateFromHTML(template))); + } + }, [isOpen]); + useEffect(() => { + const updatedTemplateVariables = Object.entries( + emailTemplateVariables + ).reduce((acc, varData): any => { + if ( + (templateData[EmailTemplateInputDataFields.EVENT_NAME] !== + emailTemplateEventNames.VERIFY_OTP && + varData[1] === emailTemplateVariables.otp) || + (templateData[EmailTemplateInputDataFields.EVENT_NAME] === + emailTemplateEventNames.VERIFY_OTP && + varData[1] === emailTemplateVariables.verification_url) + ) { + return acc; + } + return [ + ...acc, + { + text: varData[0], + value: varData[1], + }, + ]; + }, []); + setTemplateVariables(updatedTemplateVariables); + }, [templateData[EmailTemplateInputDataFields.EVENT_NAME]]); + return ( + <> + {view === UpdateModalViews.ADD ? ( + + ) : ( + Edit + )} + + + + + {view === UpdateModalViews.ADD + ? 'Add New Email Template' + : 'Edit Email Template'} + + + + + + Event Name + + + + + + Subject + + + + inputChangehandler( + EmailTemplateInputDataFields.SUBJECT, + e.currentTarget.value + ) + } + /> + + + + + Template Body + {`To select dynamic variables open curly braces "{"`} + + + + + + + + + + + + ); +}; + +export default UpdateEmailTemplate; diff --git a/dashboard/src/components/UpdateWebhookModal.tsx b/dashboard/src/components/UpdateWebhookModal.tsx index 60590f2..9749ef5 100644 --- a/dashboard/src/components/UpdateWebhookModal.tsx +++ b/dashboard/src/components/UpdateWebhookModal.tsx @@ -2,6 +2,8 @@ import React, { useEffect, useState } from 'react'; import { Button, Center, + Code, + Collapse, Flex, Input, InputGroup, @@ -20,19 +22,29 @@ import { useDisclosure, useToast, } from '@chakra-ui/react'; -import { FaMinusCircle, FaPlus } from 'react-icons/fa'; +import { + FaAngleDown, + FaAngleUp, + FaMinusCircle, + FaPlus, + FaRegClone, +} from 'react-icons/fa'; import { useClient } from 'urql'; import { webhookEventNames, ArrayInputOperations, WebhookInputDataFields, WebhookInputHeaderFields, - UpdateWebhookModalViews, + UpdateModalViews, webhookVerifiedStatus, + webhookPayloadExample, } from '../constants'; -import { capitalizeFirstLetter, validateURI } from '../utils'; +import { + capitalizeFirstLetter, + copyTextToClipboard, + validateURI, +} from '../utils'; import { AddWebhook, EditWebhook, TestEndpoint } from '../graphql/mutation'; -import { rest } from 'lodash'; import { BiCheckCircle, BiError, BiErrorCircle } from 'react-icons/bi'; interface headersDataType { @@ -54,7 +66,7 @@ interface selecetdWebhookDataTypes { } interface UpdateWebhookModalInputPropTypes { - view: UpdateWebhookModalViews; + view: UpdateModalViews; selectedWebhook?: selecetdWebhookDataTypes; fetchWebookData: Function; } @@ -103,6 +115,7 @@ const UpdateWebhookModal = ({ const { isOpen, onOpen, onClose } = useDisclosure(); const [loading, setLoading] = useState(false); const [verifyingEndpoint, setVerifyingEndpoint] = useState(false); + const [isShowingPayload, setIsShowingPayload] = useState(false); const [webhook, setWebhook] = useState({ ...initWebhookData, }); @@ -254,7 +267,7 @@ const UpdateWebhookModal = ({ const params = getParams(); let res: any = {}; if ( - view === UpdateWebhookModalViews.Edit && + view === UpdateModalViews.Edit && selectedWebhook?.[WebhookInputDataFields.ID] ) { res = await client @@ -292,12 +305,12 @@ const UpdateWebhookModal = ({ setValidator({ ...initWebhookValidatorData }); fetchWebookData(); } - view === UpdateWebhookModalViews.ADD && onClose(); + view === UpdateModalViews.ADD && onClose(); }; useEffect(() => { if ( isOpen && - view === UpdateWebhookModalViews.Edit && + view === UpdateModalViews.Edit && selectedWebhook && Object.keys(selectedWebhook || {}).length ) { @@ -347,7 +360,7 @@ const UpdateWebhookModal = ({ }; return ( <> - {view === UpdateWebhookModalViews.ADD ? ( + {view === UpdateModalViews.ADD ? ( + + + +
+											{webhookPayloadExample}
+										
+ {isShowingPayload && ( + + copyTextToClipboard(webhookPayloadExample) + } + > + + + )} +
+
+ { + const client = useClient(); + const [loading, setLoading] = useState(false); + const [emailTemplatesData, setEmailTemplatesData] = useState< + EmailTemplateDataType[] + >([]); + const [paginationProps, setPaginationProps] = useState({ + limit: 5, + page: 1, + offset: 0, + total: 0, + maxPages: 1, + }); + const getMaxPages = (pagination: paginationPropTypes) => { + const { limit, total } = pagination; + if (total > 1) { + return total % limit === 0 + ? total / limit + : parseInt(`${total / limit}`) + 1; + } else return 1; + }; + const fetchEmailTemplatesData = async () => { + setLoading(true); + const res = await client + .query(EmailTemplatesQuery, { + params: { + pagination: { + limit: paginationProps.limit, + page: paginationProps.page, + }, + }, + }) + .toPromise(); + if (res.data?._email_templates) { + const { pagination, EmailTemplates: emailTemplates } = + res.data?._email_templates; + const maxPages = getMaxPages(pagination); + if (emailTemplates?.length) { + setEmailTemplatesData(emailTemplates); + setPaginationProps({ ...paginationProps, ...pagination, maxPages }); + } else { + if (paginationProps.page !== 1) { + setPaginationProps({ + ...paginationProps, + ...pagination, + maxPages, + page: 1, + }); + } + } + } + setLoading(false); + }; + const paginationHandler = (value: Record) => { + setPaginationProps({ ...paginationProps, ...value }); + }; + useEffect(() => { + fetchEmailTemplatesData(); + }, [paginationProps.page, paginationProps.limit]); + return ( + + + + Email Templates + + + + {!loading ? ( + emailTemplatesData.length ? ( + + + + + + + + + + + {emailTemplatesData.map((templateData: EmailTemplateDataType) => ( + + + + + + + ))} + + {(paginationProps.maxPages > 1 || paginationProps.total >= 5) && ( + + + + + + paginationHandler({ + page: 1, + }) + } + isDisabled={paginationProps.page <= 1} + mr={4} + icon={} + /> + + + + paginationHandler({ + page: paginationProps.page - 1, + }) + } + isDisabled={paginationProps.page <= 1} + icon={} + /> + + + + + Page{' '} + + {paginationProps.page} + {' '} + of{' '} + + {paginationProps.maxPages} + + + + Go to page:{' '} + + paginationHandler({ + page: parseInt(value), + }) + } + value={paginationProps.page} + > + + + + + + + + + + + + + paginationHandler({ + page: paginationProps.page + 1, + }) + } + isDisabled={ + paginationProps.page >= paginationProps.maxPages + } + icon={} + /> + + + + paginationHandler({ + page: paginationProps.maxPages, + }) + } + isDisabled={ + paginationProps.page >= paginationProps.maxPages + } + ml={4} + icon={} + /> + + + + + )} +
Event NameSubjectCreated AtActions
+ {templateData[EmailTemplateInputDataFields.EVENT_NAME]} + {templateData[EmailTemplateInputDataFields.SUBJECT]} + {dayjs(templateData.created_at * 1000).format( + 'MMM DD, YYYY' + )} + + + + + + Menu + + + + + + + + + +
+ ) : ( + +
+ +
+ + No Data + +
+ ) + ) : ( +
+ +
+ )} +
+ ); +}; + +export default EmailTemplates; diff --git a/dashboard/src/pages/Webhooks.tsx b/dashboard/src/pages/Webhooks.tsx index 78d863e..00c2a3c 100644 --- a/dashboard/src/pages/Webhooks.tsx +++ b/dashboard/src/pages/Webhooks.tsx @@ -8,7 +8,6 @@ import { IconButton, Menu, MenuButton, - MenuItem, MenuList, NumberDecrementStepper, NumberIncrementStepper, @@ -40,7 +39,7 @@ import UpdateWebhookModal from '../components/UpdateWebhookModal'; import { pageLimits, WebhookInputDataFields, - UpdateWebhookModalViews, + UpdateModalViews, } from '../constants'; import { WebhooksDataQuery } from '../graphql/queries'; import DeleteWebhookModal from '../components/DeleteWebhookModal'; @@ -125,7 +124,7 @@ const Webhooks = () => { Webhooks
@@ -196,7 +195,7 @@ const Webhooks = () => { diff --git a/dashboard/src/routes/index.tsx b/dashboard/src/routes/index.tsx index 6ae73df..544c172 100644 --- a/dashboard/src/routes/index.tsx +++ b/dashboard/src/routes/index.tsx @@ -3,6 +3,7 @@ import { Outlet, Route, Routes } from 'react-router-dom'; import { useAuthContext } from '../contexts/AuthContext'; import { DashboardLayout } from '../layouts/DashboardLayout'; +import EmailTemplates from '../pages/EmailTemplates'; const Auth = lazy(() => import('../pages/Auth')); const Environment = lazy(() => import('../pages/Environment')); @@ -31,6 +32,7 @@ export const AppRoutes = () => { } /> } /> + } /> } /> diff --git a/server/db/models/email_templates.go b/server/db/models/email_templates.go index 23ac7fa..8c6de30 100644 --- a/server/db/models/email_templates.go +++ b/server/db/models/email_templates.go @@ -12,6 +12,7 @@ type EmailTemplate struct { Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty"` // for arangodb ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id"` EventName string `gorm:"unique" json:"event_name" bson:"event_name" cql:"event_name"` + Subject string `gorm:"type:text" json:"subject" bson:"subject" cql:"subject"` Template string `gorm:"type:text" json:"template" bson:"template" cql:"template"` CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"` UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"` @@ -26,6 +27,7 @@ func (e *EmailTemplate) AsAPIEmailTemplate() *model.EmailTemplate { return &model.EmailTemplate{ ID: id, EventName: e.EventName, + Subject: e.Subject, Template: e.Template, CreatedAt: refs.NewInt64Ref(e.CreatedAt), UpdatedAt: refs.NewInt64Ref(e.UpdatedAt), diff --git a/server/db/providers/cassandradb/email_template.go b/server/db/providers/cassandradb/email_template.go index 4fa9109..ea18fce 100644 --- a/server/db/providers/cassandradb/email_template.go +++ b/server/db/providers/cassandradb/email_template.go @@ -29,7 +29,7 @@ func (p *provider) AddEmailTemplate(ctx context.Context, emailTemplate models.Em return nil, fmt.Errorf("Email template with %s event_name already exists", emailTemplate.EventName) } - insertQuery := fmt.Sprintf("INSERT INTO %s (id, event_name, template, created_at, updated_at) VALUES ('%s', '%s', '%s', %d, %d)", KeySpace+"."+models.Collections.EmailTemplate, emailTemplate.ID, emailTemplate.EventName, emailTemplate.Template, emailTemplate.CreatedAt, emailTemplate.UpdatedAt) + insertQuery := fmt.Sprintf("INSERT INTO %s (id, event_name, subject, template, created_at, updated_at) VALUES ('%s', '%s', '%s','%s', %d, %d)", KeySpace+"."+models.Collections.EmailTemplate, emailTemplate.ID, emailTemplate.EventName, emailTemplate.Subject, emailTemplate.Template, emailTemplate.CreatedAt, emailTemplate.UpdatedAt) err := p.db.Query(insertQuery).Exec() if err != nil { return nil, err @@ -103,14 +103,14 @@ func (p *provider) ListEmailTemplate(ctx context.Context, pagination model.Pagin // there is no offset in cassandra // so we fetch till limit + offset // and return the results from offset to limit - query := fmt.Sprintf("SELECT id, event_name, template, created_at, updated_at FROM %s LIMIT %d", KeySpace+"."+models.Collections.EmailTemplate, pagination.Limit+pagination.Offset) + query := fmt.Sprintf("SELECT id, event_name, subject, template, created_at, updated_at FROM %s LIMIT %d", KeySpace+"."+models.Collections.EmailTemplate, pagination.Limit+pagination.Offset) scanner := p.db.Query(query).Iter().Scanner() counter := int64(0) for scanner.Next() { if counter >= pagination.Offset { var emailTemplate models.EmailTemplate - err := scanner.Scan(&emailTemplate.ID, &emailTemplate.EventName, &emailTemplate.Template, &emailTemplate.CreatedAt, &emailTemplate.UpdatedAt) + err := scanner.Scan(&emailTemplate.ID, &emailTemplate.EventName, &emailTemplate.Subject, &emailTemplate.Template, &emailTemplate.CreatedAt, &emailTemplate.UpdatedAt) if err != nil { return nil, err } @@ -128,8 +128,8 @@ func (p *provider) ListEmailTemplate(ctx context.Context, pagination model.Pagin // GetEmailTemplateByID to get EmailTemplate by id func (p *provider) GetEmailTemplateByID(ctx context.Context, emailTemplateID string) (*model.EmailTemplate, error) { var emailTemplate models.EmailTemplate - query := fmt.Sprintf(`SELECT id, event_name, template, created_at, updated_at FROM %s WHERE id = '%s' LIMIT 1`, KeySpace+"."+models.Collections.EmailTemplate, emailTemplateID) - err := p.db.Query(query).Consistency(gocql.One).Scan(&emailTemplate.ID, &emailTemplate.EventName, &emailTemplate.Template, &emailTemplate.CreatedAt, &emailTemplate.UpdatedAt) + query := fmt.Sprintf(`SELECT id, event_name, subject, template, created_at, updated_at FROM %s WHERE id = '%s' LIMIT 1`, KeySpace+"."+models.Collections.EmailTemplate, emailTemplateID) + err := p.db.Query(query).Consistency(gocql.One).Scan(&emailTemplate.ID, &emailTemplate.EventName, &emailTemplate.Subject, &emailTemplate.Template, &emailTemplate.CreatedAt, &emailTemplate.UpdatedAt) if err != nil { return nil, err } @@ -139,8 +139,8 @@ func (p *provider) GetEmailTemplateByID(ctx context.Context, emailTemplateID str // GetEmailTemplateByEventName to get EmailTemplate by event_name func (p *provider) GetEmailTemplateByEventName(ctx context.Context, eventName string) (*model.EmailTemplate, error) { var emailTemplate models.EmailTemplate - query := fmt.Sprintf(`SELECT id, event_name, template, created_at, updated_at FROM %s WHERE event_name = '%s' LIMIT 1 ALLOW FILTERING`, KeySpace+"."+models.Collections.EmailTemplate, eventName) - err := p.db.Query(query).Consistency(gocql.One).Scan(&emailTemplate.ID, &emailTemplate.EventName, &emailTemplate.Template, &emailTemplate.CreatedAt, &emailTemplate.UpdatedAt) + query := fmt.Sprintf(`SELECT id, event_name, subject, template, created_at, updated_at FROM %s WHERE event_name = '%s' LIMIT 1 ALLOW FILTERING`, KeySpace+"."+models.Collections.EmailTemplate, eventName) + err := p.db.Query(query).Consistency(gocql.One).Scan(&emailTemplate.ID, &emailTemplate.EventName, &emailTemplate.Subject, &emailTemplate.Template, &emailTemplate.CreatedAt, &emailTemplate.UpdatedAt) if err != nil { return nil, err } diff --git a/server/db/providers/cassandradb/provider.go b/server/db/providers/cassandradb/provider.go index e5a0469..08b2a26 100644 --- a/server/db/providers/cassandradb/provider.go +++ b/server/db/providers/cassandradb/provider.go @@ -214,6 +214,12 @@ func NewProvider() (*provider, error) { if err != nil { return nil, err } + // add subject on email_templates table + emailTemplateAlterQuery := fmt.Sprintf(`ALTER TABLE %s.%s ADD subject text;`, KeySpace, models.Collections.EmailTemplate) + err = session.Query(emailTemplateAlterQuery).Exec() + if err != nil { + return nil, err + } return &provider{ db: session, diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 6b159e4..7acb942 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -56,6 +56,7 @@ type ComplexityRoot struct { CreatedAt func(childComplexity int) int EventName func(childComplexity int) int ID func(childComplexity int) int + Subject func(childComplexity int) int Template func(childComplexity int) int UpdatedAt func(childComplexity int) int } @@ -403,6 +404,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.EmailTemplate.ID(childComplexity), true + case "EmailTemplate.subject": + if e.complexity.EmailTemplate.Subject == nil { + break + } + + return e.complexity.EmailTemplate.Subject(childComplexity), true + case "EmailTemplate.template": if e.complexity.EmailTemplate.Template == nil { break @@ -1978,6 +1986,7 @@ type EmailTemplate { id: ID! event_name: String! template: String! + subject: String! created_at: Int64 updated_at: Int64 } @@ -2193,6 +2202,7 @@ input TestEndpointRequest { input AddEmailTemplateRequest { event_name: String! + subject: String! template: String! } @@ -2200,6 +2210,7 @@ input UpdateEmailTemplateRequest { id: ID! event_name: String template: String + subject: String } input DeleteEmailTemplateRequest { @@ -3108,6 +3119,41 @@ func (ec *executionContext) _EmailTemplate_template(ctx context.Context, field g return ec.marshalNString2string(ctx, field.Selections, res) } +func (ec *executionContext) _EmailTemplate_subject(ctx context.Context, field graphql.CollectedField, obj *model.EmailTemplate) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "EmailTemplate", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Subject, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + func (ec *executionContext) _EmailTemplate_created_at(ctx context.Context, field graphql.CollectedField, obj *model.EmailTemplate) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -10087,6 +10133,14 @@ func (ec *executionContext) unmarshalInputAddEmailTemplateRequest(ctx context.Co if err != nil { return it, err } + case "subject": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("subject")) + it.Subject, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } case "template": var err error @@ -10866,6 +10920,14 @@ func (ec *executionContext) unmarshalInputUpdateEmailTemplateRequest(ctx context if err != nil { return it, err } + case "subject": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("subject")) + it.Subject, err = ec.unmarshalOString2áš–string(ctx, v) + if err != nil { + return it, err + } } } @@ -11632,6 +11694,11 @@ func (ec *executionContext) _EmailTemplate(ctx context.Context, sel ast.Selectio if out.Values[i] == graphql.Null { invalids++ } + case "subject": + out.Values[i] = ec._EmailTemplate_subject(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } case "created_at": out.Values[i] = ec._EmailTemplate_created_at(ctx, field, obj) case "updated_at": diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 70762dc..6d7f319 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -4,6 +4,7 @@ package model type AddEmailTemplateRequest struct { EventName string `json:"event_name"` + Subject string `json:"subject"` Template string `json:"template"` } @@ -43,6 +44,7 @@ type EmailTemplate struct { ID string `json:"id"` EventName string `json:"event_name"` Template string `json:"template"` + Subject string `json:"subject"` CreatedAt *int64 `json:"created_at"` UpdatedAt *int64 `json:"updated_at"` } @@ -240,6 +242,7 @@ type UpdateEmailTemplateRequest struct { ID string `json:"id"` EventName *string `json:"event_name"` Template *string `json:"template"` + Subject *string `json:"subject"` } type UpdateEnvInput struct { diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index ca0e7df..e163379 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -189,6 +189,7 @@ type EmailTemplate { id: ID! event_name: String! template: String! + subject: String! created_at: Int64 updated_at: Int64 } @@ -404,6 +405,7 @@ input TestEndpointRequest { input AddEmailTemplateRequest { event_name: String! + subject: String! template: String! } @@ -411,6 +413,7 @@ input UpdateEmailTemplateRequest { id: ID! event_name: String template: String + subject: String } input DeleteEmailTemplateRequest { diff --git a/server/resolvers/add_email_template.go b/server/resolvers/add_email_template.go index 1cba02f..092d017 100644 --- a/server/resolvers/add_email_template.go +++ b/server/resolvers/add_email_template.go @@ -34,6 +34,10 @@ func AddEmailTemplateResolver(ctx context.Context, params model.AddEmailTemplate return nil, fmt.Errorf("invalid event name %s", params.EventName) } + if strings.TrimSpace(params.Subject) == "" { + return nil, fmt.Errorf("empty subject not allowed") + } + if strings.TrimSpace(params.Template) == "" { return nil, fmt.Errorf("empty template not allowed") } @@ -41,6 +45,7 @@ func AddEmailTemplateResolver(ctx context.Context, params model.AddEmailTemplate _, err = db.Provider.AddEmailTemplate(ctx, models.EmailTemplate{ EventName: params.EventName, Template: params.Template, + Subject: params.Subject, }) if err != nil { log.Debug("Failed to add email template: ", err) diff --git a/server/resolvers/update_email_template.go b/server/resolvers/update_email_template.go index 95362c0..5eab5aa 100644 --- a/server/resolvers/update_email_template.go +++ b/server/resolvers/update_email_template.go @@ -51,6 +51,14 @@ func UpdateEmailTemplateResolver(ctx context.Context, params model.UpdateEmailTe emailTemplateDetails.EventName = refs.StringValue(params.EventName) } + if params.Subject != nil && emailTemplateDetails.Subject != refs.StringValue(params.Subject) { + if strings.TrimSpace(refs.StringValue(params.Subject)) == "" { + log.Debug("empty subject not allowed") + return nil, fmt.Errorf("empty subject not allowed") + } + emailTemplateDetails.Subject = refs.StringValue(params.Subject) + } + if params.Template != nil && emailTemplateDetails.Template != refs.StringValue(params.Template) { if strings.TrimSpace(refs.StringValue(params.Template)) == "" { log.Debug("empty template not allowed") diff --git a/server/test/add_email_template_test.go b/server/test/add_email_template_test.go index 743ed12..40cf94e 100644 --- a/server/test/add_email_template_test.go +++ b/server/test/add_email_template_test.go @@ -31,10 +31,21 @@ func addEmailTemplateTest(t *testing.T, s TestSetup) { assert.Nil(t, emailTemplate) }) + t.Run("should not add email template for empty template", func(t *testing.T) { + emailTemplate, err := resolvers.AddEmailTemplateResolver(ctx, model.AddEmailTemplateRequest{ + EventName: s.TestInfo.TestEmailTemplateEventTypes[0], + Template: " test ", + Subject: " ", + }) + assert.Error(t, err) + assert.Nil(t, emailTemplate) + }) + t.Run("should not add email template for empty template", func(t *testing.T) { emailTemplate, err := resolvers.AddEmailTemplateResolver(ctx, model.AddEmailTemplateRequest{ EventName: s.TestInfo.TestEmailTemplateEventTypes[0], Template: " ", + Subject: "test", }) assert.Error(t, err) assert.Nil(t, emailTemplate) @@ -43,7 +54,8 @@ func addEmailTemplateTest(t *testing.T, s TestSetup) { t.Run("should add email template for "+eventType, func(t *testing.T) { emailTemplate, err := resolvers.AddEmailTemplateResolver(ctx, model.AddEmailTemplateRequest{ EventName: eventType, - Template: `Test email`, + Template: "Test email", + Subject: "Test email", }) assert.NoError(t, err) assert.NotNil(t, emailTemplate) @@ -52,6 +64,7 @@ func addEmailTemplateTest(t *testing.T, s TestSetup) { et, err := db.Provider.GetEmailTemplateByEventName(ctx, eventType) assert.NoError(t, err) assert.Equal(t, et.EventName, eventType) + assert.Equal(t, "Test email", et.Subject) }) } }) diff --git a/server/test/update_email_template_test.go b/server/test/update_email_template_test.go index 1f23ff4..4f2a999 100644 --- a/server/test/update_email_template_test.go +++ b/server/test/update_email_template_test.go @@ -31,6 +31,7 @@ func updateEmailTemplateTest(t *testing.T, s TestSetup) { res, err := resolvers.UpdateEmailTemplateResolver(ctx, model.UpdateEmailTemplateRequest{ ID: emailTemplate.ID, Template: refs.NewStringRef("Updated test template"), + Subject: refs.NewStringRef("Updated subject"), }) assert.NoError(t, err) @@ -42,5 +43,6 @@ func updateEmailTemplateTest(t *testing.T, s TestSetup) { assert.NotNil(t, updatedEmailTemplate) assert.Equal(t, emailTemplate.ID, updatedEmailTemplate.ID) assert.Equal(t, updatedEmailTemplate.Template, "Updated test template") + assert.Equal(t, updatedEmailTemplate.Subject, "Updated subject") }) } diff --git a/templates/dashboard.tmpl b/templates/dashboard.tmpl index 598e984..8710a35 100644 --- a/templates/dashboard.tmpl +++ b/templates/dashboard.tmpl @@ -12,9 +12,901 @@ +
+