mirror of https://github.com/usememos/memos.git
refactor(web): improve MemoDetail and sidebar maintainability (#5769)
Co-authored-by: memoclaw <265580040+memoclaw@users.noreply.github.com>
This commit is contained in:
parent
e176b28c80
commit
2aaca27bd5
|
|
@ -51,7 +51,6 @@
|
||||||
"mime": "^4.1.0",
|
"mime": "^4.1.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-force-graph-2d": "^1.29.1",
|
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-i18next": "^15.7.4",
|
"react-i18next": "^15.7.4",
|
||||||
"react-leaflet": "^4.2.1",
|
"react-leaflet": "^4.2.1",
|
||||||
|
|
@ -73,7 +72,6 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.4.7",
|
"@biomejs/biome": "^2.4.7",
|
||||||
"baseline-browser-mapping": "^2.10.8",
|
|
||||||
"@bufbuild/protobuf": "^2.11.0",
|
"@bufbuild/protobuf": "^2.11.0",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/hast": "^3.0.4",
|
"@types/hast": "^3.0.4",
|
||||||
|
|
@ -89,6 +87,7 @@
|
||||||
"@types/unist": "^3.0.3",
|
"@types/unist": "^3.0.3",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-react": "^4.7.0",
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
|
"baseline-browser-mapping": "^2.10.8",
|
||||||
"long": "^5.3.2",
|
"long": "^5.3.2",
|
||||||
"terser": "^5.46.1",
|
"terser": "^5.46.1",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
|
|
|
||||||
|
|
@ -122,9 +122,6 @@ importers:
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 18.3.1(react@18.3.1)
|
version: 18.3.1(react@18.3.1)
|
||||||
react-force-graph-2d:
|
|
||||||
specifier: ^1.29.1
|
|
||||||
version: 1.29.1(react@18.3.1)
|
|
||||||
react-hot-toast:
|
react-hot-toast:
|
||||||
specifier: ^2.6.0
|
specifier: ^2.6.0
|
||||||
version: 2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
|
@ -1363,9 +1360,6 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18 || ^19
|
react: ^18 || ^19
|
||||||
|
|
||||||
'@tweenjs/tween.js@25.0.0':
|
|
||||||
resolution: {integrity: sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==}
|
|
||||||
|
|
||||||
'@types/babel__core@7.20.5':
|
'@types/babel__core@7.20.5':
|
||||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||||
|
|
||||||
|
|
@ -1557,10 +1551,6 @@ packages:
|
||||||
'@xobotyi/scrollbar-width@1.9.5':
|
'@xobotyi/scrollbar-width@1.9.5':
|
||||||
resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==}
|
resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==}
|
||||||
|
|
||||||
accessor-fn@1.5.3:
|
|
||||||
resolution: {integrity: sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
|
|
||||||
acorn@8.15.0:
|
acorn@8.15.0:
|
||||||
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
|
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
@ -1582,9 +1572,6 @@ packages:
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
bezier-js@6.1.4:
|
|
||||||
resolution: {integrity: sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==}
|
|
||||||
|
|
||||||
browserslist@4.28.0:
|
browserslist@4.28.0:
|
||||||
resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==}
|
resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==}
|
||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
|
|
@ -1600,10 +1587,6 @@ packages:
|
||||||
caniuse-lite@1.0.30001757:
|
caniuse-lite@1.0.30001757:
|
||||||
resolution: {integrity: sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==}
|
resolution: {integrity: sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==}
|
||||||
|
|
||||||
canvas-color-tracker@1.3.2:
|
|
||||||
resolution: {integrity: sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
|
|
||||||
ccount@2.0.1:
|
ccount@2.0.1:
|
||||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||||
|
|
||||||
|
|
@ -1712,9 +1695,6 @@ packages:
|
||||||
resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
|
resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
d3-binarytree@1.0.2:
|
|
||||||
resolution: {integrity: sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==}
|
|
||||||
|
|
||||||
d3-brush@3.0.0:
|
d3-brush@3.0.0:
|
||||||
resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==}
|
resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
@ -1756,10 +1736,6 @@ packages:
|
||||||
resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==}
|
resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
d3-force-3d@3.0.6:
|
|
||||||
resolution: {integrity: sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
|
|
||||||
d3-force@3.0.0:
|
d3-force@3.0.0:
|
||||||
resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
|
resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
@ -1780,9 +1756,6 @@ packages:
|
||||||
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
|
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
d3-octree@1.1.0:
|
|
||||||
resolution: {integrity: sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==}
|
|
||||||
|
|
||||||
d3-path@1.0.9:
|
d3-path@1.0.9:
|
||||||
resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==}
|
resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==}
|
||||||
|
|
||||||
|
|
@ -1952,14 +1925,6 @@ packages:
|
||||||
find-root@1.1.0:
|
find-root@1.1.0:
|
||||||
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
|
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
|
||||||
|
|
||||||
float-tooltip@1.7.5:
|
|
||||||
resolution: {integrity: sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
|
|
||||||
force-graph@1.51.0:
|
|
||||||
resolution: {integrity: sha512-aTnihCmiMA0ItLJLCbrQYS9mzriopW24goFPgUnKAAmAlPogTSmFWqoBPMXzIfPb7bs04Hur5zEI4WYgLW3Sig==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
|
@ -2073,10 +2038,6 @@ packages:
|
||||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
index-array-by@1.4.2:
|
|
||||||
resolution: {integrity: sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
|
|
||||||
inline-style-parser@0.2.7:
|
inline-style-parser@0.2.7:
|
||||||
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
|
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
|
||||||
|
|
||||||
|
|
@ -2113,10 +2074,6 @@ packages:
|
||||||
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
|
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
jerrypick@1.1.2:
|
|
||||||
resolution: {integrity: sha512-YKnxXEekXKzhpf7CLYA0A+oDP8V0OhICNCr5lv96FvSsDEmrb0GKM776JgQvHTMjr7DTTPEVv/1Ciaw0uEWzBA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
|
|
||||||
jiti@2.6.1:
|
jiti@2.6.1:
|
||||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
@ -2140,10 +2097,6 @@ packages:
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
kapsule@1.16.3:
|
|
||||||
resolution: {integrity: sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
|
|
||||||
katex@0.16.38:
|
katex@0.16.38:
|
||||||
resolution: {integrity: sha512-cjHooZUmIAUmDsHBN+1n8LaZdpmbj03LtYeYPyuYB7OuloiaeaV6N4LcfjcnHVzGWjVQmKrxxTrpDcmSzEZQwQ==}
|
resolution: {integrity: sha512-cjHooZUmIAUmDsHBN+1n8LaZdpmbj03LtYeYPyuYB7OuloiaeaV6N4LcfjcnHVzGWjVQmKrxxTrpDcmSzEZQwQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
@ -2454,10 +2407,6 @@ packages:
|
||||||
node-releases@2.0.27:
|
node-releases@2.0.27:
|
||||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||||
|
|
||||||
object-assign@4.1.1:
|
|
||||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
|
|
||||||
package-manager-detector@1.5.0:
|
package-manager-detector@1.5.0:
|
||||||
resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==}
|
resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==}
|
||||||
|
|
||||||
|
|
@ -2511,12 +2460,6 @@ packages:
|
||||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
preact@10.27.2:
|
|
||||||
resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==}
|
|
||||||
|
|
||||||
prop-types@15.8.1:
|
|
||||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
|
||||||
|
|
||||||
property-information@6.5.0:
|
property-information@6.5.0:
|
||||||
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
|
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
|
||||||
|
|
||||||
|
|
@ -2531,12 +2474,6 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.3.1
|
react: ^18.3.1
|
||||||
|
|
||||||
react-force-graph-2d@1.29.1:
|
|
||||||
resolution: {integrity: sha512-1Rl/1Z3xy2iTHKj6a0jRXGyiI86xUti81K+jBQZ+Oe46csaMikp47L5AjrzA9hY9fNGD63X8ffrqnvaORukCuQ==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
peerDependencies:
|
|
||||||
react: '*'
|
|
||||||
|
|
||||||
react-hot-toast@2.6.0:
|
react-hot-toast@2.6.0:
|
||||||
resolution: {integrity: sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==}
|
resolution: {integrity: sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
@ -2563,12 +2500,6 @@ packages:
|
||||||
react-is@16.13.1:
|
react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
react-kapsule@2.5.7:
|
|
||||||
resolution: {integrity: sha512-kifAF4ZPD77qZKc4CKLmozq6GY1sBzPEJTIJb0wWFK6HsePJatK3jXplZn2eeAt3x67CDozgi7/rO8fNQ/AL7A==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
peerDependencies:
|
|
||||||
react: '>=16.13.1'
|
|
||||||
|
|
||||||
react-leaflet-cluster@2.1.0:
|
react-leaflet-cluster@2.1.0:
|
||||||
resolution: {integrity: sha512-16X7XQpRThQFC4PH4OpXHimGg19ouWmjxjtpxOeBKpvERSvIRqTx7fvhTwkEPNMFTQ8zTfddz6fRTUmUEQul7g==}
|
resolution: {integrity: sha512-16X7XQpRThQFC4PH4OpXHimGg19ouWmjxjtpxOeBKpvERSvIRqTx7fvhTwkEPNMFTQ8zTfddz6fRTUmUEQul7g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -2809,9 +2740,6 @@ packages:
|
||||||
resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==}
|
resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
tinycolor2@1.6.0:
|
|
||||||
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
|
|
||||||
|
|
||||||
tinyexec@1.0.2:
|
tinyexec@1.0.2:
|
||||||
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
|
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
@ -3997,8 +3925,6 @@ snapshots:
|
||||||
'@tanstack/query-core': 5.90.20
|
'@tanstack/query-core': 5.90.20
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
|
|
||||||
'@tweenjs/tween.js@25.0.0': {}
|
|
||||||
|
|
||||||
'@types/babel__core@7.20.5':
|
'@types/babel__core@7.20.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.28.5
|
'@babel/parser': 7.28.5
|
||||||
|
|
@ -4224,8 +4150,6 @@ snapshots:
|
||||||
|
|
||||||
'@xobotyi/scrollbar-width@1.9.5': {}
|
'@xobotyi/scrollbar-width@1.9.5': {}
|
||||||
|
|
||||||
accessor-fn@1.5.3: {}
|
|
||||||
|
|
||||||
acorn@8.15.0: {}
|
acorn@8.15.0: {}
|
||||||
|
|
||||||
aria-hidden@1.2.6:
|
aria-hidden@1.2.6:
|
||||||
|
|
@ -4242,8 +4166,6 @@ snapshots:
|
||||||
|
|
||||||
baseline-browser-mapping@2.10.8: {}
|
baseline-browser-mapping@2.10.8: {}
|
||||||
|
|
||||||
bezier-js@6.1.4: {}
|
|
||||||
|
|
||||||
browserslist@4.28.0:
|
browserslist@4.28.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
baseline-browser-mapping: 2.10.8
|
baseline-browser-mapping: 2.10.8
|
||||||
|
|
@ -4258,10 +4180,6 @@ snapshots:
|
||||||
|
|
||||||
caniuse-lite@1.0.30001757: {}
|
caniuse-lite@1.0.30001757: {}
|
||||||
|
|
||||||
canvas-color-tracker@1.3.2:
|
|
||||||
dependencies:
|
|
||||||
tinycolor2: 1.6.0
|
|
||||||
|
|
||||||
ccount@2.0.1: {}
|
ccount@2.0.1: {}
|
||||||
|
|
||||||
character-entities-html4@2.1.0: {}
|
character-entities-html4@2.1.0: {}
|
||||||
|
|
@ -4363,8 +4281,6 @@ snapshots:
|
||||||
|
|
||||||
d3-axis@3.0.0: {}
|
d3-axis@3.0.0: {}
|
||||||
|
|
||||||
d3-binarytree@1.0.2: {}
|
|
||||||
|
|
||||||
d3-brush@3.0.0:
|
d3-brush@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
d3-dispatch: 3.0.1
|
d3-dispatch: 3.0.1
|
||||||
|
|
@ -4406,14 +4322,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
d3-dsv: 3.0.1
|
d3-dsv: 3.0.1
|
||||||
|
|
||||||
d3-force-3d@3.0.6:
|
|
||||||
dependencies:
|
|
||||||
d3-binarytree: 1.0.2
|
|
||||||
d3-dispatch: 3.0.1
|
|
||||||
d3-octree: 1.1.0
|
|
||||||
d3-quadtree: 3.0.1
|
|
||||||
d3-timer: 3.0.1
|
|
||||||
|
|
||||||
d3-force@3.0.0:
|
d3-force@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
d3-dispatch: 3.0.1
|
d3-dispatch: 3.0.1
|
||||||
|
|
@ -4432,8 +4340,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
d3-color: 3.1.0
|
d3-color: 3.1.0
|
||||||
|
|
||||||
d3-octree@1.1.0: {}
|
|
||||||
|
|
||||||
d3-path@1.0.9: {}
|
d3-path@1.0.9: {}
|
||||||
|
|
||||||
d3-path@3.1.0: {}
|
d3-path@3.1.0: {}
|
||||||
|
|
@ -4635,30 +4541,6 @@ snapshots:
|
||||||
|
|
||||||
find-root@1.1.0: {}
|
find-root@1.1.0: {}
|
||||||
|
|
||||||
float-tooltip@1.7.5:
|
|
||||||
dependencies:
|
|
||||||
d3-selection: 3.0.0
|
|
||||||
kapsule: 1.16.3
|
|
||||||
preact: 10.27.2
|
|
||||||
|
|
||||||
force-graph@1.51.0:
|
|
||||||
dependencies:
|
|
||||||
'@tweenjs/tween.js': 25.0.0
|
|
||||||
accessor-fn: 1.5.3
|
|
||||||
bezier-js: 6.1.4
|
|
||||||
canvas-color-tracker: 1.3.2
|
|
||||||
d3-array: 3.2.4
|
|
||||||
d3-drag: 3.0.0
|
|
||||||
d3-force-3d: 3.0.6
|
|
||||||
d3-scale: 4.0.2
|
|
||||||
d3-scale-chromatic: 3.1.0
|
|
||||||
d3-selection: 3.0.0
|
|
||||||
d3-zoom: 3.0.0
|
|
||||||
float-tooltip: 1.7.5
|
|
||||||
index-array-by: 1.4.2
|
|
||||||
kapsule: 1.16.3
|
|
||||||
lodash-es: 4.17.23
|
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|
@ -4827,8 +4709,6 @@ snapshots:
|
||||||
parent-module: 1.0.1
|
parent-module: 1.0.1
|
||||||
resolve-from: 4.0.0
|
resolve-from: 4.0.0
|
||||||
|
|
||||||
index-array-by@1.4.2: {}
|
|
||||||
|
|
||||||
inline-style-parser@0.2.7: {}
|
inline-style-parser@0.2.7: {}
|
||||||
|
|
||||||
inline-style-prefixer@7.0.1:
|
inline-style-prefixer@7.0.1:
|
||||||
|
|
@ -4858,8 +4738,6 @@ snapshots:
|
||||||
|
|
||||||
is-plain-obj@4.1.0: {}
|
is-plain-obj@4.1.0: {}
|
||||||
|
|
||||||
jerrypick@1.1.2: {}
|
|
||||||
|
|
||||||
jiti@2.6.1: {}
|
jiti@2.6.1: {}
|
||||||
|
|
||||||
js-cookie@2.2.1: {}
|
js-cookie@2.2.1: {}
|
||||||
|
|
@ -4872,10 +4750,6 @@ snapshots:
|
||||||
|
|
||||||
json5@2.2.3: {}
|
json5@2.2.3: {}
|
||||||
|
|
||||||
kapsule@1.16.3:
|
|
||||||
dependencies:
|
|
||||||
lodash-es: 4.17.23
|
|
||||||
|
|
||||||
katex@0.16.38:
|
katex@0.16.38:
|
||||||
dependencies:
|
dependencies:
|
||||||
commander: 8.3.0
|
commander: 8.3.0
|
||||||
|
|
@ -5412,8 +5286,6 @@ snapshots:
|
||||||
|
|
||||||
node-releases@2.0.27: {}
|
node-releases@2.0.27: {}
|
||||||
|
|
||||||
object-assign@4.1.1: {}
|
|
||||||
|
|
||||||
package-manager-detector@1.5.0: {}
|
package-manager-detector@1.5.0: {}
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
|
|
@ -5478,14 +5350,6 @@ snapshots:
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
preact@10.27.2: {}
|
|
||||||
|
|
||||||
prop-types@15.8.1:
|
|
||||||
dependencies:
|
|
||||||
loose-envify: 1.4.0
|
|
||||||
object-assign: 4.1.1
|
|
||||||
react-is: 16.13.1
|
|
||||||
|
|
||||||
property-information@6.5.0: {}
|
property-information@6.5.0: {}
|
||||||
|
|
||||||
property-information@7.1.0: {}
|
property-information@7.1.0: {}
|
||||||
|
|
@ -5498,13 +5362,6 @@ snapshots:
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
scheduler: 0.23.2
|
scheduler: 0.23.2
|
||||||
|
|
||||||
react-force-graph-2d@1.29.1(react@18.3.1):
|
|
||||||
dependencies:
|
|
||||||
force-graph: 1.51.0
|
|
||||||
prop-types: 15.8.1
|
|
||||||
react: 18.3.1
|
|
||||||
react-kapsule: 2.5.7(react@18.3.1)
|
|
||||||
|
|
||||||
react-hot-toast@2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
react-hot-toast@2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.2.3
|
csstype: 3.2.3
|
||||||
|
|
@ -5524,11 +5381,6 @@ snapshots:
|
||||||
|
|
||||||
react-is@16.13.1: {}
|
react-is@16.13.1: {}
|
||||||
|
|
||||||
react-kapsule@2.5.7(react@18.3.1):
|
|
||||||
dependencies:
|
|
||||||
jerrypick: 1.1.2
|
|
||||||
react: 18.3.1
|
|
||||||
|
|
||||||
react-leaflet-cluster@2.1.0(leaflet@1.9.4)(react-dom@18.3.1(react@18.3.1))(react-leaflet@4.2.1(leaflet@1.9.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
|
react-leaflet-cluster@2.1.0(leaflet@1.9.4)(react-dom@18.3.1(react@18.3.1))(react-leaflet@4.2.1(leaflet@1.9.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
leaflet: 1.9.4
|
leaflet: 1.9.4
|
||||||
|
|
@ -5838,8 +5690,6 @@ snapshots:
|
||||||
|
|
||||||
throttle-debounce@3.0.1: {}
|
throttle-debounce@3.0.1: {}
|
||||||
|
|
||||||
tinycolor2@1.6.0: {}
|
|
||||||
|
|
||||||
tinyexec@1.0.2: {}
|
tinyexec@1.0.2: {}
|
||||||
|
|
||||||
tinyglobby@0.2.15:
|
tinyglobby@0.2.15:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { MessageCircleIcon } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import MemoEditor from "@/components/MemoEditor";
|
||||||
|
import MemoView from "@/components/MemoView";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { extractMemoIdFromName } from "@/helpers/resource-names";
|
||||||
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
|
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
|
||||||
|
import { useTranslate } from "@/utils/i18n";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
memo: Memo;
|
||||||
|
comments: Memo[];
|
||||||
|
parentPage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemoCommentSection = ({ memo, comments, parentPage }: Props) => {
|
||||||
|
const t = useTranslate();
|
||||||
|
const currentUser = useCurrentUser();
|
||||||
|
const [showEditor, setShowEditor] = useState(false);
|
||||||
|
|
||||||
|
const showCreateButton = currentUser && !showEditor;
|
||||||
|
|
||||||
|
const handleCommentCreated = async (_memoCommentName: string) => {
|
||||||
|
setShowEditor(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pt-8 pb-16 w-full">
|
||||||
|
<h2 id="comments" className="sr-only">
|
||||||
|
{t("memo.comment.self")}
|
||||||
|
</h2>
|
||||||
|
<div className="relative mx-auto grow w-full min-h-full flex flex-col justify-start items-start gap-y-1">
|
||||||
|
{comments.length === 0 ? (
|
||||||
|
showCreateButton && (
|
||||||
|
<div className="w-full flex flex-row justify-center items-center py-6">
|
||||||
|
<Button variant="ghost" onClick={() => setShowEditor(true)}>
|
||||||
|
<span className="text-muted-foreground">{t("memo.comment.write-a-comment")}</span>
|
||||||
|
<MessageCircleIcon className="ml-2 w-5 h-auto text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="w-full flex flex-row justify-between items-center h-8 pl-3 mb-2">
|
||||||
|
<div className="flex flex-row justify-start items-center">
|
||||||
|
<MessageCircleIcon className="w-5 h-auto text-muted-foreground mr-1" />
|
||||||
|
<span className="text-muted-foreground text-sm">{t("memo.comment.self")}</span>
|
||||||
|
<span className="text-muted-foreground text-sm ml-1">({comments.length})</span>
|
||||||
|
</div>
|
||||||
|
{showCreateButton && (
|
||||||
|
<Button variant="ghost" className="text-muted-foreground" onClick={() => setShowEditor(true)}>
|
||||||
|
{t("memo.comment.write-a-comment")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showEditor && (
|
||||||
|
<div className="w-full mb-2">
|
||||||
|
<MemoEditor
|
||||||
|
cacheKey={`${memo.name}-${memo.updateTime}-comment`}
|
||||||
|
placeholder={t("editor.add-your-comment-here")}
|
||||||
|
parentMemoName={memo.name}
|
||||||
|
autoFocus
|
||||||
|
onConfirm={handleCommentCreated}
|
||||||
|
onCancel={() => setShowEditor(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{comments.map((comment) => (
|
||||||
|
<div className="w-full" key={`${comment.name}-${comment.displayTime}`} id={extractMemoIdFromName(comment.name)}>
|
||||||
|
<MemoView memo={comment} parentPage={parentPage} showCreator compact />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemoCommentSection;
|
||||||
|
|
@ -1,116 +1,104 @@
|
||||||
import { create } from "@bufbuild/protobuf";
|
import { create } from "@bufbuild/protobuf";
|
||||||
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
||||||
import { isEqual } from "lodash-es";
|
import { isEqual } from "lodash-es";
|
||||||
import { CheckCircleIcon, Code2Icon, HashIcon, LinkIcon, Share2Icon } from "lucide-react";
|
import { CheckCircleIcon, Code2Icon, HashIcon, LinkIcon, type LucideIcon, Share2Icon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import MemoSharePanel from "@/components/MemoSharePanel";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Memo, Memo_PropertySchema, MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
|
import { Memo, Memo_PropertySchema } from "@/types/proto/api/v1/memo_service_pb";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { type Translations, useTranslate } from "@/utils/i18n";
|
||||||
import { isSuperUser } from "@/utils/user";
|
import { isSuperUser } from "@/utils/user";
|
||||||
import MemoRelationForceGraph from "../MemoRelationForceGraph";
|
import MemoSharePanel from "./MemoSharePanel";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
memo: Memo;
|
memo: Memo;
|
||||||
className?: string;
|
className?: string;
|
||||||
parentPage?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SectionLabel = ({ children }: { children: React.ReactNode }) => (
|
interface PropertyBadge {
|
||||||
<p className="text-xs font-medium text-muted-foreground/50 uppercase tracking-wider">{children}</p>
|
icon: LucideIcon;
|
||||||
|
labelKey: Translations;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarSection = ({ label, count, children }: { label: string; count?: number; children: React.ReactNode }) => (
|
||||||
|
<div className="w-full space-y-2">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<p className="text-xs font-medium text-muted-foreground/50 uppercase tracking-wider">{label}</p>
|
||||||
|
{count != null && <span className="text-xs text-muted-foreground/30">({count})</span>}
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => {
|
const PROPERTY_BADGE_CLASSES =
|
||||||
|
"inline-flex items-center gap-1.5 px-2 py-1 rounded-md border border-border/60 bg-muted/60 text-xs text-muted-foreground";
|
||||||
|
|
||||||
|
const TAG_BADGE_CLASSES =
|
||||||
|
"inline-flex items-center gap-1 px-1 rounded-md border border-border/60 bg-muted/60 text-sm text-muted-foreground hover:bg-muted hover:text-foreground/80 transition-colors cursor-pointer";
|
||||||
|
|
||||||
|
const MemoDetailSidebar = ({ memo, className }: Props) => {
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
const [sharePanelOpen, setSharePanelOpen] = useState(false);
|
const [sharePanelOpen, setSharePanelOpen] = useState(false);
|
||||||
const property = create(Memo_PropertySchema, memo.property || {});
|
const property = create(Memo_PropertySchema, memo.property || {});
|
||||||
const hasSpecialProperty = property.hasLink || property.hasTaskList || property.hasCode;
|
|
||||||
const hasReferenceRelations = memo.relations.some((r) => r.type === MemoRelation_Type.REFERENCE);
|
|
||||||
const canManageShares = !memo.parent && (memo.creator === currentUser?.name || isSuperUser(currentUser));
|
const canManageShares = !memo.parent && (memo.creator === currentUser?.name || isSuperUser(currentUser));
|
||||||
|
const hasUpdated = !isEqual(memo.createTime, memo.updateTime);
|
||||||
|
|
||||||
|
const propertyBadges = useMemo(() => {
|
||||||
|
const badges: PropertyBadge[] = [];
|
||||||
|
if (property.hasLink) badges.push({ icon: LinkIcon, labelKey: "memo.links" });
|
||||||
|
if (property.hasTaskList) badges.push({ icon: CheckCircleIcon, labelKey: "memo.to-do" });
|
||||||
|
if (property.hasCode) badges.push({ icon: Code2Icon, labelKey: "memo.code" });
|
||||||
|
return badges;
|
||||||
|
}, [property.hasLink, property.hasTaskList, property.hasCode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={cn("relative w-full h-auto max-h-screen overflow-auto flex flex-col gap-5", className)}>
|
<aside className={cn("relative w-full h-auto max-h-screen overflow-auto flex flex-col gap-5", className)}>
|
||||||
{canManageShares && (
|
{canManageShares && (
|
||||||
<div className="w-full space-y-2">
|
<SidebarSection label={t("memo.share.section-label")}>
|
||||||
<SectionLabel>{t("memo.share.section-label")}</SectionLabel>
|
|
||||||
<Button variant="outline" className="w-full justify-start gap-2" onClick={() => setSharePanelOpen(true)}>
|
<Button variant="outline" className="w-full justify-start gap-2" onClick={() => setSharePanelOpen(true)}>
|
||||||
<Share2Icon className="w-4 h-4" />
|
<Share2Icon className="w-4 h-4" />
|
||||||
{t("memo.share.open-panel")}
|
{t("memo.share.open-panel")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</SidebarSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasReferenceRelations && (
|
<SidebarSection label={t("common.created-at")}>
|
||||||
<div className="w-full space-y-2">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex items-center gap-1.5">
|
<p className="text-sm text-foreground/70">{memo.createTime ? timestampDate(memo.createTime).toLocaleString() : "—"}</p>
|
||||||
<SectionLabel>{t("common.relations")}</SectionLabel>
|
{hasUpdated && (
|
||||||
<span className="text-xs text-muted-foreground/30">(Beta)</span>
|
<p className="text-xs text-muted-foreground">
|
||||||
</div>
|
{t("common.last-updated-at")}: {memo.updateTime ? timestampDate(memo.updateTime).toLocaleString() : "—"}
|
||||||
<div className="relative w-full h-36 border border-border rounded-lg bg-muted overflow-hidden">
|
</p>
|
||||||
<MemoRelationForceGraph className="w-full h-full" memo={memo} parentPage={parentPage} />
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</SidebarSection>
|
||||||
|
|
||||||
<div className="w-full space-y-1">
|
{propertyBadges.length > 0 && (
|
||||||
<SectionLabel>{t("common.created-at")}</SectionLabel>
|
<SidebarSection label={t("common.properties")}>
|
||||||
<p className="text-sm text-foreground/70">{memo.createTime ? timestampDate(memo.createTime).toLocaleString() : "—"}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isEqual(memo.createTime, memo.updateTime) && (
|
|
||||||
<div className="w-full space-y-1">
|
|
||||||
<SectionLabel>{t("common.last-updated-at")}</SectionLabel>
|
|
||||||
<p className="text-sm text-foreground/70">{memo.updateTime ? timestampDate(memo.updateTime).toLocaleString() : "—"}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasSpecialProperty && (
|
|
||||||
<div className="w-full space-y-2">
|
|
||||||
<SectionLabel>{t("common.properties")}</SectionLabel>
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{property.hasLink && (
|
{propertyBadges.map(({ icon: Icon, labelKey }) => (
|
||||||
<span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md border border-border/60 bg-muted/60 text-xs text-muted-foreground">
|
<span key={labelKey} className={PROPERTY_BADGE_CLASSES}>
|
||||||
<LinkIcon className="w-3.5 h-3.5" />
|
<Icon className="w-3.5 h-3.5" />
|
||||||
{t("memo.links")}
|
{t(labelKey)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
))}
|
||||||
{property.hasTaskList && (
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md border border-border/60 bg-muted/60 text-xs text-muted-foreground">
|
|
||||||
<CheckCircleIcon className="w-3.5 h-3.5" />
|
|
||||||
{t("memo.to-do")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{property.hasCode && (
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md border border-border/60 bg-muted/60 text-xs text-muted-foreground">
|
|
||||||
<Code2Icon className="w-3.5 h-3.5" />
|
|
||||||
{t("memo.code")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SidebarSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{memo.tags.length > 0 && (
|
{memo.tags.length > 0 && (
|
||||||
<div className="w-full space-y-2">
|
<SidebarSection label={t("common.tags")} count={memo.tags.length}>
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<SectionLabel>{t("common.tags")}</SectionLabel>
|
|
||||||
<span className="text-xs text-muted-foreground/30">({memo.tags.length})</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{memo.tags.map((tag) => (
|
{memo.tags.map((tag) => (
|
||||||
<span
|
<span key={tag} className={TAG_BADGE_CLASSES}>
|
||||||
key={tag}
|
|
||||||
className="inline-flex items-center gap-1 px-1 rounded-md border border-border/60 bg-muted/60 text-sm text-muted-foreground hover:bg-muted hover:text-foreground/80 transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<HashIcon className="w-3 h-3 opacity-50" />
|
<HashIcon className="w-3 h-3 opacity-50" />
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SidebarSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sharePanelOpen && <MemoSharePanel memoName={memo.name} open={sharePanelOpen} onClose={() => setSharePanelOpen(false)} />}
|
{sharePanelOpen && <MemoSharePanel memoName={memo.name} open={sharePanelOpen} onClose={() => setSharePanelOpen(false)} />}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,9 @@ import MemoDetailSidebar from "./MemoDetailSidebar";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
memo: Memo;
|
memo: Memo;
|
||||||
parentPage?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MemoDetailSidebarDrawer = ({ memo, parentPage }: Props) => {
|
const MemoDetailSidebarDrawer = ({ memo }: Props) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
|
@ -27,7 +26,7 @@ const MemoDetailSidebarDrawer = ({ memo, parentPage }: Props) => {
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent side="right" className="w-full sm:w-80 px-4 bg-background">
|
<SheetContent side="right" className="w-full sm:w-80 px-4 bg-background">
|
||||||
<MemoDetailSidebar className="py-4" memo={memo} parentPage={parentPage} />
|
<MemoDetailSidebar className="py-4" memo={memo} />
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import ForceGraph2D, { ForceGraphMethods, LinkObject, NodeObject } from "react-force-graph-2d";
|
|
||||||
import { extractMemoIdFromName } from "@/helpers/resource-names";
|
|
||||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Memo, MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
|
|
||||||
import { LinkType, NodeType } from "./types";
|
|
||||||
import { convertMemoRelationsToGraphData } from "./utils";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
memo: Memo;
|
|
||||||
className?: string;
|
|
||||||
parentPage?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAIN_NODE_COLOR = "#14b8a6";
|
|
||||||
const DEFAULT_NODE_COLOR = "#a1a1aa";
|
|
||||||
|
|
||||||
const MemoRelationForceGraph = ({ className, memo, parentPage }: Props) => {
|
|
||||||
const navigateTo = useNavigateTo();
|
|
||||||
const [mode] = useState<"light">("light");
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const graphRef = useRef<ForceGraphMethods<NodeObject<NodeType>, LinkObject<NodeType, LinkType>> | undefined>(undefined);
|
|
||||||
const [graphSize, setGraphSize] = useState({ width: 0, height: 0 });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!containerRef.current) return;
|
|
||||||
setGraphSize(containerRef.current.getBoundingClientRect());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onNodeClick = (node: NodeObject<NodeType>) => {
|
|
||||||
if (node.memo.name === memo.name) return;
|
|
||||||
navigateTo(`/${memo.name}`, {
|
|
||||||
state: {
|
|
||||||
from: parentPage,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={containerRef} className={cn("opacity-80", className)}>
|
|
||||||
<ForceGraph2D
|
|
||||||
ref={graphRef}
|
|
||||||
width={graphSize.width}
|
|
||||||
height={graphSize.height}
|
|
||||||
enableZoomInteraction
|
|
||||||
cooldownTicks={0}
|
|
||||||
nodeColor={(node) => (node.id === memo.name ? MAIN_NODE_COLOR : DEFAULT_NODE_COLOR)}
|
|
||||||
nodeRelSize={3}
|
|
||||||
nodeLabel={(node) => extractMemoIdFromName(node.memo.name).slice(0, 6).toLowerCase()}
|
|
||||||
linkColor={() => (mode === "light" ? "#e4e4e7" : "#3f3f46")}
|
|
||||||
graphData={convertMemoRelationsToGraphData(memo.relations.filter((r) => r.type === MemoRelation_Type.REFERENCE))}
|
|
||||||
onNodeClick={onNodeClick}
|
|
||||||
linkDirectionalArrowLength={3}
|
|
||||||
linkDirectionalArrowRelPos={1}
|
|
||||||
linkCurvature={0.25}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MemoRelationForceGraph;
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import MemoRelationForceGraph from "./MemoRelationForceGraph";
|
|
||||||
|
|
||||||
export * from "./utils";
|
|
||||||
|
|
||||||
export default MemoRelationForceGraph;
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import { MemoRelation_Memo } from "@/types/proto/api/v1/memo_service_pb";
|
|
||||||
|
|
||||||
export interface NodeType {
|
|
||||||
memo: MemoRelation_Memo;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
||||||
export interface LinkType {
|
|
||||||
// ...add more additional properties relevant to the link here.
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import { GraphData, LinkObject, NodeObject } from "react-force-graph-2d";
|
|
||||||
import { MemoRelation, MemoRelation_Memo } from "@/types/proto/api/v1/memo_service_pb";
|
|
||||||
import { LinkType, NodeType } from "./types";
|
|
||||||
|
|
||||||
export const convertMemoRelationsToGraphData = (memoRelations: MemoRelation[]): GraphData<NodeType, LinkType> => {
|
|
||||||
const nodesMap = new Map<string, NodeObject<NodeType>>();
|
|
||||||
const links: LinkObject<NodeType, LinkType>[] = [];
|
|
||||||
|
|
||||||
// Iterate through memoRelations to populate nodes and links.
|
|
||||||
memoRelations.forEach((relation) => {
|
|
||||||
const memo = relation.memo as MemoRelation_Memo;
|
|
||||||
const relatedMemo = relation.relatedMemo as MemoRelation_Memo;
|
|
||||||
|
|
||||||
// Add memo node if not already present.
|
|
||||||
if (!nodesMap.has(memo.name)) {
|
|
||||||
nodesMap.set(memo.name, { id: memo.name, memo });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add related_memo node if not already present.
|
|
||||||
if (!nodesMap.has(relatedMemo.name)) {
|
|
||||||
nodesMap.set(relatedMemo.name, { id: relatedMemo.name, memo: relatedMemo });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create link between memo and relatedMemo.
|
|
||||||
links.push({
|
|
||||||
source: memo.name,
|
|
||||||
target: relatedMemo.name,
|
|
||||||
type: relation.type, // Include the type of relation as a property of the link.
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
nodes: Array.from(nodesMap.values()),
|
|
||||||
links,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { Code, ConnectError } from "@connectrpc/connect";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||||
|
import { AUTH_REASON_PROTECTED_MEMO, redirectOnAuthFailure } from "@/utils/auth-redirect";
|
||||||
|
|
||||||
|
interface UseMemoDetailErrorOptions {
|
||||||
|
error: Error | null;
|
||||||
|
pathname: string;
|
||||||
|
search: string;
|
||||||
|
hash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useMemoDetailError = ({ error, pathname, search, hash }: UseMemoDetailErrorOptions) => {
|
||||||
|
const navigateTo = useNavigateTo();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof ConnectError) {
|
||||||
|
if (error.code === Code.Unauthenticated) {
|
||||||
|
redirectOnAuthFailure(true, {
|
||||||
|
redirect: `${pathname}${search}${hash}`,
|
||||||
|
reason: AUTH_REASON_PROTECTED_MEMO,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code === Code.PermissionDenied || error.code === Code.NotFound) {
|
||||||
|
navigateTo("/404", { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(error.message);
|
||||||
|
}, [error, hash, pathname, search, navigateTo]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useMemoDetailError;
|
||||||
|
|
@ -1,70 +1,36 @@
|
||||||
import { Code, ConnectError } from "@connectrpc/connect";
|
import { ArrowUpLeftFromCircleIcon } from "lucide-react";
|
||||||
import { ArrowUpLeftFromCircleIcon, MessageCircleIcon } from "lucide-react";
|
import { useEffect } from "react";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import { Link, useLocation, useParams } from "react-router-dom";
|
import { Link, useLocation, useParams } from "react-router-dom";
|
||||||
|
import MemoCommentSection from "@/components/MemoCommentSection";
|
||||||
import { MemoDetailSidebar, MemoDetailSidebarDrawer } from "@/components/MemoDetailSidebar";
|
import { MemoDetailSidebar, MemoDetailSidebarDrawer } from "@/components/MemoDetailSidebar";
|
||||||
import MemoEditor from "@/components/MemoEditor";
|
|
||||||
import MemoView from "@/components/MemoView";
|
import MemoView from "@/components/MemoView";
|
||||||
import MobileHeader from "@/components/MobileHeader";
|
import MobileHeader from "@/components/MobileHeader";
|
||||||
import { Button } from "@/components/ui/button";
|
import { memoNamePrefix } from "@/helpers/resource-names";
|
||||||
import { extractMemoIdFromName, memoNamePrefix } from "@/helpers/resource-names";
|
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
|
||||||
import useMediaQuery from "@/hooks/useMediaQuery";
|
import useMediaQuery from "@/hooks/useMediaQuery";
|
||||||
|
import useMemoDetailError from "@/hooks/useMemoDetailError";
|
||||||
import { useMemo, useMemoComments } from "@/hooks/useMemoQueries";
|
import { useMemo, useMemoComments } from "@/hooks/useMemoQueries";
|
||||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { AUTH_REASON_PROTECTED_MEMO, redirectOnAuthFailure } from "@/utils/auth-redirect";
|
|
||||||
import { useTranslate } from "@/utils/i18n";
|
|
||||||
|
|
||||||
const MemoDetail = () => {
|
const MemoDetail = () => {
|
||||||
const t = useTranslate();
|
|
||||||
const md = useMediaQuery("md");
|
const md = useMediaQuery("md");
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const navigateTo = useNavigateTo();
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { state: locationState, hash } = location;
|
const { state: locationState, hash } = location;
|
||||||
const currentUser = useCurrentUser();
|
const memoName = `${memoNamePrefix}${params.uid}`;
|
||||||
const uid = params.uid;
|
|
||||||
const memoName = `${memoNamePrefix}${uid}`;
|
|
||||||
const [showCommentEditor, setShowCommentEditor] = useState(false);
|
|
||||||
|
|
||||||
// Fetch main memo with React Query
|
|
||||||
const { data: memo, error, isLoading } = useMemo(memoName, { enabled: !!memoName });
|
const { data: memo, error, isLoading } = useMemo(memoName, { enabled: !!memoName });
|
||||||
|
|
||||||
// Handle errors
|
useMemoDetailError({
|
||||||
useEffect(() => {
|
error: error as Error | null,
|
||||||
if (!error) {
|
pathname: location.pathname,
|
||||||
return;
|
search: location.search,
|
||||||
}
|
hash: location.hash,
|
||||||
|
});
|
||||||
|
|
||||||
if (error instanceof ConnectError) {
|
|
||||||
if (error.code === Code.Unauthenticated) {
|
|
||||||
redirectOnAuthFailure(true, {
|
|
||||||
redirect: `${location.pathname}${location.search}${location.hash}`,
|
|
||||||
reason: AUTH_REASON_PROTECTED_MEMO,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.code === Code.PermissionDenied || error.code === Code.NotFound) {
|
|
||||||
navigateTo("/404", { replace: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.error(error.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.error((error as Error).message);
|
|
||||||
}, [error, location.hash, location.pathname, location.search, navigateTo]);
|
|
||||||
|
|
||||||
// Fetch parent memo if exists
|
|
||||||
const { data: parentMemo } = useMemo(memo?.parent || "", {
|
const { data: parentMemo } = useMemo(memo?.parent || "", {
|
||||||
enabled: !!memo?.parent,
|
enabled: !!memo?.parent,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch all comments for this memo in a single query
|
|
||||||
const { data: commentsResponse } = useMemoComments(memoName, {
|
const { data: commentsResponse } = useMemoComments(memoName, {
|
||||||
enabled: !!memo,
|
enabled: !!memo,
|
||||||
});
|
});
|
||||||
|
|
@ -76,26 +42,15 @@ const MemoDetail = () => {
|
||||||
el?.scrollIntoView({ behavior: "smooth", block: "center" });
|
el?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
}, [hash, comments]);
|
}, [hash, comments]);
|
||||||
|
|
||||||
const showCreateCommentButton = currentUser && !showCommentEditor;
|
|
||||||
|
|
||||||
if (isLoading || !memo) {
|
if (isLoading || !memo) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleShowCommentEditor = () => {
|
|
||||||
setShowCommentEditor(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCommentCreated = async (_memoCommentName: string) => {
|
|
||||||
// React Query will auto-refetch due to invalidation in the mutation
|
|
||||||
setShowCommentEditor(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8">
|
<section className="@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8">
|
||||||
{!md && (
|
{!md && (
|
||||||
<MobileHeader>
|
<MobileHeader>
|
||||||
<MemoDetailSidebarDrawer memo={memo} parentPage={locationState?.from} />
|
<MemoDetailSidebarDrawer memo={memo} />
|
||||||
</MobileHeader>
|
</MobileHeader>
|
||||||
)}
|
)}
|
||||||
<div className={cn("w-full flex flex-row justify-start items-start px-4 sm:px-6 gap-4")}>
|
<div className={cn("w-full flex flex-row justify-start items-start px-4 sm:px-6 gap-4")}>
|
||||||
|
|
@ -115,7 +70,6 @@ const MemoDetail = () => {
|
||||||
)}
|
)}
|
||||||
<MemoView
|
<MemoView
|
||||||
key={`${memo.name}-${memo.displayTime}`}
|
key={`${memo.name}-${memo.displayTime}`}
|
||||||
className="shadow hover:shadow-md transition-all"
|
|
||||||
memo={memo}
|
memo={memo}
|
||||||
compact={false}
|
compact={false}
|
||||||
parentPage={locationState?.from}
|
parentPage={locationState?.from}
|
||||||
|
|
@ -123,57 +77,11 @@ const MemoDetail = () => {
|
||||||
showVisibility
|
showVisibility
|
||||||
showPinned
|
showPinned
|
||||||
/>
|
/>
|
||||||
<div className="pt-8 pb-16 w-full">
|
<MemoCommentSection memo={memo} comments={comments} parentPage={locationState?.from} />
|
||||||
<h2 id="comments" className="sr-only">
|
|
||||||
{t("memo.comment.self")}
|
|
||||||
</h2>
|
|
||||||
<div className="relative mx-auto grow w-full min-h-full flex flex-col justify-start items-start gap-y-1">
|
|
||||||
{comments.length === 0 ? (
|
|
||||||
showCreateCommentButton && (
|
|
||||||
<div className="w-full flex flex-row justify-center items-center py-6">
|
|
||||||
<Button variant="ghost" onClick={handleShowCommentEditor}>
|
|
||||||
<span className="text-muted-foreground">{t("memo.comment.write-a-comment")}</span>
|
|
||||||
<MessageCircleIcon className="ml-2 w-5 h-auto text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className="w-full flex flex-row justify-between items-center h-8 pl-3 mb-2">
|
|
||||||
<div className="flex flex-row justify-start items-center">
|
|
||||||
<MessageCircleIcon className="w-5 h-auto text-muted-foreground mr-1" />
|
|
||||||
<span className="text-muted-foreground text-sm">{t("memo.comment.self")}</span>
|
|
||||||
<span className="text-muted-foreground text-sm ml-1">({comments.length})</span>
|
|
||||||
</div>
|
|
||||||
{showCreateCommentButton && (
|
|
||||||
<Button variant="ghost" className="text-muted-foreground" onClick={handleShowCommentEditor}>
|
|
||||||
{t("memo.comment.write-a-comment")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{showCommentEditor && (
|
|
||||||
<div className="w-full mb-2">
|
|
||||||
<MemoEditor
|
|
||||||
cacheKey={`${memo.name}-${memo.updateTime}-comment`}
|
|
||||||
placeholder={t("editor.add-your-comment-here")}
|
|
||||||
parentMemoName={memo.name}
|
|
||||||
autoFocus
|
|
||||||
onConfirm={handleCommentCreated}
|
|
||||||
onCancel={() => setShowCommentEditor(false)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{comments.map((comment) => (
|
|
||||||
<div className="w-full" key={`${comment.name}-${comment.displayTime}`} id={extractMemoIdFromName(comment.name)}>
|
|
||||||
<MemoView memo={comment} parentPage={locationState?.from} showCreator compact />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{md && (
|
{md && (
|
||||||
<div className="sticky top-0 left-0 shrink-0 -mt-6 w-56 h-full">
|
<div className="sticky top-0 left-0 shrink-0 -mt-6 w-56 h-full">
|
||||||
<MemoDetailSidebar className="py-6" memo={memo} parentPage={locationState?.from} />
|
<MemoDetailSidebar className="py-6" memo={memo} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue