bugfix(FIL-02+allgemein previewer wieder hergestellt und content preview für word und excel hinzugefügt
This commit is contained in:
parent
ab256bb094
commit
82844f0cbe
10 changed files with 1046 additions and 530 deletions
308
package-lock.json
generated
308
package-lock.json
generated
|
|
@ -14,6 +14,7 @@
|
||||||
"@types/leaflet": "^1.9.21",
|
"@types/leaflet": "^1.9.21",
|
||||||
"@xstate/react": "^5.0.0",
|
"@xstate/react": "^5.0.0",
|
||||||
"axios": "^1.8.3",
|
"axios": "^1.8.3",
|
||||||
|
"docx-preview": "^0.3.7",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"framer-motion": "^12.7.3",
|
"framer-motion": "^12.7.3",
|
||||||
|
|
@ -22,6 +23,7 @@
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"mammoth": "^1.12.0",
|
||||||
"motion": "^12.7.3",
|
"motion": "^12.7.3",
|
||||||
"pg": "^8.8.0",
|
"pg": "^8.8.0",
|
||||||
"proj4": "^2.20.2",
|
"proj4": "^2.20.2",
|
||||||
|
|
@ -34,6 +36,7 @@
|
||||||
"react-router-dom": "^7.7.1",
|
"react-router-dom": "^7.7.1",
|
||||||
"recharts": "^3.7.0",
|
"recharts": "^3.7.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
|
"xlsx": "^0.18.5",
|
||||||
"xstate": "^5.20.1"
|
"xstate": "^5.20.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -1980,6 +1983,14 @@
|
||||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@xmldom/xmldom": {
|
||||||
|
"version": "0.8.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz",
|
||||||
|
"integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@xstate/react": {
|
"node_modules/@xstate/react": {
|
||||||
"version": "5.0.5",
|
"version": "5.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@xstate/react/-/react-5.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@xstate/react/-/react-5.0.5.tgz",
|
||||||
|
|
@ -2035,6 +2046,14 @@
|
||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/adler-32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
|
|
@ -2131,6 +2150,30 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/bluebird": {
|
||||||
|
"version": "3.4.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
|
||||||
|
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="
|
||||||
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.3",
|
"version": "1.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||||
|
|
@ -2337,6 +2380,18 @@
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cfb": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"crc-32": "~1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
|
@ -2415,6 +2470,14 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/codepage": {
|
||||||
|
"version": "1.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||||
|
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
|
@ -2541,6 +2604,22 @@
|
||||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/core-util-is": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
||||||
|
},
|
||||||
|
"node_modules/crc-32": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
|
"bin": {
|
||||||
|
"crc32": "bin/crc32.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
|
|
@ -2794,6 +2873,19 @@
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dingbat-to-unicode": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w=="
|
||||||
|
},
|
||||||
|
"node_modules/docx-preview": {
|
||||||
|
"version": "0.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/docx-preview/-/docx-preview-0.3.7.tgz",
|
||||||
|
"integrity": "sha512-Lav69CTA/IYZPJTsKH7oYeoZjyg96N0wEJMNslGJnZJ+dMUZK85Lt5ASC79yUlD48ecWjuv+rkcmFt6EVPV0Xg==",
|
||||||
|
"dependencies": {
|
||||||
|
"jszip": ">=3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dom-serializer": {
|
"node_modules/dom-serializer": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
||||||
|
|
@ -2895,6 +2987,14 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/duck": {
|
||||||
|
"version": "0.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",
|
||||||
|
"integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
|
||||||
|
"dependencies": {
|
||||||
|
"underscore": "^1.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
|
@ -3608,6 +3708,14 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/frac": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/framer-motion": {
|
"node_modules/framer-motion": {
|
||||||
"version": "12.23.9",
|
"version": "12.23.9",
|
||||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.9.tgz",
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.9.tgz",
|
||||||
|
|
@ -3957,6 +4065,11 @@
|
||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immediate": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
|
||||||
|
},
|
||||||
"node_modules/immer": {
|
"node_modules/immer": {
|
||||||
"version": "10.2.0",
|
"version": "10.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
||||||
|
|
@ -4111,6 +4224,11 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
|
||||||
|
},
|
||||||
"node_modules/isexe": {
|
"node_modules/isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
|
|
@ -4258,6 +4376,17 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jszip": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||||
|
"dependencies": {
|
||||||
|
"lie": "~3.3.0",
|
||||||
|
"pako": "~1.0.2",
|
||||||
|
"readable-stream": "~2.3.6",
|
||||||
|
"setimmediate": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jwa": {
|
"node_modules/jwa": {
|
||||||
"version": "1.4.2",
|
"version": "1.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
|
||||||
|
|
@ -4318,6 +4447,14 @@
|
||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lie": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"immediate": "~3.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
|
|
@ -4405,6 +4542,16 @@
|
||||||
"loose-envify": "cli.js"
|
"loose-envify": "cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lop": {
|
||||||
|
"version": "0.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz",
|
||||||
|
"integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
|
||||||
|
"dependencies": {
|
||||||
|
"duck": "^0.1.12",
|
||||||
|
"option": "~0.2.1",
|
||||||
|
"underscore": "^1.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lower-case": {
|
"node_modules/lower-case": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
|
||||||
|
|
@ -4425,6 +4572,37 @@
|
||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mammoth": {
|
||||||
|
"version": "1.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz",
|
||||||
|
"integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@xmldom/xmldom": "^0.8.6",
|
||||||
|
"argparse": "~1.0.3",
|
||||||
|
"base64-js": "^1.5.1",
|
||||||
|
"bluebird": "~3.4.0",
|
||||||
|
"dingbat-to-unicode": "^1.0.1",
|
||||||
|
"jszip": "^3.7.1",
|
||||||
|
"lop": "^0.4.2",
|
||||||
|
"path-is-absolute": "^1.0.0",
|
||||||
|
"underscore": "^1.13.1",
|
||||||
|
"xmlbuilder": "^10.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"mammoth": "bin/mammoth"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mammoth/node_modules/argparse": {
|
||||||
|
"version": "1.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||||
|
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||||
|
"dependencies": {
|
||||||
|
"sprintf-js": "~1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/markdown-table": {
|
"node_modules/markdown-table": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
|
||||||
|
|
@ -5571,6 +5749,11 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/option": {
|
||||||
|
"version": "0.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz",
|
||||||
|
"integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A=="
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
|
|
@ -5621,6 +5804,11 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
||||||
|
},
|
||||||
"node_modules/param-case": {
|
"node_modules/param-case": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
|
||||||
|
|
@ -5700,6 +5888,14 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-is-absolute": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-key": {
|
"node_modules/path-key": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||||
|
|
@ -5910,6 +6106,11 @@
|
||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process-nextick-args": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||||
|
},
|
||||||
"node_modules/proj4": {
|
"node_modules/proj4": {
|
||||||
"version": "2.20.2",
|
"version": "2.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/proj4/-/proj4-2.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/proj4/-/proj4-2.20.2.tgz",
|
||||||
|
|
@ -6206,6 +6407,25 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
|
"dependencies": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.3",
|
||||||
|
"isarray": "~1.0.0",
|
||||||
|
"process-nextick-args": "~2.0.0",
|
||||||
|
"safe-buffer": "~5.1.1",
|
||||||
|
"string_decoder": "~1.1.1",
|
||||||
|
"util-deprecate": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/readable-stream/node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||||
|
},
|
||||||
"node_modules/recharts": {
|
"node_modules/recharts": {
|
||||||
"version": "3.7.0",
|
"version": "3.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz",
|
||||||
|
|
@ -6522,6 +6742,11 @@
|
||||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/setimmediate": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
|
||||||
|
},
|
||||||
"node_modules/setprototypeof": {
|
"node_modules/setprototypeof": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
|
|
@ -6673,6 +6898,22 @@
|
||||||
"node": ">= 10.x"
|
"node": ">= 10.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sprintf-js": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
|
||||||
|
},
|
||||||
|
"node_modules/ssf": {
|
||||||
|
"version": "0.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||||
|
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||||
|
"dependencies": {
|
||||||
|
"frac": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/state-local": {
|
"node_modules/state-local": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
|
||||||
|
|
@ -6687,6 +6928,19 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string_decoder/node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||||
|
},
|
||||||
"node_modules/stringify-entities": {
|
"node_modules/stringify-entities": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
||||||
|
|
@ -6901,6 +7155,11 @@
|
||||||
"typescript": ">=4.8.4 <5.9.0"
|
"typescript": ">=4.8.4 <5.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/underscore": {
|
||||||
|
"version": "1.13.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
|
||||||
|
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.14.0",
|
"version": "7.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
|
||||||
|
|
@ -7078,6 +7337,11 @@
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||||
|
},
|
||||||
"node_modules/utils-merge": {
|
"node_modules/utils-merge": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||||
|
|
@ -7251,6 +7515,22 @@
|
||||||
"integrity": "sha512-1ZUiV1FTwSiSrgWzV9KXJuOF2BVW91KY/mau04BhnmgOdroRQea7Q0s5TVqwGLm0D2tZwObd/tBYXW49sSxp3Q==",
|
"integrity": "sha512-1ZUiV1FTwSiSrgWzV9KXJuOF2BVW91KY/mau04BhnmgOdroRQea7Q0s5TVqwGLm0D2tZwObd/tBYXW49sSxp3Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/wmf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/word": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
|
|
@ -7261,6 +7541,34 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xlsx": {
|
||||||
|
"version": "0.18.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||||
|
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"cfb": "~1.2.1",
|
||||||
|
"codepage": "~1.15.0",
|
||||||
|
"crc-32": "~1.2.1",
|
||||||
|
"ssf": "~0.11.2",
|
||||||
|
"wmf": "~1.0.1",
|
||||||
|
"word": "~0.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"xlsx": "bin/xlsx.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlbuilder": {
|
||||||
|
"version": "10.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
|
||||||
|
"integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xstate": {
|
"node_modules/xstate": {
|
||||||
"version": "5.20.1",
|
"version": "5.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/xstate/-/xstate-5.20.1.tgz",
|
"resolved": "https://registry.npmjs.org/xstate/-/xstate-5.20.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"@types/leaflet": "^1.9.21",
|
"@types/leaflet": "^1.9.21",
|
||||||
"@xstate/react": "^5.0.0",
|
"@xstate/react": "^5.0.0",
|
||||||
"axios": "^1.8.3",
|
"axios": "^1.8.3",
|
||||||
|
"docx-preview": "^0.3.7",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"framer-motion": "^12.7.3",
|
"framer-motion": "^12.7.3",
|
||||||
|
|
@ -28,6 +29,7 @@
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"mammoth": "^1.12.0",
|
||||||
"motion": "^12.7.3",
|
"motion": "^12.7.3",
|
||||||
"pg": "^8.8.0",
|
"pg": "^8.8.0",
|
||||||
"proj4": "^2.20.2",
|
"proj4": "^2.20.2",
|
||||||
|
|
@ -40,6 +42,7 @@
|
||||||
"react-router-dom": "^7.7.1",
|
"react-router-dom": "^7.7.1",
|
||||||
"recharts": "^3.7.0",
|
"recharts": "^3.7.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
|
"xlsx": "^0.18.5",
|
||||||
"xstate": "^5.20.1"
|
"xstate": "^5.20.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -845,3 +845,121 @@
|
||||||
/* Popup-specific styles if needed */
|
/* Popup-specific styles if needed */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Word (docx-preview) ────────────────────────────────────────────── */
|
||||||
|
.docxContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background: #e5e5e5;
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docxContainer * {
|
||||||
|
background-color: initial !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docxLoading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The docx-preview library creates a wrapper with .docx-wrapper containing
|
||||||
|
section elements that are sized and styled like real pages. */
|
||||||
|
.docxContainer :global(.docx-wrapper) {
|
||||||
|
background: transparent !important;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docxContainer :global(.docx-wrapper > section.docx) {
|
||||||
|
background: #fff !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
|
||||||
|
margin: 0 auto 1.5rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docxContainer :global(section.docx) * {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Excel (manual table) ───────────────────────────────────────────── */
|
||||||
|
.excelTabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excelTab {
|
||||||
|
padding: 0.35rem 0.75rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
background: var(--color-background);
|
||||||
|
color: var(--color-text);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excelTabActive {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--color-on-primary, #fff);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.excelSheet {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--color-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.excelTable {
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-family: Calibri, "Segoe UI", Arial, sans-serif;
|
||||||
|
table-layout: fixed;
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excelTable td,
|
||||||
|
.excelTable th {
|
||||||
|
border: 1px solid #d0d7de;
|
||||||
|
padding: 2px 6px;
|
||||||
|
vertical-align: middle;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-background) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excelCorner,
|
||||||
|
.excelColHeader,
|
||||||
|
.excelRowHeader {
|
||||||
|
background: var(--color-surface, #f3f4f6) !important;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-secondary, #4b5563);
|
||||||
|
text-align: center !important;
|
||||||
|
position: sticky;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excelColHeader {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excelRowHeader {
|
||||||
|
left: 0;
|
||||||
|
min-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excelCorner {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excelCell {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { IoIosDownload, IoIosCopy } from 'react-icons/io';
|
import { IoIosDownload, IoIosCopy } from 'react-icons/io';
|
||||||
|
|
||||||
|
|
@ -14,7 +14,11 @@ import {
|
||||||
ApplicationRenderer,
|
ApplicationRenderer,
|
||||||
UnsupportedRenderer,
|
UnsupportedRenderer,
|
||||||
LoadingRenderer,
|
LoadingRenderer,
|
||||||
ErrorRenderer
|
ErrorRenderer,
|
||||||
|
WordRenderer,
|
||||||
|
ExcelRenderer,
|
||||||
|
isWordMimeType,
|
||||||
|
isExcelMimeType,
|
||||||
} from './renderers';
|
} from './renderers';
|
||||||
import styles from './ContentPreview.module.css';
|
import styles from './ContentPreview.module.css';
|
||||||
|
|
||||||
|
|
@ -31,93 +35,86 @@ export function ContentPreview({
|
||||||
onClose,
|
onClose,
|
||||||
fileId,
|
fileId,
|
||||||
fileName,
|
fileName,
|
||||||
mimeType
|
mimeType,
|
||||||
}: ContentPreviewProps) {
|
}: ContentPreviewProps) {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const { handleFilePreview, handleFileDownload, previewingFiles, previewError, downloadingFiles } = useFileOperations();
|
const {
|
||||||
|
handleFilePreview,
|
||||||
|
handleFileDownload,
|
||||||
|
previewingFiles,
|
||||||
|
previewError,
|
||||||
|
downloadingFiles,
|
||||||
|
} = useFileOperations();
|
||||||
|
|
||||||
// Debug logging to see what data we're receiving
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen && import.meta.env.DEV) {
|
|
||||||
console.log('ContentPreview received:', { fileId, fileName, mimeType });
|
|
||||||
}
|
|
||||||
}, [isOpen, fileId, fileName, mimeType]);
|
|
||||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||||
const [previewContent, setPreviewContent] = useState<string | null>(null);
|
const [blob, setBlob] = useState<Blob | null>(null);
|
||||||
|
const [textContent, setTextContent] = useState<string | null>(null);
|
||||||
|
const [resolvedMime, setResolvedMime] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [copySuccess, setCopySuccess] = useState<boolean>(false);
|
const [copySuccess, setCopySuccess] = useState(false);
|
||||||
|
|
||||||
|
const cleanup = useCallback(() => {
|
||||||
|
setPreviewUrl(prev => {
|
||||||
|
if (prev) window.URL.revokeObjectURL(prev);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
setBlob(null);
|
||||||
|
setTextContent(null);
|
||||||
|
setResolvedMime(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadPreview = useCallback(async () => {
|
||||||
|
setError(null);
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
const result = await handleFilePreview(fileId, fileName, mimeType);
|
||||||
|
if (!result.success) {
|
||||||
|
setError(result.error || t('Vorschau konnte nicht geladen werden.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreviewUrl(result.previewUrl ?? null);
|
||||||
|
setBlob(result.blob ?? null);
|
||||||
|
setTextContent(result.textContent ?? null);
|
||||||
|
setResolvedMime(result.mimeType ?? mimeType ?? null);
|
||||||
|
}, [cleanup, fileId, fileName, handleFilePreview, mimeType, t]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen || !fileId) {
|
||||||
|
cleanup();
|
||||||
|
setError(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileId === 'undefined' || fileId === 'null') {
|
||||||
|
setError(t('Ungültige Datei-ID'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!fileName || fileName === 'Unknown Item' || fileName === 'Unbekanntes Element') {
|
||||||
|
setError(t('Dateiname nicht verfügbar'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPreview();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [isOpen, fileId, fileName]);
|
||||||
|
|
||||||
// Clean up blob URL when component unmounts or preview changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (previewUrl) {
|
if (previewUrl) window.URL.revokeObjectURL(previewUrl);
|
||||||
window.URL.revokeObjectURL(previewUrl);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [previewUrl]);
|
}, [previewUrl]);
|
||||||
|
|
||||||
// Load preview when modal opens
|
const effectiveMime = resolvedMime ?? mimeType;
|
||||||
useEffect(() => {
|
const isPreviewing = previewingFiles.has(fileId);
|
||||||
if (isOpen && fileId) {
|
const hasError = error || previewError;
|
||||||
// Check if we have valid data
|
|
||||||
if (!fileId || fileId === 'undefined' || fileId === 'null') {
|
|
||||||
setError(t('Ungültige Datei-ID'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!fileName || fileName === 'Unknown Item' || fileName === 'Unbekanntes Element') {
|
|
||||||
setError(t('Dateiname nicht verfügbar'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loadPreview();
|
|
||||||
} else {
|
|
||||||
// Clean up when modal closes
|
|
||||||
if (previewUrl) {
|
|
||||||
window.URL.revokeObjectURL(previewUrl);
|
|
||||||
setPreviewUrl(null);
|
|
||||||
}
|
|
||||||
setError(null);
|
|
||||||
}
|
|
||||||
}, [isOpen, fileId, fileName, t]);
|
|
||||||
|
|
||||||
|
|
||||||
const loadPreview = async () => {
|
|
||||||
try {
|
|
||||||
setError(null);
|
|
||||||
setPreviewContent(null);
|
|
||||||
const result = await handleFilePreview(fileId, fileName, mimeType);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
if (result.previewUrl) {
|
|
||||||
setPreviewUrl(result.previewUrl);
|
|
||||||
}
|
|
||||||
if (result.decodedContent) {
|
|
||||||
setPreviewContent(result.decodedContent);
|
|
||||||
}
|
|
||||||
// If it's text content but MIME type says PDF, we'll handle it in renderPreview
|
|
||||||
} else {
|
|
||||||
setError(result.error || t('Vorschau konnte nicht geladen werden.'));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError(t('Ein unerwarteter Fehler ist aufgetreten, während'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopyContent = async () => {
|
const handleCopyContent = async () => {
|
||||||
|
if (!textContent) return;
|
||||||
try {
|
try {
|
||||||
if (previewContent) {
|
await navigator.clipboard.writeText(textContent);
|
||||||
await navigator.clipboard.writeText(previewContent);
|
setCopySuccess(true);
|
||||||
setCopySuccess(true);
|
setTimeout(() => setCopySuccess(false), 2000);
|
||||||
setTimeout(() => setCopySuccess(false), 2000);
|
|
||||||
} else {
|
|
||||||
// Fallback: try to copy from preview URL if it's a text file
|
|
||||||
if (previewUrl && mimeType?.startsWith('text/')) {
|
|
||||||
const response = await fetch(previewUrl);
|
|
||||||
const text = await response.text();
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
setCopySuccess(true);
|
|
||||||
setTimeout(() => setCopySuccess(false), 2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to copy content:', err);
|
console.error('Failed to copy content:', err);
|
||||||
}
|
}
|
||||||
|
|
@ -131,90 +128,63 @@ export function ContentPreview({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isPreviewing = previewingFiles.has(fileId);
|
|
||||||
const hasError = error || previewError;
|
|
||||||
|
|
||||||
// Check if this is a corrupted PDF (text content instead of PDF)
|
|
||||||
const isCorruptedPdf = mimeType === 'application/pdf' && previewContent && !previewUrl;
|
|
||||||
|
|
||||||
// Create action buttons for the popup header
|
|
||||||
const actions: PopupAction[] = [
|
const actions: PopupAction[] = [
|
||||||
// Copy Content Button - only show for text-based files (exclude PDFs and images) or corrupted PDFs
|
...(textContent
|
||||||
...(mimeType !== 'application/pdf' && !mimeType?.startsWith('image/') && (mimeType?.startsWith('text/') || mimeType === 'application/json' || previewContent) ? [{
|
? [
|
||||||
label: copySuccess ? t('In die Zwischenablage kopiert') : t(''),
|
{
|
||||||
icon: copySuccess ? '✓' : <IoIosCopy />,
|
label: copySuccess ? t('In die Zwischenablage kopiert') : '',
|
||||||
onClick: handleCopyContent,
|
icon: copySuccess ? '✓' : <IoIosCopy />,
|
||||||
disabled: !previewContent && !previewUrl,
|
onClick: handleCopyContent,
|
||||||
variant: 'primary' as const
|
disabled: !textContent,
|
||||||
}] : []),
|
variant: 'primary' as const,
|
||||||
|
},
|
||||||
// Download Button - hide for corrupted PDFs
|
]
|
||||||
...(isCorruptedPdf ? [] : [{
|
: []),
|
||||||
label: String(''),
|
{
|
||||||
|
label: '',
|
||||||
icon: downloadingFiles.has(fileId) ? undefined : <IoIosDownload />,
|
icon: downloadingFiles.has(fileId) ? undefined : <IoIosDownload />,
|
||||||
onClick: handleDownloadFile,
|
onClick: handleDownloadFile,
|
||||||
disabled: downloadingFiles.has(fileId),
|
disabled: downloadingFiles.has(fileId),
|
||||||
loading: downloadingFiles.has(fileId),
|
loading: downloadingFiles.has(fileId),
|
||||||
variant: 'success' as const
|
variant: 'success' as const,
|
||||||
}])
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const renderPreview = () => {
|
const renderPreview = () => {
|
||||||
// Handle text content in PDF files (corrupted files) - check this first
|
if (isPreviewing) return <LoadingRenderer />;
|
||||||
if (previewContent && !previewUrl && mimeType === 'application/pdf') {
|
if (hasError) return <ErrorRenderer error={hasError} onRetry={loadPreview} />;
|
||||||
console.log('🔍 ContentPreview: Rendering corrupted PDF with text content');
|
if (!blob || !effectiveMime) return null;
|
||||||
|
|
||||||
|
if (effectiveMime === 'application/json' && textContent) {
|
||||||
|
return <JsonRenderer previewContent={textContent} fileName={fileName} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWordMimeType(effectiveMime, fileName)) {
|
||||||
return (
|
return (
|
||||||
<PdfRenderer
|
<WordRenderer
|
||||||
previewUrl={undefined}
|
blob={blob}
|
||||||
previewContent={previewContent}
|
|
||||||
fileName={fileName}
|
fileName={fileName}
|
||||||
onError={() => setError(t('PDF-Vorschau konnte nicht geladen werden'))}
|
mimeType={effectiveMime}
|
||||||
|
onError={msg => setError(msg)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!previewUrl) {
|
if (isExcelMimeType(effectiveMime, fileName)) {
|
||||||
if (isPreviewing) {
|
|
||||||
return <LoadingRenderer />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasError) {
|
|
||||||
return <ErrorRenderer error={hasError} onRetry={loadPreview} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For JSON files with decoded content, use JsonRenderer
|
|
||||||
if (previewContent && mimeType === 'application/json') {
|
|
||||||
return <JsonRenderer previewContent={previewContent} fileName={fileName} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mimeType === 'application/json') {
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.jsonContainer}>
|
<ExcelRenderer
|
||||||
<div className={styles.jsonHeader}>
|
blob={blob}
|
||||||
<span className={styles.jsonTitle}>{t('JSON-Vorschau als Fallback')}</span>
|
fileName={fileName}
|
||||||
<div className={styles.jsonHeaderRight}>
|
onError={msg => setError(msg)}
|
||||||
<span className={styles.jsonSize}>{t('Rohinhalt')}</span>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<pre className={styles.jsonPreview}>
|
|
||||||
<code className={styles.jsonCode}>
|
|
||||||
{previewContent || t('Kein Inhalt verfügbar')}
|
|
||||||
</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine preview type based on MIME type
|
const mimePrefix = effectiveMime.split('/')[0];
|
||||||
const mimePrefix = mimeType?.split('/')[0];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
switch (mimePrefix) {
|
switch (mimePrefix) {
|
||||||
case 'image':
|
case 'image':
|
||||||
|
if (!previewUrl) return null;
|
||||||
return (
|
return (
|
||||||
<ImageRenderer
|
<ImageRenderer
|
||||||
previewUrl={previewUrl}
|
previewUrl={previewUrl}
|
||||||
|
|
@ -224,8 +194,7 @@ export function ContentPreview({
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'text':
|
case 'text':
|
||||||
// Special handling for HTML files
|
if (effectiveMime === 'text/html' && previewUrl) {
|
||||||
if (mimeType === 'text/html') {
|
|
||||||
return (
|
return (
|
||||||
<HtmlRenderer
|
<HtmlRenderer
|
||||||
previewUrl={previewUrl}
|
previewUrl={previewUrl}
|
||||||
|
|
@ -234,35 +203,27 @@ export function ContentPreview({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextRenderer
|
<TextRenderer
|
||||||
previewUrl={previewUrl}
|
previewUrl={previewUrl ?? undefined}
|
||||||
|
previewContent={textContent ?? undefined}
|
||||||
fileName={fileName}
|
fileName={fileName}
|
||||||
mimeType={mimeType}
|
mimeType={effectiveMime}
|
||||||
onError={() => setError(t('Textvorschau konnte nicht geladen werden'))}
|
onError={() => setError(t('Textvorschau konnte nicht geladen werden'))}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'application':
|
case 'application':
|
||||||
if (mimeType === 'application/pdf') {
|
if (effectiveMime === 'application/pdf' && previewUrl) {
|
||||||
console.log('🔍 ContentPreview passing normal PDF to PdfRenderer:', {
|
|
||||||
previewUrl,
|
|
||||||
previewContent: previewContent ? `${previewContent.substring(0, 50)}...` : null,
|
|
||||||
fileName,
|
|
||||||
mimeType
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<PdfRenderer
|
<PdfRenderer
|
||||||
previewUrl={previewUrl}
|
previewUrl={previewUrl}
|
||||||
previewContent={previewContent || undefined}
|
|
||||||
fileName={fileName}
|
fileName={fileName}
|
||||||
onError={() => setError(t('PDF-Vorschau konnte nicht geladen werden'))}
|
onError={() => setError(t('PDF-Vorschau konnte nicht geladen werden'))}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (effectiveMime === 'application/html' && previewUrl) {
|
||||||
if (mimeType === 'application/html') {
|
|
||||||
return (
|
return (
|
||||||
<HtmlRenderer
|
<HtmlRenderer
|
||||||
previewUrl={previewUrl}
|
previewUrl={previewUrl}
|
||||||
|
|
@ -271,20 +232,19 @@ export function ContentPreview({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApplicationRenderer
|
<ApplicationRenderer
|
||||||
previewUrl={previewUrl}
|
previewUrl={previewUrl ?? ''}
|
||||||
fileName={fileName}
|
fileName={fileName}
|
||||||
mimeType={mimeType}
|
mimeType={effectiveMime}
|
||||||
onError={() => setError(t('Vorschau wird für dieses Format nicht unterstützt'))}
|
onError={() =>
|
||||||
|
setError(t('Vorschau wird für dieses Format nicht unterstützt'))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return <UnsupportedRenderer previewUrl={previewUrl} fileName={fileName} />;
|
return <UnsupportedRenderer previewUrl={previewUrl ?? ''} fileName={fileName} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -297,12 +257,9 @@ export function ContentPreview({
|
||||||
className={styles.contentPreviewPopup}
|
className={styles.contentPreviewPopup}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
>
|
>
|
||||||
<div className={styles.previewContainer}>
|
<div className={styles.previewContainer}>{renderPreview()}</div>
|
||||||
{renderPreview()}
|
|
||||||
</div>
|
|
||||||
</Popup>
|
</Popup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ContentPreview;
|
export default ContentPreview;
|
||||||
|
|
||||||
|
|
|
||||||
281
src/components/ContentPreview/renderers/ExcelRenderer.tsx
Normal file
281
src/components/ContentPreview/renderers/ExcelRenderer.tsx
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import * as XLSX from 'xlsx';
|
||||||
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
|
import styles from '../ContentPreview.module.css';
|
||||||
|
|
||||||
|
interface ExcelRendererProps {
|
||||||
|
blob: Blob;
|
||||||
|
fileName: string;
|
||||||
|
onError: (message: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EXCEL_MIME_TYPES = new Set([
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'text/csv',
|
||||||
|
'application/csv',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function isExcelMimeType(mimeType?: string, fileName?: string): boolean {
|
||||||
|
if (mimeType && EXCEL_MIME_TYPES.has(mimeType)) return true;
|
||||||
|
if (fileName && /\.(xlsx|xls|xlsm|xlsb|ods|csv)$/i.test(fileName)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RenderedCell {
|
||||||
|
display: string;
|
||||||
|
rawType: 'n' | 's' | 'b' | 'd' | 'e' | 'z' | string;
|
||||||
|
rowspan: number;
|
||||||
|
colspan: number;
|
||||||
|
skip: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RenderedSheet {
|
||||||
|
name: string;
|
||||||
|
cols: number;
|
||||||
|
rows: number;
|
||||||
|
colWidthsPx: number[];
|
||||||
|
rowHeightsPx: (number | null)[];
|
||||||
|
cells: RenderedCell[][];
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSheet(ws: XLSX.WorkSheet, name: string): RenderedSheet {
|
||||||
|
const ref = ws['!ref'];
|
||||||
|
if (!ref) {
|
||||||
|
return { name, cols: 0, rows: 0, colWidthsPx: [], rowHeightsPx: [], cells: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = XLSX.utils.decode_range(ref);
|
||||||
|
const rows = range.e.r - range.s.r + 1;
|
||||||
|
const cols = range.e.c - range.s.c + 1;
|
||||||
|
|
||||||
|
const cells: RenderedCell[][] = Array.from({ length: rows }, () =>
|
||||||
|
Array.from({ length: cols }, () => ({
|
||||||
|
display: '',
|
||||||
|
rawType: 'z',
|
||||||
|
rowspan: 1,
|
||||||
|
colspan: 1,
|
||||||
|
skip: false,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let r = 0; r < rows; r++) {
|
||||||
|
for (let c = 0; c < cols; c++) {
|
||||||
|
const address = XLSX.utils.encode_cell({ r: r + range.s.r, c: c + range.s.c });
|
||||||
|
const cell = ws[address] as XLSX.CellObject | undefined;
|
||||||
|
if (!cell) continue;
|
||||||
|
const display = cell.w ?? (cell.v !== undefined && cell.v !== null ? String(cell.v) : '');
|
||||||
|
cells[r][c].display = display;
|
||||||
|
cells[r][c].rawType = cell.t ?? 'z';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const merges = ws['!merges'] ?? [];
|
||||||
|
for (const merge of merges) {
|
||||||
|
const rs = merge.s.r - range.s.r;
|
||||||
|
const cs = merge.s.c - range.s.c;
|
||||||
|
const re = merge.e.r - range.s.r;
|
||||||
|
const ce = merge.e.c - range.s.c;
|
||||||
|
if (rs < 0 || cs < 0 || re >= rows || ce >= cols) continue;
|
||||||
|
|
||||||
|
cells[rs][cs].rowspan = re - rs + 1;
|
||||||
|
cells[rs][cs].colspan = ce - cs + 1;
|
||||||
|
|
||||||
|
for (let r = rs; r <= re; r++) {
|
||||||
|
for (let c = cs; c <= ce; c++) {
|
||||||
|
if (r === rs && c === cs) continue;
|
||||||
|
cells[r][c].skip = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const colWidthsPx: number[] = [];
|
||||||
|
const colsMeta = ws['!cols'] ?? [];
|
||||||
|
for (let c = 0; c < cols; c++) {
|
||||||
|
const meta = colsMeta[c + range.s.c];
|
||||||
|
if (meta?.wpx) {
|
||||||
|
colWidthsPx.push(meta.wpx);
|
||||||
|
} else if (meta?.wch) {
|
||||||
|
colWidthsPx.push(Math.round(meta.wch * 7 + 8));
|
||||||
|
} else if (meta?.width) {
|
||||||
|
colWidthsPx.push(Math.round(meta.width * 7 + 8));
|
||||||
|
} else {
|
||||||
|
colWidthsPx.push(80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowHeightsPx: (number | null)[] = [];
|
||||||
|
const rowsMeta = ws['!rows'] ?? [];
|
||||||
|
for (let r = 0; r < rows; r++) {
|
||||||
|
const meta = rowsMeta[r + range.s.r];
|
||||||
|
if (meta?.hpx) {
|
||||||
|
rowHeightsPx.push(meta.hpx);
|
||||||
|
} else if (meta?.hpt) {
|
||||||
|
rowHeightsPx.push(Math.round((meta.hpt * 4) / 3));
|
||||||
|
} else {
|
||||||
|
rowHeightsPx.push(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { name, cols, rows, colWidthsPx, rowHeightsPx, cells };
|
||||||
|
}
|
||||||
|
|
||||||
|
function alignmentForCell(cell: RenderedCell): 'left' | 'right' | 'center' {
|
||||||
|
if (cell.rawType === 'n' || cell.rawType === 'd') return 'right';
|
||||||
|
if (cell.rawType === 'b') return 'center';
|
||||||
|
return 'left';
|
||||||
|
}
|
||||||
|
|
||||||
|
function colLabel(index: number): string {
|
||||||
|
let result = '';
|
||||||
|
let n = index;
|
||||||
|
do {
|
||||||
|
result = String.fromCharCode(65 + (n % 26)) + result;
|
||||||
|
n = Math.floor(n / 26) - 1;
|
||||||
|
} while (n >= 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExcelRenderer({ blob, fileName, onError }: ExcelRendererProps) {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const [sheets, setSheets] = useState<RenderedSheet[]>([]);
|
||||||
|
const [activeSheet, setActiveSheet] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [localError, setLocalError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
setLoading(true);
|
||||||
|
setLocalError(null);
|
||||||
|
|
||||||
|
blob
|
||||||
|
.arrayBuffer()
|
||||||
|
.then(buffer => {
|
||||||
|
const workbook = XLSX.read(buffer, {
|
||||||
|
type: 'array',
|
||||||
|
cellDates: true,
|
||||||
|
cellNF: true,
|
||||||
|
cellStyles: true,
|
||||||
|
});
|
||||||
|
const parsed = workbook.SheetNames.map(name =>
|
||||||
|
renderSheet(workbook.Sheets[name], name),
|
||||||
|
);
|
||||||
|
if (cancelled) return;
|
||||||
|
setSheets(parsed);
|
||||||
|
setActiveSheet(parsed[0]?.name ?? null);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (cancelled) return;
|
||||||
|
const msg = err?.message ?? t('Tabelle konnte nicht gerendert werden.');
|
||||||
|
setLocalError(msg);
|
||||||
|
setLoading(false);
|
||||||
|
onError(msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [blob, onError, t]);
|
||||||
|
|
||||||
|
const current = useMemo(
|
||||||
|
() => sheets.find(s => s.name === activeSheet) ?? null,
|
||||||
|
[sheets, activeSheet],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className={styles.textContainer}>
|
||||||
|
<div className={styles.textHeader}>
|
||||||
|
<span className={styles.textTitle}>{t('Tabelle wird geladen...')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localError) {
|
||||||
|
return (
|
||||||
|
<div className={styles.textContainer}>
|
||||||
|
<div className={styles.textHeader}>
|
||||||
|
<span className={styles.textTitle}>{fileName}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: '1rem', color: 'var(--color-error)' }}>{localError}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.textContainer}>
|
||||||
|
<div className={styles.textHeader}>
|
||||||
|
<span className={styles.textTitle}>{fileName}</span>
|
||||||
|
{sheets.length > 1 && (
|
||||||
|
<div className={styles.excelTabs}>
|
||||||
|
{sheets.map(sheet => (
|
||||||
|
<button
|
||||||
|
key={sheet.name}
|
||||||
|
onClick={() => setActiveSheet(sheet.name)}
|
||||||
|
className={`${styles.excelTab} ${
|
||||||
|
sheet.name === activeSheet ? styles.excelTabActive : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{sheet.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.excelSheet}>
|
||||||
|
{current && current.rows > 0 ? (
|
||||||
|
<table className={styles.excelTable}>
|
||||||
|
<colgroup>
|
||||||
|
<col style={{ width: 40 }} />
|
||||||
|
{current.colWidthsPx.map((w, i) => (
|
||||||
|
<col key={i} style={{ width: w }} />
|
||||||
|
))}
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className={styles.excelCorner} />
|
||||||
|
{current.colWidthsPx.map((_, i) => (
|
||||||
|
<th key={i} className={styles.excelColHeader}>
|
||||||
|
{colLabel(i)}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{current.cells.map((row, rIdx) => (
|
||||||
|
<tr
|
||||||
|
key={rIdx}
|
||||||
|
style={{
|
||||||
|
height: current.rowHeightsPx[rIdx] ?? undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<th className={styles.excelRowHeader}>{rIdx + 1}</th>
|
||||||
|
{row.map((cell, cIdx) => {
|
||||||
|
if (cell.skip) return null;
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
key={cIdx}
|
||||||
|
rowSpan={cell.rowspan}
|
||||||
|
colSpan={cell.colspan}
|
||||||
|
className={styles.excelCell}
|
||||||
|
style={{ textAlign: alignmentForCell(cell) }}
|
||||||
|
>
|
||||||
|
{cell.display}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
) : (
|
||||||
|
<div style={{ padding: '1rem', color: 'var(--color-text-secondary)' }}>
|
||||||
|
{t('Dieses Arbeitsblatt ist leer.')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
110
src/components/ContentPreview/renderers/WordRenderer.tsx
Normal file
110
src/components/ContentPreview/renderers/WordRenderer.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { renderAsync } from 'docx-preview';
|
||||||
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
|
import styles from '../ContentPreview.module.css';
|
||||||
|
|
||||||
|
interface WordRendererProps {
|
||||||
|
blob: Blob;
|
||||||
|
fileName: string;
|
||||||
|
mimeType?: string;
|
||||||
|
onError: (message: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SUPPORTED_MIME_TYPES = new Set([
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function isWordMimeType(mimeType?: string, fileName?: string): boolean {
|
||||||
|
if (mimeType && SUPPORTED_MIME_TYPES.has(mimeType)) return true;
|
||||||
|
if (fileName && /\.docx$/i.test(fileName)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WordRenderer({ blob, fileName, mimeType, onError }: WordRendererProps) {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const bodyRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const styleRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [localError, setLocalError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const isLegacyDoc = useMemo(
|
||||||
|
() => mimeType === 'application/msword' || /\.doc$/i.test(fileName),
|
||||||
|
[mimeType, fileName],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
if (isLegacyDoc) {
|
||||||
|
const msg = t(
|
||||||
|
'Das alte Word-Format (.doc) wird nicht unterstützt. Bitte konvertiere die Datei in .docx.',
|
||||||
|
);
|
||||||
|
setLocalError(msg);
|
||||||
|
setLoading(false);
|
||||||
|
onError(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = bodyRef.current;
|
||||||
|
const styleContainer = styleRef.current;
|
||||||
|
if (!body || !styleContainer) return;
|
||||||
|
|
||||||
|
body.innerHTML = '';
|
||||||
|
styleContainer.innerHTML = '';
|
||||||
|
setLoading(true);
|
||||||
|
setLocalError(null);
|
||||||
|
|
||||||
|
renderAsync(blob, body, styleContainer, {
|
||||||
|
className: 'docx-preview',
|
||||||
|
inWrapper: true,
|
||||||
|
ignoreWidth: false,
|
||||||
|
ignoreHeight: false,
|
||||||
|
ignoreFonts: false,
|
||||||
|
breakPages: true,
|
||||||
|
experimental: true,
|
||||||
|
trimXmlDeclaration: true,
|
||||||
|
useBase64URL: true,
|
||||||
|
renderHeaders: true,
|
||||||
|
renderFooters: true,
|
||||||
|
renderFootnotes: true,
|
||||||
|
renderEndnotes: true,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (cancelled) return;
|
||||||
|
const msg =
|
||||||
|
err?.message ?? t('Word-Dokument konnte nicht gerendert werden.');
|
||||||
|
setLocalError(msg);
|
||||||
|
setLoading(false);
|
||||||
|
onError(msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [blob, isLegacyDoc, onError, t]);
|
||||||
|
|
||||||
|
if (localError) {
|
||||||
|
return (
|
||||||
|
<div className={styles.textContainer}>
|
||||||
|
<div className={styles.textHeader}>
|
||||||
|
<span className={styles.textTitle}>{fileName}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: '1rem', color: 'var(--color-error)' }}>{localError}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.docxContainer}>
|
||||||
|
{loading && (
|
||||||
|
<div className={styles.docxLoading}>{t('Word-Dokument wird geladen...')}</div>
|
||||||
|
)}
|
||||||
|
<div ref={styleRef} />
|
||||||
|
<div ref={bodyRef} className={styles.docxBody} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -7,4 +7,6 @@ export { ApplicationRenderer } from './ApplicationRenderer';
|
||||||
export { UnsupportedRenderer } from './UnsupportedRenderer';
|
export { UnsupportedRenderer } from './UnsupportedRenderer';
|
||||||
export { LoadingRenderer } from './LoadingRenderer';
|
export { LoadingRenderer } from './LoadingRenderer';
|
||||||
export { ErrorRenderer } from './ErrorRenderer';
|
export { ErrorRenderer } from './ErrorRenderer';
|
||||||
|
export { WordRenderer, isWordMimeType } from './WordRenderer';
|
||||||
|
export { ExcelRenderer, isExcelMimeType } from './ExcelRenderer';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { createContext, useContext, useCallback, useState, useEffect, useMemo } from 'react';
|
import React, { createContext, useContext, useCallback, useState, useEffect, useMemo } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import { useFileOperations } from '../hooks/useFiles';
|
import { useFileOperations, type FilePreviewResult } from '../hooks/useFiles';
|
||||||
import type { FolderInfo } from '../api/fileApi';
|
import type { FolderInfo } from '../api/fileApi';
|
||||||
import type { FileNode } from '../components/FolderTree/FolderTree';
|
import type { FileNode } from '../components/FolderTree/FolderTree';
|
||||||
|
|
||||||
|
|
@ -31,7 +31,7 @@ interface FileContextType {
|
||||||
handleDownloadFolder: (folderId: string, folderName: string) => Promise<void>;
|
handleDownloadFolder: (folderId: string, folderName: string) => Promise<void>;
|
||||||
handleFileDelete: (fileId: string, onOptimisticDelete?: () => void) => Promise<boolean>;
|
handleFileDelete: (fileId: string, onOptimisticDelete?: () => void) => Promise<boolean>;
|
||||||
handleFileUpload: (file: File, workflowId?: string) => Promise<{ success: boolean; fileData?: any; error?: string }>;
|
handleFileUpload: (file: File, workflowId?: string) => Promise<{ success: boolean; fileData?: any; error?: string }>;
|
||||||
handleFilePreview: (fileId: string, fileName: string, mimeType?: string) => Promise<{ success: boolean; previewUrl?: string | null; blob?: Blob | null; isJsonContent?: boolean; decodedContent?: any; isTextContent?: boolean; error?: string }>;
|
handleFilePreview: (fileId: string, fileName: string, mimeType?: string) => Promise<FilePreviewResult>;
|
||||||
handleFileDownload: (fileId: string, fileName: string) => Promise<void>;
|
handleFileDownload: (fileId: string, fileName: string) => Promise<void>;
|
||||||
uploadingFile: boolean;
|
uploadingFile: boolean;
|
||||||
deletingFiles: Set<string>;
|
deletingFiles: Set<string>;
|
||||||
|
|
@ -316,7 +316,7 @@ export function FileProvider({ children }: { children: React.ReactNode }) {
|
||||||
handleDownloadFolder,
|
handleDownloadFolder,
|
||||||
handleFileDelete,
|
handleFileDelete,
|
||||||
handleFileUpload,
|
handleFileUpload,
|
||||||
handleFilePreview: handleFilePreview as FileContextType['handleFilePreview'],
|
handleFilePreview,
|
||||||
handleFileDownload: async (fileId: string, fileName: string) => {
|
handleFileDownload: async (fileId: string, fileName: string) => {
|
||||||
await handleFileDownload(fileId, fileName);
|
await handleFileDownload(fileId, fileName);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,15 @@ import {
|
||||||
type FolderInfo,
|
type FolderInfo,
|
||||||
} from '../api/fileApi';
|
} from '../api/fileApi';
|
||||||
|
|
||||||
|
export interface FilePreviewResult {
|
||||||
|
success: boolean;
|
||||||
|
previewUrl?: string;
|
||||||
|
blob?: Blob;
|
||||||
|
mimeType?: string;
|
||||||
|
textContent?: string | null;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// File interfaces - exactly matching backend FileItem model
|
// File interfaces - exactly matching backend FileItem model
|
||||||
export interface FileInfo {
|
export interface FileInfo {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -588,331 +597,64 @@ export function useFileOperations() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilePreview = async (fileId: string, fileName: string, mimeType?: string) => {
|
const handleFilePreview = async (
|
||||||
|
fileId: string,
|
||||||
|
fileName: string,
|
||||||
|
_mimeType?: string,
|
||||||
|
): Promise<FilePreviewResult> => {
|
||||||
setPreviewError(null);
|
setPreviewError(null);
|
||||||
setPreviewingFiles(prev => new Set(prev).add(fileId));
|
setPreviewingFiles(prev => new Set(prev).add(fileId));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const response = await api.get(`/api/files/${fileId}/preview`, {
|
||||||
|
responseType: 'json',
|
||||||
|
});
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
// For PDF files, try JSON response first (API returns base64-encoded PDF)
|
if (!data || typeof data !== 'object' || typeof data.content !== 'string' || !data.mimeType) {
|
||||||
if (mimeType === 'application/pdf') {
|
throw new Error('Invalid preview response from server');
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await api.get(`/api/files/${fileId}/preview`, {
|
|
||||||
responseType: 'json',
|
|
||||||
validateStatus: function (status: number) {
|
|
||||||
return status >= 200 && status < 300;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const jsonResponse = response.data;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Check if response has base64-encoded PDF content
|
|
||||||
if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) {
|
|
||||||
let content = jsonResponse.content;
|
|
||||||
|
|
||||||
// The content field contains base64-encoded JSON, so decode it first
|
|
||||||
if (typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) {
|
|
||||||
try {
|
|
||||||
const decodedJsonString = atob(content);
|
|
||||||
|
|
||||||
// Parse the decoded JSON string
|
|
||||||
const nestedJson = JSON.parse(decodedJsonString);
|
|
||||||
|
|
||||||
if (nestedJson && typeof nestedJson === 'object' && 'content' in nestedJson) {
|
|
||||||
const innerContent = nestedJson.content;
|
|
||||||
const isBase64 = /^[A-Za-z0-9+/=]+$/.test(innerContent);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (isBase64) {
|
|
||||||
// It's base64-encoded PDF content
|
|
||||||
content = innerContent;
|
|
||||||
} else {
|
|
||||||
// It's plain text content, not a PDF
|
|
||||||
// Return the text content for the FilePreview to handle as text
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
previewUrl: null,
|
|
||||||
blob: null,
|
|
||||||
isJsonContent: true,
|
|
||||||
decodedContent: innerContent,
|
|
||||||
isTextContent: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (decodeError) {
|
|
||||||
console.warn('⚠️ Failed to decode base64 content or parse JSON:', decodeError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Decode base64 content
|
|
||||||
let decodedContent;
|
|
||||||
try {
|
|
||||||
decodedContent = atob(content);
|
|
||||||
|
|
||||||
|
|
||||||
// Verify it's actually a PDF
|
|
||||||
const isPDF = decodedContent.startsWith('%PDF');
|
|
||||||
|
|
||||||
|
|
||||||
if (!isPDF) {
|
|
||||||
console.warn('⚠️ Decoded content does not appear to be a valid PDF');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (decodeError) {
|
|
||||||
console.error('❌ Failed to decode base64 PDF content:', decodeError);
|
|
||||||
throw new Error('Failed to decode PDF content');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a blob from the decoded PDF content
|
|
||||||
// Convert string to Uint8Array for proper binary handling
|
|
||||||
const uint8Array = new Uint8Array(decodedContent.length);
|
|
||||||
for (let i = 0; i < decodedContent.length; i++) {
|
|
||||||
uint8Array[i] = decodedContent.charCodeAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = new Blob([uint8Array], { type: 'application/pdf' });
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent };
|
|
||||||
} else {
|
|
||||||
throw new Error('No content field in PDF response');
|
|
||||||
}
|
|
||||||
} catch (jsonError) {
|
|
||||||
|
|
||||||
|
|
||||||
// Fallback to blob response
|
|
||||||
const response = await api.get(`/api/files/${fileId}/preview`, {
|
|
||||||
responseType: 'blob',
|
|
||||||
validateStatus: function (status: number) {
|
|
||||||
return status >= 200 && status < 300;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const previewData = response.data;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(previewData);
|
|
||||||
|
|
||||||
return { success: true, previewUrl: url, blob: previewData, isJsonContent: false };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For image files, try JSON response first (API returns base64-encoded images)
|
const { content, mimeType: responseMime, isText } = data as {
|
||||||
if (mimeType?.startsWith('image/')) {
|
content: string;
|
||||||
|
mimeType: string;
|
||||||
|
isText?: boolean;
|
||||||
|
encoding?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let blob: Blob;
|
||||||
|
let textContent: string | null = null;
|
||||||
|
|
||||||
try {
|
if (isText) {
|
||||||
const response = await api.get(`/api/files/${fileId}/preview`, {
|
textContent = content;
|
||||||
responseType: 'json',
|
blob = new Blob([content], { type: responseMime });
|
||||||
validateStatus: function (status: number) {
|
} else {
|
||||||
return status >= 200 && status < 300;
|
const binaryString = atob(content);
|
||||||
}
|
const bytes = new Uint8Array(binaryString.length);
|
||||||
});
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
const jsonResponse = response.data;
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Check if response has base64-encoded image content
|
|
||||||
if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) {
|
|
||||||
let content = jsonResponse.content;
|
|
||||||
const responseMimeType = jsonResponse.mimeType || mimeType;
|
|
||||||
|
|
||||||
// The content field contains base64-encoded data, decode it first
|
|
||||||
if (typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
const decodedString = atob(content);
|
|
||||||
|
|
||||||
|
|
||||||
// Check if it's JSON (nested structure) or direct image data
|
|
||||||
if (decodedString.startsWith('{')) {
|
|
||||||
// It's JSON, parse it
|
|
||||||
const nestedJson = JSON.parse(decodedString);
|
|
||||||
|
|
||||||
|
|
||||||
if (nestedJson && typeof nestedJson === 'object' && 'content' in nestedJson) {
|
|
||||||
const innerContent = nestedJson.content;
|
|
||||||
const isBase64 = /^[A-Za-z0-9+/=]+$/.test(innerContent);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (isBase64) {
|
|
||||||
// It's base64-encoded image content
|
|
||||||
content = innerContent;
|
|
||||||
} else {
|
|
||||||
throw new Error('Inner content is not base64-encoded');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (decodedString.startsWith('\x89PNG') || decodedString.startsWith('\xFF\xD8\xFF') || decodedString.startsWith('GIF8') || decodedString.startsWith('RIFF')) {
|
|
||||||
// It's direct image data, use it as is
|
|
||||||
content = btoa(decodedString); // Re-encode as base64 for processing
|
|
||||||
} else {
|
|
||||||
throw new Error('Decoded content is neither JSON nor image data');
|
|
||||||
}
|
|
||||||
} catch (decodeError) {
|
|
||||||
console.warn('⚠️ Failed to decode base64 content:', decodeError);
|
|
||||||
throw decodeError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Decode base64 content
|
|
||||||
let decodedContent;
|
|
||||||
try {
|
|
||||||
decodedContent = atob(content);
|
|
||||||
|
|
||||||
|
|
||||||
// Verify it's actually an image by checking for common image headers
|
|
||||||
const isJPEG = decodedContent.startsWith('\xFF\xD8\xFF');
|
|
||||||
const isPNG = decodedContent.startsWith('\x89PNG\r\n\x1a\n');
|
|
||||||
const isGIF = decodedContent.startsWith('GIF8');
|
|
||||||
const isWebP = decodedContent.startsWith('RIFF') && decodedContent.includes('WEBP');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (!isJPEG && !isPNG && !isGIF && !isWebP) {
|
|
||||||
console.warn('⚠️ Decoded content does not appear to be a valid image');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (decodeError) {
|
|
||||||
console.error('❌ Failed to decode base64 image content:', decodeError);
|
|
||||||
throw new Error('Failed to decode image content');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a blob from the decoded image content
|
|
||||||
// Convert string to Uint8Array for proper binary handling
|
|
||||||
const uint8Array = new Uint8Array(decodedContent.length);
|
|
||||||
for (let i = 0; i < decodedContent.length; i++) {
|
|
||||||
uint8Array[i] = decodedContent.charCodeAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = new Blob([uint8Array], { type: responseMimeType });
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent };
|
|
||||||
} else {
|
|
||||||
throw new Error('No content field in image response');
|
|
||||||
}
|
|
||||||
} catch (jsonError) {
|
|
||||||
|
|
||||||
// Fallback to blob response
|
|
||||||
const response = await api.get(`/api/files/${fileId}/preview`, {
|
|
||||||
responseType: 'blob',
|
|
||||||
validateStatus: function (status: number) {
|
|
||||||
return status >= 200 && status < 300;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const previewData = response.data;
|
|
||||||
|
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(previewData);
|
|
||||||
|
|
||||||
return { success: true, previewUrl: url, blob: previewData, isJsonContent: false };
|
|
||||||
}
|
}
|
||||||
|
blob = new Blob([bytes], { type: responseMime });
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other files, first try to get JSON response (for text-based files)
|
const previewUrl = window.URL.createObjectURL(blob);
|
||||||
try {
|
|
||||||
const response = await api.get(`/api/files/${fileId}/preview`, {
|
|
||||||
responseType: 'json',
|
|
||||||
validateStatus: function (status: number) {
|
|
||||||
return status >= 200 && status < 300;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const jsonResponse = response.data;
|
|
||||||
|
|
||||||
// Check if response has content field (structured response)
|
return {
|
||||||
if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) {
|
success: true,
|
||||||
const content = jsonResponse.content;
|
previewUrl,
|
||||||
const mimeType = jsonResponse.mimeType || 'text/plain';
|
blob,
|
||||||
|
mimeType: responseMime,
|
||||||
|
textContent,
|
||||||
|
};
|
||||||
// Check if content is base64 encoded (common pattern)
|
|
||||||
let decodedContent = content;
|
|
||||||
try {
|
|
||||||
// Try to decode as base64 if it looks like base64
|
|
||||||
if (content && typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) {
|
|
||||||
decodedContent = atob(content);
|
|
||||||
|
|
||||||
}
|
|
||||||
} catch (decodeError) {
|
|
||||||
decodedContent = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a blob from the (possibly decoded) content
|
|
||||||
const blob = new Blob([decodedContent], { type: mimeType });
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
|
|
||||||
return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent };
|
|
||||||
} else if (jsonResponse && typeof jsonResponse === 'object' && 'result' in jsonResponse) {
|
|
||||||
// Handle base64 encoded content in 'result' field
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Decode base64 content
|
|
||||||
const decodedContent = atob(jsonResponse.result);
|
|
||||||
const mimeType = jsonResponse.mimeType || 'application/json';
|
|
||||||
|
|
||||||
|
|
||||||
// Create a blob from the decoded content
|
|
||||||
const blob = new Blob([decodedContent], { type: mimeType });
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent };
|
|
||||||
} catch (decodeError) {
|
|
||||||
console.error('❌ Failed to decode base64 content:', decodeError);
|
|
||||||
// Fallback to treating as raw JSON
|
|
||||||
const blob = new Blob([JSON.stringify(jsonResponse, null, 2)], { type: 'application/json' });
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
return { success: true, previewUrl: url, blob: blob, isJsonContent: true };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If it's not structured JSON, treat as raw content
|
|
||||||
const blob = new Blob([JSON.stringify(jsonResponse, null, 2)], { type: 'application/json' });
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
return { success: true, previewUrl: url, blob: blob, isJsonContent: true };
|
|
||||||
}
|
|
||||||
} catch (jsonError) {
|
|
||||||
|
|
||||||
// Fallback to blob response for binary files
|
|
||||||
const response = await api.get(`/api/files/${fileId}/preview`, {
|
|
||||||
responseType: 'blob',
|
|
||||||
validateStatus: function (status: number) {
|
|
||||||
return status >= 200 && status < 300;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const previewData = response.data;
|
|
||||||
|
|
||||||
|
|
||||||
// Create a blob URL for preview
|
|
||||||
const url = window.URL.createObjectURL(previewData);
|
|
||||||
|
|
||||||
return { success: true, previewUrl: url, blob: previewData, isJsonContent: false };
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(`❌ Preview failed for ${fileName}:`, error);
|
console.error(`Preview failed for ${fileName}:`, error);
|
||||||
let errorMessage = error.message;
|
let errorMessage = error?.message ?? 'Unknown error';
|
||||||
|
|
||||||
if (error.response?.status === 404) {
|
if (error?.response?.status === 404) {
|
||||||
errorMessage = `File "${fileName}" not found or has been deleted.`;
|
errorMessage = `File "${fileName}" not found or has been deleted.`;
|
||||||
} else if (error.response?.status === 403) {
|
} else if (error?.response?.status === 403) {
|
||||||
errorMessage = `No permission to preview "${fileName}".`;
|
errorMessage = `No permission to preview "${fileName}".`;
|
||||||
} else if (error.response?.status === 415) {
|
} else if (error?.response?.status === 415) {
|
||||||
errorMessage = `File type "${fileName}" is not supported for preview.`;
|
errorMessage = `File type "${fileName}" is not supported for preview.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { FormGeneratorTable } from '../../components/FormGenerator/FormGenerator
|
||||||
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
||||||
import FolderTree from '../../components/FolderTree/FolderTree';
|
import FolderTree from '../../components/FolderTree/FolderTree';
|
||||||
import { useResizablePanels } from '../../hooks/useResizablePanels';
|
import { useResizablePanels } from '../../hooks/useResizablePanels';
|
||||||
import { FaSync, FaUpload, FaDownload, FaEye, FaFolderPlus } from 'react-icons/fa';
|
import { FaSync, FaUpload, FaDownload, FaFolderPlus } from 'react-icons/fa';
|
||||||
import { useToast } from '../../contexts/ToastContext';
|
import { useToast } from '../../contexts/ToastContext';
|
||||||
import { usePrompt } from '../../hooks/usePrompt';
|
import { usePrompt } from '../../hooks/usePrompt';
|
||||||
import styles from '../admin/Admin.module.css';
|
import styles from '../admin/Admin.module.css';
|
||||||
|
|
@ -66,7 +66,6 @@ export const FilesPage: React.FC = () => {
|
||||||
handleFileDeleteMultiple,
|
handleFileDeleteMultiple,
|
||||||
handleFileUpload,
|
handleFileUpload,
|
||||||
handleFileUpdate,
|
handleFileUpdate,
|
||||||
handleFilePreview,
|
|
||||||
handleInlineUpdate,
|
handleInlineUpdate,
|
||||||
deletingFiles,
|
deletingFiles,
|
||||||
downloadingFiles,
|
downloadingFiles,
|
||||||
|
|
@ -278,13 +277,6 @@ export const FilesPage: React.FC = () => {
|
||||||
await handleFileDownload(file.id, file.fileName);
|
await handleFileDownload(file.id, file.fileName);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePreview = async (file: UserFile) => {
|
|
||||||
const result = await handleFilePreview(file.id, file.fileName, file.mimeType);
|
|
||||||
if (result.success && result.previewUrl) {
|
|
||||||
window.open(result.previewUrl, '_blank');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUploadClick = () => { fileInputRef.current?.click(); };
|
const handleUploadClick = () => { fileInputRef.current?.click(); };
|
||||||
|
|
||||||
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
|
@ -465,6 +457,15 @@ export const FilesPage: React.FC = () => {
|
||||||
({ highlighted: row.id === highlightedFileId ? 'true' : 'false' })
|
({ highlighted: row.id === highlightedFileId ? 'true' : 'false' })
|
||||||
}
|
}
|
||||||
actionButtons={[
|
actionButtons={[
|
||||||
|
{
|
||||||
|
type: 'view' as const,
|
||||||
|
onAction: () => { /* ContentPreview fetches the file itself once the popup opens */ },
|
||||||
|
title: t('Vorschau'),
|
||||||
|
idField: 'id',
|
||||||
|
nameField: 'fileName',
|
||||||
|
typeField: 'mimeType',
|
||||||
|
loadingStateName: 'previewingFiles',
|
||||||
|
},
|
||||||
...(canUpdate ? [{
|
...(canUpdate ? [{
|
||||||
type: 'edit' as const,
|
type: 'edit' as const,
|
||||||
onAction: handleEditClick,
|
onAction: handleEditClick,
|
||||||
|
|
@ -486,13 +487,6 @@ export const FilesPage: React.FC = () => {
|
||||||
title: t('Herunterladen'),
|
title: t('Herunterladen'),
|
||||||
loading: (row: UserFile) => downloadingFiles.has(row.id),
|
loading: (row: UserFile) => downloadingFiles.has(row.id),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'preview',
|
|
||||||
icon: <FaEye />,
|
|
||||||
onClick: handlePreview,
|
|
||||||
title: t('Vorschau'),
|
|
||||||
loading: (row: UserFile) => previewingFiles.has(row.id),
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onDeleteMultiple={handleDeleteMultiple}
|
onDeleteMultiple={handleDeleteMultiple}
|
||||||
|
|
@ -503,6 +497,7 @@ export const FilesPage: React.FC = () => {
|
||||||
handleDelete: handleFileDelete,
|
handleDelete: handleFileDelete,
|
||||||
handleInlineUpdate,
|
handleInlineUpdate,
|
||||||
updateOptimistically: updateFileOptimistically,
|
updateOptimistically: updateFileOptimistically,
|
||||||
|
previewingFiles,
|
||||||
}}
|
}}
|
||||||
emptyMessage={t('Keine Dateien gefunden')}
|
emptyMessage={t('Keine Dateien gefunden')}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue