Compare commits

..

7 Commits

41 changed files with 1637 additions and 811 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
summerizer.py
node_modules

View File

@@ -5,7 +5,7 @@ FRONTEND_HOST="${FRONTEND_HOST:-http://localhost:80}"
KC_CLIENT_ID="${KC_CLIENT_ID:-labdev}"
KC_HOST="${KC_HOST:-https://login.hangman-lab.top}"
KC_REALM="${KC_REALM:-Hangman-Lab}"
DEBUG="${DEBUG:false}"
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",
"popup_redirect_uri": "${FRONTEND_HOST}/popup_callback",
"silent_redirect_uri": "${FRONTEND_HOST}/silent_callback"
}
},
"DEBUG": ${DEBUG}
}
EOL

View File

@@ -1,4 +1,4 @@
FROM node:18-alpine AS build-stage
FROM node:20-alpine AS build-stage
WORKDIR /app

291
package-lock.json generated
View File

@@ -9,19 +9,24 @@
"version": "1.0.0",
"license": "ISC",
"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",
"bulma": "^1.0.2",
"katex": "^0.16.11",
"oidc-client-ts": "^3.1.0",
"path-browserify": "^1.0.1",
"prismjs": "^1.30.0",
"process": "^0.11.10",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
"react-query": "^3.39.3",
"react-redux": "^9.2.0",
"react-router-dom": "^7.0.1",
"react-syntax-highlighter": "^15.6.1",
"redux": "^5.0.1",
"rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0",
@@ -2648,6 +2653,31 @@
"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": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -2672,6 +2702,69 @@
"@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": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@@ -2878,11 +2971,6 @@
"@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": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -3171,6 +3259,11 @@
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"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": {
"version": "8.5.13",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
@@ -3937,7 +4030,8 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"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": {
"version": "0.6.1",
@@ -3945,14 +4039,6 @@
"integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==",
"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": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -4009,6 +4095,7 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -4026,21 +4113,6 @@
"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": {
"version": "4.24.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz",
@@ -4490,7 +4562,8 @@
"node_modules/concat-map": {
"version": "0.0.1",
"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": {
"version": "2.0.0",
@@ -4924,7 +4997,8 @@
"node_modules/detect-node": {
"version": "2.1.0",
"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": {
"version": "1.1.0",
@@ -5791,7 +5865,8 @@
"node_modules/fs.realpath": {
"version": "1.0.0",
"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": {
"version": "2.3.3",
@@ -5894,6 +5969,7 @@
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -6594,9 +6670,9 @@
"dev": true
},
"node_modules/http-proxy-middleware": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
"integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
"dev": true,
"dependencies": {
"@types/http-proxy": "^1.17.8",
@@ -6707,6 +6783,15 @@
"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": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
@@ -6755,6 +6840,7 @@
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"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.",
"dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@@ -8224,11 +8310,6 @@
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -8541,15 +8622,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/match-sorter": {
"version": "6.3.4",
"resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz",
"integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==",
"dependencies": {
"@babel/runtime": "^7.23.8",
"remove-accents": "0.5.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -9481,11 +9553,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": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
@@ -9545,6 +9612,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -9571,14 +9639,6 @@
"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": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
@@ -9786,11 +9846,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/oblivious-set": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz",
"integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw=="
},
"node_modules/obuf": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
@@ -9833,6 +9888,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": {
"wrappy": "1"
}
@@ -10034,6 +10090,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -10467,37 +10524,33 @@
"react": ">=18"
}
},
"node_modules/react-query": {
"version": "3.39.3",
"resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz",
"integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==",
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"broadcast-channel": "^3.4.1",
"match-sorter": "^6.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"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": {
"react-dom": {
"@types/react": {
"optional": true
},
"react-native": {
"redux": {
"optional": true
}
}
},
"node_modules/react-router": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.0.1.tgz",
"integrity": "sha512-WVAhv9oWCNsja5AkK6KLpXJDSJCQizOIyOd4vvB/+eHGbYx5vkhcmcmwWjQ9yqkRClogi+xjEg9fNEOd5EX/tw==",
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.2.tgz",
"integrity": "sha512-9Rw8r199klMnlGZ8VAsV/I8WrIF6IyJ90JQUdboupx1cdkgYqwnrYjH+I/nY/7cA1X5zia4mDJqH36npP7sxGQ==",
"dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0",
"turbo-stream": "2.4.0"
@@ -10516,11 +10569,11 @@
}
},
"node_modules/react-router-dom": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.0.1.tgz",
"integrity": "sha512-duBzwAAiIabhFPZfDjcYpJ+f08TMbPMETgq254GWne2NW1ZwRHhZLj7tpSp8KGb7JvZzlLcjGUnqLxpZQVEPng==",
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.2.tgz",
"integrity": "sha512-yk1XW8Fj7gK7flpYBXF3yzd2NbX6P7Kxjvs2b5nu1M04rb5pg/Zc4fGdBNTeT4eDYL2bvzWNyKaIMJX/RKHTTg==",
"dependencies": {
"react-router": "7.0.1"
"react-router": "7.5.2"
},
"engines": {
"node": ">=20.0.0"
@@ -10597,6 +10650,19 @@
"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": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz",
@@ -10888,11 +10954,6 @@
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
@@ -10930,6 +10991,11 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"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": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -10986,21 +11052,6 @@
"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": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
@@ -12140,15 +12191,6 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -12207,6 +12249,14 @@
"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": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
@@ -12824,7 +12874,8 @@
"node_modules/wrappy": {
"version": "1.0.2",
"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": {
"version": "4.0.2",

View File

@@ -12,19 +12,24 @@
"author": "",
"license": "ISC",
"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",
"bulma": "^1.0.2",
"katex": "^0.16.11",
"oidc-client-ts": "^3.1.0",
"path-browserify": "^1.0.1",
"prismjs": "^1.30.0",
"process": "^0.11.10",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
"react-query": "^3.39.3",
"react-redux": "^9.2.0",
"react-router-dom": "^7.0.1",
"react-syntax-highlighter": "^15.6.1",
"redux": "^5.0.1",
"rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0",

View File

@@ -1,9 +1,12 @@
import React from "react";
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 SideNavigation from "./components/Navigations/SideNavigation";
import MarkdownContent from "./components/Markdowns/MarkdownContent";
import MarkdownEditor from "./components/Markdowns/MarkdownEditor";
import StandaloneMarkdownPage from "./components/Markdowns/StandaloneMarkdownPage";
import "./App.css";
import Callback from "./components/KeycloakCallbacks/Callback";
import Footer from "./components/Footer";
@@ -14,7 +17,11 @@ import MarkdownTemplateEditor from "./components/MarkdownTemplate/MarkdownTempla
const App = () => {
return (
<Provider store={store}>
<Router>
<Routes>
<Route path="/pg/*" element={<StandaloneMarkdownPage />} />
<Route path="*" element={
<div className="app-container">
<MainNavigation />
<div className="content-container">
@@ -26,21 +33,24 @@ const App = () => {
element={<Navigate to = "/markdown/1"/>}
/>
<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="/test" element={<h1>TEST</h1>}></Route>
<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="/silent_callback" element={<SilentCallback />} />
<Route path="/template/create" element={<MarkdownTemplateEditor />} />
<Route path="/template/edit/:id" element={<MarkdownTemplateEditor />} />
<Route path="/template/edit/:strId" element={<MarkdownTemplateEditor />} />
</Routes>
</main>
</div>
</div>
} />
</Routes>
<Footer />
</Router>
</Provider>
);
};

View 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;

View File

@@ -5,6 +5,7 @@ const LayoutEditor = ({layout, onChange}) => {
return (
<textarea
className="textarea"
style={{ height: "60vh" }}
value={_layout}
onChange={(e) => {
setLayout(e.target.value);

View File

@@ -10,7 +10,8 @@ const MarkdownTemplateEditor = () => {
const navigate = useNavigate();
const { id } = useParams();
const { strId } = useParams();
const id = Number(strId);
const { data: template, isFetching: templateIsFetching } = useMarkdownTemplate(id);
const saveMarkdownTemplate = useSaveMarkdownTemplate();
@@ -64,6 +65,7 @@ const MarkdownTemplateEditor = () => {
<div className="columns is-variable is-8">
<div className="column">
<h3 className="title is-5">Layout</h3>
<div className="box">
<LayoutEditor
layout={layout}
@@ -71,15 +73,15 @@ const MarkdownTemplateEditor = () => {
onChange={(newLayout) => setLayout(newLayout)}
/>
</div>
</div>
<div className="column">
<h3 className="title is-5">Parameters</h3>
<div className="box">
<ParametersManager
parameters={parameters}
onChange={(newParameters) => setParameters(newParameters)}
/>
</div>
</div>
</div>
<div className="field is-grouped">

View File

@@ -3,18 +3,36 @@ 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" }
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]);
@@ -33,6 +51,13 @@ const ParametersManager = ({ parameters, onChange }) => {
onChange(updated);
};
const toggleExpand = (index) => {
setExpandedStates(prev => ({
...prev,
[index]: !prev[index]
}));
};
return (
<div className="box">
<div className="field">
@@ -42,11 +67,14 @@ const ParametersManager = ({ parameters, onChange }) => {
</button>
</div>
</div>
<div style={{ maxHeight: "50vh", overflowY: "auto" }}>
{_parameters.map((param, index) => (
<div key={index} className="box" style={{ marginBottom: "1rem" }}>
<div className="field is-grouped is-grouped-multiline">
<div className="control is-expanded">
<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"
@@ -65,22 +93,28 @@ const ParametersManager = ({ parameters, onChange }) => {
</div>
</div>
<div className="field">
<label className="label">Type:</label>
<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) => {
const updated = [..._parameters];
updated[index].type = newType;
setParameters(updated);
onChange(updated);
}}
onChange={(newType) => handleTypeChange(index, newType)}
/>
</div>
)}
</div>
</div>
))}
</div>
</div>
);
};

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import { useMarkdownTemplates } from "../../utils/queries/markdown-template-queries";
const TemplateSelector = ({ template, onChange }) => {
const TemplateSelector = ({ template, onChange, onCreate }) => {
const { data: templates, isFetching: templatesAreFetching } = useMarkdownTemplates();
const [_template, setTemplate] = useState(
templates?.find((t) => t.id === template?.id) || {
@@ -34,13 +34,15 @@ const TemplateSelector = ({ template, onChange }) => {
value={template?.id || ""}
onChange={(e) => {
const id = parseInt(e.target.value, 10);
onChange(
templates.find((t) => t.id === id) || {
const selectedTemplate = templates.find((t) => t.id === id) || {
title: "",
parameters: [],
layout: "",
};
onChange(selectedTemplate);
if (onCreate) {
onCreate(selectedTemplate);
}
);
}}
>
<option value="">(None)</option>

View File

@@ -69,7 +69,6 @@ const TypeEditor = ({ type, onChange }) => {
case 'template':
return (
<div className="field">
<label className="label">Template</label>
<div className="control">
<TemplateSelector
template={_type.definition.template}
@@ -95,7 +94,7 @@ const TypeEditor = ({ type, onChange }) => {
return (
<div className="box">
<div className="field">
<label className="label">Type</label>
{/*<label className="label">Type</label>*/}
<div className="control">
<div className="select is-fullwidth">
<select

View File

@@ -9,10 +9,13 @@ 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 { id } = useParams();
const { strId } = useParams();
const id = Number(strId);
const [indexTitle, setIndexTitle] = useState(null);
const [isSettingModalOpen, setSettingModalOpen] = useState(false);
const {data: markdown, isLoading, error} = useMarkdown(id);
const {data: path, isFetching: isPathFetching} = usePath(markdown?.path_id);
const {data: setting, isFetching: isSettingFetching} = useMarkdownSetting(markdown?.setting_id);
@@ -35,8 +38,8 @@ const MarkdownContent = () => {
if (error) {
return <div>Error: {error.message || "Failed to load content"}</div>;
}
if (markdown.isMessage) {
return (
<div className="markdown-content-container">
<div className="notification is-info">
@@ -49,15 +52,28 @@ const MarkdownContent = () => {
return (
<div className="markdown-content-container">
<div className="field has-addons markdown-content-container-header">
<h1 className="title control">{markdown.title === "index" ? indexTitle : markdown.title}</h1>
<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']}>
<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">
Edit
</Link>
</div>
</PermissionGuard>
</div>
<MarkdownView content={JSON.parse(markdown.content)} template={template}/>
<MarkdownSettingModal
isOpen={isSettingModalOpen}
markdown={markdown}
onClose={() => setSettingModalOpen(false)}
/>
</div>
);
};

View File

@@ -11,11 +11,13 @@ import {useMarkdownTemplate, useMarkdownTemplates} from "../../utils/queries/mar
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 { roles } = useContext(AuthContext);
const navigate = useNavigate();
const { id } = useParams();
const { strId } = useParams();
const id = Number(strId);
const [title, setTitle] = useState("");
const [content, setContent] = useState({});
const [shortcut, setShortcut] = useState("");
@@ -23,7 +25,8 @@ const MarkdownEditor = () => {
const [isRawMode, setIsRawMode] = useState(false);
const [rawContent, setRawContent] = useState("");
const [jsonError, setJsonError] = useState("");
const {data: markdown, isLoading, error} = useMarkdown(id);
const [selectedTemplate, setSelectedTemplate] = useState(null);
const {data: markdown, isFetching: isMarkdownFetching, error} = useMarkdown(id);
const saveMarkdown = useSaveMarkdown();
const {data: setting, isFetching: isSettingFetching} = useMarkdownSetting(markdown?.setting_id);
const {data: templateSetting, isFetching: isTemplateSettingFetching} = useMarkdownTemplateSetting(setting?.template_setting_id);
@@ -31,9 +34,11 @@ const MarkdownEditor = () => {
const updateTemplateSetting = useUpdateMarkdownTemplateSetting();
const createTemplateSetting = useCreateMarkdownTemplateSetting();
const updateSetting = useUpdateMarkdownSetting();
const {data: templates} = useMarkdownTemplates();
const {data: templates, isFetching: templatesAreFetching} = useMarkdownTemplates();
const createMarkdownSetting = useCreateMarkdownSetting();
const notReady = isMarkdownFetching || isTemplateFetching || isSettingFetching || isTemplateSettingFetching || templatesAreFetching;
const notReady = isLoading || isTemplateFetching || isSettingFetching || isTemplateSettingFetching;
useEffect(() => {
if (markdown) {
setTitle(markdown.title);
@@ -56,22 +61,84 @@ const MarkdownEditor = () => {
}
}, [markdown, navigate]);
useEffect(() => {
if (template) {
setSelectedTemplate(template);
}
}, [template]);
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(
{id, data: {title, content: JSON.stringify(content), path_id: pathId, shortcut}},
{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: () => {
navigate("/");
}
});
}
});
} else {
navigate("/");
}
}
});
}
});
},
onError: () => {
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 = () => {
@@ -104,61 +171,31 @@ const MarkdownEditor = () => {
};
const handleTemplateChange = (newTemplate) => {
if (!newTemplate) return;
setSelectedTemplate(newTemplate);
if (templateSetting) {
updateTemplateSetting.mutate(
{
updateTemplateSetting.mutate({
id: templateSetting.id,
data: { template_id: newTemplate.id }
},
{
onSuccess: () => {
setContent({});
},
onError: () => {
alert("Error updating template");
data: {
template_id: newTemplate.id
}
}
);
} else if (setting) {
createTemplateSetting.mutate(
{ template_id: newTemplate.id },
{
onSuccess: (newTemplateSetting) => {
updateSetting.mutate(
{
id: setting.id,
data: { template_setting_id: newTemplateSetting.id }
},
{
onSuccess: () => {
setContent({});
},
onError: () => {
alert("Error updating markdown setting");
}
}
);
},
onError: () => {
alert("Error creating template setting");
}
}
);
} else {
alert("Cannot change template: No markdown setting found");
});
}
};
const hasPermission = roles.includes("admin") || roles.includes("creator");
if (!hasPermission)
return <div className="notification is-danger">Permission Denied</div>;
if(notReady)
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>;
}
if(error)
return <p>{error.message || "Failed to load markdown"}</p>;
@@ -204,7 +241,7 @@ const MarkdownEditor = () => {
<div className="field">
<div className="control">
<TemplateSelector
template={template}
template={selectedTemplate || template}
onChange={handleTemplateChange}
/>
</div>
@@ -240,9 +277,9 @@ const MarkdownEditor = () => {
</div>
) : (
<TemplatedEditor
style={{height: "70vh"}}
style={{height: "40vh"}}
content={content}
template={template}
template={!markdown?.id ? selectedTemplate : template}
onContentChanged={(k, v) => setContent(
prev => ({...prev, [k]: v})
)}
@@ -268,7 +305,11 @@ const MarkdownEditor = () => {
<div className="column is-half">
<h3 className="subtitle is-5">Preview</h3>
<MarkdownView content={content} template={template} height='70vh'/>
<MarkdownView
content={content}
template={!markdown?.id ? selectedTemplate : template}
height='70vh'
/>
</div>
</div>
</div>

View 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;

View File

@@ -28,6 +28,7 @@ const TemplatedEditorComponent = ({ variable, value, namespace, onContentChanged
<label className="label">{__namespace}</label>
<div className="control">
<textarea
style={{maxHeight: "10vh"}}
className="textarea"
value={value}
onChange={(e) => onContentChanged(variable.name, e.target.value)}
@@ -152,7 +153,7 @@ const TemplatedEditorComponent = ({ variable, value, namespace, onContentChanged
return <>{renderField()}</>;
};
const TemplatedEditor = ({ content, template, onContentChanged }) => {
const TemplatedEditor = ({ content, template, onContentChanged, style }) => {
const tpl = template || {
parameters: [{ name: "markdown", type: { base_type: "markdown", definition: {} } }],
layout: "<markdown/>",
@@ -160,7 +161,17 @@ const TemplatedEditor = ({ content, template, onContentChanged }) => {
};
return (
<div className="box">
<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}
@@ -171,6 +182,7 @@ const TemplatedEditor = ({ content, template, onContentChanged }) => {
/>
))}
</div>
</div>
);
};

View 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;

View 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;

View 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;

View File

@@ -3,7 +3,8 @@ import {useCreatePathSetting, usePathSetting} from "../../utils/queries/path-set
import WebhookSettingPanel from "../Settings/PathSettings/WebhookSettingPanel";
import React, {useState} from "react";
const PathSettingModal = ({ isOpen, path, onClose }) => {
const {data: pathSetting, isLoading: isPathSettingLoading} = usePathSetting(path?.setting_id || 0);
const settingId = path?.setting_id || 0;
const {data: pathSetting, isLoading: isPathSettingLoading} = usePathSetting(settingId);
const createPathSetting = useCreatePathSetting();
const updatePath = useUpdatePath();
@@ -17,7 +18,7 @@ const PathSettingModal = ({ isOpen, path, onClose }) => {
};
if(isPathSettingLoading)
if(settingId && isPathSettingLoading)
return (<p>Loading...</p>);
return (

View File

@@ -4,11 +4,16 @@ import { AuthContext } from "../../AuthProvider";
import "bulma/css/bulma.min.css";
import {useConfig} from "../../ConfigProvider";
import "./MainNavigation.css";
import ApiKeyCreationModal from "../Modals/ApiKeyCreationModal";
import ApiKeyRevokeModal from "../Modals/ApiKeyRevokeModal";
const MainNavigation = () => {
const { user, login, logout } = useContext(AuthContext);
const config = useConfig();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false);
const [isRevokeModalOpen, setIsRevokeModalOpen] = useState(false);
if (config===undefined) {
return <div>Loading ...</div>;
}
@@ -88,6 +93,7 @@ const MainNavigation = () => {
};
return (
<>
<nav className="navbar is-dark" role="navigation" aria-label="main navigation">
<div className="navbar-brand">
<Link className="navbar-item" to="/">
@@ -158,6 +164,20 @@ const MainNavigation = () => {
>
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
className="button is-danger dropdown-option"
onClick={logout}
@@ -165,9 +185,7 @@ const MainNavigation = () => {
>
Logout
</button>
</div>
</div>
</div>
) : (
@@ -184,6 +202,15 @@ const MainNavigation = () => {
</div>
</div>
</nav>
<ApiKeyCreationModal
isOpen={isApiKeyModalOpen}
onClose={() => setIsApiKeyModalOpen(false)}
/>
<ApiKeyRevokeModal
isOpen={isRevokeModalOpen}
onClose={() => setIsRevokeModalOpen(false)}
/>
</>
);
};

View File

@@ -1,9 +1,30 @@
import {Link} from "react-router-dom";
import {Link, useNavigate} from "react-router-dom";
import PermissionGuard from "../PermissionGuard";
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 [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 (
<li key={markdown.id}>
<div className="is-clickable field has-addons">
@@ -24,6 +45,18 @@ const MarkdownNode = ({markdown, handleMoveMarkdown}) => {
</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"}}

View File

@@ -1,4 +1,6 @@
import React, {useState} from "react";
import { useSelector, useDispatch } from 'react-redux';
import { toggleNodeExpansion } from '../../store/navigationSlice';
import { Link } from "react-router-dom";
import PermissionGuard from "../PermissionGuard";
import "./PathNode.css";
@@ -9,10 +11,13 @@ import PathSettingModal from "../Modals/PathSettingModal";
const PathNode = ({ path, isRoot = false }) => {
const [isPathSettingModalOpen, setIsPathSettingModalOpen] = useState(false);
const [isExpanded, setIsExpanded] = useState(isRoot);
const [isEditing, setIsEditing] = useState(false);
const [newName, setNewName] = useState(path.name || "");
const expandedNodes = useSelector(state => state.navigation.expandedNodes);
const dispatch = useDispatch();
const isExpanded = isRoot || expandedNodes[path.id];
const deletePath = useDeletePath();
const updatePath = useUpdatePath();
@@ -23,11 +28,13 @@ const PathNode = ({ path, isRoot = false }) => {
const expand = () => {
if(!isExpanded)
setIsExpanded(true);
if (!isExpanded) {
dispatch(toggleNodeExpansion(path.id));
}
};
const toggleExpand = () => {
setIsExpanded(!isExpanded);
dispatch(toggleNodeExpansion(path.id));
};
const handleSave = () => {

View File

@@ -1,4 +1,6 @@
import React, { useContext, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { setSelectedTab } from '../../store/navigationSlice';
import "./SideNavigation.css";
import TreeTab from "./SideTabs/TreeTab";
import TemplateTab from "./SideTabs/TemplateTab";
@@ -6,7 +8,8 @@ import { AuthContext } from "../../AuthProvider";
const SideNavigation = () => {
const { roles } = useContext(AuthContext);
const [selectedTab, setSelectedTab] = React.useState("tree");
const selectedTab = useSelector(state => state.navigation.selectedTab);
const dispatch = useDispatch();
const allTabs = [
{ id: "tree", label: "Tree", component: <TreeTab /> },
@@ -19,9 +22,9 @@ const SideNavigation = () => {
useEffect(() => {
if (!visibleTabs.find(tab => tab.id === selectedTab)) {
setSelectedTab(visibleTabs[0]?.id || "");
dispatch(setSelectedTab(visibleTabs[0]?.id || ""));
}
}, [visibleTabs, selectedTab]);
}, [visibleTabs, selectedTab, dispatch]);
const current = visibleTabs.find(t => t.id === selectedTab);
@@ -35,7 +38,7 @@ const SideNavigation = () => {
key={tab.id}
className={tab.id === selectedTab ? "is-active" : ""}
>
<a onClick={() => setSelectedTab(tab.id)}>
<a onClick={() => dispatch(setSelectedTab(tab.id))}>
{tab.label}
</a>
</li>

View File

@@ -2,10 +2,12 @@ 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 =>
@@ -16,6 +18,99 @@ const TemplateTab = () => {
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>;
@@ -38,23 +133,40 @@ const TemplateTab = () => {
Create New Template
</a>
</PermissionGuard>
{!filteredTemplates || filteredTemplates.length === 0 ? (
<p>No templates found</p>
) : (
<div className="template-list">
{filteredTemplates.map(template => (
<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
key={template.id}
className="button is-light is-fullwidth template-button"
className="button is-small control"
onClick={() => handleTemplateClick(template.id)}
style={{ marginBottom: "5px", textAlign: "left", justifyContent: "flex-start" }}
type="button"
>
{template.title}
<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>
);
};

View File

@@ -1,6 +1,6 @@
import React, {useEffect, useState, useRef, useContext} from "react";
import {useCreatePath, usePaths} from "../utils/queries/path-queries";
import { useQueryClient } from "react-query";
import { useQueryClient } from "@tanstack/react-query";
import "./PathManager.css";
import {fetch_} from "../utils/request-utils";
import {ConfigContext} from "../ConfigProvider";
@@ -24,10 +24,10 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
let current_id = pathId;
while (current_id) {
try {
const pathData = await queryClient.fetchQuery(
["path", current_id],
() => fetch_(`${config.BACKEND_HOST}/api/path/${current_id}`)
);
const pathData = await queryClient.fetchQuery({
queryKey: ["path", current_id],
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/path/${current_id}`)
});
if (!pathData) break;
path.unshift({ name: pathData.name, id: pathData.id });
current_id = pathData.parent_id;
@@ -71,8 +71,8 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
{ name: searchTerm.trim(), parent_id: currentPathId },
{
onSuccess: (newDir) => {
queryClient.setQueryData(["path", newDir.id], newDir);
queryClient.invalidateQueries(["paths", currentPathId]);
queryClient.setQueryData({queryKey: ["path", newDir.id]}, newDir);
queryClient.invalidateQueries({queryKey: ["paths", currentPathId]});
setSearchTerm("");
alert("Directory created successfully.");
},

View File

@@ -3,15 +3,16 @@ import ReactDOM from "react-dom/client";
import App from "./App";
import AuthProvider, {AuthContext} from "./AuthProvider";
import "bulma/css/bulma.min.css";
import {QueryClient, QueryClientProvider} from "react-query"
import {QueryClient, QueryClientProvider} from "@tanstack/react-query"
import ConfigProvider from "./ConfigProvider";
import ControlledReactQueryDevtools from "./components/Debug/ControlledReactQueryDevtools";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 2,
refetchOnWindowFocus: false,
staleTimeout: 5 * 60 * 1000,
staleTime: 5 * 60 * 1000,
onError: (error) => {
if (error.message === "Unauthorized"){
const {logout} = queryClient
@@ -38,7 +39,7 @@ const EnhancedAuthProvider = ({children}) => {
};
React.useEffect(() => {
queryClient.setQueryDefaults("auths", {
queryClient.setQueryDefaults(["auths"], {
context: {logout}
});
}, [logout]);
@@ -52,6 +53,7 @@ root.render(
<AuthProvider>
<EnhancedAuthProvider>
<App />
<ControlledReactQueryDevtools />
</EnhancedAuthProvider>
</AuthProvider>
</QueryClientProvider>

8
src/store/index.js Normal file
View File

@@ -0,0 +1,8 @@
import { configureStore } from '@reduxjs/toolkit';
import navigationReducer from './navigationSlice';
export const store = configureStore({
reducer: {
navigation: navigationReducer,
},
});

View 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;

45
src/utils/pathUtils.js Normal file
View 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;
};

View 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,
});
};

View File

@@ -1,17 +1,17 @@
import {useConfig} from "../../ConfigProvider";
import {useMutation, useQuery, useQueryClient} from "react-query";
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query";
import {fetch_} from "../request-utils";
export const useMarkdownPermissionSettings = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useQuery(
"markdown_permission_settings",
() => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/permission/`), {
onSuccess: (data) => {
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) {
queryClient.invalidateQueries(["markdown_permission_setting", setting.id]);
await queryClient.invalidateQueries(["markdown_permission_setting", setting.id]);
}
}
}
@@ -21,9 +21,9 @@ export const useMarkdownPermissionSettings = () => {
export const useMarkdownPermissionSetting = (setting_id) => {
const config = useConfig();
return useQuery(
["markdown_permission_setting", setting_id],
() => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/permission/${setting_id}/`), {
return useQuery({
queryKey: ["markdown_permission_setting", setting_id],
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/permission/${setting_id}`),
enabled: !!setting_id,
}
);
@@ -32,13 +32,15 @@ export const useMarkdownPermissionSetting = (setting_id) => {
export const useCreateMarkdownPermissionSetting = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation((data) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/permission/`, {
return useMutation(
{
mutationFn: (data) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/permission/`, {
method: "POST",
body: JSON.stringify(data),
}), {
onSuccess: (data) => {
queryClient.invalidateQueries(["markdown_permission_setting", data.id]);
queryClient.invalidateQueries("markdown_permission_settings");
}),
onSuccess: async (data) => {
await queryClient.invalidateQueries(["markdown_permission_setting", data.id]);
await queryClient.invalidateQueries(["markdown_permission_settings"]);
}
});
};
@@ -53,7 +55,7 @@ export const useUpdateMarkdownPermissionSetting = () => {
}),{
onSuccess: (res) => {
queryClient.invalidateQueries(["markdown_permission_setting", res.id]);
queryClient.invalidateQueries("markdown_permission_settings");
queryClient.invalidateQueries(["markdown_permission_settings"]);
}
}
);
@@ -68,7 +70,7 @@ export const useDeleteMarkdownPermissionSetting = () => {
}), {
onSuccess: (res, variables) => {
queryClient.invalidateQueries(["markdown_permission_setting", variables.id]);
queryClient.invalidateQueries("markdown_permission_settings");
queryClient.invalidateQueries(["markdown_permission_settings"]);
}
}
);

View File

@@ -1,4 +1,4 @@
import {useQuery, useMutation, useQueryClient} from 'react-query';
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query';
import {fetch_} from "../request-utils";
import {useConfig} from "../../ConfigProvider";
@@ -6,10 +6,9 @@ import {useConfig} from "../../ConfigProvider";
export const useMarkdown = (id) => {
const config = useConfig();
return useQuery(
["markdown", id],
() => fetch_(`${config.BACKEND_HOST}/api/markdown/${id}`),
{
return useQuery({
queryKey: ["markdown", id],
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/markdown/${id}`),
enabled: !!id,
});
};
@@ -17,13 +16,13 @@ export const useMarkdown = (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}`),{
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(["markdown", data.id], data);
queryClient.setQueryData({queryKey: ["markdown", data.id]}, data);
}
}
});
@@ -32,12 +31,12 @@ export const useIndexMarkdown = (path_id) => {
export const useHomeMarkdown = () => {
const queryClient = useQueryClient();
const config = useConfig();
return useQuery(
["home_markdown"],
() => fetch_(`${config.BACKEND_HOST}/api/markdown/get_home`), {
return useQuery({
queryKey: ["home_markdown"],
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/markdown/get_home`),
onSuccess: (data) => {
if (data && data.id){
queryClient.setQueryData(["markdown", data.id], data);
queryClient.setQueryData({queryKey: ["markdown", data.id]}, data);
}
}
});
@@ -46,18 +45,19 @@ export const useHomeMarkdown = () => {
export const useMarkdownsByPath = (pathId) => {
const config = useConfig();
return useQuery(
["markdownsByPath", pathId],
() => fetch_(`${config.BACKEND_HOST}/api/markdown/by_path/${pathId}`),
{
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(({id, data}) => {
return useMutation({
mutationFn: ({id, data}) => {
const url = id
? `${config.BACKEND_HOST}/api/markdown/${id}`
: `${config.BACKEND_HOST}/api/markdown/`;
@@ -65,12 +65,14 @@ export const useSaveMarkdown = () => {
return fetch_(url, {
method,
body: JSON.stringify(data),
})
},{
onSuccess: (res) => {
queryClient.invalidateQueries(["markdownsByPath", res.path_id]);
queryClient.invalidateQueries(["markdown", res.id]);
queryClient.invalidateQueries("tree");
});
},
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);
},
});
};
@@ -80,35 +82,52 @@ export const useMoveMarkdown = () => {
const queryClient = useQueryClient();
const config = useConfig();
return useMutation(
({markdown, direction}) => {
return useMutation({
mutationFn: ({markdown, direction}) => {
const apiEndpoint = `${config.BACKEND_HOST}/api/markdown/move_${direction}/${markdown.id}`;
return fetch_(apiEndpoint, {method: "PATCH"});
},
{
onSuccess: () => {
queryClient.invalidateQueries("paths");
queryClient.invalidateQueries("tree");
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(["markdownsByKeyword", keyword],
() => fetch_(
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(["links"], () => fetch_(`${config.BACKEND_HOST}/api/markdown/links`));
return useQuery({
queryKey: ["links"],
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/markdown/links`)
});
}

View File

@@ -1,61 +1,57 @@
import {useConfig} from "../../ConfigProvider";
import {useMutation, useQuery, useQueryClient} from "react-query";
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query";
import {fetch_} from "../request-utils";
export const useMarkdownSettings = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useQuery(
"markdown_setting",
() => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/`),
{
return useQuery({
queryKey: ["markdown_setting"],
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/`),
onSuccess: (data) => {
if(data){
for(const setting of data)
queryClient.invalidateQueries(["markdown_setting", setting.id]);
queryClient.setQueryData({queryKey: ["markdown_setting", setting.id]}, setting);
}
}
}
);
});
};
export const useMarkdownSetting = (setting_id) => {
const config = useConfig();
return useQuery(
["markdown_setting", setting_id],
() => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/${setting_id}`, {}), {
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(
(data) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/`, {
return useMutation({
mutationFn: (data) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/`, {
method: "POST",
body: JSON.stringify(data)
}), {
}),
onSuccess: (data) => {
queryClient.invalidateQueries(["markdown_setting", data.id]);
queryClient.invalidateQueries({queryKey: ["markdown_setting", data.id]});
}
}
);
});
};
export const useUpdateMarkdownSetting = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
({id, data}) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/${id}`, {
return useMutation({
mutationFn: ({id, data}) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/${id}`, {
method: "PATCH",
body: JSON.stringify(data)
}),{
}),
onSuccess: (data, variables) => {
queryClient.invalidateQueries(["markdown_setting", variables.id]);
queryClient.invalidateQueries({queryKey: ["markdown_setting", variables.id]});
}
});
};
@@ -63,13 +59,12 @@ export const useUpdateMarkdownSetting = () => {
export const useDeleteMarkdownSetting = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
(id) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/${id}`, {
return useMutation({
mutationFn: (id) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/${id}`, {
method: "DELETE",
}),{
}),
onSuccess: (data) => {
queryClient.invalidateQueries(["markdown_setting", data.id]);
queryClient.invalidateQueries({queryKey: ["markdown_setting", data.id]});
}
}
);
});
};

View File

@@ -1,5 +1,5 @@
import {useConfig} from "../../ConfigProvider";
import {useMutation, useQuery, useQueryClient} from "react-query";
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query";
import {fetch_} from "../request-utils";
import {template} from "@babel/core";
import {data} from "react-router-dom";
@@ -7,83 +7,78 @@ import {data} from "react-router-dom";
export const useMarkdownTemplate = (template_id) => {
const config = useConfig();
return useQuery(
["markdown_template", template_id],
() => fetch_(`${config.BACKEND_HOST}/api/template/markdown/${template_id}`), {
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(
"markdown_templates",
() => fetch_(`${config.BACKEND_HOST}/api/template/markdown/`), {
return useQuery({
queryKey: ["markdown_templates"],
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/template/markdown/`),
onSuccess: (data) => {
if(data){
for(const template of data){
queryClient.setQueryData(["markdown_template", template.id], template);
queryClient.setQueryData({queryKey: ["markdown_template", template.id]}, template);
}
}
}
}
);
});
};
export const useUpdateMarkdownTemplate = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
({id, data}) => fetch_(`${config.BACKEND_HOST}/api/template/markdown/${id}`, {
return useMutation({
mutationFn: ({id, data}) => fetch_(`${config.BACKEND_HOST}/api/template/markdown/${id}`, {
method: "PUT",
body: JSON.stringify(data),
}),
{
onSuccess: (data) => {
queryClient.invalidateQueries(["markdown_template", data.id]);
queryClient.invalidateQueries("markdown_templates");
queryClient.invalidateQueries({queryKey: ["markdown_template", data.id]});
queryClient.invalidateQueries({queryKey: ["markdown_templates"]});
}
}
);
});
}
export const useCreateMarkdownTemplate = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
(data) => fetch_(`${config.BACKEND_HOST}/api/template/markdown/`, {
return useMutation({
mutationFn: (data) => fetch_(`${config.BACKEND_HOST}/api/template/markdown/`, {
method: "POST",
body: JSON.stringify(data),
}),{
}),
onSuccess: (data) => {
queryClient.invalidateQueries(["markdown_template", data.id]);
queryClient.invalidateQueries("markdown_templates");
queryClient.invalidateQueries({queryKey: ["markdown_template", data.id]});
queryClient.invalidateQueries({queryKey: ["markdown_templates"]});
}
}
);
});
}
export const useDeleteMarkdownTemplate = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
(id) => fetch_(`${config.BACKEND_HOST}/api/template/markdown/${id}`, {
return useMutation({
mutationFn: (id) => fetch_(`${config.BACKEND_HOST}/api/template/markdown/${id}`, {
method: "DELETE"
}), {
}),
onSuccess: (res, variables) => {
queryClient.invalidateQueries(["markdown_template", variables]);
queryClient.invalidateQueries("markdown_templates");
queryClient.invalidateQueries({queryKey: ["markdown_template", variables]});
queryClient.invalidateQueries({queryKey: ["markdown_templates"]});
}
}
)
});
}
export const useSaveMarkdownTemplate = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(({id, data}) => {
return useMutation({
mutationFn: ({id, data}) => {
const url = id
? `${config.BACKEND_HOST}/api/template/markdown/${id}`
: `${config.BACKEND_HOST}/api/template/markdown/`;
@@ -91,11 +86,11 @@ export const useSaveMarkdownTemplate = () => {
return fetch_(url, {
method,
body: JSON.stringify(data),
})
},{
});
},
onSuccess: (data) => {
queryClient.invalidateQueries(["markdown_template", data.id]);
queryClient.invalidateQueries("markdown_templates");
queryClient.invalidateQueries({queryKey: ["markdown_template", data.id]});
queryClient.invalidateQueries({queryKey: ["markdown_templates"]});
}
});
}

View File

@@ -1,44 +1,43 @@
import {useConfig} from "../../ConfigProvider";
import {useMutation, useQuery, useQueryClient} from "react-query";
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query";
import {fetch_} from "../request-utils";
export const useMarkdownTemplateSettings = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useQuery(
"markdown_template_settings",
() => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/template/`), {
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.invalidateQueries(["markdown_template_setting", settings.id]);
queryClient.setQueryData({queryKey: ["markdown_template_setting", setting.id]}, setting);
}
}
}
}
);
});
};
export const useMarkdownTemplateSetting = (setting_id) => {
const config = useConfig();
return useQuery(
["markdown_template_setting", setting_id],
() => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/template/${setting_id}`), {
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((data) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/template/`, {
return useMutation({
mutationFn: (data) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/template/`, {
method: "POST",
body: JSON.stringify(data),
}), {
}),
onSuccess: (data) => {
queryClient.invalidateQueries(["markdown_template_setting", data.id]);
queryClient.invalidateQueries({queryKey: ["markdown_template_setting", data.id]});
}
});
};
@@ -46,29 +45,27 @@ export const useCreateMarkdownTemplateSetting = () => {
export const useUpdateMarkdownTemplateSetting = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
({id, data}) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/template/${id}`, {
return useMutation({
mutationFn: ({id, data}) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/template/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
}),{
}),
onSuccess: (res) => {
queryClient.invalidateQueries(["markdown_template_setting", res.id]);
queryClient.invalidateQueries("markdown_template_settings");
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(
({id}) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/template/${id}`, {
return useMutation({
mutationFn: ({id}) => fetch_(`${config.BACKEND_HOST}/api/setting/markdown/template/${id}`, {
method: "DELETE",
}), {
}),
onSuccess: (res, variables) => {
queryClient.invalidateQueries(["markdown_template_setting", variables.id]);
queryClient.invalidateQueries({queryKey: ["markdown_template_setting", variables.id]});
}
}
);
});
};

View File

@@ -1,4 +1,4 @@
import { useQuery, useMutation, useQueryClient } from "react-query";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { fetch_ } from "../request-utils";
import {useConfig} from "../../ConfigProvider";
@@ -6,87 +6,77 @@ 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}`),
{
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(["path", pth.id], pth);
queryClient.setQueryData({queryKey: ["path", pth.id]}, pth);
}
}
}
}
);
});
};
export const usePath = (id) => {
const config = useConfig();
return useQuery(
["path", id],
() => fetch_(`${config.BACKEND_HOST}/api/path/${id}`),
{
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(
(data) => fetch_(`${config.BACKEND_HOST}/api/path/`, {
return useMutation({
mutationFn: (data) => fetch_(`${config.BACKEND_HOST}/api/path/`, {
method: "POST",
body: JSON.stringify(data),
}),
{
onSuccess: (res) => {
queryClient.invalidateQueries(["paths", res.parent_id]);
queryClient.invalidateQueries("tree");
queryClient.invalidateQueries({queryKey: ["paths", res.parent_id]});
queryClient.invalidateQueries({queryKey: ["tree"]});
},
}
);
});
};
export const useUpdatePath = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
({ id, data }) => fetch_(`${config.BACKEND_HOST}/api/path/${id}`, {
return useMutation({
mutationFn: ({ id, data }) => fetch_(`${config.BACKEND_HOST}/api/path/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
}),
{
onSuccess: (res) => {
queryClient.invalidateQueries(["paths", res.parent_id]);
queryClient.invalidateQueries(["path", res.id]);
queryClient.invalidateQueries("tree");
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(
(id) => fetch_(`${config.BACKEND_HOST}/api/path/${id}`, {
return useMutation({
mutationFn: (id) => fetch_(`${config.BACKEND_HOST}/api/path/${id}`, {
method: "DELETE",
}),
{
onSuccess: () => {
queryClient.invalidateQueries("paths");
queryClient.invalidateQueries("tree");
queryClient.invalidateQueries({queryKey: ["paths"]});
queryClient.invalidateQueries({queryKey: ["tree"]});
},
}
);
});
};
@@ -94,16 +84,14 @@ export const useMovePath = () => {
const queryClient = useQueryClient();
const config = useConfig();
return useMutation(
({path, direction}) => {
return useMutation({
mutationFn: ({path, direction}) => {
const apiEndpoint = `${config.BACKEND_HOST}/api/path/move_${direction}/${path.id}`;
return fetch_(apiEndpoint, {method: "PATCH"});
},
{
onSuccess: () => {
queryClient.invalidateQueries("paths");
queryClient.invalidateQueries("tree");
queryClient.invalidateQueries({queryKey: ["paths"]});
queryClient.invalidateQueries({queryKey: ["tree"]});
}
}
);
});
};

View File

@@ -1,80 +1,73 @@
import {useConfig} from "../../ConfigProvider";
import {useMutation, useQuery, useQueryClient} from "react-query";
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query";
import {fetch_} from "../request-utils";
export const usePathSettings = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useQuery(
"path_settings",
() => fetch_(`${config.BACKEND_HOST}/api/setting/path/`),
{
return useQuery({
queryKey: ["path_settings"],
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/setting/path/`),
onSuccess: (data) => {
if(data){
for(const setting of data)
queryClient.setQueryData(["path_setting", setting.id], setting);
queryClient.setQueryData({queryKey: ["path_setting", setting.id]}, setting);
}
}
}
);
});
};
export const usePathSetting = (setting_id) => {
const config = useConfig();
return useQuery(
["path_setting", setting_id],
() => fetch_(`${config.BACKEND_HOST}/api/setting/path/${setting_id}`),
{
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(
(data) => fetch_(`${config.BACKEND_HOST}/api/setting/path/`, {
return useMutation({
mutationFn: (data) => fetch_(`${config.BACKEND_HOST}/api/setting/path/`, {
method: "POST",
body: JSON.stringify(data)
}), {
}),
onSuccess: (data) => {
queryClient.invalidateQueries(["path_setting", data.id]);
queryClient.invalidateQueries("path_settings");
queryClient.invalidateQueries({queryKey: ["path_setting", data.id]});
queryClient.invalidateQueries({queryKey: ["path_settings"]});
}
}
);
});
};
export const useUpdatePathSetting = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
({id, data}) => fetch_(`${config.BACKEND_HOST}/api/setting/path/${id}`, {
return useMutation({
mutationFn: ({id, data}) => fetch_(`${config.BACKEND_HOST}/api/setting/path/${id}`, {
method: "PATCH",
body: JSON.stringify(data)
}), {
}),
onSuccess: (data, variables) => {
queryClient.invalidateQueries(["path_setting", variables.id]);
queryClient.invalidateQueries("path_settings");
queryClient.invalidateQueries({queryKey: ["path_setting", variables.id]});
queryClient.invalidateQueries({queryKey: ["path_settings"]});
}
}
);
});
};
export const useDeletePathSetting = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
(id) => fetch_(`${config.BACKEND_HOST}/api/setting/path/${id}`, {
return useMutation({
mutationFn: (id) => fetch_(`${config.BACKEND_HOST}/api/setting/path/${id}`, {
method: "DELETE",
}),{
}),
onSuccess: (data, variables) => {
queryClient.invalidateQueries(["path_setting", variables.id]);
queryClient.invalidateQueries("path_settings");
queryClient.invalidateQueries({queryKey: ["path_setting", variables.id]});
queryClient.invalidateQueries({queryKey: ["path_settings"]});
}
}
);
});
};

View File

@@ -1,4 +1,4 @@
import {useQuery, useMutation, useQueryClient} from "react-query";
import {useQuery, useMutation, useQueryClient} from "@tanstack/react-query";
import {fetch_} from "../request-utils";
import {useConfig} from "../../ConfigProvider";
@@ -6,14 +6,12 @@ import {useConfig} from "../../ConfigProvider";
export const useTree = () => {
const queryClient = useQueryClient();
const config = useConfig();
return useQuery(
"tree",
() => fetch_(`${config.BACKEND_HOST}/api/tree/`),
{
return useQuery({
queryKey: ["tree"],
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/tree/`),
onSuccess: data => {
if(data)
queryClient.setQueryData("tree", data);
}
}
);
queryClient.setQueryData({queryKey: ["tree"]}, data);
}
});
};

View File

@@ -1,101 +1,90 @@
import {fetch_ } from "../request-utils"
import {useConfig} from "../../ConfigProvider";
import {useMutation, useQuery, useQueryClient} from "react-query";
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query";
export const useWebhooks = () =>{
const queryClient = useQueryClient();
const config = useConfig();
return useQuery(
"webhooks",
() => fetch_(`${config.BACKEND_HOST}/api/webhook/`),
{
return useQuery({
queryKey: ["webhooks"],
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/webhook/`),
onSuccess: (data) => {
if(data){
for(const webhook of data){
queryClient.setQueryData(["webhook", data.id], data);
queryClient.setQueryData({queryKey: ["webhook", data.id]}, data);
}
}
}
}
);
});
};
export const useCreateWebhook = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
(data) => fetch_(`${config.BACKEND_HOST}/api/webhook/`, {
return useMutation({
mutationFn: (data) => fetch_(`${config.BACKEND_HOST}/api/webhook/`, {
method: "POST",
body: JSON.stringify({
"hook_url": data
}),
}),
{
onSuccess: () => {
queryClient.invalidateQueries("webhooks");
queryClient.invalidateQueries({queryKey: ["webhooks"]});
}
}
);
});
};
export const useUpdateWebhook = () =>{
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
({id, data}) => fetch_(`${config.BACKEND_HOST}/api/webhook/${id}`, {
return useMutation({
mutationFn: ({id, data}) => fetch_(`${config.BACKEND_HOST}/api/webhook/${id}`, {
method: "PATCH",
body: JSON.stringify(data)
}),
{
onSuccess: (res) => {
queryClient.invalidateQueries(["webhook", res.id]);
queryClient.invalidateQueries("webhooks");
queryClient.invalidateQueries({queryKey: ["webhook", res.id]});
queryClient.invalidateQueries({queryKey: ["webhooks"]});
}
}
);
});
};
export const useDeleteWebhook = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
(id) => fetch_(`${config.BACKEND_HOST}/api/webhook/${id}`, {
return useMutation({
mutationFn: (id) => fetch_(`${config.BACKEND_HOST}/api/webhook/${id}`, {
method: "DELETE",
}),
{
onSuccess: (res, variables) => {
queryClient.invalidateQueries(["webhook", variables.id]);
queryClient.invalidateQueries("webhooks");
queryClient.invalidateQueries({queryKey: ["webhook", variables.id]});
queryClient.invalidateQueries({queryKey: ["webhooks"]});
}
}
)
});
}
export const useWebhookSettings = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useQuery(
"webhook_setting",
() => fetch_(`${config.BACKEND_HOST}/api/setting/path/webhook/`),
{
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(["webhook_setting", setting.id], setting);
queryClient.setQueryData({queryKey: ["webhook_setting", setting.id]}, setting);
}
}
}
}
);
});
};
export const useWebhookSetting = (setting_id) => {
const config = useConfig();
return useQuery(
["webhook_setting", setting_id],
() => fetch_(`${config.BACKEND_HOST}/api/setting/path/webhook/${setting_id}`),
{
return useQuery({
queryKey: ["webhook_setting", setting_id],
queryFn: () => fetch_(`${config.BACKEND_HOST}/api/setting/path/webhook/${setting_id}`),
enabled: !!setting_id,
});
};
@@ -104,49 +93,44 @@ export const useWebhookSetting = (setting_id) => {
export const useCreateWebhookSetting = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
(data) => fetch_(`${config.BACKEND_HOST}/api/setting/path/webhook/`, {
return useMutation({
mutationFn: (data) => fetch_(`${config.BACKEND_HOST}/api/setting/path/webhook/`, {
method: "POST",
body: JSON.stringify(data)
}),{
}),
onSuccess: (res) => {
queryClient.invalidateQueries(["webhook_setting", res.id]);
queryClient.invalidateQueries("webhook_setting");
queryClient.invalidateQueries({queryKey: ["webhook_setting", res.id]});
queryClient.invalidateQueries({queryKey: ["webhook_setting"]});
}
}
);
});
};
export const useUpdateWebhookSetting = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
({id, data}) => fetch_(`${config.BACKEND_HOST}/api/setting/path/webhook/${id}`, {
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(["webhook_setting", variables.id]);
queryClient.invalidateQueries("webhook_setting");
queryClient.invalidateQueries({queryKey: ["webhook_setting", variables.id]});
queryClient.invalidateQueries({queryKey: ["webhook_setting"]});
}
}
);
});
};
export const useDeleteWebhookSetting = () => {
const config = useConfig();
const queryClient = useQueryClient();
return useMutation(
(id) => fetch_(`${config.BACKEND_HOST}/api/setting/path/webhook/${id}`, {
return useMutation({
mutationFn: (id) => fetch_(`${config.BACKEND_HOST}/api/setting/path/webhook/${id}`, {
method: "DELETE",
}),
{
onSuccess: (res, variables) => {
queryClient.invalidateQueries(["webhook_setting", variables.id]);
queryClient.invalidateQueries("webhook_setting");
queryClient.invalidateQueries({queryKey: ["webhook_setting", variables.id]});
queryClient.invalidateQueries({queryKey: ["webhook_setting"]});
}
}
);
});
};

View File

@@ -30,7 +30,7 @@ module.exports = {
inject: true
}),
new webpack.ProvidePlugin({
process: 'process/browser'
process: 'process/browser.js'
})
],
@@ -42,11 +42,14 @@ module.exports = {
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"),
process: require.resolve("process/browser.js"),
}
},
devtool: 'source-map',