Compare commits
18 Commits
76b298ac8b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c9310250e4 | |||
| a08164e914 | |||
| 30a46d5064 | |||
| e5affe3465 | |||
| 101666d26d | |||
| 87b4246a9b | |||
| 1ce2eebbfa | |||
| 9ea44385ee | |||
| c20cb168ff | |||
| 137ea649f8 | |||
| 947b59e3ea | |||
| 09338a2683 | |||
| dc0ff3b406 | |||
| 2c330904e4 | |||
| 8abf54eade | |||
| dd1ee9fd5c | |||
| 2911f8722e | |||
| 39a69ca5b8 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
summerizer.py
|
summerizer.py
|
||||||
|
node_modules
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ FRONTEND_HOST="${FRONTEND_HOST:-http://localhost:80}"
|
|||||||
KC_CLIENT_ID="${KC_CLIENT_ID:-labdev}"
|
KC_CLIENT_ID="${KC_CLIENT_ID:-labdev}"
|
||||||
KC_HOST="${KC_HOST:-https://login.hangman-lab.top}"
|
KC_HOST="${KC_HOST:-https://login.hangman-lab.top}"
|
||||||
KC_REALM="${KC_REALM:-Hangman-Lab}"
|
KC_REALM="${KC_REALM:-Hangman-Lab}"
|
||||||
|
DEBUG="${DEBUG:false}"
|
||||||
rm -f /usr/share/nginx/html/config.js
|
rm -f /usr/share/nginx/html/config.js
|
||||||
|
|
||||||
|
|
||||||
@@ -24,7 +24,8 @@ cat <<EOL > /usr/share/nginx/html/config.json
|
|||||||
"scope": "openid profile email roles",
|
"scope": "openid profile email roles",
|
||||||
"popup_redirect_uri": "${FRONTEND_HOST}/popup_callback",
|
"popup_redirect_uri": "${FRONTEND_HOST}/popup_callback",
|
||||||
"silent_redirect_uri": "${FRONTEND_HOST}/silent_callback"
|
"silent_redirect_uri": "${FRONTEND_HOST}/silent_callback"
|
||||||
}
|
},
|
||||||
|
"DEBUG": ${DEBUG}
|
||||||
}
|
}
|
||||||
EOL
|
EOL
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18-alpine AS build-stage
|
FROM node:20-alpine AS build-stage
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
577
package-lock.json
generated
577
package-lock.json
generated
@@ -9,16 +9,24 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^2.7.0",
|
||||||
|
"@tanstack/react-query": "^5.75.5",
|
||||||
|
"@tanstack/react-query-devtools": "^5.75.5",
|
||||||
|
"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",
|
||||||
|
"prismjs": "^1.30.0",
|
||||||
|
"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",
|
||||||
"react-query": "^3.39.3",
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^7.0.1",
|
"react-router-dom": "^7.0.1",
|
||||||
"react-syntax-highlighter": "^15.6.1",
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
|
"redux": "^5.0.1",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
@@ -445,25 +453,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 +1796,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 +1807,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 +1862,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",
|
||||||
@@ -2645,6 +2653,31 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-XVwolG6eTqwV0N8z/oDlN93ITCIGIop6leXlGJI/4EKy+0POYkR+ABHRSdGXY+0MQvJBP8yAzh+EYFxTuvmBiQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"@standard-schema/utils": "^0.3.0",
|
||||||
|
"immer": "^10.0.3",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-thunk": "^3.1.0",
|
||||||
|
"reselect": "^5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||||
|
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@sinclair/typebox": {
|
"node_modules/@sinclair/typebox": {
|
||||||
"version": "0.27.8",
|
"version": "0.27.8",
|
||||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
||||||
@@ -2669,6 +2702,69 @@
|
|||||||
"@sinonjs/commons": "^3.0.0"
|
"@sinonjs/commons": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="
|
||||||
|
},
|
||||||
|
"node_modules/@standard-schema/utils": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "5.75.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.75.5.tgz",
|
||||||
|
"integrity": "sha512-kPDOxtoMn2Ycycb76Givx2fi+2pzo98F9ifHL/NFiahEDpDwSVW6o12PRuQ0lQnBOunhRG5etatAhQij91M3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/query-devtools": {
|
||||||
|
"version": "5.74.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.74.7.tgz",
|
||||||
|
"integrity": "sha512-nSNlfuGdnHf4yB0S+BoNYOE1o3oAH093weAYZolIHfS2stulyA/gWfSk/9H4ZFk5mAAHb5vNqAeJOmbdcGPEQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "5.75.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.75.5.tgz",
|
||||||
|
"integrity": "sha512-QrLCJe40BgBVlWdAdf2ZEVJ0cISOuEy/HKupId1aTKU6gPJZVhSvZpH+Si7csRflCJphzlQ77Yx6gUxGW9o0XQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.75.5"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query-devtools": {
|
||||||
|
"version": "5.75.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.75.5.tgz",
|
||||||
|
"integrity": "sha512-S31U00nJOQIbxydRH1kOwdLRaLBrda8O5QjzmgkRg60UZzPGdbI6+873Qa0YGUfPeILDbR2ukgWyg7CJQPy4iA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-devtools": "5.74.7"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tanstack/react-query": "^5.75.5",
|
||||||
|
"react": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@testing-library/dom": {
|
"node_modules/@testing-library/dom": {
|
||||||
"version": "10.4.0",
|
"version": "10.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
|
||||||
@@ -2875,11 +2971,6 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/cookie": {
|
|
||||||
"version": "0.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
|
||||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
|
|
||||||
},
|
|
||||||
"node_modules/@types/debug": {
|
"node_modules/@types/debug": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||||
@@ -3168,6 +3259,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="
|
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
|
||||||
|
},
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.5.13",
|
"version": "8.5.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
|
||||||
@@ -3649,6 +3745,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 +3777,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",
|
||||||
@@ -3922,7 +4030,8 @@
|
|||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/batch": {
|
"node_modules/batch": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
@@ -3930,14 +4039,6 @@
|
|||||||
"integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==",
|
"integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/big-integer": {
|
|
||||||
"version": "1.6.52",
|
|
||||||
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
|
|
||||||
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
@@ -3994,6 +4095,7 @@
|
|||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
@@ -4011,21 +4113,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/broadcast-channel": {
|
|
||||||
"version": "3.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz",
|
|
||||||
"integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.7.2",
|
|
||||||
"detect-node": "^2.1.0",
|
|
||||||
"js-sha3": "0.8.0",
|
|
||||||
"microseconds": "0.2.0",
|
|
||||||
"nano-time": "1.0.0",
|
|
||||||
"oblivious-set": "1.0.0",
|
|
||||||
"rimraf": "3.0.2",
|
|
||||||
"unload": "2.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.24.2",
|
"version": "4.24.2",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz",
|
||||||
@@ -4103,15 +4190,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"
|
||||||
@@ -4449,7 +4562,8 @@
|
|||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/connect-history-api-fallback": {
|
"node_modules/connect-history-api-fallback": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -4807,6 +4921,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",
|
||||||
@@ -4867,7 +4997,8 @@
|
|||||||
"node_modules/detect-node": {
|
"node_modules/detect-node": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||||
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="
|
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/devlop": {
|
"node_modules/devlop": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
@@ -5029,6 +5160,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 +5256,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 +5277,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",
|
||||||
@@ -5713,7 +5865,8 @@
|
|||||||
"node_modules/fs.realpath": {
|
"node_modules/fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
@@ -5756,15 +5909,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 +5940,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",
|
||||||
@@ -5799,6 +5969,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fs.realpath": "^1.0.0",
|
"fs.realpath": "^1.0.0",
|
||||||
"inflight": "^1.0.4",
|
"inflight": "^1.0.4",
|
||||||
@@ -5842,12 +6013,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 +6061,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",
|
||||||
@@ -6516,9 +6670,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/http-proxy-middleware": {
|
"node_modules/http-proxy-middleware": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
|
||||||
"integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
|
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/http-proxy": "^1.17.8",
|
"@types/http-proxy": "^1.17.8",
|
||||||
@@ -6629,6 +6783,15 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "10.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
|
||||||
|
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/immutable": {
|
"node_modules/immutable": {
|
||||||
"version": "5.0.3",
|
"version": "5.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
|
||||||
@@ -6677,6 +6840,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||||
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"once": "^1.3.0",
|
"once": "^1.3.0",
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
@@ -6918,6 +7082,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",
|
||||||
@@ -8131,11 +8310,6 @@
|
|||||||
"node": ">= 10.13.0"
|
"node": ">= 10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/js-sha3": {
|
|
||||||
"version": "0.8.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
|
|
||||||
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
|
|
||||||
},
|
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -8244,9 +8418,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"
|
||||||
@@ -8448,13 +8622,12 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/match-sorter": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "6.3.4",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
"integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==",
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
"dependencies": {
|
"engines": {
|
||||||
"@babel/runtime": "^7.23.8",
|
"node": ">= 0.4"
|
||||||
"remove-accents": "0.5.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mdast-util-find-and-replace": {
|
"node_modules/mdast-util-find-and-replace": {
|
||||||
@@ -9380,11 +9553,6 @@
|
|||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/microseconds": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz",
|
|
||||||
"integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA=="
|
|
||||||
},
|
|
||||||
"node_modules/mime": {
|
"node_modules/mime": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||||
@@ -9444,6 +9612,7 @@
|
|||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
},
|
},
|
||||||
@@ -9470,14 +9639,6 @@
|
|||||||
"multicast-dns": "cli.js"
|
"multicast-dns": "cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nano-time": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==",
|
|
||||||
"dependencies": {
|
|
||||||
"big-integer": "^1.6.16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.8",
|
"version": "3.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||||
@@ -9643,10 +9804,47 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/oblivious-set": {
|
"node_modules/object-is": {
|
||||||
"version": "1.0.0",
|
"version": "1.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
|
||||||
"integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw=="
|
"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/obuf": {
|
"node_modules/obuf": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
@@ -9690,6 +9888,7 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
@@ -9873,6 +10072,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",
|
||||||
@@ -9886,6 +10090,7 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -10103,13 +10308,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",
|
||||||
@@ -10311,37 +10524,33 @@
|
|||||||
"react": ">=18"
|
"react": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-query": {
|
"node_modules/react-redux": {
|
||||||
"version": "3.39.3",
|
"version": "9.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
"integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==",
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
"broadcast-channel": "^3.4.1",
|
"use-sync-external-store": "^1.4.0"
|
||||||
"match-sorter": "^6.0.2"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/tannerlinsley"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
"@types/react": "^18.2.25 || ^19",
|
||||||
|
"react": "^18.0 || ^19",
|
||||||
|
"redux": "^5.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"react-dom": {
|
"@types/react": {
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"react-native": {
|
"redux": {
|
||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.0.1",
|
"version": "7.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.2.tgz",
|
||||||
"integrity": "sha512-WVAhv9oWCNsja5AkK6KLpXJDSJCQizOIyOd4vvB/+eHGbYx5vkhcmcmwWjQ9yqkRClogi+xjEg9fNEOd5EX/tw==",
|
"integrity": "sha512-9Rw8r199klMnlGZ8VAsV/I8WrIF6IyJ90JQUdboupx1cdkgYqwnrYjH+I/nY/7cA1X5zia4mDJqH36npP7sxGQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/cookie": "^0.6.0",
|
|
||||||
"cookie": "^1.0.1",
|
"cookie": "^1.0.1",
|
||||||
"set-cookie-parser": "^2.6.0",
|
"set-cookie-parser": "^2.6.0",
|
||||||
"turbo-stream": "2.4.0"
|
"turbo-stream": "2.4.0"
|
||||||
@@ -10360,11 +10569,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "7.0.1",
|
"version": "7.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.2.tgz",
|
||||||
"integrity": "sha512-duBzwAAiIabhFPZfDjcYpJ+f08TMbPMETgq254GWne2NW1ZwRHhZLj7tpSp8KGb7JvZzlLcjGUnqLxpZQVEPng==",
|
"integrity": "sha512-yk1XW8Fj7gK7flpYBXF3yzd2NbX6P7Kxjvs2b5nu1M04rb5pg/Zc4fGdBNTeT4eDYL2bvzWNyKaIMJX/RKHTTg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-router": "7.0.1"
|
"react-router": "7.5.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
@@ -10441,6 +10650,19 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
|
||||||
|
},
|
||||||
|
"node_modules/redux-thunk": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/refractor": {
|
"node_modules/refractor": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz",
|
||||||
@@ -10732,11 +10954,6 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/remove-accents": {
|
|
||||||
"version": "0.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
|
|
||||||
"integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A=="
|
|
||||||
},
|
|
||||||
"node_modules/renderkid": {
|
"node_modules/renderkid": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
|
||||||
@@ -10774,6 +10991,11 @@
|
|||||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.8",
|
"version": "1.22.8",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
||||||
@@ -10830,21 +11052,6 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rimraf": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
|
||||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
|
||||||
"dependencies": {
|
|
||||||
"glob": "^7.1.3"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"rimraf": "bin.js"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/run-applescript": {
|
"node_modules/run-applescript": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
|
||||||
@@ -11984,15 +12191,6 @@
|
|||||||
"node": ">= 4.0.0"
|
"node": ">= 4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unload": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.6.2",
|
|
||||||
"detect-node": "^2.0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
@@ -12051,6 +12249,14 @@
|
|||||||
"requires-port": "^1.0.0"
|
"requires-port": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util": {
|
"node_modules/util": {
|
||||||
"version": "0.12.5",
|
"version": "0.12.5",
|
||||||
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||||
@@ -12668,7 +12874,8 @@
|
|||||||
"node_modules/wrappy": {
|
"node_modules/wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/write-file-atomic": {
|
"node_modules/write-file-atomic": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -12,16 +12,24 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^2.7.0",
|
||||||
|
"@tanstack/react-query": "^5.75.5",
|
||||||
|
"@tanstack/react-query-devtools": "^5.75.5",
|
||||||
|
"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",
|
||||||
|
"prismjs": "^1.30.0",
|
||||||
|
"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",
|
||||||
"react-query": "^3.39.3",
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^7.0.1",
|
"react-router-dom": "^7.0.1",
|
||||||
"react-syntax-highlighter": "^15.6.1",
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
|
"redux": "^5.0.1",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
|
|||||||
20
src/App.js
20
src/App.js
@@ -1,20 +1,27 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {BrowserRouter as Router, Navigate, Route, Routes} from "react-router-dom";
|
import {BrowserRouter as Router, Navigate, Route, Routes} from "react-router-dom";
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { store } from './store';
|
||||||
import MainNavigation from "./components/Navigations/MainNavigation";
|
import MainNavigation from "./components/Navigations/MainNavigation";
|
||||||
import SideNavigation from "./components/Navigations/SideNavigation";
|
import SideNavigation from "./components/Navigations/SideNavigation";
|
||||||
import MarkdownContent from "./components/Markdowns/MarkdownContent";
|
import MarkdownContent from "./components/Markdowns/MarkdownContent";
|
||||||
import MarkdownEditor from "./components/Markdowns/MarkdownEditor";
|
import MarkdownEditor from "./components/Markdowns/MarkdownEditor";
|
||||||
|
import StandaloneMarkdownPage from "./components/Markdowns/StandaloneMarkdownPage";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import Callback from "./components/KeycloakCallbacks/Callback";
|
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 = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
<Router>
|
<Router>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/pg/*" element={<StandaloneMarkdownPage />} />
|
||||||
|
<Route path="*" element={
|
||||||
<div className="app-container">
|
<div className="app-container">
|
||||||
<MainNavigation />
|
<MainNavigation />
|
||||||
<div className="content-container">
|
<div className="content-container">
|
||||||
@@ -26,19 +33,24 @@ const App = () => {
|
|||||||
element={<Navigate to = "/markdown/1"/>}
|
element={<Navigate to = "/markdown/1"/>}
|
||||||
/>
|
/>
|
||||||
<Route path="/testx" element={<h2>test2</h2>}/>
|
<Route path="/testx" element={<h2>test2</h2>}/>
|
||||||
<Route path="/markdown/:id" element={<MarkdownContent />} />
|
<Route path="/markdown/:strId" element={<MarkdownContent />} />
|
||||||
<Route path="/callback" element={<Callback />} />
|
<Route path="/callback" element={<Callback />} />
|
||||||
<Route path="/test" element={<h1>TEST</h1>}></Route>
|
<Route path="/test" element={<h1>TEST</h1>}></Route>
|
||||||
<Route path="/markdown/create" element={<MarkdownEditor />} />
|
<Route path="/markdown/create" element={<MarkdownEditor />} />
|
||||||
<Route path="/markdown/edit/:id" element={<MarkdownEditor />} />
|
<Route path="/markdown/edit/:strId" 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/:strId" element={<MarkdownTemplateEditor />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
} />
|
||||||
|
</Routes>
|
||||||
<Footer />
|
<Footer />
|
||||||
</Router>
|
</Router>
|
||||||
|
</Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// src/AuthProvider.js
|
|
||||||
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
|
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { UserManager } from "oidc-client-ts";
|
import { UserManager } from "oidc-client-ts";
|
||||||
import { ConfigContext } from "./ConfigProvider";
|
import { ConfigContext } from "./ConfigProvider";
|
||||||
|
|||||||
@@ -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);
|
||||||
})
|
})
|
||||||
|
|||||||
14
src/components/Debug/ControlledReactQueryDevtools.js
Normal file
14
src/components/Debug/ControlledReactQueryDevtools.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {useConfig} from "../../ConfigProvider";
|
||||||
|
import {ReactQueryDevtools} from "@tanstack/react-query-devtools";
|
||||||
|
|
||||||
|
export const ControlledReactQueryDevtools = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
if(config.DEBUG)
|
||||||
|
return (<ReactQueryDevtools />);
|
||||||
|
|
||||||
|
return (<></>);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ControlledReactQueryDevtools;
|
||||||
@@ -32,7 +32,6 @@ const Footer = () => {
|
|||||||
<a href="https://git.hangman-lab.top/hzhang/HangmanLab">git</a>
|
<a href="https://git.hangman-lab.top/hzhang/HangmanLab">git</a>
|
||||||
|
|
||||||
|
|
||||||
v0.0.10
|
|
||||||
</span>
|
</span>
|
||||||
)}</p>
|
)}</p>
|
||||||
{
|
{
|
||||||
@@ -79,7 +78,6 @@ const Footer = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
65
src/components/MarkdownTemplate/EnumsEditor.js
Normal file
65
src/components/MarkdownTemplate/EnumsEditor.js
Normal 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;
|
||||||
18
src/components/MarkdownTemplate/LayoutEditor.js
Normal file
18
src/components/MarkdownTemplate/LayoutEditor.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
const LayoutEditor = ({layout, onChange}) => {
|
||||||
|
const [_layout, setLayout] = useState(layout || "");
|
||||||
|
useEffect(() => {setLayout(layout)}, [layout]);
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className="textarea"
|
||||||
|
style={{ height: "60vh" }}
|
||||||
|
value={_layout}
|
||||||
|
onChange={(e) => {
|
||||||
|
setLayout(e.target.value);
|
||||||
|
onChange(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LayoutEditor;
|
||||||
99
src/components/MarkdownTemplate/MarkdownTemplateEditor.js
Normal file
99
src/components/MarkdownTemplate/MarkdownTemplateEditor.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import React, {useContext, useEffect, useState} from "react";
|
||||||
|
import { AuthContext } from "../../AuthProvider";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { useMarkdownTemplate, useSaveMarkdownTemplate } from "../../utils/queries/markdown-template-queries";
|
||||||
|
import LayoutEditor from "./LayoutEditor";
|
||||||
|
import ParametersManager from "./ParametersManager";
|
||||||
|
import "bulma/css/bulma.min.css";
|
||||||
|
|
||||||
|
const MarkdownTemplateEditor = () => {
|
||||||
|
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { strId } = useParams();
|
||||||
|
const id = Number(strId);
|
||||||
|
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 || "");
|
||||||
|
const { roles } = useContext(AuthContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTitle(template?.title || "");
|
||||||
|
setParameters(template?.parameters || []);
|
||||||
|
setLayout(template?.layout || "");
|
||||||
|
}, [template]);
|
||||||
|
|
||||||
|
if (templateIsFetching) {
|
||||||
|
return <p>Loading...</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!roles.includes("admin") || roles.includes("creator"))
|
||||||
|
return <div className="notification is-danger">Permission Denied</div>;
|
||||||
|
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>
|
||||||
|
<ParametersManager
|
||||||
|
parameters={parameters}
|
||||||
|
onChange={(newParameters) => setParameters(newParameters)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</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;
|
||||||
121
src/components/MarkdownTemplate/ParametersManager.js
Normal file
121
src/components/MarkdownTemplate/ParametersManager.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import React, {useEffect, useState} from "react";
|
||||||
|
import TypeEditor from "./TypeEditor";
|
||||||
|
|
||||||
|
const ParametersManager = ({ parameters, onChange }) => {
|
||||||
|
const [_parameters, setParameters] = useState(parameters || []);
|
||||||
|
const [expandedStates, setExpandedStates] = useState({});
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
const updated = [
|
||||||
|
..._parameters,
|
||||||
|
{
|
||||||
|
name: "",
|
||||||
|
type: {
|
||||||
|
base_type: "string",
|
||||||
|
definition: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
setParameters(updated);
|
||||||
|
onChange(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTypeChange = (index, newType) => {
|
||||||
|
const updated = [..._parameters];
|
||||||
|
if (newType.base_type === "list" && !newType.extend_type) {
|
||||||
|
newType.extend_type = {
|
||||||
|
base_type: "string",
|
||||||
|
definition: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
updated[index].type = newType;
|
||||||
|
setParameters(updated);
|
||||||
|
onChange(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setParameters(parameters);
|
||||||
|
}, [parameters]);
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleExpand = (index) => {
|
||||||
|
setExpandedStates(prev => ({
|
||||||
|
...prev,
|
||||||
|
[index]: !prev[index]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="box">
|
||||||
|
<div className="field">
|
||||||
|
<div className="control">
|
||||||
|
<button className="button is-primary" onClick={handleAdd}>
|
||||||
|
Add Parameter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ maxHeight: "50vh", overflowY: "auto" }}>
|
||||||
|
{_parameters.map((param, index) => (
|
||||||
|
<div key={index} className="box" style={{ marginBottom: "0.5rem" }}>
|
||||||
|
<div className="field is-grouped is-align-items-end">
|
||||||
|
<div className="control">
|
||||||
|
<label className="label">Name:</label>
|
||||||
|
</div>
|
||||||
|
<div className="control is-expanded">
|
||||||
|
<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">
|
||||||
|
<div className="is-flex is-justify-content-space-between is-align-items-center mb-1">
|
||||||
|
<label className="label mb-0">Type:</label>
|
||||||
|
<button
|
||||||
|
className="button is-small"
|
||||||
|
onClick={() => toggleExpand(index)}
|
||||||
|
>
|
||||||
|
{expandedStates[index] ? "-" : "+"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{expandedStates[index] && (
|
||||||
|
<div className="control">
|
||||||
|
<TypeEditor
|
||||||
|
type={param.type}
|
||||||
|
onChange={(newType) => handleTypeChange(index, newType)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ParametersManager;
|
||||||
61
src/components/MarkdownTemplate/TemplateSelector.js
Normal file
61
src/components/MarkdownTemplate/TemplateSelector.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useMarkdownTemplates } from "../../utils/queries/markdown-template-queries";
|
||||||
|
|
||||||
|
const TemplateSelector = ({ template, onChange, onCreate }) => {
|
||||||
|
const { data: templates, isFetching: templatesAreFetching } = useMarkdownTemplates();
|
||||||
|
const [_template, setTemplate] = useState(
|
||||||
|
templates?.find((t) => t.id === template?.id) || {
|
||||||
|
title: "",
|
||||||
|
parameters: [],
|
||||||
|
layout: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTemplate(
|
||||||
|
templates?.find((t) => t.id === template?.id) || {
|
||||||
|
title: "",
|
||||||
|
parameters: [],
|
||||||
|
layout: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, [template, templates]);
|
||||||
|
|
||||||
|
if (templatesAreFetching) {
|
||||||
|
return <p>Loading...</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Select Template</label>
|
||||||
|
<div className="control">
|
||||||
|
<div className="select is-fullwidth is-primary">
|
||||||
|
<select
|
||||||
|
value={template?.id || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const id = parseInt(e.target.value, 10);
|
||||||
|
const selectedTemplate = templates.find((t) => t.id === id) || {
|
||||||
|
title: "",
|
||||||
|
parameters: [],
|
||||||
|
layout: "",
|
||||||
|
};
|
||||||
|
onChange(selectedTemplate);
|
||||||
|
if (onCreate) {
|
||||||
|
onCreate(selectedTemplate);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">(None)</option>
|
||||||
|
{templates.map((tmpl) => (
|
||||||
|
<option key={tmpl.id} value={tmpl.id}>
|
||||||
|
{tmpl.title}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TemplateSelector;
|
||||||
121
src/components/MarkdownTemplate/TypeEditor.js
Normal file
121
src/components/MarkdownTemplate/TypeEditor.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import EnumsEditor from './EnumsEditor';
|
||||||
|
import TemplateSelector from './TemplateSelector';
|
||||||
|
|
||||||
|
const TypeEditor = ({ type, onChange }) => {
|
||||||
|
const [_type, setType] = React.useState(type || {});
|
||||||
|
|
||||||
|
const updateType = (updated) => {
|
||||||
|
setType(updated);
|
||||||
|
onChange(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderExtraFields = () => {
|
||||||
|
switch (_type.base_type) {
|
||||||
|
case 'enum':
|
||||||
|
return (
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Enums</label>
|
||||||
|
<div className="control">
|
||||||
|
<EnumsEditor
|
||||||
|
enums={_type.definition.enums}
|
||||||
|
onChange={(newEnums) => {
|
||||||
|
updateType({
|
||||||
|
..._type,
|
||||||
|
definition: { ..._type.definition, enums: newEnums },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'list':
|
||||||
|
return (
|
||||||
|
<div className="box">
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Extend Type</label>
|
||||||
|
<div className="control">
|
||||||
|
<TypeEditor
|
||||||
|
type={_type.extend_type}
|
||||||
|
onChange={(extendType) => {
|
||||||
|
updateType({ ..._type, extend_type: extendType });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Iter Layout</label>
|
||||||
|
<div className="control">
|
||||||
|
<textarea
|
||||||
|
className="textarea"
|
||||||
|
value={_type.definition.iter_layout || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
updateType({
|
||||||
|
..._type,
|
||||||
|
definition: {
|
||||||
|
..._type.definition,
|
||||||
|
iter_layout: e.target.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'template':
|
||||||
|
return (
|
||||||
|
<div className="field">
|
||||||
|
<div className="control">
|
||||||
|
<TemplateSelector
|
||||||
|
template={_type.definition.template}
|
||||||
|
onChange={(newTemplate) => {
|
||||||
|
updateType({
|
||||||
|
..._type,
|
||||||
|
definition: {
|
||||||
|
..._type.definition,
|
||||||
|
template: newTemplate,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="box">
|
||||||
|
<div className="field">
|
||||||
|
{/*<label className="label">Type</label>*/}
|
||||||
|
<div className="control">
|
||||||
|
<div className="select is-fullwidth">
|
||||||
|
<select
|
||||||
|
value={_type.base_type || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updated = { base_type: e.target.value, definition: {} };
|
||||||
|
updateType(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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{renderExtraFields()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TypeEditor;
|
||||||
@@ -4,15 +4,23 @@ 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/markdown-setting-queries";
|
||||||
|
import {useMarkdownTemplate} from "../../utils/queries/markdown-template-queries";
|
||||||
|
import {useMarkdownTemplateSetting} from "../../utils/queries/markdown-template-setting-queries";
|
||||||
|
import MarkdownSettingModal from "../Modals/MarkdownSettingModal";
|
||||||
|
|
||||||
const MarkdownContent = () => {
|
const MarkdownContent = () => {
|
||||||
const { id } = useParams();
|
const { strId } = useParams();
|
||||||
|
const id = Number(strId);
|
||||||
const [indexTitle, setIndexTitle] = useState(null);
|
const [indexTitle, setIndexTitle] = useState(null);
|
||||||
|
const [isSettingModalOpen, setSettingModalOpen] = useState(false);
|
||||||
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: templateSetting, isFetching: isTemplateSettingFetching} = useMarkdownTemplateSetting(setting?.template_setting_id);
|
||||||
|
const {data: template, isFetching: isTemplateFetching} = useMarkdownTemplate(templateSetting?.template_id);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(markdown && markdown.title === "index" && path){
|
if(markdown && markdown.title === "index" && path){
|
||||||
@@ -21,26 +29,51 @@ const MarkdownContent = () => {
|
|||||||
}, [markdown, path]);
|
}, [markdown, path]);
|
||||||
|
|
||||||
|
|
||||||
if (isLoading || isPathFetching) {
|
const notReady = isLoading || isPathFetching || isSettingFetching || isTemplateSettingFetching || isTemplateFetching;
|
||||||
|
|
||||||
|
if (notReady) {
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <div>Error: {error.message || "Failed to load content"}</div>;
|
return <div>Error: {error.message || "Failed to load content"}</div>;
|
||||||
}
|
}
|
||||||
|
if (markdown.isMessage) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="markdown-content-container">
|
<div className="markdown-content-container">
|
||||||
<div className="field has-addons markdown-content-container-header">
|
<div className="notification is-info">
|
||||||
<h1 className="title control">{markdown.title === "index" ? indexTitle : markdown.title}</h1>
|
<h4 className="title is-4">{markdown.title}</h4>
|
||||||
|
<p>{markdown.content}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="markdown-content-container">
|
||||||
|
<div className="is-flex is-justify-content-space-between is-align-items-center markdown-content-container-header">
|
||||||
|
<h1 className="title">{markdown.title === "index" ? indexTitle : markdown.title}</h1>
|
||||||
<PermissionGuard rolesRequired={['admin']}>
|
<PermissionGuard rolesRequired={['admin']}>
|
||||||
|
<div className="field has-addons">
|
||||||
|
<button
|
||||||
|
className="control button is-info is-light"
|
||||||
|
onClick={() => setSettingModalOpen(true)}
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
<Link to={`/markdown/edit/${id}`} className="control button is-primary is-light">
|
<Link to={`/markdown/edit/${id}`} className="control button is-primary is-light">
|
||||||
Edit
|
Edit
|
||||||
</Link>
|
</Link>
|
||||||
|
</div>
|
||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
</div>
|
</div>
|
||||||
|
<MarkdownView content={JSON.parse(markdown.content)} template={template}/>
|
||||||
<MarkdownView content={markdown.content}/>
|
<MarkdownSettingModal
|
||||||
|
isOpen={isSettingModalOpen}
|
||||||
|
markdown={markdown}
|
||||||
|
onClose={() => setSettingModalOpen(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
.markdown-editor-container {
|
.markdown-editor-container {
|
||||||
max-width: 800px;
|
max-width: 90vw;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
@@ -93,3 +93,18 @@ pre {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
.raw-editor {
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre;
|
||||||
|
tab-size: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toggle-button {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-error {
|
||||||
|
color: red;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,61 +5,206 @@ 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, useUpdateMarkdownSetting} from "../../utils/queries/markdown-setting-queries";
|
||||||
|
import {useMarkdownTemplate, useMarkdownTemplates} from "../../utils/queries/markdown-template-queries";
|
||||||
|
import TemplatedEditor from "./TemplatedEditor";
|
||||||
|
import {useMarkdownTemplateSetting, useUpdateMarkdownTemplateSetting, useCreateMarkdownTemplateSetting} from "../../utils/queries/markdown-template-setting-queries";
|
||||||
|
import TemplateSelector from "../MarkdownTemplate/TemplateSelector";
|
||||||
|
import {useCreateMarkdownSetting} from "../../utils/queries/markdown-setting-queries";
|
||||||
|
|
||||||
const MarkdownEditor = () => {
|
const MarkdownEditor = () => {
|
||||||
const { roles } = useContext(AuthContext);
|
const { roles } = useContext(AuthContext);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { id } = useParams();
|
const { strId } = useParams();
|
||||||
|
const id = Number(strId);
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState({});
|
||||||
|
const [shortcut, setShortcut] = useState("");
|
||||||
const [pathId, setPathId] = useState(1);
|
const [pathId, setPathId] = useState(1);
|
||||||
const {data: markdown, isLoading, error} = useMarkdown(id);
|
const [isRawMode, setIsRawMode] = useState(false);
|
||||||
|
const [rawContent, setRawContent] = useState("");
|
||||||
|
const [jsonError, setJsonError] = useState("");
|
||||||
|
const [selectedTemplate, setSelectedTemplate] = useState(null);
|
||||||
|
const {data: markdown, isFetching: isMarkdownFetching, error} = useMarkdown(id);
|
||||||
const saveMarkdown = useSaveMarkdown();
|
const saveMarkdown = useSaveMarkdown();
|
||||||
|
const {data: setting, isFetching: isSettingFetching} = useMarkdownSetting(markdown?.setting_id);
|
||||||
|
const {data: templateSetting, isFetching: isTemplateSettingFetching} = useMarkdownTemplateSetting(setting?.template_setting_id);
|
||||||
|
const {data: template, isFetching: isTemplateFetching} = useMarkdownTemplate(templateSetting?.template_id);
|
||||||
|
const updateTemplateSetting = useUpdateMarkdownTemplateSetting();
|
||||||
|
const createTemplateSetting = useCreateMarkdownTemplateSetting();
|
||||||
|
const updateSetting = useUpdateMarkdownSetting();
|
||||||
|
const {data: templates, isFetching: templatesAreFetching} = useMarkdownTemplates();
|
||||||
|
const createMarkdownSetting = useCreateMarkdownSetting();
|
||||||
|
|
||||||
|
const notReady = isMarkdownFetching || isTemplateFetching || isSettingFetching || isTemplateSettingFetching || templatesAreFetching;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(markdown){
|
if (markdown) {
|
||||||
setTitle(markdown.title);
|
setTitle(markdown.title);
|
||||||
setContent(markdown.content);
|
if (markdown.isMessage) {
|
||||||
setPathId(markdown.path_id);
|
navigate("/");
|
||||||
|
alert(markdown.content || "Cannot edit this markdown");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, [markdown]);
|
try {
|
||||||
|
const parsedContent = JSON.parse(markdown.content);
|
||||||
|
setContent(parsedContent);
|
||||||
|
setRawContent(JSON.stringify(parsedContent, null, 2));
|
||||||
|
setShortcut(markdown.shortcut);
|
||||||
|
setPathId(markdown.path_id);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error parsing markdown content:", e);
|
||||||
|
alert("Error parsing markdown content");
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [markdown, navigate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (template) {
|
||||||
|
setSelectedTemplate(template);
|
||||||
|
}
|
||||||
|
}, [template]);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
|
if (isRawMode && jsonError) {
|
||||||
|
alert("Please fix the JSON errors before saving");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveData = {
|
||||||
|
title,
|
||||||
|
content: JSON.stringify(content),
|
||||||
|
path_id: pathId,
|
||||||
|
shortcut
|
||||||
|
};
|
||||||
|
console.log("markdown", markdown);
|
||||||
|
console.log(markdown?.id ? "update" : "create",)
|
||||||
|
if (!markdown?.id) {
|
||||||
saveMarkdown.mutate(
|
saveMarkdown.mutate(
|
||||||
{id, data: {title, content, path_id: pathId}},
|
{data: saveData},
|
||||||
{
|
{
|
||||||
|
onSuccess: (newMarkdown) => {
|
||||||
|
createMarkdownSetting.mutate({}, {
|
||||||
|
onSuccess: (settingRes) => {
|
||||||
|
saveMarkdown.mutate({
|
||||||
|
id: newMarkdown.id,
|
||||||
|
data: {
|
||||||
|
setting_id: settingRes.id
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
onSuccess: () => {
|
||||||
|
if (selectedTemplate?.id) {
|
||||||
|
createTemplateSetting.mutate({
|
||||||
|
template_id: selectedTemplate.id
|
||||||
|
}, {
|
||||||
|
onSuccess: (templateSettingRes) => {
|
||||||
|
updateSetting.mutate({
|
||||||
|
id: settingRes.id,
|
||||||
|
data: {
|
||||||
|
template_setting_id: templateSettingRes.id
|
||||||
|
}
|
||||||
|
}, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
navigate("/");
|
navigate("/");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
alert("Error saving markdown file");
|
alert("Error saving markdown file");
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log("try update");
|
||||||
|
saveMarkdown.mutate(
|
||||||
|
{id, data: saveData},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
navigate("/markdown/" + id);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
alert("Error saving markdown file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleEditMode = () => {
|
||||||
|
if (isRawMode) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(rawContent);
|
||||||
|
setContent(parsed);
|
||||||
|
setJsonError("");
|
||||||
|
setIsRawMode(false);
|
||||||
|
} catch (e) {
|
||||||
|
setJsonError("Invalid JSON: " + e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setRawContent(JSON.stringify(content, null, 2));
|
||||||
|
setIsRawMode(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRawContentChange = (e) => {
|
||||||
|
const newRawContent = e.target.value;
|
||||||
|
setRawContent(newRawContent);
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(newRawContent);
|
||||||
|
setContent(parsed);
|
||||||
|
setJsonError("");
|
||||||
|
} catch (e) {
|
||||||
|
setJsonError("Invalid JSON: " + e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTemplateChange = (newTemplate) => {
|
||||||
|
setSelectedTemplate(newTemplate);
|
||||||
|
if (templateSetting) {
|
||||||
|
updateTemplateSetting.mutate({
|
||||||
|
id: templateSetting.id,
|
||||||
|
data: {
|
||||||
|
template_id: newTemplate.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const hasPermission = roles.includes("admin") || roles.includes("creator");
|
const hasPermission = roles.includes("admin") || roles.includes("creator");
|
||||||
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) {
|
||||||
|
console.log("=============");
|
||||||
|
console.log("isMarkdownFetching", isMarkdownFetching );
|
||||||
|
console.log("isTemplateFetching", isTemplateFetching );
|
||||||
|
console.log("isSettingFetching", isSettingFetching );
|
||||||
|
console.log("isTemplateSettingFetching", isTemplateSettingFetching);
|
||||||
|
console.log( "TemplatesAreFetching", templatesAreFetching);
|
||||||
|
console.log("----------------");
|
||||||
return <p>Loading...</p>;
|
return <p>Loading...</p>;
|
||||||
|
}
|
||||||
|
|
||||||
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>
|
||||||
<div className="columns">
|
<div className="columns">
|
||||||
{/* Editor Column */}
|
|
||||||
<div className="column is-half">
|
<div className="column is-half">
|
||||||
<form>
|
<form>
|
||||||
{/* Title Field */}
|
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label className="label">Title</label>
|
<label className="label">Title</label>
|
||||||
<div className="control">
|
<div className="control">
|
||||||
@@ -73,7 +218,18 @@ const MarkdownEditor = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* PathManager Field */}
|
<div className="field">
|
||||||
|
<label className="label">Shortcut</label>
|
||||||
|
<div className="control">
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter shortcut"
|
||||||
|
value={shortcut}
|
||||||
|
onChange={(e) => setShortcut(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label className="label">Path</label>
|
<label className="label">Path</label>
|
||||||
<PathManager
|
<PathManager
|
||||||
@@ -82,21 +238,56 @@ const MarkdownEditor = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content Field */}
|
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label className="label">Content</label>
|
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<textarea
|
<TemplateSelector
|
||||||
style={{ height: "70vh" }}
|
template={selectedTemplate || template}
|
||||||
className="textarea"
|
onChange={handleTemplateChange}
|
||||||
placeholder="Enter Markdown content"
|
/>
|
||||||
value={content}
|
</div>
|
||||||
onChange={(e) => setContent(e.target.value)}
|
</div>
|
||||||
></textarea>
|
|
||||||
|
<div className="field">
|
||||||
|
<div className="is-flex is-justify-content-space-between is-align-items-center mb-2">
|
||||||
|
<label className="label mb-0">Content</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`button is-small editor-toggle-button ${isRawMode ? 'is-info' : 'is-light'}`}
|
||||||
|
onClick={toggleEditMode}
|
||||||
|
>
|
||||||
|
{isRawMode ? 'Switch to Template Editor' : 'Switch to Raw Editor'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="control">
|
||||||
|
{isRawMode ? (
|
||||||
|
<div>
|
||||||
|
<p className="help mb-2">
|
||||||
|
Edit the JSON directly. Make sure it's valid JSON before saving.
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
className={`textarea raw-editor ${jsonError ? 'is-danger' : ''}`}
|
||||||
|
style={{height: "70vh"}}
|
||||||
|
value={rawContent}
|
||||||
|
onChange={handleRawContentChange}
|
||||||
|
placeholder="Enter JSON content here"
|
||||||
|
/>
|
||||||
|
{jsonError && (
|
||||||
|
<p className="help is-danger json-error">{jsonError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<TemplatedEditor
|
||||||
|
style={{height: "40vh"}}
|
||||||
|
content={content}
|
||||||
|
template={!markdown?.id ? selectedTemplate : template}
|
||||||
|
onContentChanged={(k, v) => setContent(
|
||||||
|
prev => ({...prev, [k]: v})
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Save Button */}
|
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<button
|
<button
|
||||||
@@ -112,10 +303,13 @@ const MarkdownEditor = () => {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Preview Column */}
|
|
||||||
<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={!markdown?.id ? selectedTemplate : template}
|
||||||
|
height='70vh'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -93,3 +93,20 @@ pre {
|
|||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown-preview table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview th,
|
||||||
|
.markdown-preview td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview th {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,12 +8,71 @@ 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/queries/markdown-queries";
|
||||||
|
|
||||||
|
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)).join("");
|
||||||
|
}
|
||||||
|
if(variable.type.base_type === "template"){
|
||||||
|
return ParseTemplate({
|
||||||
|
template: variable.type.definition.template,
|
||||||
|
variables: value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const ParseTemplate = ({template, variables}) => {
|
||||||
|
if(!template || !Array.isArray(template.parameters)) return '';
|
||||||
|
const vars = variables || {};
|
||||||
|
let res = template.layout ?? '';
|
||||||
|
for (const parameter of template.parameters) {
|
||||||
|
res = res.replaceAll(`<${parameter.name}/>`, Translate({
|
||||||
|
variable: parameter,
|
||||||
|
value: vars[parameter.name]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const MarkdownView = ({ content, template, height="auto" }) => {
|
||||||
|
const {data: links, isLoading} = useLinks();
|
||||||
|
|
||||||
|
if (isLoading)
|
||||||
|
return (<p>Loading...</p>);
|
||||||
|
const linkDefinitions = "\n<!-- Definitions -->\n" + links.join("\n");
|
||||||
|
const _template = template || {
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: "markdown",
|
||||||
|
type: {
|
||||||
|
base_type: "markdown",
|
||||||
|
definition: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
layout: "<markdown/>",
|
||||||
|
title: "default"
|
||||||
|
};
|
||||||
|
|
||||||
const MarkdownView = ({ content, height="auto" }) => {
|
|
||||||
return (
|
return (
|
||||||
<div className="markdown-preview" style={{height}}>
|
<div className="markdown-preview" style={{height}}>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
children={content}
|
children={ParseTemplate({
|
||||||
|
template: _template,
|
||||||
|
variables: content
|
||||||
|
}) + "\n" + linkDefinitions}
|
||||||
remarkPlugins={[remarkMath, remarkGfm]}
|
remarkPlugins={[remarkMath, remarkGfm]}
|
||||||
rehypePlugins={[rehypeKatex, rehypeRaw]}
|
rehypePlugins={[rehypeKatex, rehypeRaw]}
|
||||||
components={{
|
components={{
|
||||||
|
|||||||
100
src/components/Markdowns/StandaloneMarkdownPage.js
Normal file
100
src/components/Markdowns/StandaloneMarkdownPage.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import "katex/dist/katex.min.css";
|
||||||
|
import "./MarkdownContent.css";
|
||||||
|
import MarkdownView from "./MarkdownView";
|
||||||
|
import { useMarkdown } from "../../utils/queries/markdown-queries";
|
||||||
|
import { useMarkdownSetting } from "../../utils/queries/markdown-setting-queries";
|
||||||
|
import { useMarkdownTemplate } from "../../utils/queries/markdown-template-queries";
|
||||||
|
import { useMarkdownTemplateSetting } from "../../utils/queries/markdown-template-setting-queries";
|
||||||
|
import { useTree } from "../../utils/queries/tree-queries";
|
||||||
|
import { getMarkdownIdByPath } from "../../utils/pathUtils";
|
||||||
|
|
||||||
|
const StandaloneMarkdownPage = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const [indexTitle, setIndexTitle] = useState(null);
|
||||||
|
const [markdownId, setMarkdownId] = useState(null);
|
||||||
|
|
||||||
|
// Extract path from /pg/project/index -> project/index
|
||||||
|
const pathString = location.pathname.replace(/^\/pg\//, '');
|
||||||
|
|
||||||
|
const { data: tree, isLoading: isTreeLoading } = useTree();
|
||||||
|
const { data: markdown, isLoading: isMarkdownLoading, error } = useMarkdown(markdownId);
|
||||||
|
const { data: setting, isFetching: isSettingFetching } = useMarkdownSetting(markdown?.setting_id);
|
||||||
|
const { data: templateSetting, isFetching: isTemplateSettingFetching } = useMarkdownTemplateSetting(setting?.template_setting_id);
|
||||||
|
const { data: template, isFetching: isTemplateFetching } = useMarkdownTemplate(templateSetting?.template_id);
|
||||||
|
|
||||||
|
// Resolve markdown ID from path using tree
|
||||||
|
useEffect(() => {
|
||||||
|
if (tree && pathString) {
|
||||||
|
const resolvedId = getMarkdownIdByPath(tree, pathString);
|
||||||
|
setMarkdownId(resolvedId);
|
||||||
|
}
|
||||||
|
}, [tree, pathString]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (markdown && markdown.title === "index" && pathString) {
|
||||||
|
const pathParts = pathString.split('/').filter(part => part.length > 0);
|
||||||
|
|
||||||
|
if (pathParts.length === 0) {
|
||||||
|
// Root index: /pg/ or /pg
|
||||||
|
setIndexTitle("Home");
|
||||||
|
} else {
|
||||||
|
// Directory index: /pg/Projects or /pg/Projects/project1
|
||||||
|
// Use the last directory name as title
|
||||||
|
const directoryName = pathParts[pathParts.length - 1];
|
||||||
|
setIndexTitle(directoryName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [markdown, pathString]);
|
||||||
|
|
||||||
|
const notReady = isTreeLoading || isMarkdownLoading || isSettingFetching || isTemplateSettingFetching || isTemplateFetching;
|
||||||
|
|
||||||
|
if (notReady) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: "2rem", textAlign: "center" }}>
|
||||||
|
<div>Loading...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: "2rem", textAlign: "center" }}>
|
||||||
|
<div>Error: {error.message || "Failed to load content"}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!notReady && !markdownId) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: "2rem", textAlign: "center" }}>
|
||||||
|
<div>Markdown not found for path: {pathString}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (markdown?.isMessage) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: "2rem" }}>
|
||||||
|
<div className="notification is-info">
|
||||||
|
<h4 className="title is-4">{markdown.title}</h4>
|
||||||
|
<p>{markdown.content}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: "2rem", maxWidth: "100%", margin: "0 auto" }}>
|
||||||
|
<div style={{ marginBottom: "2rem" }}>
|
||||||
|
<h1 className="title">{markdown?.title === "index" ? indexTitle : markdown?.title}</h1>
|
||||||
|
</div>
|
||||||
|
{markdown && (
|
||||||
|
<MarkdownView content={JSON.parse(markdown.content)} template={template} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StandaloneMarkdownPage;
|
||||||
189
src/components/Markdowns/TemplatedEditor.js
Normal file
189
src/components/Markdowns/TemplatedEditor.js
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import React, {useState} from "react";
|
||||||
|
import {useMarkdownTemplate} from "../../utils/queries/markdown-template-queries";
|
||||||
|
|
||||||
|
const TemplatedEditorComponent = ({ variable, value, namespace, onContentChanged }) => {
|
||||||
|
console.log("variable", variable);
|
||||||
|
const __namespace = `${variable.name}(${variable.type.base_type})`;
|
||||||
|
|
||||||
|
const renderField = () => {
|
||||||
|
switch (variable.type.base_type) {
|
||||||
|
case "string":
|
||||||
|
return (
|
||||||
|
<div className="box has-background-danger-soft">
|
||||||
|
<label className="label">{__namespace}</label>
|
||||||
|
<div className="control">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input"
|
||||||
|
value={value ?? ""}
|
||||||
|
onChange={(e) => onContentChanged(variable.name, e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "markdown":
|
||||||
|
return (
|
||||||
|
<div className="box has-background-primary-soft">
|
||||||
|
<label className="label">{__namespace}</label>
|
||||||
|
<div className="control">
|
||||||
|
<textarea
|
||||||
|
style={{maxHeight: "10vh"}}
|
||||||
|
className="textarea"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onContentChanged(variable.name, e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "enum":
|
||||||
|
return (
|
||||||
|
<div className="box has-background-info-soft">
|
||||||
|
<label className="label">{__namespace}</label>
|
||||||
|
<div className="control">
|
||||||
|
<div className="select is-fullwidth">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "list": {
|
||||||
|
const [cache, setCache] = useState(value || []);
|
||||||
|
const defaultValue = variable.type.definition.default;
|
||||||
|
|
||||||
|
const addItem = () => {
|
||||||
|
const newCache = [...cache, defaultValue];
|
||||||
|
setCache(newCache);
|
||||||
|
onContentChanged(variable.name, newCache);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeItem = (index) => {
|
||||||
|
const newCache = cache.filter((_, i) => i !== index);
|
||||||
|
setCache(newCache);
|
||||||
|
onContentChanged(variable.name, newCache);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onItemChange = (index, val) => {
|
||||||
|
const newCache = [...cache];
|
||||||
|
newCache[index] = val;
|
||||||
|
setCache(newCache);
|
||||||
|
onContentChanged(variable.name, newCache);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="box has-background-white-soft">
|
||||||
|
<label className="label">{__namespace}</label>
|
||||||
|
{cache.map((item, idx) => (
|
||||||
|
<div className="field is-grouped" key={idx}>
|
||||||
|
<div className="control is-expanded">
|
||||||
|
<TemplatedEditorComponent
|
||||||
|
variable={{ name: idx, type: variable.type.extend_type }}
|
||||||
|
value={item}
|
||||||
|
namespace={__namespace}
|
||||||
|
onContentChanged={(subKey, subVal) => onItemChange(idx, subVal)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="control">
|
||||||
|
<button
|
||||||
|
className="button is-danger"
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeItem(idx)}
|
||||||
|
>
|
||||||
|
DELETE
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="field">
|
||||||
|
<div className="control">
|
||||||
|
<button
|
||||||
|
className="button is-warning"
|
||||||
|
type="button"
|
||||||
|
onClick={addItem}
|
||||||
|
>
|
||||||
|
ADD
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "template": {
|
||||||
|
const { data: _template, isFetching: loading } = useMarkdownTemplate(variable.type.definition.template.id);
|
||||||
|
if (loading) return <p>Loading...</p>;
|
||||||
|
const _parameters = _template.parameters;
|
||||||
|
|
||||||
|
const handleSubChange = (key, val) => {
|
||||||
|
const updated = { ...(value || {}), [key]: val };
|
||||||
|
onContentChanged(variable.name, updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="box has-background-grey-light">
|
||||||
|
<label className="label">{__namespace}</label>
|
||||||
|
{_parameters.map((param, i) => (
|
||||||
|
<div className="field" key={i}>
|
||||||
|
<TemplatedEditorComponent
|
||||||
|
variable={param}
|
||||||
|
value={(value || {})[param.name]}
|
||||||
|
namespace={__namespace}
|
||||||
|
onContentChanged={handleSubChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <>{renderField()}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TemplatedEditor = ({ content, template, onContentChanged, style }) => {
|
||||||
|
const tpl = template || {
|
||||||
|
parameters: [{ name: "markdown", type: { base_type: "markdown", definition: {} } }],
|
||||||
|
layout: "<markdown/>",
|
||||||
|
title: "default",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="box" style={{
|
||||||
|
...style,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "hidden"
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
flex: 1,
|
||||||
|
overflowY: "auto",
|
||||||
|
padding: "1rem"
|
||||||
|
}}>
|
||||||
|
{tpl.parameters.map((variable, idx) => (
|
||||||
|
<TemplatedEditorComponent
|
||||||
|
key={idx}
|
||||||
|
variable={variable}
|
||||||
|
value={content[variable.name]}
|
||||||
|
namespace={tpl.title}
|
||||||
|
onContentChanged={onContentChanged}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TemplatedEditor;
|
||||||
164
src/components/Modals/ApiKeyCreationModal.js
Normal file
164
src/components/Modals/ApiKeyCreationModal.js
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useCreateApiKey } from '../../utils/queries/apikey-queries';
|
||||||
|
|
||||||
|
const AVAILABLE_ROLES = ['guest', 'creator', 'admin'];
|
||||||
|
|
||||||
|
const ApiKeyCreationModal = ({ isOpen, onClose }) => {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [roles, setRoles] = useState(['guest']);
|
||||||
|
const [generatedKey, setGeneratedKey] = useState(null);
|
||||||
|
const createApiKeyMutation = useCreateApiKey();
|
||||||
|
|
||||||
|
const handleAddRole = () => {
|
||||||
|
const availableRoles = AVAILABLE_ROLES.filter(role => !roles.includes(role));
|
||||||
|
if (availableRoles.length > 0) {
|
||||||
|
setRoles([...roles, availableRoles[0]]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRoleChange = (index, value) => {
|
||||||
|
if (roles.includes(value) && roles.findIndex(r => r === value) !== index) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newRoles = [...roles];
|
||||||
|
newRoles[index] = value;
|
||||||
|
setRoles(newRoles);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveRole = (index) => {
|
||||||
|
const newRoles = roles.filter((_, i) => i !== index);
|
||||||
|
setRoles(newRoles);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGenerate = async () => {
|
||||||
|
if (!name.trim()) {
|
||||||
|
alert('API key name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await createApiKeyMutation.mutateAsync({
|
||||||
|
name: name.trim(),
|
||||||
|
roles: roles
|
||||||
|
});
|
||||||
|
setGeneratedKey(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('failed to create api key', error);
|
||||||
|
alert('failed to create api key');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = () => {
|
||||||
|
navigator.clipboard.writeText(generatedKey)
|
||||||
|
.then(() => alert('API key copied to clipboard'))
|
||||||
|
.catch(err => console.error('failed to copy api key:', err));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRemainingRoles = (currentIndex) => {
|
||||||
|
return AVAILABLE_ROLES.filter(role =>
|
||||||
|
!roles.find((r, i) => r === role && i !== currentIndex)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal is-active">
|
||||||
|
<div className="modal-background" onClick={onClose}></div>
|
||||||
|
<div className="modal-card">
|
||||||
|
<header className="modal-card-head">
|
||||||
|
<p className="modal-card-title">Create API Key</p>
|
||||||
|
<button className="delete" aria-label="close" onClick={onClose}></button>
|
||||||
|
</header>
|
||||||
|
<section className="modal-card-body">
|
||||||
|
{!generatedKey ? (
|
||||||
|
<div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Name</label>
|
||||||
|
<div className="control">
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="text"
|
||||||
|
placeholder="API key name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Roles</label>
|
||||||
|
{roles.map((role, index) => (
|
||||||
|
<div key={index} className="field has-addons">
|
||||||
|
<div className="control">
|
||||||
|
<div className="select">
|
||||||
|
<select
|
||||||
|
value={role}
|
||||||
|
onChange={(e) => handleRoleChange(index, e.target.value)}
|
||||||
|
>
|
||||||
|
{getRemainingRoles(index).map(availableRole => (
|
||||||
|
<option key={availableRole} value={availableRole}>
|
||||||
|
{availableRole}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="control">
|
||||||
|
<button
|
||||||
|
className="button is-danger"
|
||||||
|
onClick={() => handleRemoveRole(index)}
|
||||||
|
disabled={roles.length === 1}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
className="button is-info mt-2"
|
||||||
|
onClick={handleAddRole}
|
||||||
|
disabled={roles.length === AVAILABLE_ROLES.length}
|
||||||
|
>
|
||||||
|
Add Role
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<div className="notification is-warning">
|
||||||
|
Please copy your API key immediately! It will only be displayed once!
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Your API Key:</label>
|
||||||
|
<div className="control">
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="text"
|
||||||
|
value={generatedKey.key}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button className="button is-info" onClick={handleCopy}>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
<footer className="modal-card-foot">
|
||||||
|
{!generatedKey && (
|
||||||
|
<button
|
||||||
|
className="button is-primary"
|
||||||
|
onClick={handleGenerate}
|
||||||
|
disabled={createApiKeyMutation.isLoading || !name.trim()}
|
||||||
|
>
|
||||||
|
Generate
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button className="button" onClick={onClose}>Close</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApiKeyCreationModal;
|
||||||
63
src/components/Modals/ApiKeyRevokeModal.js
Normal file
63
src/components/Modals/ApiKeyRevokeModal.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useRevokeApiKey } from '../../utils/queries/apikey-queries';
|
||||||
|
|
||||||
|
const ApiKeyRevokeModal = ({ isOpen, onClose }) => {
|
||||||
|
const [apiKey, setApiKey] = useState('');
|
||||||
|
const revokeApiKeyMutation = useRevokeApiKey();
|
||||||
|
|
||||||
|
const handleRevoke = async () => {
|
||||||
|
if (!apiKey.trim()) {
|
||||||
|
alert('Please enter an API key');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await revokeApiKeyMutation.mutateAsync(apiKey);
|
||||||
|
alert('API key revoked successfully');
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to revoke API key:', error);
|
||||||
|
alert('Failed to revoke API key');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal is-active">
|
||||||
|
<div className="modal-background" onClick={onClose}></div>
|
||||||
|
<div className="modal-card">
|
||||||
|
<header className="modal-card-head">
|
||||||
|
<p className="modal-card-title">Revoke API Key</p>
|
||||||
|
<button className="delete" aria-label="close" onClick={onClose}></button>
|
||||||
|
</header>
|
||||||
|
<section className="modal-card-body">
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">API Key</label>
|
||||||
|
<div className="control">
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter API key to revoke"
|
||||||
|
value={apiKey}
|
||||||
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<footer className="modal-card-foot">
|
||||||
|
<button
|
||||||
|
className="button is-danger"
|
||||||
|
onClick={handleRevoke}
|
||||||
|
disabled={revokeApiKeyMutation.isLoading}
|
||||||
|
>
|
||||||
|
Revoke
|
||||||
|
</button>
|
||||||
|
<button className="button" onClick={onClose}>Cancel</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApiKeyRevokeModal;
|
||||||
42
src/components/Modals/JsonSchemaModal.js
Normal file
42
src/components/Modals/JsonSchemaModal.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const JsonSchemaModal = ({ isActive, onClose, schema }) => {
|
||||||
|
const handleCopy = () => {
|
||||||
|
navigator.clipboard.writeText(JSON.stringify(schema, null, 2));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`modal ${isActive ? 'is-active' : ''}`}>
|
||||||
|
<div className="modal-background" onClick={onClose}></div>
|
||||||
|
<div className="modal-card">
|
||||||
|
<header className="modal-card-head">
|
||||||
|
<p className="modal-card-title">JSON Schema</p>
|
||||||
|
<button className="delete" aria-label="close" onClick={onClose}></button>
|
||||||
|
</header>
|
||||||
|
<section className="modal-card-body">
|
||||||
|
<div className="field">
|
||||||
|
<div className="control">
|
||||||
|
<textarea
|
||||||
|
className="textarea"
|
||||||
|
value={JSON.stringify(schema, null, 2)}
|
||||||
|
readOnly
|
||||||
|
style={{ height: "50vh" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<footer className="modal-card-foot">
|
||||||
|
<button className="button is-primary" onClick={handleCopy}>
|
||||||
|
<span className="icon">
|
||||||
|
<i className="fas fa-copy"></i>
|
||||||
|
</span>
|
||||||
|
<span>copy</span>
|
||||||
|
</button>
|
||||||
|
<button className="button" onClick={onClose}>close</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default JsonSchemaModal;
|
||||||
86
src/components/Modals/MarkdownSettingModal.js
Normal file
86
src/components/Modals/MarkdownSettingModal.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import {useCreateMarkdownSetting, useMarkdownSetting} from "../../utils/queries/markdown-setting-queries";
|
||||||
|
import {useSaveMarkdown} from "../../utils/queries/markdown-queries";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import MarkdownTemplateSettingPanel from "../Settings/MarkdownSettings/MarkdownTemplateSettingPanel";
|
||||||
|
import MarkdownPermissionSettingPanel from "../Settings/MarkdownSettings/MarkdownPermissionSettingPanel";
|
||||||
|
|
||||||
|
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">
|
||||||
|
<div className="tabs">
|
||||||
|
<ul>
|
||||||
|
<li className={activeTab==="template" ? "is-active" : ""}>
|
||||||
|
<a onClick={() => setActiveTab("template")}>Template</a>
|
||||||
|
</li>
|
||||||
|
<li className={activeTab==="permission" ? "is-active" : ""}>
|
||||||
|
<a onClick={() => setActiveTab("permission")}>Permission</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{activeTab === "template" && (
|
||||||
|
<MarkdownTemplateSettingPanel
|
||||||
|
markdownSetting={markdownSetting}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeTab === "permission" && (
|
||||||
|
<MarkdownPermissionSettingPanel
|
||||||
|
markdownSetting={markdownSetting}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
|
<section className="modal-card-body">
|
||||||
|
<button
|
||||||
|
className="button is-primary"
|
||||||
|
type="button"
|
||||||
|
onClick={handleCreateMarkdownSetting}
|
||||||
|
>
|
||||||
|
Create Markdown Setting
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
export default MarkdownSettingModal;
|
||||||
77
src/components/Modals/PathSettingModal.js
Normal file
77
src/components/Modals/PathSettingModal.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import {useUpdatePath} from "../../utils/queries/path-queries";
|
||||||
|
import {useCreatePathSetting, usePathSetting} from "../../utils/queries/path-setting-queries";
|
||||||
|
import WebhookSettingPanel from "../Settings/PathSettings/WebhookSettingPanel";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
const PathSettingModal = ({ isOpen, path, onClose }) => {
|
||||||
|
const settingId = path?.setting_id || 0;
|
||||||
|
const {data: pathSetting, isLoading: isPathSettingLoading} = usePathSetting(settingId);
|
||||||
|
const createPathSetting = useCreatePathSetting();
|
||||||
|
const updatePath = useUpdatePath();
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState("webhook");
|
||||||
|
const handleCreatePathSetting = () => {
|
||||||
|
createPathSetting.mutate({}, {
|
||||||
|
onSuccess: (res) => {
|
||||||
|
updatePath.mutate({id: path.id, data: {setting_id: res.id}});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if(settingId && isPathSettingLoading)
|
||||||
|
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">Path Settings</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="delete"
|
||||||
|
aria-label="close"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
{!pathSetting ? (
|
||||||
|
<section className="modal-card-body">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button is-primary"
|
||||||
|
onClick={handleCreatePathSetting}
|
||||||
|
>
|
||||||
|
Create Path Setting
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
|
<section className="modal-card-body">
|
||||||
|
<div className="tabs">
|
||||||
|
<ul>
|
||||||
|
<li className={activeTab === "webhook" ? "is-active" : ""}>
|
||||||
|
<a onClick={() => setActiveTab("webhook")}>Webhook</a>
|
||||||
|
</li>
|
||||||
|
<li className={activeTab === "template" ? "is-active" : ""}>
|
||||||
|
<a onClick={() => setActiveTab("template")}>Template</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{activeTab === "webhook" && (
|
||||||
|
<WebhookSettingPanel
|
||||||
|
pathSetting={pathSetting}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeTab === "template" && (
|
||||||
|
<div></div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PathSettingModal;
|
||||||
@@ -4,14 +4,59 @@ import { AuthContext } from "../../AuthProvider";
|
|||||||
import "bulma/css/bulma.min.css";
|
import "bulma/css/bulma.min.css";
|
||||||
import {useConfig} from "../../ConfigProvider";
|
import {useConfig} from "../../ConfigProvider";
|
||||||
import "./MainNavigation.css";
|
import "./MainNavigation.css";
|
||||||
|
import ApiKeyCreationModal from "../Modals/ApiKeyCreationModal";
|
||||||
|
import ApiKeyRevokeModal from "../Modals/ApiKeyRevokeModal";
|
||||||
|
|
||||||
const MainNavigation = () => {
|
const MainNavigation = () => {
|
||||||
const { user, login, logout } = useContext(AuthContext);
|
const { user, login, logout } = useContext(AuthContext);
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
|
const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false);
|
||||||
|
const [isRevokeModalOpen, setIsRevokeModalOpen] = useState(false);
|
||||||
|
|
||||||
if (config===undefined) {
|
if (config===undefined) {
|
||||||
return <div>Loading ...</div>;
|
return <div>Loading ...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleLoadBackup = async () => {
|
||||||
|
try{
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "file";
|
||||||
|
input.accept=".zip";
|
||||||
|
input.onchange = async (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if(!file)
|
||||||
|
return;
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
try{
|
||||||
|
const response = await fetch(
|
||||||
|
`${config.BACKEND_HOST}/api/backup/load`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if(response.ok){
|
||||||
|
const result = await response.json();
|
||||||
|
alert("Backup loaded");
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
alert(`failed to load ${error.error}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert("error when loading backup");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert(`Unexpected error`);
|
||||||
|
}
|
||||||
|
}
|
||||||
const handleGetBackup = async () => {
|
const handleGetBackup = async () => {
|
||||||
try{
|
try{
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
@@ -28,13 +73,9 @@ const MainNavigation = () => {
|
|||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
const contentDisposition = response.headers.get("Content-Disposition");
|
const contentDisposition = response.headers.get("Content-Disposition");
|
||||||
let filename = "backup.zip";
|
let filename = "backup.zip";
|
||||||
console.log(response.headers);
|
|
||||||
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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,6 +93,7 @@ const MainNavigation = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<nav className="navbar is-dark" role="navigation" aria-label="main navigation">
|
<nav className="navbar is-dark" role="navigation" aria-label="main navigation">
|
||||||
<div className="navbar-brand">
|
<div className="navbar-brand">
|
||||||
<Link className="navbar-item" to="/">
|
<Link className="navbar-item" to="/">
|
||||||
@@ -115,6 +157,27 @@ const MainNavigation = () => {
|
|||||||
>
|
>
|
||||||
Get Backup
|
Get Backup
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="button is-primary dropdown-option"
|
||||||
|
onClick={handleLoadBackup}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Load Backup
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="button is-info dropdown-option"
|
||||||
|
onClick={() => setIsApiKeyModalOpen(true)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Create API Key
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="button is-warning dropdown-option"
|
||||||
|
onClick={() => setIsRevokeModalOpen(true)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Revoke API Key
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className="button is-danger dropdown-option"
|
className="button is-danger dropdown-option"
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
@@ -122,9 +185,7 @@ const MainNavigation = () => {
|
|||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -141,6 +202,15 @@ const MainNavigation = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
<ApiKeyCreationModal
|
||||||
|
isOpen={isApiKeyModalOpen}
|
||||||
|
onClose={() => setIsApiKeyModalOpen(false)}
|
||||||
|
/>
|
||||||
|
<ApiKeyRevokeModal
|
||||||
|
isOpen={isRevokeModalOpen}
|
||||||
|
onClose={() => setIsRevokeModalOpen(false)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,30 @@
|
|||||||
import {Link} from "react-router-dom";
|
import {Link, useNavigate} from "react-router-dom";
|
||||||
import PermissionGuard from "../PermissionGuard";
|
import PermissionGuard from "../PermissionGuard";
|
||||||
import React from "react";
|
import React, {useState} from "react";
|
||||||
|
import MarkdownSettingModal from "../Modals/MarkdownSettingModal";
|
||||||
|
import {useDeleteMarkdown} from "../../utils/queries/markdown-queries";
|
||||||
|
import {useDeleteMarkdownSetting} from "../../utils/queries/markdown-setting-queries";
|
||||||
const MarkdownNode = ({markdown, handleMoveMarkdown}) => {
|
const MarkdownNode = ({markdown, handleMoveMarkdown}) => {
|
||||||
|
const [isMarkdownSettingModalOpen, setIsMarkdownSettingModalOpen] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const deleteMarkdown = useDeleteMarkdown();
|
||||||
|
const deleteMarkdownSetting = useDeleteMarkdownSetting();
|
||||||
|
|
||||||
|
const handleDeleteMarkdown = async () => {
|
||||||
|
if (!window.confirm(`delete markdown "${markdown.title}" ? this action cannot be undone.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteMarkdown.mutateAsync(markdown.id);
|
||||||
|
|
||||||
|
if (window.location.pathname === `/markdown/${markdown.id}`) {
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('failed: ' + (error.message || 'unknown error'));
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<li key={markdown.id}>
|
<li key={markdown.id}>
|
||||||
<div className="is-clickable field has-addons">
|
<div className="is-clickable field has-addons">
|
||||||
@@ -12,7 +34,33 @@ const MarkdownNode = ({markdown, handleMoveMarkdown}) => {
|
|||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
<PermissionGuard rolesRequired={['admin']}>
|
<PermissionGuard rolesRequired={['admin']}>
|
||||||
<div className="control">
|
<p className="control">
|
||||||
|
<button
|
||||||
|
className="button is-small is-success"
|
||||||
|
onClick={() => setIsMarkdownSettingModalOpen(true)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className="icon">
|
||||||
|
<i className="fas fa-cog"/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<p className="control">
|
||||||
|
<button
|
||||||
|
className="button is-small is-danger"
|
||||||
|
onClick={handleDeleteMarkdown}
|
||||||
|
type="button"
|
||||||
|
disabled={deleteMarkdown.isLoading || deleteMarkdownSetting.isLoading}
|
||||||
|
>
|
||||||
|
<span className="icon">
|
||||||
|
<i className="fas fa-trash"/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
className="control is-flex is-flex-direction-column is-align-items-center"
|
||||||
|
style={{marginLeft: "0.5rem"}}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
className="button is-small mb-1 move-forward"
|
className="button is-small mb-1 move-forward"
|
||||||
style={{height: "1rem", padding: "0.25rem"}}
|
style={{height: "1rem", padding: "0.25rem"}}
|
||||||
@@ -28,6 +76,11 @@ const MarkdownNode = ({markdown, handleMoveMarkdown}) => {
|
|||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<MarkdownSettingModal
|
||||||
|
isOpen={isMarkdownSettingModalOpen}
|
||||||
|
markdown={markdown}
|
||||||
|
onClose={() => setIsMarkdownSettingModalOpen(false)}
|
||||||
|
/>
|
||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,45 @@
|
|||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { toggleNodeExpansion } from '../../store/navigationSlice';
|
||||||
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, usePaths, useUpdatePath} from "../../utils/path-queries";
|
import {useDeletePath, useMovePath, useUpdatePath} from "../../utils/queries/path-queries";
|
||||||
import {useIndexMarkdown, useMarkdownsByPath, 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";
|
||||||
|
|
||||||
const PathNode = ({ path, isRoot = false }) => {
|
const PathNode = ({ path, isRoot = false }) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(isRoot);
|
const [isPathSettingModalOpen, setIsPathSettingModalOpen] = useState(false);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [newName, setNewName] = useState(path.name);
|
const [newName, setNewName] = useState(path.name || "");
|
||||||
|
|
||||||
|
const expandedNodes = useSelector(state => state.navigation.expandedNodes);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isExpanded = isRoot || expandedNodes[path.id];
|
||||||
|
|
||||||
const { data: childPaths, isLoading: isChildLoading, error: childError } = usePaths(path.id);
|
|
||||||
const { data: markdowns, isLoading: isMarkdownLoading, error: markdownError } = useMarkdownsByPath(path.id);
|
|
||||||
const deletePath = useDeletePath();
|
const deletePath = useDeletePath();
|
||||||
const updatePath = useUpdatePath();
|
const updatePath = useUpdatePath();
|
||||||
|
|
||||||
const {data: indexMarkdown, isLoading: isIndexLoading, error: indexMarkdownError} = useIndexMarkdown(path.id);
|
const {data: indexMarkdown} = useIndexMarkdown(path.id);
|
||||||
|
|
||||||
const movePath = useMovePath();
|
const movePath = useMovePath();
|
||||||
const moveMarkdown = useMoveMarkdown();
|
const moveMarkdown = useMoveMarkdown();
|
||||||
|
|
||||||
|
|
||||||
const expand = () => {
|
const expand = () => {
|
||||||
if(!isExpanded)
|
if (!isExpanded) {
|
||||||
setIsExpanded(true);
|
dispatch(toggleNodeExpansion(path.id));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleExpand = () => {
|
const toggleExpand = () => {
|
||||||
setIsExpanded(!isExpanded);
|
dispatch(toggleNodeExpansion(path.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
console.log(`handleSave ${path.id}`);
|
|
||||||
updatePath.mutate({id: path.id, data: {name: newName}}, {
|
updatePath.mutate({id: path.id, data: {name: newName}}, {
|
||||||
onsuccess: () => setIsEditing(false),
|
onSuccess: () => setIsEditing(false),
|
||||||
onError: err => alert("failed to update this path"),
|
onError: err => alert("failed to update this path"),
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@@ -60,20 +66,40 @@ const PathNode = ({ path, isRoot = false }) => {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const childPaths = path.children.filter(x => x.type==="path");
|
||||||
const sortedPaths = childPaths
|
const sortedPaths = childPaths
|
||||||
? childPaths.slice().sort((a, b) => a.order.localeCompare(b.order))
|
? childPaths.slice().sort((a, b) => a.order.localeCompare(b.order))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const markdowns = path.children.filter(x => x.type==="markdown");
|
||||||
const sortedMarkdowns = markdowns
|
const sortedMarkdowns = markdowns
|
||||||
? markdowns.filter(md => md.title !== "index").sort((a, b) => a.order.localeCompare(b.order))
|
? markdowns.filter(md => md.title !== "index").sort((a, b) => a.order.localeCompare(b.order))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
if(childError || markdownError){
|
|
||||||
return <li>Error...</li>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if(isRoot)
|
||||||
return (
|
return (
|
||||||
<li>
|
<ul className="menu-list">
|
||||||
|
{sortedPaths.map((path) => (
|
||||||
|
<PathNode
|
||||||
|
key={path.id}
|
||||||
|
path={path}
|
||||||
|
isRoot={false}
|
||||||
|
onSave={handleSave}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{sortedMarkdowns.filter(md => md.title !== "index").map((markdown) => (
|
||||||
|
<MarkdownNode
|
||||||
|
markdown={markdown}
|
||||||
|
handleMoveMarkdown={handleMoveMarkdown}
|
||||||
|
key={markdown.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<li key={path.id}>
|
||||||
<div className="path-node-header field has-addons">
|
<div className="path-node-header field has-addons">
|
||||||
<span className="control has-text-weight-bold path-toggle" onClick={isRoot ? undefined : toggleExpand}>
|
<span className="control has-text-weight-bold path-toggle" onClick={isRoot ? undefined : toggleExpand}>
|
||||||
{isExpanded ? "-" : "+"}
|
{isExpanded ? "-" : "+"}
|
||||||
@@ -107,6 +133,19 @@ const PathNode = ({ path, isRoot = false }) => {
|
|||||||
|
|
||||||
<PermissionGuard rolesRequired={["admin"]}>
|
<PermissionGuard rolesRequired={["admin"]}>
|
||||||
<div className="field has-addons actions control is-justify-content-flex-end">
|
<div className="field has-addons actions control is-justify-content-flex-end">
|
||||||
|
<p className="control">
|
||||||
|
<button
|
||||||
|
className="button is-small is-success"
|
||||||
|
onClick={() => {
|
||||||
|
setIsPathSettingModalOpen(true);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className="icon">
|
||||||
|
<i className="fas fa-cog"/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<p className="control">
|
<p className="control">
|
||||||
<button
|
<button
|
||||||
@@ -162,12 +201,16 @@ const PathNode = ({ path, isRoot = false }) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<PathSettingModal
|
||||||
|
isOpen={isPathSettingModalOpen}
|
||||||
|
path={path}
|
||||||
|
onClose={() => setIsPathSettingModalOpen(false)}
|
||||||
|
/>
|
||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<ul>
|
<ul>
|
||||||
{isChildLoading && <p>Loading...</p>}
|
|
||||||
{sortedPaths.map((child) => (
|
{sortedPaths.map((child) => (
|
||||||
<PathNode
|
<PathNode
|
||||||
key={child.id}
|
key={child.id}
|
||||||
@@ -179,6 +222,7 @@ const PathNode = ({ path, isRoot = false }) => {
|
|||||||
<MarkdownNode
|
<MarkdownNode
|
||||||
markdown={markdown}
|
markdown={markdown}
|
||||||
handleMoveMarkdown={handleMoveMarkdown}
|
handleMoveMarkdown={handleMoveMarkdown}
|
||||||
|
key={markdown.id}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
height: 90vh;
|
height: 90vh;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
min-width: 25vw;
|
min-width: 15vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-label {
|
.menu-label {
|
||||||
@@ -59,3 +59,24 @@
|
|||||||
.markdown-node {
|
.markdown-node {
|
||||||
background-color: #fffbe6 !important;
|
background-color: #fffbe6 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.tabs ul {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs li {
|
||||||
|
flex: 1;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav {
|
||||||
|
min-width: 15vw;
|
||||||
|
border-right: 1px solid #ddd;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,132 +1,53 @@
|
|||||||
import PermissionGuard from "../PermissionGuard";
|
import React, { useContext, useEffect } from 'react';
|
||||||
import PathNode from "./PathNode";
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { setSelectedTab } from '../../store/navigationSlice';
|
||||||
import "./SideNavigation.css";
|
import "./SideNavigation.css";
|
||||||
import {useDeletePath, usePaths, useUpdatePath} from "../../utils/path-queries";
|
import TreeTab from "./SideTabs/TreeTab";
|
||||||
import React from 'react';
|
import TemplateTab from "./SideTabs/TemplateTab";
|
||||||
import {useSearchMarkdown} from "../../utils/markdown-queries";
|
import { AuthContext } from "../../AuthProvider";
|
||||||
import MarkdownNode from "./MarkdownNode";
|
|
||||||
|
|
||||||
const SideNavigation = () => {
|
const SideNavigation = () => {
|
||||||
const {data: paths, isLoading, error } = usePaths(1);
|
const { roles } = useContext(AuthContext);
|
||||||
const deletePath = useDeletePath();
|
const selectedTab = useSelector(state => state.navigation.selectedTab);
|
||||||
const updatePath = useUpdatePath();
|
const dispatch = useDispatch();
|
||||||
const [searchTerm, setSearchTerm] = React.useState("");
|
|
||||||
const [keyword, setKeyword] = React.useState("");
|
|
||||||
const [searchMode, setSearchMode] = React.useState(false);
|
|
||||||
|
|
||||||
const {data: searchResults, isLoading: isSearching} = useSearchMarkdown(keyword, {
|
const allTabs = [
|
||||||
enabled: searchMode && !!searchMode,
|
{ id: "tree", label: "Tree", component: <TreeTab /> },
|
||||||
});
|
{ id: "templates", label: "Templates", component: <TemplateTab /> },
|
||||||
const sortedPaths = paths
|
];
|
||||||
? paths.slice().sort((a, b) => a.order.localeCompare(b.order))
|
|
||||||
: [];
|
|
||||||
const handleDelete = (id) => {
|
|
||||||
if (window.confirm("Are you sure you want to delete this path?")){
|
|
||||||
deletePath.mutate(id, {
|
|
||||||
onError: (err) => {
|
|
||||||
alert("Failed to delete path");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSearch = () => {
|
const visibleTabs = roles.includes("admin")
|
||||||
setSearchMode(true);
|
? allTabs
|
||||||
setKeyword(searchTerm);
|
: allTabs.filter(tab => tab.id === "tree");
|
||||||
}
|
|
||||||
const exitSearch = () => {
|
|
||||||
setSearchMode(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSave = (id, newName) => {
|
useEffect(() => {
|
||||||
updatePath.mutate({ id, data: {name: newName }} , {
|
if (!visibleTabs.find(tab => tab.id === selectedTab)) {
|
||||||
onError: (err) => {
|
dispatch(setSelectedTab(visibleTabs[0]?.id || ""));
|
||||||
alert("Failed to update path");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
if(!searchMode && isLoading){
|
|
||||||
return <aside className="menu"><p>Loading...</p></aside>;
|
|
||||||
}
|
|
||||||
if(searchMode && isSearching){
|
|
||||||
return <aside className="menu"><p>Loading...</p></aside>;
|
|
||||||
}
|
|
||||||
if(error){
|
|
||||||
return <aside className="menu"><p>Error...</p></aside>;
|
|
||||||
}
|
}
|
||||||
|
}, [visibleTabs, selectedTab, dispatch]);
|
||||||
|
|
||||||
|
|
||||||
|
const current = visibleTabs.find(t => t.id === selectedTab);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="menu">
|
<aside className="side-nav">
|
||||||
<div className="field has-addons mb-2">
|
<div className="tabs is-small">
|
||||||
<div className="control is-expanded">
|
|
||||||
<input
|
|
||||||
className="input is-small"
|
|
||||||
type="text"
|
|
||||||
placeholder="Search..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="control">
|
|
||||||
<button
|
|
||||||
className="button is-small is-info"
|
|
||||||
onClick={handleSearch}
|
|
||||||
disabled={!searchTerm.trim()}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span className="icon">
|
|
||||||
<i className="fa fa-search"></i>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{searchMode && (
|
|
||||||
<div className="control">
|
|
||||||
<button
|
|
||||||
className="button is-small is-danger"
|
|
||||||
onClick={exitSearch}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span className="icon">
|
|
||||||
<i className={"fa fa-window-close"}></i>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<PermissionGuard rolesRequired={["admin", "creator"]}>
|
|
||||||
<a
|
|
||||||
href="/markdown/create"
|
|
||||||
className="button is-primary is-small"
|
|
||||||
>
|
|
||||||
Create New Markdown
|
|
||||||
</a>
|
|
||||||
</PermissionGuard>
|
|
||||||
{searchMode ? (
|
|
||||||
<ul>
|
<ul>
|
||||||
{searchResults.map((markdown, i) => (
|
{visibleTabs.map(tab => (
|
||||||
<MarkdownNode
|
<li
|
||||||
markdown={markdown}
|
key={tab.id}
|
||||||
handleMoveMarkdown={() => {}}
|
className={tab.id === selectedTab ? "is-active" : ""}
|
||||||
/>
|
>
|
||||||
|
<a onClick={() => dispatch(setSelectedTab(tab.id))}>
|
||||||
|
{tab.label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
</div>
|
||||||
<ul className="menu-list">
|
<div className="tab-content">
|
||||||
{isLoading && <p>Loading...</p>}
|
{current?.component}
|
||||||
{sortedPaths.map((path) => (
|
</div>
|
||||||
<PathNode
|
|
||||||
key={path.id}
|
|
||||||
path={path}
|
|
||||||
isRoot={false}
|
|
||||||
onSave={handleSave}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
174
src/components/Navigations/SideTabs/TemplateTab.js
Normal file
174
src/components/Navigations/SideTabs/TemplateTab.js
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useMarkdownTemplates } from "../../../utils/queries/markdown-template-queries";
|
||||||
|
import PermissionGuard from "../../PermissionGuard";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import JsonSchemaModal from "../../Modals/JsonSchemaModal";
|
||||||
|
|
||||||
|
const TemplateTab = () => {
|
||||||
|
const { data: templates, isLoading, error } = useMarkdownTemplates();
|
||||||
|
const [keyword, setKeyword] = useState("");
|
||||||
|
const [selectedSchema, setSelectedSchema] = useState(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const filteredTemplates = templates?.filter(template =>
|
||||||
|
template.title.toLowerCase().includes(keyword.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTemplateClick = (templateId) => {
|
||||||
|
navigate(`/template/edit/${templateId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateJsonSchema = (template) => {
|
||||||
|
const schema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
$defs: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateTypeSchema = (param, defName) => {
|
||||||
|
switch (param.type.base_type) {
|
||||||
|
case "string":
|
||||||
|
return {
|
||||||
|
type: "string"
|
||||||
|
};
|
||||||
|
case "markdown":
|
||||||
|
return {
|
||||||
|
type: "string",
|
||||||
|
description: "Markdown content"
|
||||||
|
};
|
||||||
|
case "enum":
|
||||||
|
return {
|
||||||
|
type: "string",
|
||||||
|
enum: param.type.definition.enums
|
||||||
|
};
|
||||||
|
case "list":
|
||||||
|
if (param.type.extend_type.base_type === "string") {
|
||||||
|
return {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "string"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else if (param.type.extend_type.base_type === "list" ||
|
||||||
|
param.type.extend_type.base_type === "template") {
|
||||||
|
const itemsDefName = `${defName}_items`;
|
||||||
|
schema.$defs[itemsDefName] = generateTypeSchema(
|
||||||
|
{ type: param.type.extend_type },
|
||||||
|
itemsDefName
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
$ref: `#/$defs/${itemsDefName}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: "array",
|
||||||
|
items: generateTypeSchema(
|
||||||
|
{ type: param.type.extend_type },
|
||||||
|
`${defName}_items`
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "template":
|
||||||
|
const nestedTemplate = templates.find(t => t.id === param.type.definition.template.id);
|
||||||
|
if (nestedTemplate) {
|
||||||
|
const nestedSchema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {}
|
||||||
|
};
|
||||||
|
nestedTemplate.parameters.forEach(nestedParam => {
|
||||||
|
nestedSchema.properties[nestedParam.name] = generateTypeSchema(
|
||||||
|
nestedParam,
|
||||||
|
`${defName}_${nestedParam.name}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return nestedSchema;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: "object",
|
||||||
|
properties: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
type: "object",
|
||||||
|
properties: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template.parameters.forEach(param => {
|
||||||
|
const defName = `param_${param.name}`;
|
||||||
|
schema.properties[param.name] = generateTypeSchema(param, defName);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(schema.$defs).length === 0) {
|
||||||
|
delete schema.$defs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <p>Loading...</p>;
|
||||||
|
if (error) return <p>Error loading templates</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className="menu">
|
||||||
|
<div className="control is-expanded">
|
||||||
|
<input
|
||||||
|
className="input is-small"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search templates..."
|
||||||
|
onChange={(e) => setKeyword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<PermissionGuard rolesRequired={["admin", "creator"]}>
|
||||||
|
<a
|
||||||
|
href="/template/create"
|
||||||
|
className="button is-primary is-small is-fullwidth"
|
||||||
|
style={{ marginBottom: "10px" }}
|
||||||
|
>
|
||||||
|
Create New Template
|
||||||
|
</a>
|
||||||
|
</PermissionGuard>
|
||||||
|
<ul className="menu-list">
|
||||||
|
{filteredTemplates?.map((template) => (
|
||||||
|
<li key={template.id}>
|
||||||
|
<div className="is-flex is-justify-content-space-between is-align-items-center">
|
||||||
|
<span>{template.title}</span>
|
||||||
|
<div className="field has-addons is-justify-content-flex-end">
|
||||||
|
<button
|
||||||
|
className="button is-small control"
|
||||||
|
onClick={() => handleTemplateClick(template.id)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className="icon">
|
||||||
|
<i className="fas fa-edit"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="button is-small control"
|
||||||
|
onClick={() => setSelectedSchema(generateJsonSchema(template))}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className="icon">
|
||||||
|
<i className="fas fa-code"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<JsonSchemaModal
|
||||||
|
isActive={selectedSchema !== null}
|
||||||
|
onClose={() => setSelectedSchema(null)}
|
||||||
|
schema={selectedSchema}
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TemplateTab;
|
||||||
88
src/components/Navigations/SideTabs/TreeTab.js
Normal file
88
src/components/Navigations/SideTabs/TreeTab.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import PermissionGuard from "../../PermissionGuard";
|
||||||
|
import PathNode from "../PathNode";
|
||||||
|
import React from "react";
|
||||||
|
import {useTree} from "../../../utils/queries/tree-queries";
|
||||||
|
import {useDeletePath, useUpdatePath} from "../../../utils/queries/path-queries";
|
||||||
|
|
||||||
|
const TreeTab = () => {
|
||||||
|
const {data: tree, isLoading, error} = useTree();
|
||||||
|
const deletePath = useDeletePath();
|
||||||
|
const updatePath = useUpdatePath();
|
||||||
|
const [keyword, setKeyword] = React.useState("");
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
if (window.confirm("Are you sure you want to delete this path?")){
|
||||||
|
deletePath.mutate(id, {
|
||||||
|
onError: (err) => {
|
||||||
|
alert("Failed to delete path");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterTree = (t, k) => {
|
||||||
|
if(t === undefined)
|
||||||
|
return undefined;
|
||||||
|
if (t.type === "path") {
|
||||||
|
if (t.name.includes(k)) {
|
||||||
|
return { ...t };
|
||||||
|
}
|
||||||
|
const filteredChildren = (t.children || [])
|
||||||
|
.map(c => filterTree(c, k))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (filteredChildren.length > 0) {
|
||||||
|
return { ...t, children: filteredChildren };
|
||||||
|
}
|
||||||
|
} else if (t.type === "markdown") {
|
||||||
|
if (t.title.includes(k)) {
|
||||||
|
return { ...t };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredTree = filterTree(tree, keyword);
|
||||||
|
|
||||||
|
const handleSave = (id, newName) => {
|
||||||
|
updatePath.mutate({ id, data: {name: newName }} , {
|
||||||
|
onError: (err) => {
|
||||||
|
alert("Failed to update path");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (isLoading) return <p>Loading...</p>;
|
||||||
|
if (error) return <p>Error loading tree</p>;
|
||||||
|
return (
|
||||||
|
<aside className="menu">
|
||||||
|
|
||||||
|
<div className="control is-expanded">
|
||||||
|
<input
|
||||||
|
className="input is-small"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search..."
|
||||||
|
onChange={(e) => setKeyword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<PermissionGuard rolesRequired={["admin", "creator"]}>
|
||||||
|
<a
|
||||||
|
href="/markdown/create"
|
||||||
|
className="button is-primary is-small"
|
||||||
|
>
|
||||||
|
Create New Markdown
|
||||||
|
</a>
|
||||||
|
</PermissionGuard>
|
||||||
|
{!filteredTree || filteredTree.length === 0 ?
|
||||||
|
<p>No Result</p> :
|
||||||
|
<PathNode
|
||||||
|
key={1}
|
||||||
|
path={filteredTree}
|
||||||
|
isRoot={true}
|
||||||
|
onSave={handleSave}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TreeTab;
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
|
|
||||||
import React, {useEffect, useState, useRef, useContext} from "react";
|
import React, {useEffect, useState, useRef, useContext} from "react";
|
||||||
import { useCreatePath, usePaths } from "../utils/path-queries";
|
import {useCreatePath, usePaths} from "../utils/queries/path-queries";
|
||||||
import { useQueryClient } from "react-query";
|
import { useQueryClient } from "@tanstack/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";
|
||||||
|
|
||||||
|
|
||||||
const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
||||||
const [currentPath, setCurrentPath] = 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);
|
||||||
|
|
||||||
@@ -18,15 +18,16 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
|||||||
const createPath = useCreatePath();
|
const createPath = useCreatePath();
|
||||||
const config = useContext(ConfigContext).config;
|
const config = useContext(ConfigContext).config;
|
||||||
|
|
||||||
const buildPath = async (pathId) => {
|
|
||||||
|
const buildFullPath = async (pathId) => {
|
||||||
const path = [];
|
const path = [];
|
||||||
let current_id = pathId;
|
let current_id = pathId;
|
||||||
while (current_id) {
|
while (current_id) {
|
||||||
try {
|
try {
|
||||||
const pathData = await queryClient.fetchQuery(
|
const pathData = await queryClient.fetchQuery({
|
||||||
["path", current_id],
|
queryKey: ["path", current_id],
|
||||||
() => fetch_(`${config.BACKEND_HOST}/api/path/${current_id}`)
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/path/${current_id}`)
|
||||||
);
|
});
|
||||||
if (!pathData) break;
|
if (!pathData) break;
|
||||||
path.unshift({ name: pathData.name, id: pathData.id });
|
path.unshift({ name: pathData.name, id: pathData.id });
|
||||||
current_id = pathData.parent_id;
|
current_id = pathData.parent_id;
|
||||||
@@ -40,21 +41,21 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
const path = await buildPath(currentPathId);
|
const path = await buildFullPath(currentPathId);
|
||||||
setCurrentPath(path);
|
setCurrentFullPath(path);
|
||||||
};
|
};
|
||||||
init();
|
init();
|
||||||
}, [currentPathId, queryClient]);
|
}, [currentPathId, queryClient]);
|
||||||
|
|
||||||
const handlePathClick = (pathId, pathIndex) => {
|
const handlePathClick = (pathId, pathIndex) => {
|
||||||
const newPath = currentPath.slice(0, pathIndex + 1);
|
const newPath = currentFullPath.slice(0, pathIndex + 1);
|
||||||
setCurrentPath(newPath);
|
setCurrentFullPath(newPath);
|
||||||
onPathChange(pathId);
|
onPathChange(pathId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubPathSelect = (subPath) => {
|
const handleSubPathSelect = (subPath) => {
|
||||||
const updatedPath = [...currentPath, { name: subPath.name, id: subPath.id }];
|
const updatedPath = [...currentFullPath, { name: subPath.name, id: subPath.id }];
|
||||||
setCurrentPath(updatedPath);
|
setCurrentFullPath(updatedPath);
|
||||||
onPathChange(subPath.id);
|
onPathChange(subPath.id);
|
||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
setDropdownActive(false);
|
setDropdownActive(false);
|
||||||
@@ -70,8 +71,8 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
|||||||
{ name: searchTerm.trim(), parent_id: currentPathId },
|
{ name: searchTerm.trim(), parent_id: currentPathId },
|
||||||
{
|
{
|
||||||
onSuccess: (newDir) => {
|
onSuccess: (newDir) => {
|
||||||
queryClient.setQueryData(["path", newDir.id], newDir);
|
queryClient.setQueryData({queryKey: ["path", newDir.id]}, newDir);
|
||||||
queryClient.invalidateQueries(["paths", currentPathId]);
|
queryClient.invalidateQueries({queryKey: ["paths", currentPathId]});
|
||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
alert("Directory created successfully.");
|
alert("Directory created successfully.");
|
||||||
},
|
},
|
||||||
@@ -104,19 +105,21 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="path-manager">
|
<div className="path-manager">
|
||||||
<div className="path-manager-header">
|
<div className="path-manager-header field has-addons">
|
||||||
<div className="current-path">
|
<div className="current-path control">
|
||||||
{currentPath.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 < currentPath.length - 1 && " / "}
|
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="control">
|
||||||
|
<span> </span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="path-manager-body">
|
<div className="path-manager-body">
|
||||||
@@ -144,6 +147,7 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
|||||||
Create "{searchTerm}"
|
Create "{searchTerm}"
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{dropdownActive && (
|
{dropdownActive && (
|
||||||
<div className="dropdown is-active">
|
<div className="dropdown is-active">
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import {
|
||||||
|
useCreateMarkdownPermissionSetting,
|
||||||
|
useMarkdownPermissionSetting,
|
||||||
|
useUpdateMarkdownPermissionSetting
|
||||||
|
} from "../../../utils/queries/markdown-permission-setting-queries";
|
||||||
|
import React, {useEffect, useState} from "react";
|
||||||
|
import {useUpdateMarkdownSetting} from "../../../utils/queries/markdown-setting-queries";
|
||||||
|
|
||||||
|
const MarkdownPermissionSettingPanel = ({markdownSetting, onClose}) => {
|
||||||
|
const {data: setting, isFetching: settingIsFetching } = useMarkdownPermissionSetting(markdownSetting?.permission_setting_id);
|
||||||
|
const [permission, setPermission] = useState("");
|
||||||
|
|
||||||
|
const createMarkdownPermissionSetting = useCreateMarkdownPermissionSetting();
|
||||||
|
const updateMarkdownSetting = useUpdateMarkdownSetting();
|
||||||
|
const updateMarkdownPermissionSetting = useUpdateMarkdownPermissionSetting();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (setting && setting.permission !== undefined && setting.permission !== null) {
|
||||||
|
setPermission(setting.permission);
|
||||||
|
}
|
||||||
|
}, [setting]);
|
||||||
|
|
||||||
|
const handleCreatePermissionSetting = () => {
|
||||||
|
createMarkdownPermissionSetting.mutate({permission: null}, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
updateMarkdownSetting.mutate({
|
||||||
|
id: markdownSetting.id,
|
||||||
|
data: {
|
||||||
|
permission_setting_id: data.id,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveMarkdownPermissionSetting = () => {
|
||||||
|
const permissionValue = permission === "" ? null : permission;
|
||||||
|
|
||||||
|
updateMarkdownPermissionSetting.mutate({
|
||||||
|
id: setting.id,
|
||||||
|
data: {
|
||||||
|
permission: permissionValue,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
onSuccess: () => alert("Saved"),
|
||||||
|
onError: () => alert("Failed to save"),
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (settingIsFetching) {
|
||||||
|
return (<p>Loading...</p>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return setting ? (
|
||||||
|
<div className="box" style={{marginTop: "1rem"}}>
|
||||||
|
<h4 className="title is-5">Permission Setting</h4>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Permission</label>
|
||||||
|
<div className="select is-fullwidth">
|
||||||
|
<select
|
||||||
|
value={permission}
|
||||||
|
onChange={(e) => setPermission(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">(None)</option>
|
||||||
|
<option value="public">public</option>
|
||||||
|
<option value="protected">protected</option>
|
||||||
|
<option value="private">private</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="button is-primary"
|
||||||
|
type="button"
|
||||||
|
onClick={handleSaveMarkdownPermissionSetting}
|
||||||
|
>
|
||||||
|
Save Permission Setting
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="button is-primary"
|
||||||
|
type="button"
|
||||||
|
onClick={handleCreatePermissionSetting}
|
||||||
|
>
|
||||||
|
Create Permission Setting
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarkdownPermissionSettingPanel;
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import {
|
||||||
|
useMarkdownTemplate,
|
||||||
|
useMarkdownTemplates,
|
||||||
|
} from "../../../utils/queries/markdown-template-queries";
|
||||||
|
import {
|
||||||
|
useCreateMarkdownTemplateSetting,
|
||||||
|
useMarkdownTemplateSetting,
|
||||||
|
useUpdateMarkdownTemplateSetting
|
||||||
|
} from "../../../utils/queries/markdown-template-setting-queries";
|
||||||
|
import React, {useEffect, useState} from "react";
|
||||||
|
import {useUpdateMarkdownSetting} from "../../../utils/queries/markdown-setting-queries";
|
||||||
|
|
||||||
|
const MarkdownTemplateSettingPanel = ({markdownSetting, onClose}) => {
|
||||||
|
const {data: setting, isFetching: settingIsFetching } = useMarkdownTemplateSetting(markdownSetting?.template_setting_id);
|
||||||
|
const {data: templates, isFetching: templatesAreFetching}=useMarkdownTemplates();
|
||||||
|
const {data: template, isFetching: templateIsFetching} = useMarkdownTemplate(setting?.template_id);
|
||||||
|
const [selectedTemplateId, setSelectedTemplateId] = useState(template?.id ?? undefined);
|
||||||
|
|
||||||
|
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: selectedTemplateId,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
onSuccess: () => alert("Saved"),
|
||||||
|
onError: () => alert("Failed to save"),
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(template?.id && selectedTemplateId === undefined) {
|
||||||
|
setSelectedTemplateId(template?.id ?? undefined);
|
||||||
|
}
|
||||||
|
},[template, selectedTemplateId]);
|
||||||
|
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={selectedTemplateId}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedTemplateId(e.target.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">(default)</option>
|
||||||
|
{templates.map((_template, index) => (
|
||||||
|
<option key={index} value={_template.id}>{_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;
|
||||||
433
src/components/Settings/PathSettings/WebhookSettingPanel.js
Normal file
433
src/components/Settings/PathSettings/WebhookSettingPanel.js
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
useWebhookSetting,
|
||||||
|
useCreateWebhookSetting,
|
||||||
|
useUpdateWebhookSetting,
|
||||||
|
useWebhooks,
|
||||||
|
} from "../../../utils/queries/webhook-queries";
|
||||||
|
|
||||||
|
import {
|
||||||
|
useCreateWebhook,
|
||||||
|
useUpdateWebhook,
|
||||||
|
useDeleteWebhook,
|
||||||
|
} from "../../../utils/queries/webhook-queries";
|
||||||
|
import {useUpdatePathSetting} from "../../../utils/queries/path-setting-queries";
|
||||||
|
|
||||||
|
const WebhookSettingPanel = ({pathSetting, onClose}) => {
|
||||||
|
|
||||||
|
const {data: setting} = useWebhookSetting(pathSetting?.webhook_setting_id || 0);
|
||||||
|
const {data: webhooks, isLoading: isWebhooksLoading} = useWebhooks();
|
||||||
|
|
||||||
|
const createWebhookSetting = useCreateWebhookSetting();
|
||||||
|
const updateWebhookSetting = useUpdateWebhookSetting();
|
||||||
|
|
||||||
|
const createWebhook = useCreateWebhook();
|
||||||
|
const updateWebhook = useUpdateWebhook();
|
||||||
|
const deleteWebhook = useDeleteWebhook();
|
||||||
|
|
||||||
|
const updatePathSetting = useUpdatePathSetting();
|
||||||
|
|
||||||
|
|
||||||
|
const [enabled, setEnabled] = useState(false);
|
||||||
|
const [isRecursive, setIsRecursive] = useState(false);
|
||||||
|
const [onEvents, setOnEvents] = useState(0);
|
||||||
|
const [selectedUrl, setSelectedUrl] = useState("");
|
||||||
|
const [headerList, setHeaderList] = useState([]);
|
||||||
|
const [additionalHeaders, setAdditionalHeaders] = useState({});
|
||||||
|
|
||||||
|
const [isOnMarkdownCreated, setIsOnMarkdownCreated] = useState(false);
|
||||||
|
const [isOnMarkdownUpdated, setIsOnMarkdownUpdated] = useState(false);
|
||||||
|
const [isOnMarkdownDeleted, setIsOnMarkdownDeleted] = useState(false);
|
||||||
|
const [isOnPathCreated, setIsOnPathCreated] = useState(false);
|
||||||
|
const [isOnPathUpdated, setIsOnPathUpdated] = useState(false);
|
||||||
|
const [isOnPathDeleted, setIsOnPathDeleted] = useState(false);
|
||||||
|
|
||||||
|
const assignFromOnEvents = (bits) => {
|
||||||
|
setIsOnMarkdownCreated(!!(bits & 1));
|
||||||
|
setIsOnMarkdownUpdated(!!(bits & 2));
|
||||||
|
setIsOnMarkdownDeleted(!!(bits & 4));
|
||||||
|
setIsOnPathCreated(!!(bits & 8));
|
||||||
|
setIsOnPathUpdated(!!(bits & 16));
|
||||||
|
setIsOnPathDeleted(!!(bits & 32));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTriggerEventsUpdate = (eventType, isChecked) => {
|
||||||
|
setOnEvents((prev) => {
|
||||||
|
let nextVal = prev;
|
||||||
|
switch (eventType) {
|
||||||
|
case "MARKDOWN_CREATED":
|
||||||
|
nextVal = isChecked ? nextVal | 1 : nextVal & ~1;
|
||||||
|
setIsOnMarkdownCreated(isChecked);
|
||||||
|
break;
|
||||||
|
case "MARKDOWN_UPDATED":
|
||||||
|
nextVal = isChecked ? nextVal | 2 : nextVal & ~2;
|
||||||
|
setIsOnMarkdownUpdated(isChecked);
|
||||||
|
break;
|
||||||
|
case "MARKDOWN_DELETED":
|
||||||
|
nextVal = isChecked ? nextVal | 4 : nextVal & ~4;
|
||||||
|
setIsOnMarkdownDeleted(isChecked);
|
||||||
|
break;
|
||||||
|
case "PATH_CREATED":
|
||||||
|
nextVal = isChecked ? nextVal | 8 : nextVal & ~8;
|
||||||
|
setIsOnPathCreated(isChecked);
|
||||||
|
break;
|
||||||
|
case "PATH_UPDATED":
|
||||||
|
nextVal = isChecked ? nextVal | 16 : nextVal & ~16;
|
||||||
|
setIsOnPathUpdated(isChecked);
|
||||||
|
break;
|
||||||
|
case "PATH_DELETED":
|
||||||
|
nextVal = isChecked ? nextVal | 32 : nextVal & ~32;
|
||||||
|
setIsOnPathUpdated(isChecked);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return nextVal;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateWebhookSetting = () => {
|
||||||
|
createWebhookSetting.mutate({}, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
updatePathSetting.mutate({
|
||||||
|
id: pathSetting.id,
|
||||||
|
data: {
|
||||||
|
webhook_setting_id: data.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (setting && webhooks) {
|
||||||
|
setEnabled(setting.enabled);
|
||||||
|
setIsRecursive(setting.recursive);
|
||||||
|
setOnEvents(setting.on_events);
|
||||||
|
assignFromOnEvents(setting.on_events);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers = setting.additional_header
|
||||||
|
? JSON.parse(setting.additional_header)
|
||||||
|
: {};
|
||||||
|
setAdditionalHeaders(headers);
|
||||||
|
setHeaderList(
|
||||||
|
Object.entries(headers).map(([k, v]) => ({ key: k, value: v }))
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
setAdditionalHeaders({});
|
||||||
|
setHeaderList([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const found = webhooks.find((wh) => wh.id === setting.webhook_id);
|
||||||
|
setSelectedUrl(found ? found.hook_url : "");
|
||||||
|
} else {
|
||||||
|
setEnabled(false);
|
||||||
|
setIsRecursive(false);
|
||||||
|
setOnEvents(0);
|
||||||
|
assignFromOnEvents(0);
|
||||||
|
setAdditionalHeaders({});
|
||||||
|
setHeaderList([]);
|
||||||
|
setSelectedUrl("");
|
||||||
|
}
|
||||||
|
}, [setting, webhooks]);
|
||||||
|
|
||||||
|
const handleAddHeader = () => {
|
||||||
|
setHeaderList([...headerList, { key: "", value: "" }]);
|
||||||
|
};
|
||||||
|
const handleHeaderChange = (index, field, val) => {
|
||||||
|
const updated = [...headerList];
|
||||||
|
updated[index][field] = val;
|
||||||
|
setHeaderList(updated);
|
||||||
|
};
|
||||||
|
const handleApplyHeaders = () => {
|
||||||
|
const out = {};
|
||||||
|
headerList.forEach(({ key, value }) => {
|
||||||
|
if (key.trim()) out[key] = value;
|
||||||
|
});
|
||||||
|
setAdditionalHeaders(out);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateWebhook = () => {
|
||||||
|
const newUrl = prompt("Enter the new webhook URL");
|
||||||
|
if (!newUrl) return;
|
||||||
|
createWebhook.mutate(newUrl, {
|
||||||
|
onSuccess: () => alert("Created new Webhook successfully"),
|
||||||
|
onError: () => alert("Failed to create new Webhook"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleUpdateWebhook = () => {
|
||||||
|
if (!setting || !setting.webhook_id) {
|
||||||
|
alert("No webhook selected. Must pick from dropdown first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newUrl = prompt("Enter updated Webhook URL", selectedUrl);
|
||||||
|
if (!newUrl) return;
|
||||||
|
updateWebhook.mutate(
|
||||||
|
{ id: setting.webhook_id, data: { hook_url: newUrl } },
|
||||||
|
{
|
||||||
|
onSuccess: () => alert("Updated Webhook successfully"),
|
||||||
|
onError: () => alert("Failed to update Webhook"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const handleDeleteWebhook = () => {
|
||||||
|
if (!setting || !setting.webhook_id) {
|
||||||
|
alert("No webhook selected to delete");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!window.confirm("Are you sure?")) return;
|
||||||
|
deleteWebhook.mutate(setting.webhook_id, {
|
||||||
|
onSuccess: () => alert("Deleted Webhook successfully"),
|
||||||
|
onError: () => alert("Failed to delete Webhook"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveWebhookSetting = async () => {
|
||||||
|
const hook = webhooks.find((wh) => wh.hook_url === selectedUrl);
|
||||||
|
const payload = {
|
||||||
|
webhook_id: hook? hook.id : null,
|
||||||
|
recursive: isRecursive,
|
||||||
|
additional_header: JSON.stringify(additionalHeaders),
|
||||||
|
enabled,
|
||||||
|
on_events: onEvents,
|
||||||
|
|
||||||
|
};
|
||||||
|
if(!setting || !setting.id){
|
||||||
|
createWebhookSetting.mutate(payload, {
|
||||||
|
onSuccess: (res) => {
|
||||||
|
updatePathSetting.mutate({id: pathSetting.id, data: {webhook_setting_id: res.id}},{
|
||||||
|
onSuccess: () => alert("Webhook setting successfully created"),
|
||||||
|
onError: () => alert("Failed to save Webhook"),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: () => alert("Failed to save Webhook"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateWebhookSetting.mutate({id: setting.id, data: payload}, {
|
||||||
|
onSuccess: () => alert("Updated Webhook successfully"),
|
||||||
|
onError: () => alert("Failed to update Webhook"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return setting ? (
|
||||||
|
<div className="box" style={{ marginTop: "1rem" }}>
|
||||||
|
<h4 className="title is-5">Webhook Setting</h4>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Select or Create a Webhook</label>
|
||||||
|
<div className="field has-addons">
|
||||||
|
<div className="control is-expanded">
|
||||||
|
{isWebhooksLoading ? (
|
||||||
|
<p>Loading...</p>
|
||||||
|
) : (
|
||||||
|
<div className="select is-fullwidth">
|
||||||
|
<select
|
||||||
|
value={selectedUrl}
|
||||||
|
onChange={(e) => setSelectedUrl(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">(none)</option>
|
||||||
|
{webhooks.map((hook) => (
|
||||||
|
<option key={hook.id} value={hook.hook_url}>
|
||||||
|
{hook.hook_url}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="control">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button is-primary"
|
||||||
|
onClick={handleCreateWebhook}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{setting?.webhook_id && (
|
||||||
|
<div className="buttons" style={{ marginTop: "0.5rem" }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button is-info"
|
||||||
|
onClick={handleUpdateWebhook}
|
||||||
|
>
|
||||||
|
Update Webhook URL
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button is-danger"
|
||||||
|
onClick={handleDeleteWebhook}
|
||||||
|
>
|
||||||
|
Delete Webhook
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field">
|
||||||
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={enabled}
|
||||||
|
onChange={(e) => setEnabled(e.target.checked)}
|
||||||
|
/>
|
||||||
|
Enabled
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">On Events</label>
|
||||||
|
<div className="box">
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column">
|
||||||
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isOnMarkdownCreated}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleTriggerEventsUpdate("MARKDOWN_CREATED", e.target.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Markdown Created
|
||||||
|
</label>
|
||||||
|
<br />
|
||||||
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isOnMarkdownUpdated}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleTriggerEventsUpdate("MARKDOWN_UPDATED", e.target.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Markdown Updated
|
||||||
|
</label>
|
||||||
|
<br />
|
||||||
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isOnMarkdownDeleted}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleTriggerEventsUpdate("MARKDOWN_DELETED", e.target.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Markdown Deleted
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="column">
|
||||||
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isOnPathCreated}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleTriggerEventsUpdate("PATH_CREATED", e.target.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Path Created
|
||||||
|
</label>
|
||||||
|
<br />
|
||||||
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isOnPathUpdated}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleTriggerEventsUpdate("PATH_UPDATED", e.target.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Path Updated
|
||||||
|
</label>
|
||||||
|
<br />
|
||||||
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isOnPathDeleted}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleTriggerEventsUpdate("PATH_DELETED", e.target.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Path Deleted
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field">
|
||||||
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isRecursive}
|
||||||
|
onChange={(e) => setIsRecursive(e.target.checked)}
|
||||||
|
/>
|
||||||
|
Recursive
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Additional Headers</label>
|
||||||
|
<div className="box">
|
||||||
|
{headerList.map((h, idx) => (
|
||||||
|
<div className="columns" key={idx}>
|
||||||
|
<div className="column">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input"
|
||||||
|
placeholder="key"
|
||||||
|
value={h.key}
|
||||||
|
onChange={(e) => handleHeaderChange(idx, "key", e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="column">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input"
|
||||||
|
placeholder="value"
|
||||||
|
value={h.value}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleHeaderChange(idx, "value", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button is-small is-info"
|
||||||
|
onClick={handleAddHeader}
|
||||||
|
>
|
||||||
|
+ Header
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button is-small is-success"
|
||||||
|
onClick={handleApplyHeaders}
|
||||||
|
style={{ marginLeft: "0.5rem" }}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button is-primary"
|
||||||
|
onClick={handleSaveWebhookSetting}
|
||||||
|
>
|
||||||
|
Save Webhook Setting
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="button is-primary"
|
||||||
|
type="button"
|
||||||
|
onClick={handleCreateWebhookSetting}
|
||||||
|
>
|
||||||
|
Create Webhook Setting
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default WebhookSettingPanel
|
||||||
@@ -3,15 +3,16 @@ import ReactDOM from "react-dom/client";
|
|||||||
import App from "./App";
|
import App from "./App";
|
||||||
import AuthProvider, {AuthContext} from "./AuthProvider";
|
import AuthProvider, {AuthContext} from "./AuthProvider";
|
||||||
import "bulma/css/bulma.min.css";
|
import "bulma/css/bulma.min.css";
|
||||||
import {QueryClient, QueryClientProvider} from "react-query"
|
import {QueryClient, QueryClientProvider} from "@tanstack/react-query"
|
||||||
import ConfigProvider from "./ConfigProvider";
|
import ConfigProvider from "./ConfigProvider";
|
||||||
|
import ControlledReactQueryDevtools from "./components/Debug/ControlledReactQueryDevtools";
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
retry: 2,
|
retry: 2,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
staleTimeout: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (error.message === "Unauthorized"){
|
if (error.message === "Unauthorized"){
|
||||||
const {logout} = queryClient
|
const {logout} = queryClient
|
||||||
@@ -38,7 +39,7 @@ const EnhancedAuthProvider = ({children}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
queryClient.setQueryDefaults("auths", {
|
queryClient.setQueryDefaults(["auths"], {
|
||||||
context: {logout}
|
context: {logout}
|
||||||
});
|
});
|
||||||
}, [logout]);
|
}, [logout]);
|
||||||
@@ -52,6 +53,7 @@ root.render(
|
|||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<EnhancedAuthProvider>
|
<EnhancedAuthProvider>
|
||||||
<App />
|
<App />
|
||||||
|
<ControlledReactQueryDevtools />
|
||||||
</EnhancedAuthProvider>
|
</EnhancedAuthProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
8
src/store/index.js
Normal file
8
src/store/index.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
import navigationReducer from './navigationSlice';
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
navigation: navigationReducer,
|
||||||
|
},
|
||||||
|
});
|
||||||
24
src/store/navigationSlice.js
Normal file
24
src/store/navigationSlice.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
const navigationSlice = createSlice({
|
||||||
|
name: 'navigation',
|
||||||
|
initialState: {
|
||||||
|
selectedTab: "tree",
|
||||||
|
expandedNodes: {}
|
||||||
|
},
|
||||||
|
reducers: {
|
||||||
|
setSelectedTab: (state, action) => {
|
||||||
|
state.selectedTab = action.payload;
|
||||||
|
},
|
||||||
|
toggleNodeExpansion: (state, action) => {
|
||||||
|
const nodeId = action.payload;
|
||||||
|
state.expandedNodes[nodeId] = !state.expandedNodes[nodeId];
|
||||||
|
},
|
||||||
|
setExpandedNodes: (state, action) => {
|
||||||
|
state.expandedNodes = action.payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setSelectedTab, toggleNodeExpansion, setExpandedNodes } = navigationSlice.actions;
|
||||||
|
export default navigationSlice.reducer;
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import {useQuery, useMutation, useQueryClient} from 'react-query';
|
|
||||||
import {fetch_} from "./request-utils";
|
|
||||||
import {useConfig} from "../ConfigProvider";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const useMarkdown = (id) => {
|
|
||||||
const config = useConfig();
|
|
||||||
return useQuery(
|
|
||||||
["markdown", id],
|
|
||||||
() => fetch_(`${config.BACKEND_HOST}/api/markdown/${id}`),
|
|
||||||
{
|
|
||||||
enabled: !!id,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useIndexMarkdown = (path_id) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const config = useConfig();
|
|
||||||
return useQuery(
|
|
||||||
["index_markdown", path_id],
|
|
||||||
() => fetch_(`${config.BACKEND_HOST}/api/markdown/get_index/${path_id}`),{
|
|
||||||
enabled: !!path_id,
|
|
||||||
onSuccess: (data) => {
|
|
||||||
if(data && data.id){
|
|
||||||
queryClient.setQueryData(["markdown", data.id], data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useHomeMarkdown = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const config = useConfig();
|
|
||||||
return useQuery(
|
|
||||||
["home_markdown"],
|
|
||||||
() => fetch_(`${config.BACKEND_HOST}/api/markdown/get_home`), {
|
|
||||||
onSuccess: (data) => {
|
|
||||||
if (data && data.id){
|
|
||||||
queryClient.setQueryData(["markdown", data.id], data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const useMarkdownsByPath = (pathId) => {
|
|
||||||
const config = useConfig();
|
|
||||||
return useQuery(
|
|
||||||
["markdownsByPath", pathId],
|
|
||||||
() => fetch_(`${config.BACKEND_HOST}/api/markdown/by_path/${pathId}`),
|
|
||||||
{
|
|
||||||
enabled: !!pathId
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSaveMarkdown = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const config = useConfig();
|
|
||||||
return useMutation(({id, data}) => {
|
|
||||||
const url = id
|
|
||||||
? `${config.BACKEND_HOST}/api/markdown/${id}`
|
|
||||||
: `${config.BACKEND_HOST}/api/markdown/`;
|
|
||||||
const method = id ? "PUT" : "POST";
|
|
||||||
return fetch_(url, {
|
|
||||||
method,
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
})
|
|
||||||
},{
|
|
||||||
onSuccess: (res, variables) => {
|
|
||||||
queryClient.invalidateQueries(["markdownsByPath", variables.data.path_id]);
|
|
||||||
queryClient.invalidateQueries(["markdown", variables.data.id]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const useMoveMarkdown = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const config = useConfig();
|
|
||||||
|
|
||||||
return useMutation(
|
|
||||||
({markdown, direction}) => {
|
|
||||||
const apiEndpoint = `${config.BACKEND_HOST}/api/markdown/move_${direction}/${markdown.id}`;
|
|
||||||
return fetch_(apiEndpoint, {method: "PATCH"});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries("paths");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSearchMarkdown = (keyword) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const config = useConfig();
|
|
||||||
return useQuery(["markdownsByKeyword", keyword],
|
|
||||||
() => fetch_(
|
|
||||||
`${config.BACKEND_HOST}/api/markdown/search/${encodeURIComponent(keyword)}`,
|
|
||||||
),
|
|
||||||
{
|
|
||||||
enabled: !!keyword,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from "react-query";
|
|
||||||
import { fetch_ } from "./request-utils";
|
|
||||||
import {useConfig} from "../ConfigProvider";
|
|
||||||
|
|
||||||
|
|
||||||
export const usePaths = (parent_id) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const config = useConfig();
|
|
||||||
return useQuery(
|
|
||||||
["paths", parent_id],
|
|
||||||
() => fetch_(`${config.BACKEND_HOST}/api/path/parent/${parent_id}`),
|
|
||||||
{
|
|
||||||
enabled: !!parent_id,
|
|
||||||
onSuccess: (data) => {
|
|
||||||
if(data) {
|
|
||||||
for (const pth of data)
|
|
||||||
{
|
|
||||||
queryClient.setQueryData(["path", pth.id], pth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const usePath = (id) => {
|
|
||||||
const config = useConfig();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const cachedData = queryClient.getQueryData(["path", id]);
|
|
||||||
|
|
||||||
|
|
||||||
return useQuery(
|
|
||||||
["path", id],
|
|
||||||
() => fetch_(`${config.BACKEND_HOST}/api/path/${id}`),
|
|
||||||
{
|
|
||||||
enabled: !!id,
|
|
||||||
onSuccess: (data) => {
|
|
||||||
console.log(`path ${id} - ${cachedData}` );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useCreatePath = () => {
|
|
||||||
const config = useConfig();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return useMutation(
|
|
||||||
(data) => fetch_(`${config.BACKEND_HOST}/api/path/`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
onSuccess: (res, variables) => {
|
|
||||||
console.log(JSON.stringify(variables));
|
|
||||||
queryClient.invalidateQueries(["paths", variables.parent_id]);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useUpdatePath = () => {
|
|
||||||
const config = useConfig();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return useMutation(
|
|
||||||
({ id, data }) => fetch_(`${config.BACKEND_HOST}/api/path/${id}`, {
|
|
||||||
method: "PATCH",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
onSuccess: (res, variables) => {
|
|
||||||
queryClient.invalidateQueries(["paths", res.parent_id]);
|
|
||||||
queryClient.invalidateQueries(["path", variables.data.id]);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useDeletePath = () => {
|
|
||||||
const config = useConfig();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return useMutation(
|
|
||||||
(id) => fetch_(`${config.BACKEND_HOST}/api/path/${id}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries("paths");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const useMovePath = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const config = useConfig();
|
|
||||||
|
|
||||||
return useMutation(
|
|
||||||
({path, direction}) => {
|
|
||||||
const apiEndpoint = `${config.BACKEND_HOST}/api/path/move_${direction}/${path.id}`;
|
|
||||||
return fetch_(apiEndpoint, {method: "PATCH"});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries("paths");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
45
src/utils/pathUtils.js
Normal file
45
src/utils/pathUtils.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
export const findMarkdownByPath = (tree, pathString) => {
|
||||||
|
if (!tree || !pathString) return null;
|
||||||
|
|
||||||
|
const pathSegments = pathString.split('/').filter(segment => segment.length > 0);
|
||||||
|
|
||||||
|
if (pathSegments.length === 0) {
|
||||||
|
const rootIndex = tree.children?.find(
|
||||||
|
child => child.type === 'markdown' && child.title === 'index'
|
||||||
|
);
|
||||||
|
return rootIndex || null;
|
||||||
|
}
|
||||||
|
let currentNode = tree;
|
||||||
|
|
||||||
|
for (let i = 0; i < pathSegments.length; i++) {
|
||||||
|
const segment = pathSegments[i];
|
||||||
|
|
||||||
|
const childPath = currentNode.children?.find(
|
||||||
|
child => child.type === 'path' && child.name === segment
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!childPath) {
|
||||||
|
if (i === pathSegments.length - 1) {
|
||||||
|
const markdownNode = currentNode.children?.find(
|
||||||
|
child => child.type === 'markdown' && child.title === segment
|
||||||
|
);
|
||||||
|
return markdownNode || null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentNode = childPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const indexMarkdown = currentNode.children?.find(
|
||||||
|
child => child.type === 'markdown' && child.title === 'index'
|
||||||
|
);
|
||||||
|
|
||||||
|
return indexMarkdown || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMarkdownIdByPath = (tree, pathString) => {
|
||||||
|
const markdownNode = findMarkdownByPath(tree, pathString);
|
||||||
|
return markdownNode?.id || null;
|
||||||
|
};
|
||||||
32
src/utils/queries/apikey-queries.js
Normal file
32
src/utils/queries/apikey-queries.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { useConfig } from "../../ConfigProvider";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { fetch_ } from "../request-utils";
|
||||||
|
|
||||||
|
export const useCreateApiKey = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ name, roles }) => {
|
||||||
|
const response = await fetch_(`${config.BACKEND_HOST}/api/apikey/`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ name, roles }),
|
||||||
|
});
|
||||||
|
console.log("response", response);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
cacheTime: 0,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRevokeApiKey = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (apiKey) => {
|
||||||
|
const response = await fetch_(`${config.BACKEND_HOST}/api/apikey/revoke`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ apiKey }),
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
cacheTime: 0,
|
||||||
|
});
|
||||||
|
};
|
||||||
77
src/utils/queries/markdown-permission-setting-queries.js
Normal file
77
src/utils/queries/markdown-permission-setting-queries.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import {useConfig} from "../../ConfigProvider";
|
||||||
|
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query";
|
||||||
|
import {fetch_} from "../request-utils";
|
||||||
|
|
||||||
|
export const useMarkdownPermissionSettings = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["markdown_permission_settings"],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/permission/`),
|
||||||
|
onSuccess: async (data) => {
|
||||||
|
if (data) {
|
||||||
|
for (const setting of data) {
|
||||||
|
await queryClient.invalidateQueries(["markdown_permission_setting", setting.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMarkdownPermissionSetting = (setting_id) => {
|
||||||
|
const config = useConfig();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["markdown_permission_setting", setting_id],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/permission/${setting_id}`),
|
||||||
|
enabled: !!setting_id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateMarkdownPermissionSetting = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation(
|
||||||
|
{
|
||||||
|
mutationFn: (data) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/permission/`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}),
|
||||||
|
onSuccess: async (data) => {
|
||||||
|
await queryClient.invalidateQueries(["markdown_permission_setting", data.id]);
|
||||||
|
await queryClient.invalidateQueries(["markdown_permission_settings"]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateMarkdownPermissionSetting = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation(
|
||||||
|
({id, data}) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/permission/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}),{
|
||||||
|
onSuccess: (res) => {
|
||||||
|
queryClient.invalidateQueries(["markdown_permission_setting", res.id]);
|
||||||
|
queryClient.invalidateQueries(["markdown_permission_settings"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteMarkdownPermissionSetting = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation(
|
||||||
|
({id}) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/permission/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
}), {
|
||||||
|
onSuccess: (res, variables) => {
|
||||||
|
queryClient.invalidateQueries(["markdown_permission_setting", variables.id]);
|
||||||
|
queryClient.invalidateQueries(["markdown_permission_settings"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
133
src/utils/queries/markdown-queries.js
Normal file
133
src/utils/queries/markdown-queries.js
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query';
|
||||||
|
import {fetch_} from "../request-utils";
|
||||||
|
import {useConfig} from "../../ConfigProvider";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const useMarkdown = (id) => {
|
||||||
|
const config = useConfig();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["markdown", id],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/markdown/${id}`),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useIndexMarkdown = (path_id) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const config = useConfig();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["index_markdown", path_id],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/markdown/get_index/${path_id}`),
|
||||||
|
enabled: !!path_id,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if(data && data.id){
|
||||||
|
queryClient.setQueryData({queryKey: ["markdown", data.id]}, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useHomeMarkdown = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const config = useConfig();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["home_markdown"],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/markdown/get_home`),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data && data.id){
|
||||||
|
queryClient.setQueryData({queryKey: ["markdown", data.id]}, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const useMarkdownsByPath = (pathId) => {
|
||||||
|
const config = useConfig();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["markdownsByPath", pathId],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/markdown/by_path/${pathId}`),
|
||||||
|
enabled: !!pathId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const useSaveMarkdown = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const config = useConfig();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({id, data}) => {
|
||||||
|
const url = id
|
||||||
|
? `${config.BACKEND_HOST}/api/markdown/${id}`
|
||||||
|
: `${config.BACKEND_HOST}/api/markdown/`;
|
||||||
|
const method = id ? "PATCH" : "POST";
|
||||||
|
return fetch_(url, {
|
||||||
|
method,
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: async (res) => {
|
||||||
|
await queryClient.invalidateQueries({queryKey: ["markdownsByPath", res.path_id]});
|
||||||
|
await queryClient.invalidateQueries({queryKey: ["markdown", res.id]});
|
||||||
|
await queryClient.invalidateQueries({queryKey: ["tree"]});
|
||||||
|
|
||||||
|
console.log("invalidateQueries: ", res.id, typeof res.id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const useMoveMarkdown = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const config = useConfig();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({markdown, direction}) => {
|
||||||
|
const apiEndpoint = `${config.BACKEND_HOST}/api/markdown/move_${direction}/${markdown.id}`;
|
||||||
|
return fetch_(apiEndpoint, {method: "PATCH"});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["paths"]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["tree"]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteMarkdown = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const config = useConfig();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (markdownId) => {
|
||||||
|
return fetch_(`${config.BACKEND_HOST}/api/markdown/${markdownId}`, {
|
||||||
|
method: "DELETE"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: (data, markdownId) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["markdown", markdownId]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["tree"]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["markdownsByPath"]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSearchMarkdown = (keyword) => {
|
||||||
|
const config = useConfig();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["markdownsByKeyword", keyword],
|
||||||
|
queryFn: () => fetch_(
|
||||||
|
`${config.BACKEND_HOST}/api/markdown/search/${encodeURIComponent(keyword)}`,
|
||||||
|
),
|
||||||
|
enabled: !!keyword,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const useLinks = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["links"],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/markdown/links`)
|
||||||
|
});
|
||||||
|
}
|
||||||
70
src/utils/queries/markdown-setting-queries.js
Normal file
70
src/utils/queries/markdown-setting-queries.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import {useConfig} from "../../ConfigProvider";
|
||||||
|
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query";
|
||||||
|
import {fetch_} from "../request-utils";
|
||||||
|
|
||||||
|
export const useMarkdownSettings = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["markdown_setting"],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/`),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if(data){
|
||||||
|
for(const setting of data)
|
||||||
|
queryClient.setQueryData({queryKey: ["markdown_setting", setting.id]}, setting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMarkdownSetting = (setting_id) => {
|
||||||
|
const config = useConfig();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["markdown_setting", setting_id],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/${setting_id}`, {}),
|
||||||
|
enabled: !!setting_id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const useCreateMarkdownSetting = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
}),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["markdown_setting", data.id]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const useUpdateMarkdownSetting = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({id, data}) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
}),
|
||||||
|
onSuccess: (data, variables) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["markdown_setting", variables.id]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteMarkdownSetting = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
}),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["markdown_setting", data.id]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
96
src/utils/queries/markdown-template-queries.js
Normal file
96
src/utils/queries/markdown-template-queries.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import {useConfig} from "../../ConfigProvider";
|
||||||
|
import {useMutation, useQuery, useQueryClient} from "@tanstack/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({
|
||||||
|
queryKey: ["markdown_template", template_id],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/template/markdown/${template_id}`),
|
||||||
|
enabled: !!template_id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMarkdownTemplates = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["markdown_templates"],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/template/markdown/`),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if(data){
|
||||||
|
for(const template of data){
|
||||||
|
queryClient.setQueryData({queryKey: ["markdown_template", template.id]}, template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateMarkdownTemplate = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({id, data}) => fetch_(`${config.BACKEND_HOST}/api/template/markdown/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["markdown_template", data.id]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["markdown_templates"]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCreateMarkdownTemplate = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data) => fetch_(`${config.BACKEND_HOST}/api/template/markdown/`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["markdown_template", data.id]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["markdown_templates"]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeleteMarkdownTemplate = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id) => fetch_(`${config.BACKEND_HOST}/api/template/markdown/${id}`, {
|
||||||
|
method: "DELETE"
|
||||||
|
}),
|
||||||
|
onSuccess: (res, variables) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["markdown_template", variables]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["markdown_templates"]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSaveMarkdownTemplate = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({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({queryKey: ["markdown_template", data.id]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["markdown_templates"]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
71
src/utils/queries/markdown-template-setting-queries.js
Normal file
71
src/utils/queries/markdown-template-setting-queries.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import {useConfig} from "../../ConfigProvider";
|
||||||
|
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query";
|
||||||
|
import {fetch_} from "../request-utils";
|
||||||
|
|
||||||
|
export const useMarkdownTemplateSettings = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["markdown_template_settings"],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/template/`),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if(data){
|
||||||
|
for(const setting of data){
|
||||||
|
queryClient.setQueryData({queryKey: ["markdown_template_setting", setting.id]}, setting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMarkdownTemplateSetting = (setting_id) => {
|
||||||
|
const config = useConfig();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["markdown_template_setting", setting_id],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/template/${setting_id}`),
|
||||||
|
enabled: !!setting_id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const useCreateMarkdownTemplateSetting = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/template/`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["markdown_template_setting", data.id]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateMarkdownTemplateSetting = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({id, data}) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/template/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}),
|
||||||
|
onSuccess: (res) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["markdown_template_setting", res.id]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["markdown_template_settings"]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteMarkdownTemplateSetting = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({id}) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/template/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
}),
|
||||||
|
onSuccess: (res, variables) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["markdown_template_setting", variables.id]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
97
src/utils/queries/path-queries.js
Normal file
97
src/utils/queries/path-queries.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { fetch_ } from "../request-utils";
|
||||||
|
import {useConfig} from "../../ConfigProvider";
|
||||||
|
|
||||||
|
|
||||||
|
export const usePaths = (parent_id) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const config = useConfig();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["paths", parent_id],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/path/parent/${parent_id}`),
|
||||||
|
enabled: !!parent_id,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if(data) {
|
||||||
|
for (const pth of data)
|
||||||
|
{
|
||||||
|
queryClient.setQueryData({queryKey: ["path", pth.id]}, pth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const usePath = (id) => {
|
||||||
|
const config = useConfig();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["path", id],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/path/${id}`),
|
||||||
|
enabled: !!id
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreatePath = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data) => fetch_(`${config.BACKEND_HOST}/api/path/`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}),
|
||||||
|
onSuccess: (res) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["paths", res.parent_id]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["tree"]});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdatePath = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, data }) => fetch_(`${config.BACKEND_HOST}/api/path/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}),
|
||||||
|
onSuccess: (res) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["paths", res.parent_id]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["path", res.id]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["tree"]});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeletePath = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id) => fetch_(`${config.BACKEND_HOST}/api/path/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["paths"]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["tree"]});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const useMovePath = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const config = useConfig();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({path, direction}) => {
|
||||||
|
const apiEndpoint = `${config.BACKEND_HOST}/api/path/move_${direction}/${path.id}`;
|
||||||
|
return fetch_(apiEndpoint, {method: "PATCH"});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["paths"]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["tree"]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
73
src/utils/queries/path-setting-queries.js
Normal file
73
src/utils/queries/path-setting-queries.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import {useConfig} from "../../ConfigProvider";
|
||||||
|
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query";
|
||||||
|
import {fetch_} from "../request-utils";
|
||||||
|
|
||||||
|
export const usePathSettings = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["path_settings"],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/setting/path/`),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if(data){
|
||||||
|
for(const setting of data)
|
||||||
|
queryClient.setQueryData({queryKey: ["path_setting", setting.id]}, setting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePathSetting = (setting_id) => {
|
||||||
|
const config = useConfig();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["path_setting", setting_id],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/setting/path/${setting_id}`),
|
||||||
|
enabled: !!setting_id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreatePathSetting = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data) => fetch_(`${config.BACKEND_HOST}/api/setting/path/`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
}),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["path_setting", data.id]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["path_settings"]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const useUpdatePathSetting = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({id, data}) => fetch_(`${config.BACKEND_HOST}/api/setting/path/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
}),
|
||||||
|
onSuccess: (data, variables) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["path_setting", variables.id]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["path_settings"]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const useDeletePathSetting = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id) => fetch_(`${config.BACKEND_HOST}/api/setting/path/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
}),
|
||||||
|
onSuccess: (data, variables) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["path_setting", variables.id]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["path_settings"]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
17
src/utils/queries/tree-queries.js
Normal file
17
src/utils/queries/tree-queries.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {useQuery, useMutation, useQueryClient} from "@tanstack/react-query";
|
||||||
|
import {fetch_} from "../request-utils";
|
||||||
|
import {useConfig} from "../../ConfigProvider";
|
||||||
|
|
||||||
|
|
||||||
|
export const useTree = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const config = useConfig();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["tree"],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/tree/`),
|
||||||
|
onSuccess: data => {
|
||||||
|
if(data)
|
||||||
|
queryClient.setQueryData({queryKey: ["tree"]}, data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
136
src/utils/queries/webhook-queries.js
Normal file
136
src/utils/queries/webhook-queries.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import {fetch_ } from "../request-utils"
|
||||||
|
import {useConfig} from "../../ConfigProvider";
|
||||||
|
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export const useWebhooks = () =>{
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const config = useConfig();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["webhooks"],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/webhook/`),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if(data){
|
||||||
|
for(const webhook of data){
|
||||||
|
queryClient.setQueryData({queryKey: ["webhook", data.id]}, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateWebhook = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data) => fetch_(`${config.BACKEND_HOST}/api/webhook/`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
"hook_url": data
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["webhooks"]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateWebhook = () =>{
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({id, data}) => fetch_(`${config.BACKEND_HOST}/api/webhook/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
}),
|
||||||
|
onSuccess: (res) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["webhook", res.id]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["webhooks"]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteWebhook = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id) => fetch_(`${config.BACKEND_HOST}/api/webhook/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
}),
|
||||||
|
onSuccess: (res, variables) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["webhook", variables.id]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["webhooks"]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWebhookSettings = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["webhook_setting"],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/setting/path/webhook/`),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if(data){
|
||||||
|
for(const setting of data){
|
||||||
|
queryClient.setQueryData({queryKey: ["webhook_setting", setting.id]}, setting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useWebhookSetting = (setting_id) => {
|
||||||
|
const config = useConfig();
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["webhook_setting", setting_id],
|
||||||
|
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/setting/path/webhook/${setting_id}`),
|
||||||
|
enabled: !!setting_id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const useCreateWebhookSetting = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data) => fetch_(`${config.BACKEND_HOST}/api/setting/path/webhook/`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
}),
|
||||||
|
onSuccess: (res) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["webhook_setting", res.id]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["webhook_setting"]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateWebhookSetting = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({id, data}) => fetch_(`${config.BACKEND_HOST}/api/setting/path/webhook/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
}),
|
||||||
|
onSuccess: (res, variables) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["webhook_setting", variables.id]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["webhook_setting"]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const useDeleteWebhookSetting = () => {
|
||||||
|
const config = useConfig();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id) => fetch_(`${config.BACKEND_HOST}/api/setting/path/webhook/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
}),
|
||||||
|
onSuccess: (res, variables) => {
|
||||||
|
queryClient.invalidateQueries({queryKey: ["webhook_setting", variables.id]});
|
||||||
|
queryClient.invalidateQueries({queryKey: ["webhook_setting"]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -24,5 +24,16 @@ export async function fetch_(url, init = {}) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.status === 203) {
|
||||||
|
const data = await response.json();
|
||||||
|
return {
|
||||||
|
id: null,
|
||||||
|
content: data.msg || "Non-authoritative information received",
|
||||||
|
title: "Message",
|
||||||
|
isMessage: true,
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
@@ -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.js'
|
||||||
|
})
|
||||||
|
|
||||||
],
|
],
|
||||||
devServer: {
|
devServer: {
|
||||||
static: path.join(__dirname, 'public'),
|
static: path.join(__dirname, 'public'),
|
||||||
@@ -36,5 +40,17 @@ module.exports = {
|
|||||||
open: true,
|
open: true,
|
||||||
hot: true,
|
hot: true,
|
||||||
historyApiFallback: true,
|
historyApiFallback: true,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'process/browser': require.resolve('process/browser.js')
|
||||||
|
},
|
||||||
|
fallback: {
|
||||||
|
path: require.resolve('path-browserify'),
|
||||||
|
fs: false,
|
||||||
|
assert: require.resolve("assert/"),
|
||||||
|
process: require.resolve("process/browser.js"),
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
devtool: 'source-map',
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user