mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 15:44:21 +00:00
Compare commits
21 Commits
v0.5.1
...
efdc42cbd7
| Author | SHA1 | Date | |
|---|---|---|---|
| efdc42cbd7 | |||
| 3926954b74 | |||
| 6c408fdfbe | |||
| 6753f32520 | |||
| de06939b01 | |||
| c11d956ced | |||
| 9a232819a8 | |||
| f9ce8b9e9f | |||
| a6d2663a7d | |||
| 071e99f4da | |||
| b13ee987c7 | |||
| 543dbe6ffe | |||
| d0842c515f | |||
| 01d9a984fc | |||
|
|
a3975c9acd | ||
| e82f25a2ed | |||
| ec89f95880 | |||
| f101376bef | |||
| d582b1a1e9 | |||
| aca127e52e | |||
| 9ca8a46093 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -162,3 +162,6 @@ go.work.sum
|
||||
main
|
||||
*.db
|
||||
data
|
||||
|
||||
# Feature specifications
|
||||
spec.md
|
||||
506
app/package-lock.json
generated
506
app/package-lock.json
generated
@@ -24,9 +24,8 @@
|
||||
"react": "^18.3.1",
|
||||
"react-arborist": "^3.4.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-syntax-highlighter": "^15.6.6",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-mathjax": "^6.0.0",
|
||||
"rehype-prism": "^2.3.3",
|
||||
"rehype-react": "^8.0.0",
|
||||
"remark": "^15.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
@@ -43,7 +42,6 @@
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/react": "^18.3.20",
|
||||
"@types/react-dom": "^18.3.6",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
@@ -57,7 +55,7 @@
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"sass": "^1.80.4",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.3.6",
|
||||
"vite": "^6.4.1",
|
||||
"vite-plugin-compression2": "^1.3.0",
|
||||
"vitest": "^3.1.4"
|
||||
}
|
||||
@@ -2676,16 +2674,6 @@
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-syntax-highlighter": {
|
||||
"version": "15.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
|
||||
"integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||
@@ -3376,12 +3364,6 @@
|
||||
"require-from-string": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
@@ -3791,22 +3773,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-selector-parser": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.0.5.tgz",
|
||||
"integrity": "sha512-3itoDFbKUNx1eKmVpYMFyqKX04Ww9osZ+dLgrk6GEv6KMVeXUhUnp4I5X+evw+u3ZxVU6RFXSSRxlTeMh8bA+g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/mdevils"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://patreon.com/mdevils"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||
@@ -4791,19 +4757,6 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fault": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
|
||||
"integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"format": "^0.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@@ -4917,14 +4870,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/format": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
|
||||
"integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
|
||||
"engines": {
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -5270,74 +5215,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-from-html": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
|
||||
"integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"devlop": "^1.1.0",
|
||||
"hast-util-from-parse5": "^8.0.0",
|
||||
"parse5": "^7.0.0",
|
||||
"vfile": "^6.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-from-parse5": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz",
|
||||
"integrity": "sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"hastscript": "^8.0.0",
|
||||
"property-information": "^6.0.0",
|
||||
"vfile": "^6.0.0",
|
||||
"vfile-location": "^5.0.0",
|
||||
"web-namespaces": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-from-parse5/node_modules/hast-util-parse-selector": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
|
||||
"integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-from-parse5/node_modules/hastscript": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz",
|
||||
"integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"comma-separated-tokens": "^2.0.0",
|
||||
"hast-util-parse-selector": "^4.0.0",
|
||||
"property-information": "^6.0.0",
|
||||
"space-separated-tokens": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-is-element": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
|
||||
@@ -5351,16 +5228,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-parse-selector": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
|
||||
"integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-to-jsx-runtime": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz",
|
||||
@@ -5417,86 +5284,15 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hastscript": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz",
|
||||
"integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^2.0.0",
|
||||
"comma-separated-tokens": "^1.0.0",
|
||||
"hast-util-parse-selector": "^2.0.0",
|
||||
"property-information": "^5.0.0",
|
||||
"space-separated-tokens": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hastscript/node_modules/@types/hast": {
|
||||
"version": "2.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz",
|
||||
"integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/hastscript/node_modules/@types/unist": {
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
|
||||
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hastscript/node_modules/comma-separated-tokens": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
|
||||
"integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/hastscript/node_modules/property-information": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz",
|
||||
"integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/hastscript/node_modules/space-separated-tokens": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
|
||||
"integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "10.7.3",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
|
||||
"version": "11.11.1",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
|
||||
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/highlightjs-vue": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
|
||||
"integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
@@ -6417,13 +6213,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lowlight": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
|
||||
"integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
|
||||
"integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fault": "^1.0.0",
|
||||
"highlight.js": "~10.7.0"
|
||||
"@types/hast": "^3.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"highlight.js": "~11.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -7306,18 +7103,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/nwsapi": {
|
||||
"version": "2.2.20",
|
||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
|
||||
@@ -7847,15 +7632,6 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||||
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
@@ -8102,23 +7878,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-syntax-highlighter": {
|
||||
"version": "15.6.6",
|
||||
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz",
|
||||
"integrity": "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"highlight.js": "^10.4.1",
|
||||
"highlightjs-vue": "^1.0.0",
|
||||
"lowlight": "^1.17.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"refractor": "^3.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 0.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-textarea-autosize": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz",
|
||||
@@ -8212,122 +7971,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz",
|
||||
"integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hastscript": "^6.0.0",
|
||||
"parse-entities": "^2.0.0",
|
||||
"prismjs": "~1.27.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor/node_modules/character-entities": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
|
||||
"integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor/node_modules/character-entities-legacy": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
|
||||
"integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor/node_modules/character-reference-invalid": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
|
||||
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor/node_modules/is-alphabetical": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
|
||||
"integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor/node_modules/is-alphanumerical": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
|
||||
"integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-alphabetical": "^1.0.0",
|
||||
"is-decimal": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor/node_modules/is-decimal": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
|
||||
"integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor/node_modules/is-hexadecimal": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
|
||||
"integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor/node_modules/parse-entities": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
|
||||
"integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"character-entities": "^1.0.0",
|
||||
"character-entities-legacy": "^1.0.0",
|
||||
"character-reference-invalid": "^1.0.0",
|
||||
"is-alphanumerical": "^1.0.0",
|
||||
"is-decimal": "^1.0.0",
|
||||
"is-hexadecimal": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/refractor/node_modules/prismjs": {
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz",
|
||||
"integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
@@ -8355,6 +7998,23 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-highlight": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz",
|
||||
"integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"hast-util-to-text": "^4.0.0",
|
||||
"lowlight": "^3.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-mathjax": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rehype-mathjax/-/rehype-mathjax-6.0.0.tgz",
|
||||
@@ -8436,68 +8096,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-parse": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz",
|
||||
"integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"hast-util-from-html": "^2.0.0",
|
||||
"unified": "^11.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-prism": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/rehype-prism/-/rehype-prism-2.3.3.tgz",
|
||||
"integrity": "sha512-J9mhio/CwcJRDyIhsp5hgXmyGeQsFN+/1eNEKnBRxfdJAx2CqH41kV0dqn/k2OgMdjk21IoGFgar0MfVtGYTSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hastscript": "^8.0.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"rehype-parse": "^9.0.1",
|
||||
"unist-util-is": "^6.0.0",
|
||||
"unist-util-select": "^5.1.0",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"unified": "^10 || ^11"
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-prism/node_modules/hast-util-parse-selector": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
|
||||
"integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-prism/node_modules/hastscript": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz",
|
||||
"integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"comma-separated-tokens": "^2.0.0",
|
||||
"hast-util-parse-selector": "^4.0.0",
|
||||
"property-information": "^6.0.0",
|
||||
"space-separated-tokens": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-react": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rehype-react/-/rehype-react-8.0.0.tgz",
|
||||
@@ -9868,23 +9466,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-select": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-select/-/unist-util-select-5.1.0.tgz",
|
||||
"integrity": "sha512-4A5mfokSHG/rNQ4g7gSbdEs+H586xyd24sdJqF1IWamqrLHvYb+DH48fzxowyOhOfK7YSqX+XlCojAyuuyyT2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"css-selector-parser": "^3.0.0",
|
||||
"devlop": "^1.1.0",
|
||||
"nth-check": "^2.0.0",
|
||||
"zwitch": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-stringify-position": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
|
||||
@@ -10100,20 +9681,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/vfile-location": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz",
|
||||
"integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/vfile-message": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz",
|
||||
@@ -10129,9 +9696,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
|
||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -10679,15 +10246,6 @@
|
||||
"node": ">=0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@@ -44,9 +44,8 @@
|
||||
"react": "^18.3.1",
|
||||
"react-arborist": "^3.4.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-syntax-highlighter": "^15.6.6",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-mathjax": "^6.0.0",
|
||||
"rehype-prism": "^2.3.3",
|
||||
"rehype-react": "^8.0.0",
|
||||
"remark": "^15.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
@@ -63,7 +62,6 @@
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/react": "^18.3.20",
|
||||
"@types/react-dom": "^18.3.6",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
@@ -77,7 +75,7 @@
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"sass": "^1.80.4",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.3.6",
|
||||
"vite": "^6.4.1",
|
||||
"vite-plugin-compression2": "^1.3.0",
|
||||
"vitest": "^3.1.4"
|
||||
},
|
||||
|
||||
@@ -29,10 +29,16 @@ export const apiCall = async (
|
||||
// Set up headers with CSRF token for non-GET requests
|
||||
const method = options.method || 'GET';
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers as Record<string, string>),
|
||||
};
|
||||
|
||||
// Only set Content-Type to application/json if not already set and body is not FormData
|
||||
// FormData requires the browser to set Content-Type with the boundary parameter
|
||||
const isFormData = options.body instanceof FormData;
|
||||
if (!headers['Content-Type'] && !isFormData) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
// Add CSRF token for non-GET methods
|
||||
if (method !== 'GET') {
|
||||
const csrfToken = getCsrfToken();
|
||||
@@ -41,11 +47,18 @@ export const apiCall = async (
|
||||
}
|
||||
}
|
||||
|
||||
// For FormData, don't include Content-Type in headers - let the browser set it
|
||||
const fetchHeaders = isFormData
|
||||
? Object.fromEntries(
|
||||
Object.entries(headers).filter(([key]) => key !== 'Content-Type')
|
||||
)
|
||||
: headers;
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
// Include credentials to send/receive cookies
|
||||
credentials: 'include',
|
||||
headers,
|
||||
headers: fetchHeaders,
|
||||
});
|
||||
console.debug(`Response status: ${response.status} for URL: ${url}`);
|
||||
|
||||
|
||||
@@ -120,6 +120,34 @@ describe('MarkdownPreview', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('renders code blocks with correct structure for theme switching', async () => {
|
||||
const content = '```javascript\nconst hello = "world";\n```';
|
||||
|
||||
render(
|
||||
<MarkdownPreview
|
||||
content={content}
|
||||
handleFileSelect={mockHandleFileSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
// Check that rehype-highlight generates the correct structure
|
||||
const preElement = screen
|
||||
.getByRole('code', { hidden: true })
|
||||
.closest('pre');
|
||||
const codeElement = preElement?.querySelector('code');
|
||||
|
||||
expect(preElement).toBeInTheDocument();
|
||||
expect(codeElement).toBeInTheDocument();
|
||||
|
||||
// The code element should have hljs class for theme switching to work
|
||||
expect(codeElement).toHaveClass('hljs');
|
||||
|
||||
// Should also have language class
|
||||
expect(codeElement).toHaveClass('language-javascript');
|
||||
});
|
||||
});
|
||||
|
||||
it('handles image loading errors gracefully', async () => {
|
||||
const content = '';
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React, { useState, useEffect, useMemo, type ReactNode } from 'react';
|
||||
import { unified, type Preset } from 'unified';
|
||||
import { unified } from 'unified';
|
||||
import remarkParse from 'remark-parse';
|
||||
import remarkMath from 'remark-math';
|
||||
import remarkRehype from 'remark-rehype';
|
||||
import rehypeMathjax from 'rehype-mathjax';
|
||||
import rehypeReact, { type Options } from 'rehype-react';
|
||||
import rehypePrism from 'rehype-prism';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import * as prod from 'react/jsx-runtime';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { remarkWikiLinks } from '../../utils/remarkWikiLinks';
|
||||
import { useWorkspace } from '../../hooks/useWorkspace';
|
||||
import { useHighlightTheme } from '../../hooks/useHighlightTheme';
|
||||
|
||||
interface MarkdownPreviewProps {
|
||||
content: string;
|
||||
@@ -28,12 +29,6 @@ interface MarkdownLinkProps {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface MarkdownCodeProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const MarkdownPreview: React.FC<MarkdownPreviewProps> = ({
|
||||
content,
|
||||
handleFileSelect,
|
||||
@@ -42,7 +37,10 @@ const MarkdownPreview: React.FC<MarkdownPreviewProps> = ({
|
||||
null
|
||||
);
|
||||
const baseUrl = window.API_BASE_URL;
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentWorkspace, colorScheme } = useWorkspace();
|
||||
|
||||
// Use the highlight theme hook
|
||||
useHighlightTheme(colorScheme === 'auto' ? 'light' : colorScheme);
|
||||
|
||||
const processor = useMemo(() => {
|
||||
const handleLinkClick = (
|
||||
@@ -82,7 +80,7 @@ const MarkdownPreview: React.FC<MarkdownPreviewProps> = ({
|
||||
.use(remarkMath)
|
||||
.use(remarkRehype)
|
||||
.use(rehypeMathjax)
|
||||
.use(rehypePrism as Preset)
|
||||
.use(rehypeHighlight)
|
||||
.use(rehypeReact, {
|
||||
jsx: prod.jsx,
|
||||
jsxs: prod.jsxs,
|
||||
@@ -107,13 +105,6 @@ const MarkdownPreview: React.FC<MarkdownPreviewProps> = ({
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
code: ({ children, className, ...props }: MarkdownCodeProps) => {
|
||||
return (
|
||||
<pre className={className}>
|
||||
<code {...props}>{children}</code>
|
||||
</pre>
|
||||
);
|
||||
},
|
||||
},
|
||||
} as Options);
|
||||
}, [currentWorkspace?.name, baseUrl, handleFileSelect]);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import React from 'react';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import DeleteUserModal from './DeleteUserModal';
|
||||
import { UserRole, type User } from '@/types/models';
|
||||
import { UserRole, Theme, type User } from '@/types/models';
|
||||
|
||||
// Mock notifications
|
||||
vi.mock('@mantine/notifications', () => ({
|
||||
@@ -36,6 +36,7 @@ describe('DeleteUserModal', () => {
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
role: UserRole.Editor,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import React from 'react';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import EditUserModal from './EditUserModal';
|
||||
import { UserRole, type User } from '@/types/models';
|
||||
import { UserRole, Theme, type User } from '@/types/models';
|
||||
|
||||
// Mock notifications
|
||||
vi.mock('@mantine/notifications', () => ({
|
||||
@@ -36,6 +36,7 @@ describe('EditUserModal', () => {
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
role: UserRole.Editor,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
};
|
||||
@@ -187,6 +188,7 @@ describe('EditUserModal', () => {
|
||||
email: 'newuser@example.com',
|
||||
displayName: 'New User',
|
||||
role: UserRole.Admin,
|
||||
theme: Theme.Dark,
|
||||
};
|
||||
|
||||
rerender(
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
import { render } from '../../test/utils';
|
||||
import UserMenu from './UserMenu';
|
||||
import { UserRole } from '../../types/models';
|
||||
import { UserRole, Theme } from '../../types/models';
|
||||
|
||||
// Mock the contexts
|
||||
vi.mock('../../contexts/AuthContext', () => ({
|
||||
@@ -37,6 +37,7 @@ describe('UserMenu', () => {
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
role: UserRole.Editor,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
};
|
||||
@@ -53,6 +54,7 @@ describe('UserMenu', () => {
|
||||
login: vi.fn(),
|
||||
refreshToken: vi.fn(),
|
||||
refreshUser: vi.fn(),
|
||||
updateProfile: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -84,6 +86,7 @@ describe('UserMenu', () => {
|
||||
login: vi.fn(),
|
||||
refreshToken: vi.fn(),
|
||||
refreshUser: vi.fn(),
|
||||
updateProfile: vi.fn(),
|
||||
});
|
||||
|
||||
const { getByLabelText, getByText } = render(
|
||||
@@ -145,6 +148,7 @@ describe('UserMenu', () => {
|
||||
id: mockUser.id,
|
||||
email: mockUser.email,
|
||||
role: mockUser.role,
|
||||
theme: mockUser.theme,
|
||||
createdAt: mockUser.createdAt,
|
||||
lastWorkspaceId: mockUser.lastWorkspaceId,
|
||||
};
|
||||
@@ -157,6 +161,7 @@ describe('UserMenu', () => {
|
||||
login: vi.fn(),
|
||||
refreshToken: vi.fn(),
|
||||
refreshUser: vi.fn(),
|
||||
updateProfile: vi.fn(),
|
||||
});
|
||||
|
||||
const { getByLabelText, getByText } = render(
|
||||
|
||||
@@ -89,6 +89,7 @@ const AccountSettings: React.FC<AccountSettingsProps> = ({
|
||||
email: user.email,
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
theme: user.theme,
|
||||
};
|
||||
dispatch({
|
||||
type: SettingsActionType.INIT_SETTINGS,
|
||||
@@ -107,6 +108,13 @@ const AccountSettings: React.FC<AccountSettingsProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleThemeChange = (theme: string): void => {
|
||||
dispatch({
|
||||
type: SettingsActionType.UPDATE_LOCAL_SETTINGS,
|
||||
payload: { theme } as UserProfileSettings,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (): Promise<void> => {
|
||||
const updates: UserProfileSettings = {};
|
||||
const needsPasswordConfirmation =
|
||||
@@ -117,6 +125,14 @@ const AccountSettings: React.FC<AccountSettingsProps> = ({
|
||||
updates.displayName = state.localSettings.displayName || '';
|
||||
}
|
||||
|
||||
// Add theme if changed
|
||||
if (
|
||||
state.localSettings.theme &&
|
||||
state.localSettings.theme !== state.initialSettings.theme
|
||||
) {
|
||||
updates.theme = state.localSettings.theme;
|
||||
}
|
||||
|
||||
// Handle password change
|
||||
if (state.localSettings.newPassword) {
|
||||
if (!state.localSettings.currentPassword) {
|
||||
@@ -216,6 +232,7 @@ const AccountSettings: React.FC<AccountSettingsProps> = ({
|
||||
<ProfileSettings
|
||||
settings={state.localSettings}
|
||||
onInputChange={handleInputChange}
|
||||
onThemeChange={handleThemeChange}
|
||||
/>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
||||
@@ -1,36 +1,70 @@
|
||||
import React from 'react';
|
||||
import { Box, Stack, TextInput } from '@mantine/core';
|
||||
import type { UserProfileSettings } from '@/types/models';
|
||||
import { Box, Stack, TextInput, Group, Text, Switch } from '@mantine/core';
|
||||
import { IconMoon, IconSun } from '@tabler/icons-react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { Theme, type UserProfileSettings } from '@/types/models';
|
||||
|
||||
interface ProfileSettingsProps {
|
||||
settings: UserProfileSettings;
|
||||
onInputChange: (key: keyof UserProfileSettings, value: string) => void;
|
||||
onThemeChange?: (theme: Theme) => void;
|
||||
}
|
||||
|
||||
const ProfileSettings: React.FC<ProfileSettingsProps> = ({
|
||||
settings,
|
||||
onInputChange,
|
||||
}) => (
|
||||
<Box>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Display Name"
|
||||
type="text"
|
||||
value={settings.displayName || ''}
|
||||
onChange={(e) => onInputChange('displayName', e.currentTarget.value)}
|
||||
placeholder="Enter display name"
|
||||
data-testid="display-name-input"
|
||||
/>
|
||||
<TextInput
|
||||
label="Email"
|
||||
type="email"
|
||||
value={settings.email || ''}
|
||||
onChange={(e) => onInputChange('email', e.currentTarget.value)}
|
||||
placeholder="Enter email"
|
||||
data-testid="email-input"
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
onThemeChange,
|
||||
}) => {
|
||||
const { user } = useAuth();
|
||||
const currentTheme = settings.theme || user?.theme || Theme.Dark;
|
||||
|
||||
const handleThemeToggle = () => {
|
||||
const newTheme = currentTheme === Theme.Dark ? Theme.Light : Theme.Dark;
|
||||
if (onThemeChange) {
|
||||
onThemeChange(newTheme);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Display Name"
|
||||
type="text"
|
||||
value={settings.displayName || ''}
|
||||
onChange={(e) => onInputChange('displayName', e.currentTarget.value)}
|
||||
placeholder="Enter display name"
|
||||
data-testid="display-name-input"
|
||||
/>
|
||||
<TextInput
|
||||
label="Email"
|
||||
type="email"
|
||||
value={settings.email || ''}
|
||||
onChange={(e) => onInputChange('email', e.currentTarget.value)}
|
||||
placeholder="Enter email"
|
||||
data-testid="email-input"
|
||||
/>
|
||||
<Group justify="space-between" align="flex-start">
|
||||
<div>
|
||||
<Text size="sm" fw={500}>
|
||||
Default Theme
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
Sets the default theme for new workspaces
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={currentTheme === Theme.Dark}
|
||||
onChange={handleThemeToggle}
|
||||
size="lg"
|
||||
onLabel={<IconMoon size={16} />}
|
||||
offLabel={<IconSun size={16} />}
|
||||
data-testid="theme-toggle"
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileSettings;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { render as rtlRender, screen, fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import AdminDashboard from './AdminDashboard';
|
||||
import { UserRole, type User } from '@/types/models';
|
||||
import { UserRole, Theme, type User } from '@/types/models';
|
||||
|
||||
// Mock the auth context
|
||||
const mockCurrentUser: User = {
|
||||
@@ -11,6 +11,7 @@ const mockCurrentUser: User = {
|
||||
email: 'admin@example.com',
|
||||
displayName: 'Admin User',
|
||||
role: UserRole.Admin,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import React from 'react';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import AdminUsersTab from './AdminUsersTab';
|
||||
import { UserRole, type User } from '@/types/models';
|
||||
import { UserRole, Theme, type User } from '@/types/models';
|
||||
|
||||
// Mock the user admin hook
|
||||
const mockCreate = vi.fn();
|
||||
@@ -123,6 +123,7 @@ describe('AdminUsersTab', () => {
|
||||
email: 'admin@example.com',
|
||||
displayName: 'Admin User',
|
||||
role: UserRole.Admin,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
};
|
||||
@@ -134,6 +135,7 @@ describe('AdminUsersTab', () => {
|
||||
email: 'editor@example.com',
|
||||
displayName: 'Editor User',
|
||||
role: UserRole.Editor,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-01-15T00:00:00Z',
|
||||
lastWorkspaceId: 2,
|
||||
},
|
||||
@@ -142,6 +144,7 @@ describe('AdminUsersTab', () => {
|
||||
email: 'viewer@example.com',
|
||||
displayName: 'Viewer User',
|
||||
role: UserRole.Viewer,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-02-01T00:00:00Z',
|
||||
lastWorkspaceId: 3,
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { AuthProvider, useAuth } from './AuthContext';
|
||||
import { UserRole, type User } from '@/types/models';
|
||||
import { UserRole, Theme, type User } from '@/types/models';
|
||||
|
||||
// Set up mocks before imports are used
|
||||
vi.mock('@/api/auth', () => {
|
||||
@@ -42,6 +42,7 @@ const mockUser: User = {
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
role: UserRole.Editor,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
};
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
refreshToken as apiRefreshToken,
|
||||
getCurrentUser,
|
||||
} from '@/api/auth';
|
||||
import type { User } from '@/types/models';
|
||||
import { updateProfile as apiUpdateProfile } from '@/api/user';
|
||||
import type { User, UserProfileSettings } from '@/types/models';
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
@@ -22,6 +23,7 @@ interface AuthContextType {
|
||||
logout: () => Promise<void>;
|
||||
refreshToken: () => Promise<boolean>;
|
||||
refreshUser: () => Promise<void>;
|
||||
updateProfile: (updates: UserProfileSettings) => Promise<User>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | null>(null);
|
||||
@@ -109,6 +111,31 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateProfile = useCallback(
|
||||
async (updates: UserProfileSettings): Promise<User> => {
|
||||
try {
|
||||
const updatedUser = await apiUpdateProfile(updates);
|
||||
setUser(updatedUser);
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'Profile updated successfully',
|
||||
color: 'green',
|
||||
});
|
||||
return updatedUser;
|
||||
} catch (error) {
|
||||
console.error('Failed to update profile:', error);
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message:
|
||||
error instanceof Error ? error.message : 'Failed to update profile',
|
||||
color: 'red',
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
loading,
|
||||
@@ -117,6 +144,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
logout,
|
||||
refreshToken,
|
||||
refreshUser,
|
||||
updateProfile,
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAdminData } from './useAdminData';
|
||||
import * as adminApi from '@/api/admin';
|
||||
import {
|
||||
UserRole,
|
||||
Theme,
|
||||
type SystemStats,
|
||||
type User,
|
||||
type WorkspaceStats,
|
||||
@@ -35,6 +36,7 @@ const mockUsers: User[] = [
|
||||
email: 'admin@example.com',
|
||||
displayName: 'Admin User',
|
||||
role: UserRole.Admin,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
},
|
||||
@@ -43,6 +45,7 @@ const mockUsers: User[] = [
|
||||
email: 'editor@example.com',
|
||||
displayName: 'Editor User',
|
||||
role: UserRole.Editor,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-01-02T00:00:00Z',
|
||||
lastWorkspaceId: 2,
|
||||
},
|
||||
|
||||
@@ -122,7 +122,8 @@ export const useFileOperations = (): UseFileOperationsResult => {
|
||||
if (!currentWorkspace) return false;
|
||||
|
||||
try {
|
||||
await uploadFile(currentWorkspace.name, targetPath || '', files);
|
||||
// Default to '.' (root directory) if no target path is provided
|
||||
await uploadFile(currentWorkspace.name, targetPath || '.', files);
|
||||
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
|
||||
36
app/src/hooks/useHighlightTheme.ts
Normal file
36
app/src/hooks/useHighlightTheme.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useEffect } from 'react';
|
||||
// Import theme CSS as text that will be bundled
|
||||
import atomOneLightTheme from 'highlight.js/styles/atom-one-light.css?inline';
|
||||
import atomOneDarkTheme from 'highlight.js/styles/atom-one-dark.css?inline';
|
||||
|
||||
export const useHighlightTheme = (colorScheme: 'light' | 'dark') => {
|
||||
useEffect(() => {
|
||||
// Remove existing highlight theme
|
||||
const existingStylesheet = document.querySelector(
|
||||
'style[data-highlight-theme]'
|
||||
);
|
||||
if (existingStylesheet) {
|
||||
existingStylesheet.remove();
|
||||
}
|
||||
|
||||
// Add new theme stylesheet using bundled CSS
|
||||
const style = document.createElement('style');
|
||||
style.setAttribute('data-highlight-theme', 'true');
|
||||
|
||||
if (colorScheme === 'dark') {
|
||||
style.textContent = atomOneDarkTheme as string;
|
||||
} else {
|
||||
style.textContent = atomOneLightTheme as string;
|
||||
}
|
||||
|
||||
document.head.appendChild(style);
|
||||
|
||||
return () => {
|
||||
// Cleanup on unmount
|
||||
const stylesheet = document.querySelector('style[data-highlight-theme]');
|
||||
if (stylesheet) {
|
||||
stylesheet.remove();
|
||||
}
|
||||
};
|
||||
}, [colorScheme]);
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { renderHook, act } from '@testing-library/react';
|
||||
import { useProfileSettings } from './useProfileSettings';
|
||||
import * as userApi from '@/api/user';
|
||||
import type { UpdateProfileRequest } from '@/types/api';
|
||||
import { UserRole, type User } from '@/types/models';
|
||||
import { UserRole, Theme, type User } from '@/types/models';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/api/user');
|
||||
@@ -22,6 +22,7 @@ const mockUser: User = {
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
role: UserRole.Editor,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { renderHook, act } from '@testing-library/react';
|
||||
import { useUserAdmin } from './useUserAdmin';
|
||||
import * as adminApi from '@/api/admin';
|
||||
import type { CreateUserRequest, UpdateUserRequest } from '@/types/api';
|
||||
import { UserRole, type User } from '@/types/models';
|
||||
import { UserRole, Theme, type User } from '@/types/models';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/api/admin');
|
||||
@@ -35,6 +35,7 @@ const mockUsers: User[] = [
|
||||
email: 'admin@example.com',
|
||||
displayName: 'Admin User',
|
||||
role: UserRole.Admin,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
},
|
||||
@@ -43,6 +44,7 @@ const mockUsers: User[] = [
|
||||
email: 'editor@example.com',
|
||||
displayName: 'Editor User',
|
||||
role: UserRole.Editor,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-01-02T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
},
|
||||
@@ -112,6 +114,7 @@ describe('useUserAdmin', () => {
|
||||
email: 'newuser@example.com',
|
||||
displayName: 'New User',
|
||||
role: UserRole.Viewer,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-01-03T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
};
|
||||
@@ -124,6 +127,7 @@ describe('useUserAdmin', () => {
|
||||
displayName: 'New User',
|
||||
password: 'password123',
|
||||
role: UserRole.Viewer,
|
||||
theme: Theme.Dark,
|
||||
};
|
||||
|
||||
let createResult: boolean | undefined;
|
||||
@@ -152,6 +156,7 @@ describe('useUserAdmin', () => {
|
||||
displayName: 'Test User',
|
||||
password: 'password123',
|
||||
role: UserRole.Editor,
|
||||
theme: Theme.Dark,
|
||||
};
|
||||
|
||||
let createResult: boolean | undefined;
|
||||
@@ -179,6 +184,7 @@ describe('useUserAdmin', () => {
|
||||
displayName: 'Test User',
|
||||
password: 'password123',
|
||||
role: UserRole.Editor,
|
||||
theme: Theme.Dark,
|
||||
};
|
||||
|
||||
let createResult: boolean | undefined;
|
||||
@@ -204,6 +210,7 @@ describe('useUserAdmin', () => {
|
||||
email: user.email,
|
||||
displayName: 'Updated Editor',
|
||||
role: user.role,
|
||||
theme: user.theme,
|
||||
createdAt: user.createdAt,
|
||||
lastWorkspaceId: user.lastWorkspaceId,
|
||||
};
|
||||
@@ -238,6 +245,7 @@ describe('useUserAdmin', () => {
|
||||
email: 'newemail@example.com',
|
||||
displayName: user.displayName || '',
|
||||
role: UserRole.Admin,
|
||||
theme: Theme.Dark,
|
||||
createdAt: user.createdAt,
|
||||
lastWorkspaceId: user.lastWorkspaceId,
|
||||
};
|
||||
@@ -248,6 +256,7 @@ describe('useUserAdmin', () => {
|
||||
const updateRequest: UpdateUserRequest = {
|
||||
email: 'newemail@example.com',
|
||||
role: UserRole.Admin,
|
||||
theme: Theme.Dark,
|
||||
};
|
||||
|
||||
let updateResult: boolean | undefined;
|
||||
@@ -436,6 +445,7 @@ describe('useUserAdmin', () => {
|
||||
displayName: 'Test',
|
||||
password: 'pass',
|
||||
role: UserRole.Viewer,
|
||||
theme: Theme.Dark,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -474,6 +484,7 @@ describe('useUserAdmin', () => {
|
||||
displayName: 'Test',
|
||||
password: 'pass',
|
||||
role: UserRole.Viewer,
|
||||
theme: Theme.Dark,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -500,6 +511,7 @@ describe('useUserAdmin', () => {
|
||||
email: 'user1@example.com',
|
||||
displayName: 'User 1',
|
||||
role: UserRole.Viewer,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-01-03T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
})
|
||||
@@ -508,6 +520,7 @@ describe('useUserAdmin', () => {
|
||||
email: 'user2@example.com',
|
||||
displayName: 'User 2',
|
||||
role: UserRole.Editor,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-01-04T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
});
|
||||
@@ -520,12 +533,14 @@ describe('useUserAdmin', () => {
|
||||
displayName: 'User 1',
|
||||
password: 'pass1',
|
||||
role: UserRole.Viewer,
|
||||
theme: Theme.Dark,
|
||||
},
|
||||
{
|
||||
email: 'user2@example.com',
|
||||
displayName: 'User 2',
|
||||
password: 'pass2',
|
||||
role: UserRole.Editor,
|
||||
theme: Theme.Dark,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -555,12 +570,14 @@ describe('useUserAdmin', () => {
|
||||
displayName: 'Success User',
|
||||
password: 'pass1',
|
||||
role: UserRole.Viewer,
|
||||
theme: Theme.Dark,
|
||||
},
|
||||
{
|
||||
email: 'fail@example.com',
|
||||
displayName: 'Fail User',
|
||||
password: 'pass2',
|
||||
role: UserRole.Editor,
|
||||
theme: Theme.Dark,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
type SaveFileResponse,
|
||||
type UploadFilesResponse,
|
||||
} from './api';
|
||||
import { UserRole, type User } from './models';
|
||||
import { UserRole, Theme, type User } from './models';
|
||||
|
||||
// Mock user data for testing
|
||||
const mockUser: User = {
|
||||
@@ -17,6 +17,7 @@ const mockUser: User = {
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
role: UserRole.Editor,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isUser, type User, type UserRole } from './models';
|
||||
import { isUser, type User, type UserRole, type Theme } from './models';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -55,6 +55,7 @@ export interface CreateUserRequest {
|
||||
displayName: string;
|
||||
password: string;
|
||||
role: UserRole;
|
||||
theme?: Theme;
|
||||
}
|
||||
|
||||
// UpdateUserRequest holds the request fields for updating a user
|
||||
@@ -63,6 +64,7 @@ export interface UpdateUserRequest {
|
||||
displayName?: string;
|
||||
password?: string;
|
||||
role?: UserRole;
|
||||
theme?: Theme;
|
||||
}
|
||||
|
||||
export interface LookupResponse {
|
||||
@@ -126,6 +128,7 @@ export interface UpdateProfileRequest {
|
||||
email?: string;
|
||||
currentPassword?: string;
|
||||
newPassword?: string;
|
||||
theme?: Theme;
|
||||
}
|
||||
|
||||
// DeleteAccountRequest represents a user account deletion request
|
||||
|
||||
5
app/src/types/css-inline.d.ts
vendored
Normal file
5
app/src/types/css-inline.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
// Type declarations for CSS imports with ?inline modifier
|
||||
declare module '*.css?inline' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
@@ -63,6 +63,7 @@ describe('Models Type Guards', () => {
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
role: UserRole.Editor,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
};
|
||||
@@ -76,6 +77,7 @@ describe('Models Type Guards', () => {
|
||||
id: 1,
|
||||
email: 'test@example.com',
|
||||
role: UserRole.Editor,
|
||||
theme: Theme.Dark,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface User {
|
||||
email: string;
|
||||
displayName?: string;
|
||||
role: UserRole;
|
||||
theme: Theme;
|
||||
createdAt: string;
|
||||
lastWorkspaceId: number;
|
||||
}
|
||||
@@ -28,6 +29,8 @@ export function isUser(value: unknown): value is User {
|
||||
: true) &&
|
||||
'role' in value &&
|
||||
isUserRole((value as User).role) &&
|
||||
'theme' in value &&
|
||||
(value as User).theme in Theme &&
|
||||
'createdAt' in value &&
|
||||
typeof (value as User).createdAt === 'string' &&
|
||||
'lastWorkspaceId' in value &&
|
||||
@@ -309,6 +312,7 @@ export interface UserProfileSettings {
|
||||
email?: string;
|
||||
currentPassword?: string;
|
||||
newPassword?: string;
|
||||
theme?: Theme;
|
||||
}
|
||||
|
||||
export interface ProfileSettingsState {
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('fileHelpers', () => {
|
||||
const filePath = 'folder/file.md';
|
||||
|
||||
const expectedUrl =
|
||||
'http://localhost:8080/api/v1/workspaces/my-workspace/files/folder%2Ffile.md';
|
||||
'http://localhost:8080/api/v1/workspaces/my-workspace/files/content?file_path=folder%2Ffile.md';
|
||||
const actualUrl = getFileUrl(workspaceName, filePath);
|
||||
|
||||
expect(actualUrl).toBe(expectedUrl);
|
||||
@@ -94,7 +94,7 @@ describe('fileHelpers', () => {
|
||||
const filePath = 'file.md';
|
||||
|
||||
const expectedUrl =
|
||||
'http://localhost:8080/api/v1/workspaces/my%20workspace%20with%20spaces/files/file.md';
|
||||
'http://localhost:8080/api/v1/workspaces/my%20workspace%20with%20spaces/files/content?file_path=file.md';
|
||||
const actualUrl = getFileUrl(workspaceName, filePath);
|
||||
|
||||
expect(actualUrl).toBe(expectedUrl);
|
||||
@@ -105,7 +105,7 @@ describe('fileHelpers', () => {
|
||||
const filePath = 'folder with spaces/file with spaces.md';
|
||||
|
||||
const expectedUrl =
|
||||
'http://localhost:8080/api/v1/workspaces/workspace/files/folder%20with%20spaces%2Ffile%20with%20spaces.md';
|
||||
'http://localhost:8080/api/v1/workspaces/workspace/files/content?file_path=folder%20with%20spaces%2Ffile%20with%20spaces.md';
|
||||
const actualUrl = getFileUrl(workspaceName, filePath);
|
||||
|
||||
expect(actualUrl).toBe(expectedUrl);
|
||||
@@ -116,7 +116,7 @@ describe('fileHelpers', () => {
|
||||
const filePath = 'file?name=test.md';
|
||||
|
||||
const expectedUrl =
|
||||
'http://localhost:8080/api/v1/workspaces/test%26workspace/files/file%3Fname%3Dtest.md';
|
||||
'http://localhost:8080/api/v1/workspaces/test%26workspace/files/content?file_path=file%3Fname%3Dtest.md';
|
||||
const actualUrl = getFileUrl(workspaceName, filePath);
|
||||
|
||||
expect(actualUrl).toBe(expectedUrl);
|
||||
@@ -127,7 +127,7 @@ describe('fileHelpers', () => {
|
||||
const filePath = 'ファイル.md';
|
||||
|
||||
const expectedUrl =
|
||||
'http://localhost:8080/api/v1/workspaces/%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88/files/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.md';
|
||||
'http://localhost:8080/api/v1/workspaces/%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88/files/content?file_path=%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.md';
|
||||
const actualUrl = getFileUrl(workspaceName, filePath);
|
||||
|
||||
expect(actualUrl).toBe(expectedUrl);
|
||||
@@ -138,7 +138,7 @@ describe('fileHelpers', () => {
|
||||
const filePath = 'projects/2024/q1/report.md';
|
||||
|
||||
const expectedUrl =
|
||||
'http://localhost:8080/api/v1/workspaces/docs/files/projects%2F2024%2Fq1%2Freport.md';
|
||||
'http://localhost:8080/api/v1/workspaces/docs/files/content?file_path=projects%2F2024%2Fq1%2Freport.md';
|
||||
const actualUrl = getFileUrl(workspaceName, filePath);
|
||||
|
||||
expect(actualUrl).toBe(expectedUrl);
|
||||
@@ -146,20 +146,20 @@ describe('fileHelpers', () => {
|
||||
|
||||
it('handles edge cases with empty strings', () => {
|
||||
expect(getFileUrl('', '')).toBe(
|
||||
'http://localhost:8080/api/v1/workspaces//files/'
|
||||
'http://localhost:8080/api/v1/workspaces//files/content?file_path='
|
||||
);
|
||||
expect(getFileUrl('workspace', '')).toBe(
|
||||
'http://localhost:8080/api/v1/workspaces/workspace/files/'
|
||||
'http://localhost:8080/api/v1/workspaces/workspace/files/content?file_path='
|
||||
);
|
||||
expect(getFileUrl('', 'file.md')).toBe(
|
||||
'http://localhost:8080/api/v1/workspaces//files/file.md'
|
||||
'http://localhost:8080/api/v1/workspaces//files/content?file_path=file.md'
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the API base URL correctly', () => {
|
||||
const url = getFileUrl('test', 'file.md');
|
||||
expect(url).toBe(
|
||||
'http://localhost:8080/api/v1/workspaces/test/files/file.md'
|
||||
'http://localhost:8080/api/v1/workspaces/test/files/content?file_path=file.md'
|
||||
);
|
||||
expect(url).toContain(window.API_BASE_URL);
|
||||
});
|
||||
|
||||
@@ -13,5 +13,5 @@ export const isImageFile = (filePath: string): boolean => {
|
||||
export const getFileUrl = (workspaceName: string, filePath: string) => {
|
||||
return `${API_BASE_URL}/workspaces/${encodeURIComponent(
|
||||
workspaceName
|
||||
)}/files/${encodeURIComponent(filePath)}`;
|
||||
)}/files/content?file_path=${encodeURIComponent(filePath)}`;
|
||||
};
|
||||
|
||||
@@ -52,9 +52,8 @@ export default defineConfig(({ mode }) => ({
|
||||
|
||||
// Markdown processing
|
||||
markdown: [
|
||||
'react-syntax-highlighter',
|
||||
'rehype-highlight',
|
||||
'rehype-mathjax',
|
||||
'rehype-prism',
|
||||
'rehype-react',
|
||||
'remark',
|
||||
'remark-math',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -118,6 +118,7 @@ func setupAdminUser(database db.Database, storageManager storage.Manager, cfg *C
|
||||
DisplayName: "Admin",
|
||||
PasswordHash: string(hashedPassword),
|
||||
Role: models.RoleAdmin,
|
||||
Theme: "dark", // default theme
|
||||
}
|
||||
|
||||
createdUser, err := database.CreateUser(adminUser)
|
||||
@@ -132,7 +133,8 @@ func setupAdminUser(database db.Database, storageManager storage.Manager, cfg *C
|
||||
|
||||
logging.Info("admin user setup completed",
|
||||
"userId", createdUser.ID,
|
||||
"workspaceId", createdUser.LastWorkspaceID)
|
||||
"workspaceId", createdUser.LastWorkspaceID,
|
||||
"theme", createdUser.Theme)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -64,16 +64,16 @@ func setupRouter(o Options) *chi.Mux {
|
||||
|
||||
// API routes
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
// Rate limiting for API routes
|
||||
if o.Config.RateLimitRequests > 0 {
|
||||
r.Use(httprate.LimitByIP(
|
||||
o.Config.RateLimitRequests,
|
||||
o.Config.RateLimitWindow,
|
||||
))
|
||||
}
|
||||
|
||||
// Public routes (no authentication required)
|
||||
r.Group(func(r chi.Router) {
|
||||
// Rate limiting for authentication endpoints to prevent brute force attacks
|
||||
if o.Config.RateLimitRequests > 0 {
|
||||
r.Use(httprate.LimitByIP(
|
||||
o.Config.RateLimitRequests,
|
||||
o.Config.RateLimitWindow,
|
||||
))
|
||||
}
|
||||
|
||||
r.Post("/auth/login", handler.Login(o.SessionManager, o.CookieService))
|
||||
r.Post("/auth/refresh", handler.RefreshToken(o.SessionManager, o.CookieService))
|
||||
})
|
||||
@@ -134,7 +134,7 @@ func setupRouter(o Options) *chi.Mux {
|
||||
r.Get("/lookup", handler.LookupFileByName())
|
||||
|
||||
r.Post("/upload", handler.UploadFile())
|
||||
r.Put("/move", handler.MoveFile())
|
||||
r.Post("/move", handler.MoveFile())
|
||||
|
||||
r.Post("/", handler.SaveFile())
|
||||
r.Get("/content", handler.GetFileContent())
|
||||
@@ -152,7 +152,9 @@ func setupRouter(o Options) *chi.Mux {
|
||||
})
|
||||
|
||||
// Handle all other routes with static file server
|
||||
r.Get("/*", handlers.NewStaticHandler(o.Config.StaticPath).ServeHTTP)
|
||||
staticHandler := handlers.NewStaticHandler(o.Config.StaticPath)
|
||||
r.Get("/*", staticHandler.ServeHTTP)
|
||||
r.Head("/*", staticHandler.ServeHTTP)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
display_name TEXT,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL CHECK(role IN ('admin', 'editor', 'viewer')),
|
||||
theme TEXT NOT NULL DEFAULT 'dark' CHECK(theme IN ('light', 'dark')),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_workspace_id INTEGER
|
||||
);
|
||||
@@ -19,7 +20,7 @@ CREATE TABLE IF NOT EXISTS workspaces (
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_opened_file_path TEXT,
|
||||
-- Settings fields
|
||||
theme TEXT NOT NULL DEFAULT 'light' CHECK(theme IN ('light', 'dark')),
|
||||
theme TEXT NOT NULL DEFAULT 'dark' CHECK(theme IN ('light', 'dark')),
|
||||
auto_save BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
git_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
git_url TEXT,
|
||||
|
||||
@@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
display_name TEXT,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL CHECK(role IN ('admin', 'editor', 'viewer')),
|
||||
theme TEXT NOT NULL DEFAULT 'dark' CHECK(theme IN ('light', 'dark')),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_workspace_id INTEGER
|
||||
);
|
||||
@@ -18,7 +19,7 @@ CREATE TABLE IF NOT EXISTS workspaces (
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_opened_file_path TEXT,
|
||||
-- Settings fields
|
||||
theme TEXT NOT NULL DEFAULT 'light' CHECK(theme IN ('light', 'dark')),
|
||||
theme TEXT NOT NULL DEFAULT 'dark' CHECK(theme IN ('light', 'dark')),
|
||||
auto_save BOOLEAN NOT NULL DEFAULT 0,
|
||||
git_enabled BOOLEAN NOT NULL DEFAULT 0,
|
||||
git_url TEXT,
|
||||
|
||||
@@ -22,6 +22,7 @@ type CreateUserRequest struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
Password string `json:"password"`
|
||||
Role models.UserRole `json:"role"`
|
||||
Theme string `json:"theme,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateUserRequest holds the request fields for updating a user
|
||||
@@ -30,6 +31,7 @@ type UpdateUserRequest struct {
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Role models.UserRole `json:"role,omitempty"`
|
||||
Theme string `json:"theme,omitempty"`
|
||||
}
|
||||
|
||||
// WorkspaceStats holds workspace statistics
|
||||
@@ -164,11 +166,24 @@ func (h *Handler) AdminCreateUser() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle theme with validation and default
|
||||
theme := req.Theme
|
||||
if theme == "" {
|
||||
theme = "dark" // Default theme
|
||||
} else if theme != "light" && theme != "dark" {
|
||||
// Invalid theme, fallback to dark
|
||||
log.Debug("invalid theme value in user creation, falling back to dark",
|
||||
"theme", theme,
|
||||
)
|
||||
theme = "dark"
|
||||
}
|
||||
|
||||
user := &models.User{
|
||||
Email: req.Email,
|
||||
DisplayName: req.DisplayName,
|
||||
PasswordHash: string(hashedPassword),
|
||||
Role: req.Role,
|
||||
Theme: theme,
|
||||
}
|
||||
|
||||
insertedUser, err := h.DB.CreateUser(user)
|
||||
@@ -196,6 +211,7 @@ func (h *Handler) AdminCreateUser() http.HandlerFunc {
|
||||
"newUserID", insertedUser.ID,
|
||||
"email", insertedUser.Email,
|
||||
"role", insertedUser.Role,
|
||||
"theme", insertedUser.Theme,
|
||||
)
|
||||
respondJSON(w, insertedUser)
|
||||
}
|
||||
@@ -322,6 +338,17 @@ func (h *Handler) AdminUpdateUser() http.HandlerFunc {
|
||||
user.Role = req.Role
|
||||
updates["role"] = req.Role
|
||||
}
|
||||
if req.Theme != "" {
|
||||
// Validate theme value, fallback to "dark" if invalid
|
||||
if req.Theme != "light" && req.Theme != "dark" {
|
||||
log.Debug("invalid theme value, falling back to dark",
|
||||
"theme", req.Theme,
|
||||
)
|
||||
req.Theme = "dark"
|
||||
}
|
||||
user.Theme = req.Theme
|
||||
updates["theme"] = req.Theme
|
||||
}
|
||||
if req.Password != "" {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,9 +2,11 @@ package handlers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"lemma/internal/context"
|
||||
@@ -205,7 +207,13 @@ func (h *Handler) GetFileContent() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
// Detect MIME type based on file extension
|
||||
contentType := mime.TypeByExtension(filepath.Ext(decodedPath))
|
||||
if contentType == "" {
|
||||
// Fallback to text/plain if MIME type cannot be determined
|
||||
contentType = "text/plain"
|
||||
}
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
_, err = w.Write(content)
|
||||
if err != nil {
|
||||
log.Error("failed to write response",
|
||||
@@ -402,7 +410,8 @@ func (h *Handler) UploadFile() http.HandlerFunc {
|
||||
}
|
||||
}()
|
||||
|
||||
filePath := decodedPath + "/" + formFile.Filename
|
||||
// Use filepath.Join to properly construct the path
|
||||
filePath := filepath.Join(decodedPath, formFile.Filename)
|
||||
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
|
||||
@@ -156,6 +156,54 @@ func testFileHandlers(t *testing.T, dbConfig DatabaseConfig) {
|
||||
assert.Equal(t, http.StatusNotFound, rr.Code)
|
||||
})
|
||||
|
||||
t.Run("move file", func(t *testing.T) {
|
||||
srcPath := "original.md"
|
||||
destPath := "moved.md"
|
||||
content := "This file will be moved"
|
||||
|
||||
// Create file
|
||||
rr := h.makeRequestRaw(t, http.MethodPost, baseURL+"?file_path="+url.QueryEscape(srcPath), strings.NewReader(content), h.RegularTestUser)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
// Move file
|
||||
moveURL := baseURL + "/move?src_path=" + url.QueryEscape(srcPath) + "&dest_path=" + url.QueryEscape(destPath)
|
||||
rr = h.makeRequest(t, http.MethodPost, moveURL, nil, h.RegularTestUser)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
// Verify source is gone
|
||||
rr = h.makeRequest(t, http.MethodGet, baseURL+"/content?file_path="+url.QueryEscape(srcPath), nil, h.RegularTestUser)
|
||||
assert.Equal(t, http.StatusNotFound, rr.Code)
|
||||
|
||||
// Verify destination exists with correct content
|
||||
rr = h.makeRequest(t, http.MethodGet, baseURL+"/content?file_path="+url.QueryEscape(destPath), nil, h.RegularTestUser)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
assert.Equal(t, content, rr.Body.String())
|
||||
})
|
||||
|
||||
t.Run("rename file in directory", func(t *testing.T) {
|
||||
srcPath := "folder/old-name.md"
|
||||
destPath := "folder/new-name.md"
|
||||
content := "This file will be renamed"
|
||||
|
||||
// Create file
|
||||
rr := h.makeRequestRaw(t, http.MethodPost, baseURL+"?file_path="+url.QueryEscape(srcPath), strings.NewReader(content), h.RegularTestUser)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
// Rename file (move within same directory)
|
||||
moveURL := baseURL + "/move?src_path=" + url.QueryEscape(srcPath) + "&dest_path=" + url.QueryEscape(destPath)
|
||||
rr = h.makeRequest(t, http.MethodPost, moveURL, nil, h.RegularTestUser)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
// Verify source is gone
|
||||
rr = h.makeRequest(t, http.MethodGet, baseURL+"/content?file_path="+url.QueryEscape(srcPath), nil, h.RegularTestUser)
|
||||
assert.Equal(t, http.StatusNotFound, rr.Code)
|
||||
|
||||
// Verify destination exists with correct content
|
||||
rr = h.makeRequest(t, http.MethodGet, baseURL+"/content?file_path="+url.QueryEscape(destPath), nil, h.RegularTestUser)
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
assert.Equal(t, content, rr.Body.String())
|
||||
})
|
||||
|
||||
t.Run("last opened file", func(t *testing.T) {
|
||||
// Initially should be empty
|
||||
rr := h.makeRequest(t, http.MethodGet, baseURL+"/last", nil, h.RegularTestUser)
|
||||
@@ -304,15 +352,6 @@ func testFileHandlers(t *testing.T, dbConfig DatabaseConfig) {
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
})
|
||||
|
||||
t.Run("upload with missing file_path parameter", func(t *testing.T) {
|
||||
fileName := "test.txt"
|
||||
fileContent := "test content"
|
||||
files := map[string]string{fileName: fileContent}
|
||||
|
||||
rr := h.makeUploadRequest(t, baseURL+"/upload", files, h.RegularTestUser)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
})
|
||||
|
||||
t.Run("upload with invalid file_path", func(t *testing.T) {
|
||||
fileName := "test.txt"
|
||||
fileContent := "test content"
|
||||
|
||||
@@ -16,6 +16,7 @@ type UpdateProfileRequest struct {
|
||||
Email string `json:"email"`
|
||||
CurrentPassword string `json:"currentPassword"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
Theme string `json:"theme"`
|
||||
}
|
||||
|
||||
// DeleteAccountRequest represents a user account deletion request
|
||||
@@ -149,6 +150,19 @@ func (h *Handler) UpdateProfile() http.HandlerFunc {
|
||||
updates["displayNameChanged"] = true
|
||||
}
|
||||
|
||||
// Update theme if provided
|
||||
if req.Theme != "" {
|
||||
// Validate theme value, fallback to "dark" if invalid
|
||||
if req.Theme != "light" && req.Theme != "dark" {
|
||||
log.Debug("invalid theme value, falling back to dark",
|
||||
"theme", req.Theme,
|
||||
)
|
||||
req.Theme = "dark"
|
||||
}
|
||||
user.Theme = req.Theme
|
||||
updates["themeChanged"] = true
|
||||
}
|
||||
|
||||
// Update user in database
|
||||
if err := h.DB.UpdateUser(user); err != nil {
|
||||
log.Error("failed to update user in database",
|
||||
|
||||
@@ -87,7 +87,7 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc {
|
||||
"clientIP", r.RemoteAddr,
|
||||
)
|
||||
|
||||
var workspace models.Workspace
|
||||
var workspace models.Workspace
|
||||
if err := json.NewDecoder(r.Body).Decode(&workspace); err != nil {
|
||||
log.Debug("invalid request body received",
|
||||
"error", err.Error(),
|
||||
@@ -104,7 +104,21 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Get user to access their theme preference
|
||||
user, err := h.DB.GetUserByID(ctx.UserID)
|
||||
if err != nil {
|
||||
log.Error("failed to fetch user from database",
|
||||
"error", err.Error(),
|
||||
)
|
||||
respondError(w, "Failed to get user", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
workspace.UserID = ctx.UserID
|
||||
// Use user's theme as default if not provided
|
||||
if workspace.Theme == "" {
|
||||
workspace.Theme = user.Theme
|
||||
}
|
||||
if err := h.DB.CreateWorkspace(&workspace); err != nil {
|
||||
log.Error("failed to create workspace in database",
|
||||
"error", err.Error(),
|
||||
@@ -145,6 +159,7 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc {
|
||||
log.Info("workspace created",
|
||||
"workspaceID", workspace.ID,
|
||||
"workspaceName", workspace.Name,
|
||||
"theme", workspace.Theme,
|
||||
"gitEnabled", workspace.GitEnabled,
|
||||
)
|
||||
respondJSON(w, workspace)
|
||||
|
||||
@@ -25,6 +25,7 @@ type User struct {
|
||||
DisplayName string `json:"displayName" db:"display_name"`
|
||||
PasswordHash string `json:"-" db:"password_hash"`
|
||||
Role UserRole `json:"role" db:"role" validate:"required,oneof=admin editor viewer"`
|
||||
Theme string `json:"theme" db:"theme" validate:"required,oneof=light dark"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at,default"`
|
||||
LastWorkspaceID int `json:"lastWorkspaceId" db:"last_workspace_id"`
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ type Workspace struct {
|
||||
LastOpenedFilePath string `json:"lastOpenedFilePath" db:"last_opened_file_path"`
|
||||
|
||||
// Integrated settings
|
||||
Theme string `json:"theme" db:"theme" validate:"oneof=light dark"`
|
||||
Theme string `json:"theme" db:"theme" validate:"required,oneof=light dark"`
|
||||
AutoSave bool `json:"autoSave" db:"auto_save"`
|
||||
ShowHiddenFiles bool `json:"showHiddenFiles" db:"show_hidden_files"`
|
||||
GitEnabled bool `json:"gitEnabled" db:"git_enabled"`
|
||||
@@ -40,7 +40,7 @@ func (w *Workspace) ValidateGitSettings() error {
|
||||
func (w *Workspace) SetDefaultSettings() {
|
||||
|
||||
if w.Theme == "" {
|
||||
w.Theme = "light"
|
||||
w.Theme = "dark"
|
||||
}
|
||||
|
||||
w.AutoSave = w.AutoSave || false
|
||||
|
||||
Reference in New Issue
Block a user