add: template editor

This commit is contained in:
h z
2025-04-14 17:02:22 +01:00
parent 09338a2683
commit 947b59e3ea
29 changed files with 1277 additions and 166 deletions

292
package-lock.json generated
View File

@@ -9,10 +9,13 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"assert": "^2.1.0",
"axios": "^1.7.9", "axios": "^1.7.9",
"bulma": "^1.0.2", "bulma": "^1.0.2",
"katex": "^0.16.11", "katex": "^0.16.11",
"oidc-client-ts": "^3.1.0", "oidc-client-ts": "^3.1.0",
"path-browserify": "^1.0.1",
"process": "^0.11.10",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
@@ -445,25 +448,25 @@
} }
}, },
"node_modules/@babel/helpers": { "node_modules/@babel/helpers": {
"version": "7.26.0", "version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
"integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/template": "^7.25.9", "@babel/template": "^7.27.0",
"@babel/types": "^7.26.0" "@babel/types": "^7.27.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.26.2", "version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
"integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/types": "^7.26.0" "@babel/types": "^7.27.0"
}, },
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@@ -1788,9 +1791,9 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.26.0", "version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@@ -1799,14 +1802,14 @@
} }
}, },
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.25.9", "version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.25.9", "@babel/code-frame": "^7.26.2",
"@babel/parser": "^7.25.9", "@babel/parser": "^7.27.0",
"@babel/types": "^7.25.9" "@babel/types": "^7.27.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -1854,9 +1857,9 @@
"dev": true "dev": true
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.26.0", "version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
"integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.25.9", "@babel/helper-string-parser": "^7.25.9",
@@ -3649,6 +3652,18 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"dev": true "dev": true
}, },
"node_modules/assert": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
"integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
"dependencies": {
"call-bind": "^1.0.2",
"is-nan": "^1.3.2",
"object-is": "^1.1.5",
"object.assign": "^4.1.4",
"util": "^0.12.5"
}
},
"node_modules/asynckit": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -3669,9 +3684,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.7.9", "version": "1.8.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.0",
@@ -4103,15 +4118,41 @@
} }
}, },
"node_modules/call-bind": { "node_modules/call-bind": {
"version": "1.0.7", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0", "es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4", "get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.1" "set-function-length": "^1.2.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -4807,6 +4848,22 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/define-properties": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dependencies": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -5029,6 +5086,19 @@
"webpack": "^4 || ^5" "webpack": "^4 || ^5"
} }
}, },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -5112,12 +5182,9 @@
} }
}, },
"node_modules/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
@@ -5136,6 +5203,17 @@
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
"dev": true "dev": true
}, },
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escalade": { "node_modules/escalade": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -5756,15 +5834,20 @@
} }
}, },
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.2.4", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2", "function-bind": "^1.1.2",
"has-proto": "^1.0.1", "get-proto": "^1.0.1",
"has-symbols": "^1.0.3", "gopd": "^1.2.0",
"hasown": "^2.0.0" "has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -5782,6 +5865,18 @@
"node": ">=8.0.0" "node": ">=8.0.0"
} }
}, },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-stream": { "node_modules/get-stream": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -5842,12 +5937,9 @@
} }
}, },
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.1.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.1.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-FQoVQnqcdk4hVM4JN1eromaun4iuS34oStkdlLENLdpULsuQcTyXj8w7ayhuUfPwEYZ1ZOooOTT6fdA9Vmx/RA==", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@@ -5893,20 +5985,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-proto": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.1.0.tgz",
"integrity": "sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q==",
"dependencies": {
"call-bind": "^1.0.7"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": { "node_modules/has-symbols": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -6918,6 +6996,21 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/is-nan": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
"integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
"dependencies": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-network-error": { "node_modules/is-network-error": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz",
@@ -8244,9 +8337,9 @@
} }
}, },
"node_modules/katex": { "node_modules/katex": {
"version": "0.16.11", "version": "0.16.21",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz",
"integrity": "sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==", "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==",
"funding": [ "funding": [
"https://opencollective.com/katex", "https://opencollective.com/katex",
"https://github.com/sponsors/katex" "https://github.com/sponsors/katex"
@@ -8457,6 +8550,14 @@
"remove-accents": "0.5.0" "remove-accents": "0.5.0"
} }
}, },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mdast-util-find-and-replace": { "node_modules/mdast-util-find-and-replace": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
@@ -9643,6 +9744,48 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/object-is": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/object.assign": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
"integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.3",
"define-properties": "^1.2.1",
"es-object-atoms": "^1.0.0",
"has-symbols": "^1.1.0",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/oblivious-set": { "node_modules/oblivious-set": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz",
@@ -9873,6 +10016,11 @@
"tslib": "^2.0.3" "tslib": "^2.0.3"
} }
}, },
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -10103,13 +10251,21 @@
} }
}, },
"node_modules/prismjs": { "node_modules/prismjs": {
"version": "1.29.0", "version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/process-nextick-args": { "node_modules/process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",

View File

@@ -12,10 +12,13 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"assert": "^2.1.0",
"axios": "^1.7.9", "axios": "^1.7.9",
"bulma": "^1.0.2", "bulma": "^1.0.2",
"katex": "^0.16.11", "katex": "^0.16.11",
"oidc-client-ts": "^3.1.0", "oidc-client-ts": "^3.1.0",
"path-browserify": "^1.0.1",
"process": "^0.11.10",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",

View File

@@ -9,7 +9,7 @@ import Callback from "./components/KeycloakCallbacks/Callback";
import Footer from "./components/Footer"; import Footer from "./components/Footer";
import PopupCallback from "./components/KeycloakCallbacks/PopupCallback"; import PopupCallback from "./components/KeycloakCallbacks/PopupCallback";
import SilentCallback from "./components/KeycloakCallbacks/SilentCallback"; import SilentCallback from "./components/KeycloakCallbacks/SilentCallback";
import {useHomeMarkdown} from "./utils/markdown-queries"; import MarkdownTemplateEditor from "./components/MarkdownTemplate/MarkdownTemplateEditor";
const App = () => { const App = () => {
@@ -32,7 +32,9 @@ const App = () => {
<Route path="/markdown/create" element={<MarkdownEditor />} /> <Route path="/markdown/create" element={<MarkdownEditor />} />
<Route path="/markdown/edit/:id" element={<MarkdownEditor />} /> <Route path="/markdown/edit/:id" element={<MarkdownEditor />} />
<Route path="/popup_callback" element={<PopupCallback />} /> <Route path="/popup_callback" element={<PopupCallback />} />
<Route path="silent_callback" element={<SilentCallback />} /> <Route path="/silent_callback" element={<SilentCallback />} />
<Route path="/template/create" element={<MarkdownTemplateEditor />} />
<Route path="/template/edit/:id" element={<MarkdownTemplateEditor />} />
</Routes> </Routes>
</main> </main>
</div> </div>

View File

@@ -33,7 +33,6 @@ const ConfigProvider = ({ children }) => {
return res.json(); return res.json();
}) })
.then((data) => { .then((data) => {
console.log(data);
setConfig(data); setConfig(data);
setIsLoading(false); setIsLoading(false);
}) })

View File

@@ -0,0 +1,65 @@
import React, { useState } from "react";
const EnumsEditor = ({ enums, onChange }) => {
const [_enums, setEnums] = useState(enums || []);
return (
<div className="box">
<ul>
{_enums.map((item, index) => (
<li key={index} className="field has-addons" style={{ marginBottom: "0.5rem" }}>
<div className="control is-expanded">
<input
className="input is-small"
type="text"
value={item}
onChange={(e) => {
const updated = [..._enums];
updated[index] = e.target.value;
setEnums(updated);
onChange(updated);
}}
/>
</div>
<div className="control">
<button
className="button is-small is-danger"
type="button"
onClick={() => {
const updated = [..._enums];
updated.splice(index, 1);
setEnums(updated);
onChange(updated);
}}
>
<span className="icon is-small">
<i className="fas fa-times" />
</span>
</button>
</div>
</li>
))}
</ul>
<div className="field">
<div className="control">
<button
className="button is-small is-primary"
type="button"
onClick={() => {
const updated = [..._enums, ""];
setEnums(updated);
onChange(updated);
}}
>
<span className="icon is-small">
<i className="fas fa-plus" />
</span>
<span>Add Enum</span>
</button>
</div>
</div>
</div>
);
};
export default EnumsEditor;

View File

@@ -0,0 +1,16 @@
import React, {useState} from 'react';
const LayoutEditor = ({layout, onChange}) => {
const [_layout, setLayout] = useState(layout || "");
return (
<textarea
className="textarea"
value={_layout}
onChange={(e) => {
setLayout(e.target.value);
onChange(layout);
}}
/>
);
};
export default LayoutEditor;

View File

@@ -0,0 +1,90 @@
import React, { useContext, useState } from "react";
import { AuthContext } from "../../AuthProvider";
import { useNavigate, useParams } from "react-router-dom";
import { useMarkdownTemplate, useSaveMarkdownTemplate } from "../../utils/queries/template-queries";
import LayoutEditor from "./LayoutEditor";
import ParametersManager from "./ParametersManager";
import "bulma/css/bulma.min.css";
const MarkdownTemplateEditor = () => {
const { roles } = useContext(AuthContext);
if (!roles.includes("admin") || roles.includes("creator"))
return <div className="notification is-danger">Permission Denied</div>;
const navigate = useNavigate();
const { id } = useParams();
const { data: template, isFetching: templateIsFetching } = useMarkdownTemplate(id);
const saveMarkdownTemplate = useSaveMarkdownTemplate();
const [title, setTitle] = useState(template?.id || "");
const [parameters, setParameters] = useState(template?.parameters || []);
const [layout, setLayout] = useState(template?.layout || "");
if (templateIsFetching) {
return <p>Loading...</p>;
}
const handleSave = () => {
saveMarkdownTemplate.mutate(
{ id, data: { title, parameters, layout } },
{
onSuccess: () => {
navigate("/");
},
onError: () => {
alert("Error saving template.");
}
}
);
};
return (
<section className="section">
<div className="container">
<h2 className="title is-4">Markdown Template Editor</h2>
<div className="field">
<label className="label">Title:</label>
<div className="control">
<input
className="input"
type="text"
placeholder="Enter template title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
</div>
<div className="columns is-variable is-8">
<div className="column">
<h3 className="title is-5">Layout</h3>
<div className="box">
<LayoutEditor
layout={layout}
parameters={parameters}
onChange={(newLayout) => setLayout(newLayout)}
/>
</div>
</div>
<div className="column">
<h3 className="title is-5">Parameters</h3>
<div className="box">
<ParametersManager
parameters={parameters}
onChange={(newParameters) => setParameters(newParameters)}
/>
</div>
</div>
</div>
<div className="field is-grouped">
<div className="control">
<button className="button is-primary" onClick={handleSave}>
Save Template
</button>
</div>
</div>
</div>
</section>
);
};
export default MarkdownTemplateEditor;

View File

@@ -0,0 +1,84 @@
import React, { useState } from "react";
import TypeEditor from "./TypeEditor";
const ParametersManager = ({ parameters, onChange }) => {
const [_parameters, setParameters] = useState(parameters || []);
const handleAdd = () => {
const updated = [
..._parameters,
{
name: "",
type: { base_type: "string" }
}
];
setParameters(updated);
onChange(updated);
};
const handleNameChange = (index, newName) => {
const updated = [..._parameters];
updated[index].name = newName;
setParameters(updated);
onChange(updated);
};
const handleDelete = (index) => {
const updated = [..._parameters];
updated.splice(index, 1);
setParameters(updated);
onChange(updated);
};
return (
<div className="box">
<div className="field">
<div className="control">
<button className="button is-primary" onClick={handleAdd}>
Add Parameter
</button>
</div>
</div>
{_parameters.map((param, index) => (
<div key={index} className="box" style={{ marginBottom: "1rem" }}>
<div className="field is-grouped is-grouped-multiline">
<div className="control is-expanded">
<label className="label">Name:</label>
<input
type="text"
className="input"
value={param.name}
onChange={(e) => handleNameChange(index, e.target.value)}
placeholder="Parameter name"
/>
</div>
<div className="control">
<button
className="button is-danger"
onClick={() => handleDelete(index)}
>
Delete
</button>
</div>
</div>
<div className="field">
<label className="label">Type:</label>
<div className="control">
<TypeEditor
type={param.type}
onChange={(newType) => {
const updated = [..._parameters];
updated[index].type = newType;
setParameters(updated);
onChange(updated);
}}
/>
</div>
</div>
</div>
))}
</div>
);
};
export default ParametersManager;

View File

@@ -0,0 +1,30 @@
import React, {useState} from "react";
import {useMarkdownTemplates} from "../../utils/queries/template-queries";
const TemplateSelector = ({template, onChange}) => {
const [_template, setTemplate] = useState(template || {
title: "",
parameters: [],
layout: ""
});
const {data:templates, isFetching: templatesAreFetching} = useMarkdownTemplates();
if(templatesAreFetching) {
return <p>Loading...</p>
}
return (
<select
value={_template.title}
onChange={(e) => {
setTemplate(e.target.value);
onChange(e.target.value);
}}>
{
templates.map((tmpl, index) => (
<option key={index} value={tmpl} >{tmpl.title}</option>
))
}
</select>
);
};
export default TemplateSelector;

View File

@@ -0,0 +1,94 @@
import React from 'react';
import EnumsEditor from './EnumsEditor';
import TemplateSelector from './TemplateSelector';
const TypeEditor = ({ type, onChange }) => {
const [_type, setType] = React.useState(type || {});
const renderExtraFields = () => {
switch (_type.base_type) {
case 'enum':
return <EnumsEditor
enums={_type.definition.enums}
onChange={(newEnums) => {
const updated = {
..._type,
definition: {
..._type.definition,
enums: newEnums
}
};
setType(updated);
onChange(updated);
}}
/>;
case 'list':
return (
<div>
<TypeEditor
extendType={_type.extend_type}
onChange={(extendType) => {
const updated = {..._type, extend_type: extendType};
setType(updated);
onChange(updated);
}}
/>
<textarea
className="textarea"
value={_type.definition.iter_layout}
onChange={(e) => {
const updated = {
..._type,
definition: {
..._type.definition,
iter_layout: e.target.value
}
};
setType(updated);
onChange(updated);
}}
/>
</div>
);
case 'template':
return <TemplateSelector
template={_type.definition.template}
onChange={(newTemplate) => {
const updated = {
..._type,
definition: {
..._type.definition,
template: newTemplate
}
};
setType(updated);
onChange(updated);
}}
/>;
default:
return null;
}
};
return (
<div>
<label>type:</label>
<select value={_type.base_type} onChange={(e) => {
const updated = {
base_type: e.target.value,
definition: {}
};
setType(updated);
onChange(updated);
}}>
<option value="string">string</option>
<option value="markdown">markdown</option>
<option value="enum">enum</option>
<option value="list">list</option>
<option value="template">template</option>
</select>
{renderExtraFields()}
</div>
);
};
export default TypeEditor;

View File

@@ -4,14 +4,18 @@ import "katex/dist/katex.min.css";
import "./MarkdownContent.css"; import "./MarkdownContent.css";
import MarkdownView from "./MarkdownView"; import MarkdownView from "./MarkdownView";
import PermissionGuard from "../PermissionGuard"; import PermissionGuard from "../PermissionGuard";
import {useMarkdown} from "../../utils/markdown-queries"; import {useMarkdown} from "../../utils/queries/markdown-queries";
import {usePath} from "../../utils/path-queries"; import {usePath} from "../../utils/queries/path-queries";
import {useMarkdownSetting} from "../../utils/queries/setting-queries";
import {useMarkdownTemplate} from "../../utils/queries/template-queries";
const MarkdownContent = () => { const MarkdownContent = () => {
const { id } = useParams(); const { id } = useParams();
const [indexTitle, setIndexTitle] = useState(null); const [indexTitle, setIndexTitle] = useState(null);
const {data: markdown, isLoading, error} = useMarkdown(id); const {data: markdown, isLoading, error} = useMarkdown(id);
const {data: path, isFetching: isPathFetching} = usePath(markdown?.path_id); const {data: path, isFetching: isPathFetching} = usePath(markdown?.path_id);
const {data: setting, isFetching: isSettingFetching} = useMarkdownSetting(markdown?.setting_id);
const {data: template_setting, isFetching: isTemplateSettingFetching} = useMarkdownTemplate(setting?.template_setting_id);
useEffect(() => { useEffect(() => {
@@ -21,7 +25,9 @@ const MarkdownContent = () => {
}, [markdown, path]); }, [markdown, path]);
if (isLoading || isPathFetching) { const notReady = isLoading || isPathFetching || isSettingFetching || isTemplateSettingFetching;
if (notReady) {
return <div>Loading...</div>; return <div>Loading...</div>;
} }
@@ -39,8 +45,7 @@ const MarkdownContent = () => {
</Link> </Link>
</PermissionGuard> </PermissionGuard>
</div> </div>
<MarkdownView content={JSON.parse(markdown.content)} template={template_setting}/>
<MarkdownView content={markdown.content}/>
</div> </div>
); );
}; };

View File

@@ -5,23 +5,29 @@ import "katex/dist/katex.min.css";
import "./MarkdownEditor.css"; import "./MarkdownEditor.css";
import PathManager from "../PathManager"; import PathManager from "../PathManager";
import MarkdownView from "./MarkdownView"; import MarkdownView from "./MarkdownView";
import { useMarkdown, useSaveMarkdown } from "../../utils/markdown-queries"; import { useMarkdown, useSaveMarkdown } from "../../utils/queries/markdown-queries";
import {useMarkdownSetting} from "../../utils/queries/setting-queries";
import {useMarkdownTemplate} from "../../utils/queries/template-queries";
import TemplatedEditor from "./TemplatedEditor";
const MarkdownEditor = () => { const MarkdownEditor = () => {
const { roles } = useContext(AuthContext); const { roles } = useContext(AuthContext);
const navigate = useNavigate(); const navigate = useNavigate();
const { id } = useParams(); const { id } = useParams();
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [content, setContent] = useState(""); const [content, setContent] = useState({});
const [shortcut, setShortcut] = useState(""); const [shortcut, setShortcut] = useState("");
const [pathId, setPathId] = useState(1); const [pathId, setPathId] = useState(1);
const {data: markdown, isLoading, error} = useMarkdown(id); const {data: markdown, isLoading, error} = useMarkdown(id);
const saveMarkdown = useSaveMarkdown(); const saveMarkdown = useSaveMarkdown();
const {data: setting, isFetching: isSettingFetching} = useMarkdownSetting(markdown?.id);
const {data: template, isFetching: isTemplateFetching} = useMarkdownTemplate(setting?.id);
const notReady = isLoading || isTemplateFetching || isSettingFetching;
useEffect(() => { useEffect(() => {
if(markdown){ if(markdown){
setTitle(markdown.title); setTitle(markdown.title);
setContent(markdown.content); setContent(JSON.parse(markdown.content));
setShortcut(markdown.shortcut); setShortcut(markdown.shortcut);
setPathId(markdown.path_id); setPathId(markdown.path_id);
} }
@@ -29,7 +35,7 @@ const MarkdownEditor = () => {
const handleSave = () => { const handleSave = () => {
saveMarkdown.mutate( saveMarkdown.mutate(
{id, data: {title, content, path_id: pathId, shortcut}}, {id, data: {title, content: JSON.stringify(content), path_id: pathId, shortcut}},
{ {
onSuccess: () => { onSuccess: () => {
navigate("/"); navigate("/");
@@ -45,12 +51,12 @@ const MarkdownEditor = () => {
if (!hasPermission) if (!hasPermission)
return <div className="notification is-danger">Permission Denied</div>; return <div className="notification is-danger">Permission Denied</div>;
if(isLoading) if(notReady)
return <p>Loading...</p>; return <p>Loading...</p>;
console.log(markdown?.id);
if(error) if(error)
return <p>{error.message || "Failed to load markdown"}</p>; return <p>{error.message || "Failed to load markdown"}</p>;
return ( return (
<div className="container mt-5 markdown-editor-container"> <div className="container mt-5 markdown-editor-container">
<h2 className="title is-4">{id ? "Edit Markdown" : "Create Markdown"}</h2> <h2 className="title is-4">{id ? "Edit Markdown" : "Create Markdown"}</h2>
@@ -93,13 +99,14 @@ const MarkdownEditor = () => {
<div className="field"> <div className="field">
<label className="label">Content</label> <label className="label">Content</label>
<div className="control"> <div className="control">
<textarea <TemplatedEditor
style={{ height: "70vh" }} style={{height: "70vh"}}
className="textarea" content={content}
placeholder="Enter Markdown content" template={template}
value={content} onContentChanged={(k, v) => setContent(
onChange={(e) => setContent(e.target.value)} prev => ({...prev, [k]: v})
></textarea> )}
/>
</div> </div>
</div> </div>
@@ -120,7 +127,7 @@ const MarkdownEditor = () => {
<div className="column is-half"> <div className="column is-half">
<h3 className="subtitle is-5">Preview</h3> <h3 className="subtitle is-5">Preview</h3>
<MarkdownView content={content} height='70vh'/> <MarkdownView content={content} template={template} height='70vh'/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -8,18 +8,68 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { okaidia } from "react-syntax-highlighter/dist/esm/styles/prism"; import { okaidia } from "react-syntax-highlighter/dist/esm/styles/prism";
import "katex/dist/katex.min.css"; import "katex/dist/katex.min.css";
import "./MarkdownView.css"; import "./MarkdownView.css";
import {useLinks} from "../../utils/markdown-queries"; import {useLinks} from "../../utils/queries/markdown-queries";
const MarkdownView = ({ content, height="auto" }) => { const Translate = ({variable, value}) => {
if (variable.type.base_type === "markdown" || variable.type.base_type === "string" || variable.type.base_type === "enum") {
return value;
}
if(variable.type.base_type === "list"){
if (!variable.type.extend_type)
return [];
return value.map((item, index) => Translate({
variable: {name: index, type: variable.type.extend_type},
value: item,
})).map((item) => variable.type.definition.iter_layout.replaceAll('<item/>', item));
}
if(variable.type.base_type === "template"){
return ParseTemplate({
template: variable.type.definition.template,
variables: value
});
}
};
const ParseTemplate = ({template, variables}) => {
let res = template.layout;
for (const parameter of template.parameters) {
res = res.replaceAll(`<${parameter.name}/>`, Translate({
variable: parameter,
value: variables[parameter.name]
}));
}
return res;
};
const MarkdownView = ({ content, template, height="auto" }) => {
const {data: links, isLoading} = useLinks(); const {data: links, isLoading} = useLinks();
if (isLoading) if (isLoading)
return <p>Loading...</p> return (<p>Loading...</p>);
const definitions = "\n<!-- Definitions -->\n" + links.join("\n"); const linkDefinitions = "\n<!-- Definitions -->\n" + links.join("\n");
const _template = template || {
parameters: [
{
name: "markdown",
type: {
base_type: "markdown",
definition: {}
}
}
],
layout: "<markdown/>",
title: "default"
};
return ( return (
<div className="markdown-preview" style={{height}}> <div className="markdown-preview" style={{height}}>
<ReactMarkdown <ReactMarkdown
children={content + "\n" + definitions} children={ParseTemplate({
template: _template,
variables: content
}) + "\n" + linkDefinitions}
remarkPlugins={[remarkMath, remarkGfm]} remarkPlugins={[remarkMath, remarkGfm]}
rehypePlugins={[rehypeKatex, rehypeRaw]} rehypePlugins={[rehypeKatex, rehypeRaw]}
components={{ components={{

View File

@@ -0,0 +1,172 @@
import React, {useState} from "react";
import {useMarkdownTemplate} from "../../utils/queries/template-queries";
const TemplatedEditorComponent = ({ variable, value, namespace, onContentChanged }) => {
if(variable.type.base_type === "string") {
return (
<div>
<label>{namespace}{variable.name} - {variable.type.base_type}</label>
<input
type="text"
className="input"
value={value}
onChange={(e) => onContentChanged(variable.name, e.target.value)}
/>
<hr/>
</div>
);
}
if (variable.type.base_type === 'markdown') {
return(
<div>
<label>{namespace}{variable.name} - {variable.type.base_type}</label>
<textarea
className="textarea"
value={value}
onChange={(e) => onContentChanged(variable.name, e.target.value)}
/>
<hr/>
</div>
);
}
if(variable.type.base_type === "enum"){
return (
<div>
<label>{namespace}{variable.name} - {variable.type.base_type}</label>
<select
value={value}
onChange={(e) => onContentChanged(variable.name, e.target.value)}
>
{variable.type.definition.enums.map((item) => (
<option key={item} value={item}>{item}</option>
))}
</select>
<hr/>
</div>
);
}
if(variable.type.base_type === "list"){
const [cache, setCache] = useState(value);
const defaultValue = variable.type.definition.default || undefined;
const addItem = () => {
setCache(prev => {
const newCache = [...prev, defaultValue];
onContentChanged(variable.name, newCache);
return newCache;
});
};
const removeItem = (index) => {
setCache(prev => {
const newCache = prev.filter((_, i) => i!==index);
onContentChanged(variable.name, newCache);
return newCache;
})
};
const _onContentChanged = (index, value) => {
setCache(prev => {
const newCache = [...prev];
newCache[index] = value;
onContentChanged(variable.name, newCache);
return newCache;
});
};
return (
<div>
<label>{namespace}{variable.name} - {variable.type.base_type}[{variable.type.extend_type.base_type}]</label>
{cache.map((item, index) =>
(<div key={index}>
<TemplatedEditorComponent
variable={{name: index, type: variable.type.extend_type}}
value={item}
namespace={namespace}
onContentChanged={_onContentChanged}
/>
<button
className="button is-danger"
type="button"
onClick={() => removeItem(index)}
>
DELETE
</button>
</div>)
)}
<button className="button is-warning" type="button" onClick={addItem}>
ADD
</button>
<hr/>
</div>
);
}
if(variable.type.base_type === 'template'){
const {data: _template, isFetching: _templateIsFetching} = useMarkdownTemplate(variable.type.definition.template_id);
if(_templateIsFetching){
return(<p>Loading...</p>);
}
const _parameters = _template.parameters;
const _namespace = namespace + _template.title+ "::"
const _onSubChanged = (subKey, subValue) => {
const updated = {
...value,
[subKey]: subValue
};
onContentChanged(variable.name, updated);
};
return (
<div>
<label>{namespace}{variable.name} - {variable.type.base_type}</label>
{
_parameters.map((_variable, index) => (
<div key={index}>
<TemplatedEditorComponent
variable={_variable}
value={value[_variable.name]}
namespace={_namespace}
onContentChanged={_onSubChanged}
/>
</div>
))
}
<hr/>
</div>
);
}
};
const TemplatedEditor = ({content, template, onContentChanged}) => {
if(!template){
template = {
parameters: [
{
name: "markdown",
type: {
base_type: "markdown",
definition: {}
}
}
],
layout: "<markdown/>",
title: "default"
};
}
return(
<div>
{
template.parameters.map((variable, index) => (
<div key={index}>
<TemplatedEditorComponent
variable={variable}
value={content[variable.name]}
namespace={template.title+"::"}
onContentChanged={onContentChanged}
/>
</div>
))
}
</div>
);
};
export default TemplatedEditor;

View File

@@ -0,0 +1,76 @@
import {useCreateMarkdownSetting, useMarkdownSetting} from "../../utils/queries/setting-queries";
import {useSaveMarkdown} from "../../utils/queries/markdown-queries";
import {useState} from "react";
import MarkdownTemplateSettingPanel from "../Settings/MarkdownSettings/MarkdownTemplateSettingPanel";
const MarkdownSettingModal = ({isOpen, markdown, onClose}) => {
const {data: markdownSetting, isFetching: markdownSettingIsFetching} = useMarkdownSetting(markdown?.setting_id || 0);
const createMarkdownSetting = useCreateMarkdownSetting();
const updateMarkdown = useSaveMarkdown();
const [activeTab, setActiveTab] = useState("template");
const handleCreateMarkdownSetting = () => {
createMarkdownSetting.mutate({}, {
onSuccess: (res) => {
updateMarkdown.mutate({
id: markdown.id,
data: {
setting_id: res.id
}
});
}
});
};
if(markdownSettingIsFetching)
return(<p>Loading...</p>);
return (
<div className={`modal ${isOpen ? "is-active" : ""}`}>
<div className="modal-background" onClick={onClose} />
<div className="modal-card" style={{width: "60vw"}}>
<header className="modal-card-head">
<p className="modal-card-title">Markdown Settings</p>
<button
className="delete"
type="button"
aria-label="close"
onClick={onClose}
/>
</header>
{
markdownSetting ? (
<section className="modal-card-body">
<button
className="button is-primary"
type="button"
onClick={handleCreateMarkdownSetting}
>
Create Markdown Setting
</button>
</section>
) : (
<section className="modal-card-body">
<div className="tabs">
<ul>
<li className={activeTab==="template" ? "is-active" : ""}>
<a onClick={() => setActiveTab("template")}>Template</a>
</li>
</ul>
</div>
{activeTab === "template" && (
<MarkdownTemplateSettingPanel
markdown={markdownSetting}
onClose={onClose}
/>
)}
</section>
)
}
</div>
</div>
);
};

View File

@@ -1,5 +1,5 @@
import {useUpdatePath} from "../../utils/path-queries"; import {useUpdatePath} from "../../utils/queries/path-queries";
import {useCreatePathSetting, usePathSetting} from "../../utils/setting-queries"; import {useCreatePathSetting, usePathSetting} from "../../utils/queries/setting-queries";
import WebhookSettingPanel from "../Settings/PathSettings/WebhookSettingPanel"; import WebhookSettingPanel from "../Settings/PathSettings/WebhookSettingPanel";
import React, {useState} from "react"; import React, {useState} from "react";
const PathSettingModal = ({ isOpen, path, onClose }) => { const PathSettingModal = ({ isOpen, path, onClose }) => {
@@ -18,17 +18,22 @@ const PathSettingModal = ({ isOpen, path, onClose }) => {
if(isPathSettingLoading) if(isPathSettingLoading)
return <p>Loading...</p> return (<p>Loading...</p>);
return ( return (
<div className={`modal ${isOpen ? "is-active" : ""}`}> <div className={`modal ${isOpen ? "is-active" : ""}`}>
<div className="modal-background" onClick={onClose}></div> <div className="modal-background" onClick={onClose} />
<div className="modal-card" style={{width: "60vw"}}> <div className="modal-card" style={{width: "60vw"}}>
<header className="modal-card-head"> <header className="modal-card-head">
<p className="modal-card-title">Path Settings</p> <p className="modal-card-title">Path Settings</p>
<button type="button" className="delete" aria-label="close" onClick={onClose} /> <button
type="button"
className="delete"
aria-label="close"
onClick={onClose}
/>
</header> </header>
{!pathSetting && !isPathSettingLoading ? ( {!pathSetting ? (
<section className="modal-card-body"> <section className="modal-card-body">
<button <button
type="button" type="button"

View File

@@ -72,9 +72,7 @@ const MainNavigation = () => {
console.log(contentDisposition); console.log(contentDisposition);
if (contentDisposition) { if (contentDisposition) {
const match = contentDisposition.match(/filename="?([^"]+)"?/); const match = contentDisposition.match(/filename="?([^"]+)"?/);
console.log(match);
if (match && match[1]) { if (match && match[1]) {
console.log(match[1]);
filename = match[1]; filename = match[1];
} }
} }

View File

@@ -2,8 +2,8 @@ import React, {useState} from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import PermissionGuard from "../PermissionGuard"; import PermissionGuard from "../PermissionGuard";
import "./PathNode.css"; import "./PathNode.css";
import {useDeletePath, useMovePath, useUpdatePath} from "../../utils/path-queries"; import {useDeletePath, useMovePath, useUpdatePath} from "../../utils/queries/path-queries";
import {useIndexMarkdown, useMoveMarkdown} from "../../utils/markdown-queries"; import {useIndexMarkdown, useMoveMarkdown} from "../../utils/queries/markdown-queries";
import MarkdownNode from "./MarkdownNode"; import MarkdownNode from "./MarkdownNode";
import PathSettingModal from "../Modals/PathSettingModal"; import PathSettingModal from "../Modals/PathSettingModal";
@@ -11,7 +11,7 @@ const PathNode = ({ path, isRoot = false }) => {
const [isPathSettingModalOpen, setIsPathSettingModalOpen] = useState(false); const [isPathSettingModalOpen, setIsPathSettingModalOpen] = useState(false);
const [isExpanded, setIsExpanded] = useState(isRoot); const [isExpanded, setIsExpanded] = useState(isRoot);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [newName, setNewName] = useState(path.name); const [newName, setNewName] = useState(path.name || "");
const deletePath = useDeletePath(); const deletePath = useDeletePath();
const updatePath = useUpdatePath(); const updatePath = useUpdatePath();
@@ -130,8 +130,6 @@ const PathNode = ({ path, isRoot = false }) => {
<button <button
className="button is-small is-success" className="button is-small is-success"
onClick={() => { onClick={() => {
console.log("path", path);
setIsPathSettingModalOpen(true); setIsPathSettingModalOpen(true);
}} }}
type="button" type="button"
@@ -217,6 +215,7 @@ const PathNode = ({ path, isRoot = false }) => {
<MarkdownNode <MarkdownNode
markdown={markdown} markdown={markdown}
handleMoveMarkdown={handleMoveMarkdown} handleMoveMarkdown={handleMoveMarkdown}
key={markdown.id}
/> />
))} ))}
</ul> </ul>

View File

@@ -1,9 +1,9 @@
import PermissionGuard from "../PermissionGuard"; import PermissionGuard from "../PermissionGuard";
import PathNode from "./PathNode"; import PathNode from "./PathNode";
import "./SideNavigation.css"; import "./SideNavigation.css";
import {useDeletePath, usePaths, useUpdatePath} from "../../utils/path-queries"; import {useDeletePath, usePaths, useUpdatePath} from "../../utils/queries/path-queries";
import React from 'react'; import React from 'react';
import {useTree} from "../../utils/tree-queries"; import {useTree} from "../../utils/queries/tree-queries";
const SideNavigation = () => { const SideNavigation = () => {
const {data: tree, isLoading, error} = useTree(); const {data: tree, isLoading, error} = useTree();
@@ -71,6 +71,12 @@ const SideNavigation = () => {
> >
Create New Markdown Create New Markdown
</a> </a>
<a
href="/template/create"
className="button is-primary is-small"
>
Create New Template
</a>
</PermissionGuard> </PermissionGuard>
{!filteredTree || filteredTree.length === 0 ? {!filteredTree || filteredTree.length === 0 ?
<p>No Result</p> : <p>No Result</p> :

View File

@@ -1,15 +1,12 @@
import React, {useEffect, useState, useRef, useContext} from "react"; import React, {useEffect, useState, useRef, useContext} from "react";
import {useCreatePath, usePath, usePaths} from "../utils/path-queries"; import {useCreatePath, usePaths} from "../utils/queries/path-queries";
import { useQueryClient } from "react-query"; import { useQueryClient } from "react-query";
import "./PathManager.css"; import "./PathManager.css";
import {fetch_} from "../utils/request-utils"; import {fetch_} from "../utils/request-utils";
import {ConfigContext} from "../ConfigProvider"; import {ConfigContext} from "../ConfigProvider";
import PathSettingModal from "./Modals/PathSettingModal";
const PathManager = ({ currentPathId = 1, onPathChange }) => { const PathManager = ({ currentPathId = 1, onPathChange }) => {
const { data: currentPath } = usePath(currentPathId);
const [currentFullPath, setCurrentFullPath] = useState([{ name: "Root", id: 1 }]); const [currentFullPath, setCurrentFullPath] = useState([{ name: "Root", id: 1 }]);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [dropdownActive, setDropdownActive] = useState(false); const [dropdownActive, setDropdownActive] = useState(false);
@@ -20,11 +17,7 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
const { data: subPaths, isLoading: isSubPathsLoading, error: subPathsError } = usePaths(currentPathId); const { data: subPaths, isLoading: isSubPathsLoading, error: subPathsError } = usePaths(currentPathId);
const createPath = useCreatePath(); const createPath = useCreatePath();
const config = useContext(ConfigContext).config; const config = useContext(ConfigContext).config;
const [isPathSettingModalOpen, setIsPathSettingModalModalOpen] = useState(false);
const handleSettingClick = () => {
setIsPathSettingModalModalOpen(true);
}
const buildFullPath = async (pathId) => { const buildFullPath = async (pathId) => {
const path = []; const path = [];
@@ -117,31 +110,16 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
{currentFullPath.map((path, index) => ( {currentFullPath.map((path, index) => (
<span <span
key={path.id} key={path.id}
className="breadcrumb-item is-clickable" className="tag is-clickable is-link is-light"
onClick={() => handlePathClick(path.id, index)} onClick={() => handlePathClick(path.id, index)}
> >
{path.name} {path.name + "/"}
{index < currentFullPath.length - 1 && " / "}
</span> </span>
))} ))}
</div> </div>
<div className="control"> <div className="control">
<span>&nbsp;&nbsp;&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;&nbsp;&nbsp;</span>
</div> </div>
<div className="control">
<button
className="button is-small is-primary"
type="button"
onClick={handleSettingClick}
>
Settings
</button>
<PathSettingModal
isOpen={isPathSettingModalOpen}
path={currentPath}
onClose={() => setIsPathSettingModalModalOpen(false)}
/>
</div>
</div> </div>
<div className="path-manager-body"> <div className="path-manager-body">

View File

@@ -0,0 +1,85 @@
import {
useCreateMarkdownTemplateSetting,
useMarkdownTemplate,
useMarkdownTemplates,
useMarkdownTemplateSetting, useUpdateMarkdownTemplateSetting
} from "../../../utils/queries/template-queries";
import {useState} from "react";
import {useUpdateMarkdownSetting} from "../../../utils/queries/setting-queries";
const MarkdownTemplateSettingPanel = ({markdownSetting, onClose}) => {
const {data: setting, isFetching: settingIsFetching } = useMarkdownTemplateSetting(markdownSetting.template_setting_id || 0);
const {data: templates, isFetching: templatesAreFetching}=useMarkdownTemplates();
const {data: template, isFetching: templateIsFetching} = useMarkdownTemplate(setting?.template_id);
const [selectedTemplate, setSelectedTemplate] = useState(template);
const createMarkdownTemplateSetting = useCreateMarkdownTemplateSetting();
const updateMarkdownSetting = useUpdateMarkdownSetting();
const updateMarkdownTemplateSetting = useUpdateMarkdownTemplateSetting();
const handleCreateTemplateSetting = () => {
createMarkdownTemplateSetting.mutate({}, {
onSuccess: (data) => {
updateMarkdownSetting.mutate({
id: markdownSetting.id,
data: {
template_setting_id: data.id,
}
});
}
});
};
const handleSaveMarkdownTemplateSetting = () => {
updateMarkdownTemplateSetting.mutate({
id: setting.id,
data: {
template_id: selectedTemplate.id,
}
}, {
onSuccess: () => alert("Saved"),
onError: () => alert("Failed to save"),
});
onClose();
};
if (settingIsFetching || templatesAreFetching || templatesAreFetching || templateIsFetching) {
return (<p>Loading...</p>);
}
return setting ? (
<div className="box" style={{marginTop: "1rem"}}>
<h4 className="title is-5">Template Setting</h4>
<div className="field">
<label className="label">Use Template</label>
<div className="select is-fullwidth">
<select
value={selectedTemplate.title}
onChange={(e) => setSelectedTemplate(e.target.value)}
>
<option value="">(default)</option>
{templates.map((_template, index) => (
<option key={index} value={_template}>{_template.title}</option>
))}
</select>
</div>
</div>
<button
className="button is-primary"
type="button"
onClick={handleSaveMarkdownTemplateSetting}
>
Save Template Setting
</button>
</div>
) : (
<button
className="button is-primary"
type="button"
onClick={handleCreateTemplateSetting}
>
Create Template Setting
</button>
);
};
export default MarkdownTemplateSettingPanel;

View File

@@ -4,14 +4,14 @@ import {
useCreateWebhookSetting, useCreateWebhookSetting,
useUpdateWebhookSetting, useUpdateWebhookSetting,
useWebhooks, useWebhooks,
} from "../../../utils/webhook-queries"; } from "../../../utils/queries/webhook-queries";
import { import {
useCreateWebhook, useCreateWebhook,
useUpdateWebhook, useUpdateWebhook,
useDeleteWebhook, useDeleteWebhook,
} from "../../../utils/webhook-queries"; } from "../../../utils/queries/webhook-queries";
import {useUpdatePathSetting} from "../../../utils/setting-queries"; import {useUpdatePathSetting} from "../../../utils/queries/setting-queries";
const WebhookSettingPanel = ({pathSetting, onClose}) => { const WebhookSettingPanel = ({pathSetting, onClose}) => {
@@ -89,7 +89,12 @@ const WebhookSettingPanel = ({pathSetting, onClose}) => {
const handleCreateWebhookSetting = () => { const handleCreateWebhookSetting = () => {
createWebhookSetting.mutate({}, { createWebhookSetting.mutate({}, {
onSuccess: (data) => { onSuccess: (data) => {
updatePathSetting.mutate({id: pathSetting.id, data: {webhook_setting_id: data.id}}); updatePathSetting.mutate({
id: pathSetting.id,
data: {
webhook_setting_id: data.id
}
});
} }
}); });
}; };
@@ -115,9 +120,6 @@ const WebhookSettingPanel = ({pathSetting, onClose}) => {
} }
const found = webhooks.find((wh) => wh.id === setting.webhook_id); const found = webhooks.find((wh) => wh.id === setting.webhook_id);
console.log("found", found);
console.log("webhooks", webhooks);
console.log("setting.webhook_id", setting.webhook_id);
setSelectedUrl(found ? found.hook_url : ""); setSelectedUrl(found ? found.hook_url : "");
} else { } else {
setEnabled(false); setEnabled(false);
@@ -191,8 +193,6 @@ const WebhookSettingPanel = ({pathSetting, onClose}) => {
on_events: onEvents, on_events: onEvents,
}; };
console.log(webhooks);
console.log(payload);
if(!setting || !setting.id){ if(!setting || !setting.id){
createWebhookSetting.mutate(payload, { createWebhookSetting.mutate(payload, {
onSuccess: (res) => { onSuccess: (res) => {

View File

@@ -1,6 +1,6 @@
import {useQuery, useMutation, useQueryClient} from 'react-query'; import {useQuery, useMutation, useQueryClient} from 'react-query';
import {fetch_} from "./request-utils"; import {fetch_} from "../request-utils";
import {useConfig} from "../ConfigProvider"; import {useConfig} from "../../ConfigProvider";

View File

@@ -1,6 +1,6 @@
import { useQuery, useMutation, useQueryClient } from "react-query"; import { useQuery, useMutation, useQueryClient } from "react-query";
import { fetch_ } from "./request-utils"; import { fetch_ } from "../request-utils";
import {useConfig} from "../ConfigProvider"; import {useConfig} from "../../ConfigProvider";
export const usePaths = (parent_id) => { export const usePaths = (parent_id) => {

View File

@@ -1,5 +1,5 @@
import {fetch_} from "./request-utils"; import {fetch_} from "../request-utils";
import {useConfig} from "../ConfigProvider"; import {useConfig} from "../../ConfigProvider";
import {useMutation, useQuery, useQueryClient} from "react-query"; import {useMutation, useQuery, useQueryClient} from "react-query";
export const usePathSettings = () => { export const usePathSettings = () => {
@@ -99,7 +99,7 @@ export const useMarkdownSetting = (setting_id) => {
const config = useConfig(); const config = useConfig();
return useQuery( return useQuery(
["markdown_setting", setting_id], ["markdown_setting", setting_id],
() => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/`), { () => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/${setting_id}`, {}), {
enabled: !!setting_id, enabled: !!setting_id,
} }
); );

View File

@@ -0,0 +1,173 @@
import {useConfig} from "../../ConfigProvider";
import {useMutation, useQuery, useQueryClient} from "react-query";
import {fetch_} from "../request-utils";
import {template} from "@babel/core";
import {data} from "react-router-dom";
export const useMarkdownTemplate = (template_id) => {
const config = useConfig();
return useQuery(
["markdown_template", template_id],
() => fetch_(`${config.BACKEND_HOST}/api/template/markdown/${template_id}`), {
enabled: !!template_id,
}
);
};
export const useMarkdownTemplates = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useQuery(
"markdown_templates",
() => fetch_(`${config.BACKEND_HOST}/api/template/markdown/`), {
onSuccess: (data) => {
if(data){
for(const template of data){
queryClient.setQueryData(["markdown_template", template.id], data);
}
}
}
}
);
};
export const useUpdateMarkdownTemplate = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
({id, data}) => fetch_(`${config.BACKEND_HOST}/api/template/markdown/${id}`, {
method: "PUT",
body: JSON.stringify(data),
}),
{
onSuccess: (data) => {
queryClient.invalidateQueries(["markdown_template", data.id]);
queryClient.invalidateQueries("markdown_templates");
}
}
);
}
export const useCreateMarkdownTemplate = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
(data) => fetch_(`${config.BACKEND_HOST}/api/template/markdown/`, {
method: "POST",
body: JSON.stringify(data),
}),{
onSuccess: (data) => {
queryClient.invalidateQueries(["markdown_template", data.id]);
queryClient.invalidateQueries("markdown_templates");
}
}
);
}
export const useDeleteMarkdownTemplate = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
(id) => fetch_(`${config.BACKEND_HOST}/api/template/markdown/${id}`, {
method: "DELETE"
}), {
onSuccess: (res, variables) => {
queryClient.invalidateQueries(["markdown_template", variables]);
queryClient.invalidateQueries("markdown_templates");
}
}
)
}
export const useSaveMarkdownTemplate = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(({id, data}) => {
const url = id
? `${config.BACKEND_HOST}/api/template/markdown/${id}`
: `${config.BACKEND_HOST}/api/template/markdown/`;
const method = id? "PUT": "POST";
return fetch_(url, {
method,
body: JSON.stringify(data),
})
},{
onSuccess: (data) => {
queryClient.invalidateQueries(["markdown_template", data.id]);
queryClient.invalidateQueries("markdown_templates");
}
});
}
export const useMarkdownTemplateSettings = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useQuery(
"markdown_template_settings",
() => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/template/`), {
onSuccess: (data) => {
if(data){
for(const setting of data){
queryClient.invalidateQueries(["markdown_template_setting", settings.id]);
}
}
}
}
);
};
export const useMarkdownTemplateSetting = (setting_id) => {
const config = useConfig();
const queryClient = useQueryClient();
return useQuery(
["markdown_template_setting", setting_id],
() => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/template/${settings.id}`), {
enabled: !!setting_id,
}
);
};
export const useCreateMarkdownTemplateSetting = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(({id, data}) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/template/${id}`, {
method: "POST",
body: JSON.stringify(data),
}), {
onSuccess: (data) => {
queryClient.invalidateQueries(["markdown_template_setting", data.id]);
}
});
};
export const useUpdateMarkdownTemplateSetting = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
({id, data}) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/template/${id}`, {
method: "PUT",
body: JSON.stringify(data),
}),{
onSuccess: (res) => {
queryClient.invalidateQueries(["markdown_template_setting", res.id]);
queryClient.invalidateQueries("markdown_template_settings");
}
}
);
};
export const useDeleteMarkdownTemplateSetting = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
({id}) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/template/${id}`, {
method: "DELETE",
}), {
onSuccess: (res, variables) => {
queryClient.invalidateQueries(["markdown_template_setting", variables.id]);
}
}
);
};

View File

@@ -1,6 +1,6 @@
import {useQuery, useMutation, useQueryClient} from "react-query"; import {useQuery, useMutation, useQueryClient} from "react-query";
import {fetch_} from "./request-utils"; import {fetch_} from "../request-utils";
import {useConfig} from "../ConfigProvider"; import {useConfig} from "../../ConfigProvider";
export const useTree = () => { export const useTree = () => {

View File

@@ -1,5 +1,5 @@
import {fetch_ } from "./request-utils" import {fetch_ } from "../request-utils"
import {useConfig} from "../ConfigProvider"; import {useConfig} from "../../ConfigProvider";
import {useMutation, useQuery, useQueryClient} from "react-query"; import {useMutation, useQuery, useQueryClient} from "react-query";
export const useWebhooks = () =>{ export const useWebhooks = () =>{
@@ -10,8 +10,11 @@ export const useWebhooks = () =>{
() => fetch_(`${config.BACKEND_HOST}/api/webhook/`), () => fetch_(`${config.BACKEND_HOST}/api/webhook/`),
{ {
onSuccess: (data) => { onSuccess: (data) => {
if(data) if(data){
queryClient.setQueryData("webhooks", data); for(const webhook of data){
queryClient.setQueryData(["webhook", data.id], data);
}
}
} }
} }
); );
@@ -45,7 +48,8 @@ export const useUpdateWebhook = () =>{
body: JSON.stringify(data) body: JSON.stringify(data)
}), }),
{ {
onSuccess: () => { onSuccess: (res) => {
queryClient.invalidateQueries(["webhook", res.id]);
queryClient.invalidateQueries("webhooks"); queryClient.invalidateQueries("webhooks");
} }
} }
@@ -60,7 +64,8 @@ export const useDeleteWebhook = () => {
method: "DELETE", method: "DELETE",
}), }),
{ {
onSuccess: () => { onSuccess: (res, variables) => {
queryClient.invalidateQueries(["webhook", variables.id]);
queryClient.invalidateQueries("webhooks"); queryClient.invalidateQueries("webhooks");
} }
} }
@@ -72,7 +77,7 @@ export const useWebhookSettings = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useQuery( return useQuery(
"webhook_setting", "webhook_setting",
() => fetch_(`${config.BACKEND_HOST}/api/webhook/setting/`), () => fetch_(`${config.BACKEND_HOST}/api/setting/webhook/`),
{ {
onSuccess: (data) => { onSuccess: (data) => {
if(data){ if(data){
@@ -89,7 +94,7 @@ export const useWebhookSetting = (setting_id) => {
const config = useConfig(); const config = useConfig();
return useQuery( return useQuery(
["webhook_setting", setting_id], ["webhook_setting", setting_id],
() => fetch_(`${config.BACKEND_HOST}/api/webhook/setting/${setting_id}`), () => fetch_(`${config.BACKEND_HOST}/api/setting/webhook/${setting_id}`),
{ {
enabled: !!setting_id, enabled: !!setting_id,
}); });
@@ -100,7 +105,7 @@ export const useCreateWebhookSetting = () => {
const config = useConfig(); const config = useConfig();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation( return useMutation(
(data) => fetch_(`${config.BACKEND_HOST}/api/webhook/setting/`, { (data) => fetch_(`${config.BACKEND_HOST}/api/setting/webhook/`, {
method: "POST", method: "POST",
body: JSON.stringify(data) body: JSON.stringify(data)
}),{ }),{
@@ -116,7 +121,7 @@ export const useUpdateWebhookSetting = () => {
const config = useConfig(); const config = useConfig();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation( return useMutation(
({id, data}) => fetch_(`${config.BACKEND_HOST}/api/webhook/setting/${id}`, { ({id, data}) => fetch_(`${config.BACKEND_HOST}/api/setting/webhook/${id}`, {
method: "PATCH", method: "PATCH",
body: JSON.stringify(data) body: JSON.stringify(data)
}),{ }),{
@@ -133,7 +138,7 @@ export const useDeleteWebhookSetting = () => {
const config = useConfig(); const config = useConfig();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation( return useMutation(
(id) => fetch_(`${config.BACKEND_HOST}/api/webhook/setting/${id}`, { (id) => fetch_(`${config.BACKEND_HOST}/api/setting/webhook/${id}`, {
method: "DELETE", method: "DELETE",
}), }),
{ {

View File

@@ -1,6 +1,6 @@
const path = require('path'); const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
module.exports = { module.exports = {
entry: './src/index.js', entry: './src/index.js',
output: { output: {
@@ -29,6 +29,10 @@ module.exports = {
template: "./public/index.html", template: "./public/index.html",
inject: true inject: true
}), }),
new webpack.ProvidePlugin({
process: 'process/browser'
})
], ],
devServer: { devServer: {
static: path.join(__dirname, 'public'), static: path.join(__dirname, 'public'),
@@ -36,5 +40,14 @@ module.exports = {
open: true, open: true,
hot: true, hot: true,
historyApiFallback: true, historyApiFallback: true,
} },
resolve: {
fallback: {
path: require.resolve('path-browserify'),
fs: false,
assert: require.resolve("assert/"),
process: require.resolve("process/browser"),
}
},
devtool: 'source-map',
}; };