fix: add image preview and replace Monaco with CodeMirror 6
Markdown and image previews were rendering blank because @monaco-editor/react loads its core assets from cdn.jsdelivr.net at runtime, which the deployment server's network can't reach. Switching to @uiw/react-codemirror removes the CDN dependency entirely (all assets ship in the build) and cuts bundle size from ~2.5MB to ~770KB. Also add proper image/* content-type detection so images render via <img> instead of being force-decoded as editor text.
This commit is contained in:
parent
542361c63a
commit
954b9bede4
@ -49,11 +49,23 @@ def _extension_of(key: str) -> str:
|
||||
return "." + name.rsplit(".", 1)[-1].lower()
|
||||
|
||||
|
||||
_IMAGE_CONTENT_TYPES = {
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
}
|
||||
|
||||
|
||||
def _guess_content_type(key: str) -> str:
|
||||
if key.endswith(".md"):
|
||||
return "text/markdown"
|
||||
if key.endswith(".html") or key.endswith(".htm"):
|
||||
return "text/html"
|
||||
extension = _extension_of(key)
|
||||
if extension in _IMAGE_CONTENT_TYPES:
|
||||
return _IMAGE_CONTENT_TYPES[extension]
|
||||
return "application/octet-stream"
|
||||
|
||||
|
||||
@ -150,7 +162,7 @@ class DocumentService:
|
||||
failed = 0
|
||||
for key in keys:
|
||||
content_type = _guess_content_type(key)
|
||||
if content_type == "application/octet-stream":
|
||||
if content_type not in ("text/markdown", "text/html"):
|
||||
continue
|
||||
try:
|
||||
data = self._file_client.get_object(key)
|
||||
|
||||
368
frontend/package-lock.json
generated
368
frontend/package-lock.json
generated
@ -8,7 +8,9 @@
|
||||
"name": "dochub-frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "4.6.0",
|
||||
"@codemirror/lang-html": "^6.4.10",
|
||||
"@codemirror/lang-markdown": "^6.4.0",
|
||||
"@uiw/react-codemirror": "^4.25.11",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
},
|
||||
@ -305,7 +307,6 @@
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@ -359,6 +360,159 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.20.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
|
||||
"integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands": {
|
||||
"version": "6.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz",
|
||||
"integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.7.0",
|
||||
"@codemirror/view": "^6.27.0",
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-css": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
|
||||
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.0.2",
|
||||
"@lezer/css": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-html": {
|
||||
"version": "6.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.10.tgz",
|
||||
"integrity": "sha512-h/SceTVsN5r+WE+TVP2g3KDvNoSzbSrtZXCKo4vkKdbfT5t4otuVgngGdFukOO/rwRD2++pCxoh6xD4TEVMkQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/lang-css": "^6.0.0",
|
||||
"@codemirror/lang-javascript": "^6.0.0",
|
||||
"@codemirror/language": "^6.4.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/css": "^1.1.0",
|
||||
"@lezer/html": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-javascript": {
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
|
||||
"integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/javascript": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-markdown": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.4.0.tgz",
|
||||
"integrity": "sha512-ZeArR54seh4laFbUTVy0ZmQgO+C/cxxlW4jEoQMhL3HALScBpZBeZcLzrQmJsTEx4is9GzOe0bFAke2B1KZqeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.7.1",
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/language": "^6.3.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.2.1",
|
||||
"@lezer/markdown": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
|
||||
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lint": {
|
||||
"version": "6.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz",
|
||||
"integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.42.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/search": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz",
|
||||
"integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.37.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz",
|
||||
"integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/theme-one-dark": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
|
||||
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.43.7",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.7.tgz",
|
||||
"integrity": "sha512-FZsExxkoxnAN+d9TgqXLg5g4A1oQwzX9WlkOT5i2PKkcW7xx3Bmu0vs90g6fo9Mpdsb/l96dnAraQ8932aO4/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.7.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||
@ -915,29 +1069,79 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@monaco-editor/loader": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
|
||||
"integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
|
||||
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/css": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.4.tgz",
|
||||
"integrity": "sha512-N+tn9tej2hPvyKgHEApMOQfHczDJCwxrRFS3SPn9QjYN+uwHvEDnCgKRrb3mxDYxRS8sKMM8fhC3+lc04Abz5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"state-local": "^1.0.6"
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@monaco-editor/react": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz",
|
||||
"integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==",
|
||||
"node_modules/@lezer/highlight": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@monaco-editor/loader": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"monaco-editor": ">= 0.25.0 < 1",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
"@lezer/common": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/html": {
|
||||
"version": "1.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz",
|
||||
"integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/javascript": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
|
||||
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/lr": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
|
||||
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/markdown": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.7.2.tgz",
|
||||
"integrity": "sha512-iTkYvoVcKt3WkeL7qUDyXHONZEwLio4wj8KTNi2dnjQEXBZKMV63BpQrPqfsM+OkvuRbiSTAcycYAsQzLhRNoQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
|
||||
"integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.62.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.3.tgz",
|
||||
@ -1468,13 +1672,58 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"node_modules/@uiw/codemirror-extensions-basic-setup": {
|
||||
"version": "4.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.11.tgz",
|
||||
"integrity": "sha512-otyFa+n9IOYtEjaKOxPedHkj15fTPUF21wdR9pv0GpZPfuGl27cvmcv6+tognbRu9VvEcsHKE+ESoszeo3KfTw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/autocomplete": ">=6.0.0",
|
||||
"@codemirror/commands": ">=6.0.0",
|
||||
"@codemirror/language": ">=6.0.0",
|
||||
"@codemirror/lint": ">=6.0.0",
|
||||
"@codemirror/search": ">=6.0.0",
|
||||
"@codemirror/state": ">=6.0.0",
|
||||
"@codemirror/view": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/react-codemirror": {
|
||||
"version": "4.25.11",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.11.tgz",
|
||||
"integrity": "sha512-DYVFAKLX+F/4JS9N/7xexh+TICrlncwkX9HKKInrP1bwO0tSfc3k0GB6oawTYhelVKh20cX3TuRx+NJSkVXuMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.6",
|
||||
"@codemirror/commands": "^6.1.0",
|
||||
"@codemirror/state": "^6.1.1",
|
||||
"@codemirror/theme-one-dark": "^6.0.0",
|
||||
"@uiw/codemirror-extensions-basic-setup": "4.25.11",
|
||||
"codemirror": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/runtime": ">=7.11.0",
|
||||
"@codemirror/state": ">=6.0.0",
|
||||
"@codemirror/theme-one-dark": ">=6.0.0",
|
||||
"@codemirror/view": ">=6.0.0",
|
||||
"codemirror": ">=6.0.0",
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.3.1",
|
||||
@ -1806,6 +2055,21 @@
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
||||
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@ -1846,6 +2110,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
|
||||
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@ -1973,16 +2243,6 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.4.8",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz",
|
||||
"integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"peer": true,
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@ -2550,19 +2810,6 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
|
||||
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@ -2626,17 +2873,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.56.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.56.0.tgz",
|
||||
"integrity": "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"dompurify": "3.4.8",
|
||||
"marked": "14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@ -3076,12 +3312,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/state-local": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
|
||||
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||
@ -3115,6 +3345,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@ -3415,6 +3651,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
|
||||
@ -10,7 +10,9 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "4.6.0",
|
||||
"@codemirror/lang-html": "^6.4.10",
|
||||
"@codemirror/lang-markdown": "^6.4.0",
|
||||
"@uiw/react-codemirror": "^4.25.11",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
},
|
||||
|
||||
@ -14,6 +14,7 @@ export interface DocumentContent {
|
||||
key: string;
|
||||
content: string;
|
||||
contentType: string;
|
||||
encoding: 'utf-8' | 'base64';
|
||||
}
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? '';
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { deleteDocument, getDocument, saveDocument } from '../api/client';
|
||||
|
||||
interface DocumentEditorProps {
|
||||
@ -19,6 +21,12 @@ export default function DocumentEditor({ documentKey, onDirtyChange, onDeleted }
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
|
||||
const isImage = contentType.startsWith('image/');
|
||||
const extensions = useMemo(
|
||||
() => [contentType === 'text/markdown' ? markdown() : html()],
|
||||
[contentType],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setLoadError(null);
|
||||
@ -42,8 +50,8 @@ export default function DocumentEditor({ documentKey, onDirtyChange, onDeleted }
|
||||
onDirtyChange?.(isDirty);
|
||||
}, [isDirty, onDirtyChange]);
|
||||
|
||||
function handleChange(value: string | undefined) {
|
||||
setContent(value ?? '');
|
||||
function handleChange(value: string) {
|
||||
setContent(value);
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
@ -115,19 +123,28 @@ export default function DocumentEditor({ documentKey, onDirtyChange, onDeleted }
|
||||
<button className="btn-secondary" onClick={handleDelete} disabled={deleting}>
|
||||
{deleting ? '删除中…' : '删除'}
|
||||
</button>
|
||||
{!isImage && (
|
||||
<button className="btn-save" onClick={handleSave} disabled={!isDirty || saving}>
|
||||
{saving ? '保存中…' : '保存'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isImage ? (
|
||||
<div className="image-preview-area" data-testid="image-preview">
|
||||
<img src={`data:${contentType};base64,${content}`} alt={documentKey.split('/').pop()} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="editor-area">
|
||||
<Editor
|
||||
<CodeMirror
|
||||
value={content}
|
||||
onChange={handleChange}
|
||||
language={contentType === 'text/markdown' ? 'markdown' : 'html'}
|
||||
options={{ automaticLayout: true }}
|
||||
extensions={extensions}
|
||||
height="100%"
|
||||
style={{ height: '100%', flex: 1 }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -427,8 +427,31 @@ body {
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
.editor-area > section {
|
||||
.editor-area > div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.editor-area .cm-editor {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image-preview-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: auto;
|
||||
background: var(--paper);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.image-preview-area img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
box-shadow: var(--shadow);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
/* ---------- Search overlay ---------- */
|
||||
|
||||
@ -7,16 +7,19 @@ import { listDocuments, getDocument, search, uploadDocument } from '../src/api/c
|
||||
|
||||
vi.mock('../src/api/client');
|
||||
|
||||
vi.mock('@monaco-editor/react', () => ({
|
||||
default: ({ value, onChange }: { value: string; onChange: (value: string | undefined) => void }) => (
|
||||
vi.mock('@uiw/react-codemirror', () => ({
|
||||
default: ({ value, onChange }: { value: string; onChange: (value: string) => void }) => (
|
||||
<textarea
|
||||
data-testid="monaco-editor-mock"
|
||||
data-testid="codemirror-mock"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@codemirror/lang-markdown', () => ({ markdown: () => ({}) }));
|
||||
vi.mock('@codemirror/lang-html', () => ({ html: () => ({}) }));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(listDocuments).mockReset();
|
||||
vi.mocked(getDocument).mockReset();
|
||||
@ -27,6 +30,7 @@ beforeEach(() => {
|
||||
key: '',
|
||||
content: '',
|
||||
contentType: 'text/markdown',
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
vi.mocked(search).mockResolvedValue({ results: [] });
|
||||
});
|
||||
@ -61,6 +65,7 @@ test('selecting a file from the tree loads it into the editor', async () => {
|
||||
key: '产品文档/架构设计.md',
|
||||
content: '# 架构设计',
|
||||
contentType: 'text/markdown',
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
|
||||
@ -69,7 +74,7 @@ test('selecting a file from the tree loads it into the editor', async () => {
|
||||
const fileRow = await screen.findByText('架构设计.md');
|
||||
await user.click(fileRow);
|
||||
|
||||
const textarea = await screen.findByTestId('monaco-editor-mock');
|
||||
const textarea = await screen.findByTestId('codemirror-mock');
|
||||
expect(textarea).toHaveValue('# 架构设计');
|
||||
});
|
||||
|
||||
@ -82,6 +87,7 @@ test('searching shows the results overlay, and clicking a result opens the docum
|
||||
key: '产品文档/架构设计.md',
|
||||
content: '# 架构设计',
|
||||
contentType: 'text/markdown',
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
|
||||
@ -102,7 +108,7 @@ test('searching shows the results overlay, and clicking a result opens the docum
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('search-results')).not.toBeInTheDocument();
|
||||
});
|
||||
const textarea = await screen.findByTestId('monaco-editor-mock');
|
||||
const textarea = await screen.findByTestId('codemirror-mock');
|
||||
expect(textarea).toHaveValue('# 架构设计');
|
||||
});
|
||||
|
||||
@ -115,6 +121,7 @@ test('jumping to a search result clears the search input so it does not show sta
|
||||
key: '产品文档/架构设计.md',
|
||||
content: '# 架构设计',
|
||||
contentType: 'text/markdown',
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
|
||||
@ -165,7 +172,7 @@ test('switching files while the current document is dirty prompts for confirmati
|
||||
],
|
||||
});
|
||||
vi.mocked(getDocument).mockImplementation((key: string) =>
|
||||
Promise.resolve({ key, content: `内容-${key}`, contentType: 'text/markdown' })
|
||||
Promise.resolve({ key, content: `内容-${key}`, contentType: 'text/markdown', encoding: 'utf-8' as const })
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false);
|
||||
@ -173,13 +180,13 @@ test('switching files while the current document is dirty prompts for confirmati
|
||||
render(<App />);
|
||||
|
||||
await user.click(await screen.findByText('文档A.md'));
|
||||
const textarea = await screen.findByTestId('monaco-editor-mock');
|
||||
const textarea = await screen.findByTestId('codemirror-mock');
|
||||
await user.type(textarea, '修改内容');
|
||||
|
||||
await user.click(screen.getByText('文档B.md'));
|
||||
|
||||
expect(confirmSpy).toHaveBeenCalled();
|
||||
expect(await screen.findByTestId('monaco-editor-mock')).toHaveValue('内容-文档A.md修改内容');
|
||||
expect(await screen.findByTestId('codemirror-mock')).toHaveValue('内容-文档A.md修改内容');
|
||||
});
|
||||
|
||||
test('confirming the discard prompt proceeds to switch files', async () => {
|
||||
@ -191,7 +198,7 @@ test('confirming the discard prompt proceeds to switch files', async () => {
|
||||
],
|
||||
});
|
||||
vi.mocked(getDocument).mockImplementation((key: string) =>
|
||||
Promise.resolve({ key, content: `内容-${key}`, contentType: 'text/markdown' })
|
||||
Promise.resolve({ key, content: `内容-${key}`, contentType: 'text/markdown', encoding: 'utf-8' as const })
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
@ -199,13 +206,13 @@ test('confirming the discard prompt proceeds to switch files', async () => {
|
||||
render(<App />);
|
||||
|
||||
await user.click(await screen.findByText('文档A.md'));
|
||||
const textarea = await screen.findByTestId('monaco-editor-mock');
|
||||
const textarea = await screen.findByTestId('codemirror-mock');
|
||||
await user.type(textarea, '修改内容');
|
||||
|
||||
await user.click(screen.getByText('文档B.md'));
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(await screen.findByTestId('monaco-editor-mock')).toHaveValue('内容-文档B.md');
|
||||
expect(await screen.findByTestId('codemirror-mock')).toHaveValue('内容-文档B.md');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -1,51 +1,71 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { vi, test, expect, beforeEach } from 'vitest';
|
||||
import DocumentEditor from '../../src/components/DocumentEditor';
|
||||
import { deleteDocument, getDocument, saveDocument } from '../../src/api/client';
|
||||
|
||||
vi.mock('../../src/api/client');
|
||||
|
||||
vi.mock('@monaco-editor/react', () => ({
|
||||
default: ({ value, onChange }: { value: string; onChange: (value: string | undefined) => void }) => (
|
||||
vi.mock('@uiw/react-codemirror', () => ({
|
||||
default: ({ value, onChange }: { value: string; onChange: (value: string) => void }) => (
|
||||
<textarea
|
||||
data-testid="monaco-editor-mock"
|
||||
data-testid="codemirror-mock"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@codemirror/lang-markdown', () => ({ markdown: () => ({}) }));
|
||||
vi.mock('@codemirror/lang-html', () => ({ html: () => ({}) }));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(getDocument).mockReset();
|
||||
vi.mocked(saveDocument).mockReset();
|
||||
vi.mocked(deleteDocument).mockReset();
|
||||
});
|
||||
|
||||
test('shows an image preview instead of the code editor for image content types', async () => {
|
||||
vi.mocked(getDocument).mockResolvedValue({
|
||||
key: '产品文档/图片.png',
|
||||
content: 'iVBORw0KGgo=',
|
||||
contentType: 'image/png',
|
||||
encoding: 'base64',
|
||||
});
|
||||
|
||||
render(<DocumentEditor documentKey="产品文档/图片.png" onDeleted={vi.fn()} />);
|
||||
|
||||
const preview = await screen.findByTestId('image-preview');
|
||||
const img = preview.querySelector('img');
|
||||
expect(img).toHaveAttribute('src', 'data:image/png;base64,iVBORw0KGgo=');
|
||||
expect(screen.queryByTestId('codemirror-mock')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '保存' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows a loading state, then loads and displays document content', async () => {
|
||||
vi.mocked(getDocument).mockResolvedValue({
|
||||
key: '产品文档/架构设计.md',
|
||||
content: '# 架构设计',
|
||||
contentType: 'text/markdown',
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
render(<DocumentEditor documentKey="产品文档/架构设计.md" onDeleted={vi.fn()} />);
|
||||
|
||||
expect(screen.getByTestId('document-editor-loading')).toBeInTheDocument();
|
||||
|
||||
const textarea = await screen.findByTestId('monaco-editor-mock');
|
||||
const textarea = await screen.findByTestId('codemirror-mock');
|
||||
expect(textarea).toHaveValue('# 架构设计');
|
||||
expect(getDocument).toHaveBeenCalledWith('产品文档/架构设计.md');
|
||||
expect(screen.getByTestId('save-status')).toHaveTextContent('已归档');
|
||||
});
|
||||
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
test('typing marks the document dirty and saving clears the dirty state', async () => {
|
||||
vi.mocked(getDocument).mockResolvedValue({
|
||||
key: '产品文档/架构设计.md',
|
||||
content: '# 架构设计',
|
||||
contentType: 'text/markdown',
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
vi.mocked(saveDocument).mockResolvedValue({
|
||||
key: '产品文档/架构设计.md',
|
||||
@ -55,7 +75,7 @@ test('typing marks the document dirty and saving clears the dirty state', async
|
||||
|
||||
render(<DocumentEditor documentKey="产品文档/架构设计.md" onDeleted={vi.fn()} />);
|
||||
|
||||
const textarea = await screen.findByTestId('monaco-editor-mock');
|
||||
const textarea = await screen.findByTestId('codemirror-mock');
|
||||
await user.clear(textarea);
|
||||
await user.type(textarea, '# 新内容');
|
||||
|
||||
@ -77,6 +97,7 @@ test('save failure keeps the editor and unsaved content visible, shows an error,
|
||||
key: '产品文档/架构设计.md',
|
||||
content: '# 架构设计',
|
||||
contentType: 'text/markdown',
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
vi.mocked(saveDocument).mockRejectedValueOnce(new Error('network error'));
|
||||
vi.mocked(saveDocument).mockResolvedValueOnce({
|
||||
@ -87,7 +108,7 @@ test('save failure keeps the editor and unsaved content visible, shows an error,
|
||||
|
||||
render(<DocumentEditor documentKey="产品文档/架构设计.md" onDeleted={vi.fn()} />);
|
||||
|
||||
const textarea = await screen.findByTestId('monaco-editor-mock');
|
||||
const textarea = await screen.findByTestId('codemirror-mock');
|
||||
await user.clear(textarea);
|
||||
await user.type(textarea, '# 未保存的修改');
|
||||
|
||||
@ -98,7 +119,7 @@ test('save failure keeps the editor and unsaved content visible, shows an error,
|
||||
expect(screen.getByTestId('save-error')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('document-editor')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('monaco-editor-mock')).toHaveValue('# 未保存的修改');
|
||||
expect(screen.getByTestId('codemirror-mock')).toHaveValue('# 未保存的修改');
|
||||
expect(screen.getByTestId('save-status')).toHaveTextContent('未保存');
|
||||
|
||||
await user.click(saveButton);
|
||||
@ -114,13 +135,14 @@ test('calls onDirtyChange when dirty state changes', async () => {
|
||||
key: '产品文档/架构设计.md',
|
||||
content: '# 架构设计',
|
||||
contentType: 'text/markdown',
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const onDirtyChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<DocumentEditor documentKey="产品文档/架构设计.md" onDirtyChange={onDirtyChange} onDeleted={vi.fn()} />);
|
||||
|
||||
const textarea = await screen.findByTestId('monaco-editor-mock');
|
||||
const textarea = await screen.findByTestId('codemirror-mock');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onDirtyChange).toHaveBeenLastCalledWith(false);
|
||||
@ -138,6 +160,7 @@ test('clicking delete confirms, calls deleteDocument, and calls onDeleted on suc
|
||||
key: '产品文档/架构设计.md',
|
||||
content: '# 架构设计',
|
||||
contentType: 'text/markdown',
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
vi.mocked(deleteDocument).mockResolvedValue(undefined);
|
||||
const onDeleted = vi.fn();
|
||||
@ -146,7 +169,7 @@ test('clicking delete confirms, calls deleteDocument, and calls onDeleted on suc
|
||||
|
||||
render(<DocumentEditor documentKey="产品文档/架构设计.md" onDeleted={onDeleted} />);
|
||||
|
||||
await screen.findByTestId('monaco-editor-mock');
|
||||
await screen.findByTestId('codemirror-mock');
|
||||
const deleteButton = screen.getByRole('button', { name: '删除' });
|
||||
await user.click(deleteButton);
|
||||
|
||||
@ -164,6 +187,7 @@ test('clicking delete does nothing if the confirmation is declined', async () =>
|
||||
key: '产品文档/架构设计.md',
|
||||
content: '# 架构设计',
|
||||
contentType: 'text/markdown',
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const onDeleted = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
@ -171,7 +195,7 @@ test('clicking delete does nothing if the confirmation is declined', async () =>
|
||||
|
||||
render(<DocumentEditor documentKey="产品文档/架构设计.md" onDeleted={onDeleted} />);
|
||||
|
||||
await screen.findByTestId('monaco-editor-mock');
|
||||
await screen.findByTestId('codemirror-mock');
|
||||
const deleteButton = screen.getByRole('button', { name: '删除' });
|
||||
await user.click(deleteButton);
|
||||
|
||||
@ -184,6 +208,7 @@ test('shows an inline error and does not call onDeleted when delete fails', asyn
|
||||
key: '产品文档/架构设计.md',
|
||||
content: '# 架构设计',
|
||||
contentType: 'text/markdown',
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
vi.mocked(deleteDocument).mockRejectedValue(new Error('network error'));
|
||||
const onDeleted = vi.fn();
|
||||
@ -192,7 +217,7 @@ test('shows an inline error and does not call onDeleted when delete fails', asyn
|
||||
|
||||
render(<DocumentEditor documentKey="产品文档/架构设计.md" onDeleted={onDeleted} />);
|
||||
|
||||
await screen.findByTestId('monaco-editor-mock');
|
||||
await screen.findByTestId('codemirror-mock');
|
||||
const deleteButton = screen.getByRole('button', { name: '删除' });
|
||||
await user.click(deleteButton);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user