mirror of
https://github.com/alphaXiv/OpenResearch.git
synced 2026-10-02 09:44:51 +08:00
Fix copying file contents in the viewer (#423)
* Fix copying file contents in viewer (#421) * fix(ui): scope file copy shortcuts and support WebKit * ci: verify committed UI bundle matches source --------- Co-authored-by: Daniel Kim <sox8502@gmail.com>
This commit is contained in:
co-authored by
Daniel Kim
parent
de469a3f44
commit
fe8db9f48b
@@ -65,6 +65,17 @@ jobs:
|
||||
- name: UI unit tests
|
||||
run: node --test --experimental-strip-types ui/tests/*.test.mjs
|
||||
|
||||
- name: Committed UI bundle matches source
|
||||
working-directory: ui
|
||||
run: |
|
||||
pnpm exec vite build
|
||||
stale=$(git status --porcelain -- dist)
|
||||
if [ -n "$stale" ]; then
|
||||
echo "$stale"
|
||||
echo "::error::ui/dist is stale; run pnpm build in ui/ and commit the result."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
|
||||
Vendored
-1123
File diff suppressed because one or more lines are too long
+220
-220
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -49,7 +49,7 @@
|
||||
html { background: #ffffff; }
|
||||
html[data-theme="dark"] { background: #0e0c0c; }
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-CTdE8kO2.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-yT9oNk64.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D_9P_q05.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -176,6 +176,8 @@
|
||||
"experiments_table_stop_failed": "فشل الإيقاف:",
|
||||
"file_viewer_binary_file_no_inline_preview": "ملف ثنائي — لا توجد معاينة مضمّنة.",
|
||||
"file_viewer_copy_install_command": "نسخ أمر التثبيت",
|
||||
"file_viewer_copy_contents": "نسخ محتوى الملف",
|
||||
"file_viewer_copy_failed": "تعذّر نسخ محتوى الملف",
|
||||
"file_viewer_discard_my_edits_and_reload": "تجاهل تعديلاتي وإعادة التحميل",
|
||||
"file_viewer_dismiss": "تجاهل",
|
||||
"file_viewer_dismiss_compile_message": "تجاهل رسالة التجميع",
|
||||
|
||||
@@ -176,6 +176,8 @@
|
||||
"experiments_table_stop_failed": "Stop failed:",
|
||||
"file_viewer_binary_file_no_inline_preview": "Binary file — no inline preview.",
|
||||
"file_viewer_copy_install_command": "Copy install command",
|
||||
"file_viewer_copy_contents": "Copy file contents",
|
||||
"file_viewer_copy_failed": "Could not copy file contents",
|
||||
"file_viewer_discard_my_edits_and_reload": "Discard my edits and reload",
|
||||
"file_viewer_dismiss": "Dismiss",
|
||||
"file_viewer_dismiss_compile_message": "Dismiss compile message",
|
||||
|
||||
@@ -176,6 +176,8 @@
|
||||
"experiments_table_stop_failed": "Error al detener:",
|
||||
"file_viewer_binary_file_no_inline_preview": "Archivo binario — sin vista previa.",
|
||||
"file_viewer_copy_install_command": "Copiar comando de instalación",
|
||||
"file_viewer_copy_contents": "Copiar contenido del archivo",
|
||||
"file_viewer_copy_failed": "No se pudo copiar el contenido del archivo",
|
||||
"file_viewer_discard_my_edits_and_reload": "Descartar mis cambios y recargar",
|
||||
"file_viewer_dismiss": "Cerrar",
|
||||
"file_viewer_dismiss_compile_message": "Cerrar mensaje de compilación",
|
||||
|
||||
@@ -176,6 +176,8 @@
|
||||
"experiments_table_stop_failed": "توقف ناموفق بود:",
|
||||
"file_viewer_binary_file_no_inline_preview": "فایل دودویی است — پیشنمایش درونخطی ندارد.",
|
||||
"file_viewer_copy_install_command": "کپی فرمان نصب",
|
||||
"file_viewer_copy_contents": "کپی محتوای فایل",
|
||||
"file_viewer_copy_failed": "کپی محتوای فایل ممکن نشد",
|
||||
"file_viewer_discard_my_edits_and_reload": "نادیده گرفتن ویرایشهای من و بارگیری دوباره",
|
||||
"file_viewer_dismiss": "بستن",
|
||||
"file_viewer_dismiss_compile_message": "بستن پیام کامپایل",
|
||||
|
||||
@@ -176,6 +176,8 @@
|
||||
"experiments_table_stop_failed": "रोकना विफल:",
|
||||
"file_viewer_binary_file_no_inline_preview": "बाइनरी फ़ाइल — इनलाइन प्रीव्यू नहीं है।",
|
||||
"file_viewer_copy_install_command": "इंस्टॉल कमांड कॉपी करें",
|
||||
"file_viewer_copy_contents": "फ़ाइल की सामग्री कॉपी करें",
|
||||
"file_viewer_copy_failed": "फ़ाइल की सामग्री कॉपी नहीं हो सकी",
|
||||
"file_viewer_discard_my_edits_and_reload": "मेरे बदलाव छोड़ें और फिर से लोड करें",
|
||||
"file_viewer_dismiss": "खारिज करें",
|
||||
"file_viewer_dismiss_compile_message": "कंपाइल संदेश खारिज करें",
|
||||
|
||||
@@ -176,6 +176,8 @@
|
||||
"experiments_table_stop_failed": "停止失败:",
|
||||
"file_viewer_binary_file_no_inline_preview": "二进制文件——无法内嵌预览。",
|
||||
"file_viewer_copy_install_command": "复制安装命令",
|
||||
"file_viewer_copy_contents": "复制文件内容",
|
||||
"file_viewer_copy_failed": "无法复制文件内容",
|
||||
"file_viewer_discard_my_edits_and_reload": "放弃我的编辑并重新加载",
|
||||
"file_viewer_dismiss": "关闭",
|
||||
"file_viewer_dismiss_compile_message": "关闭编译消息",
|
||||
|
||||
@@ -64,7 +64,7 @@ import { HtmlPreview } from "./HtmlPreview";
|
||||
import { OverleafButton } from "./OverleafPanel";
|
||||
import { MediaPreview, mediaPreviewKind } from "./MediaPreview";
|
||||
import { Md } from "./Md";
|
||||
import { Button, IconButton, IconButtonLink, Spinner } from "./ui";
|
||||
import { Button, IconButton, IconButtonLink, Spinner, showAlert } from "./ui";
|
||||
|
||||
export interface FileScrollPosition {
|
||||
top: number;
|
||||
@@ -241,6 +241,8 @@ export function FileViewer({
|
||||
const saveError = bufferSession.saveError;
|
||||
const setSaveError = bufferSession.setSaveError;
|
||||
const bodyRef = useRef<HTMLDivElement>(null);
|
||||
const selectableContentRef = useRef<HTMLDivElement>(null);
|
||||
const [copiedContents, setCopiedContents] = useState(false);
|
||||
const scrollPositionRef = useRef(scrollPosition);
|
||||
const data = loaded?.file ?? null;
|
||||
// A cited `artifacts/…` file can answer from either name in the checkout, so
|
||||
@@ -395,6 +397,41 @@ export function FileViewer({
|
||||
const showingEditor = (editable || showingUnsafeDraft) &&
|
||||
!(rendersByDefault && !showSource) &&
|
||||
!showingPdf;
|
||||
const canCopyContents = data !== null && !data.notFound && !data.binary && !mediaKind && !showingPdf;
|
||||
const canSelectContents = canCopyContents && !showingEditor && (!isHtml || showSource);
|
||||
|
||||
const copyContents = async () => {
|
||||
if (!data) return;
|
||||
try {
|
||||
if (data.truncated && !showingEditor) {
|
||||
// WebKit requires the clipboard write to start during the click.
|
||||
const content = fetch(rawFileUrl(filePath)).then(async (response) => {
|
||||
if (!response.ok) throw new Error();
|
||||
return new Blob([await response.text()], { type: "text/plain" });
|
||||
});
|
||||
await navigator.clipboard.write([new ClipboardItem({ "text/plain": content })]);
|
||||
} else {
|
||||
await navigator.clipboard.writeText(showingEditor ? draft : data.content);
|
||||
}
|
||||
setCopiedContents(true);
|
||||
window.setTimeout(() => setCopiedContents(false), 1500);
|
||||
} catch {
|
||||
showAlert(m.file_viewer_copy_failed(), "error");
|
||||
}
|
||||
};
|
||||
|
||||
const selectViewerContents = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if ((!event.metaKey && !event.ctrlKey) || event.altKey || event.shiftKey || event.key.toLowerCase() !== "a") return;
|
||||
if (!(event.target instanceof Node) || !event.currentTarget.contains(event.target)) return;
|
||||
const content = selectableContentRef.current;
|
||||
if (!content) return;
|
||||
event.preventDefault();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(content);
|
||||
const selection = window.getSelection();
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
};
|
||||
|
||||
// `#toolbar=0` asks the browser's PDF viewer to drop its own chrome, so the
|
||||
// pane shows the document and this view's header owns the controls.
|
||||
@@ -526,7 +563,7 @@ export function FileViewer({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="file-view flex flex-col h-full min-h-0 min-w-0">
|
||||
<div className="file-view flex flex-col h-full min-h-0 min-w-0" onKeyDown={selectViewerContents}>
|
||||
<div className="file-view-header flex w-full min-w-0 min-h-9 items-center gap-1 px-4 py-1 bg-background text-text shrink-0">
|
||||
<FileTypeIcon name={filePath} />
|
||||
<span className="file-view-path flex-1 min-w-0 truncate text-sm text-subtext" data-tip={ltr(filePath)}>
|
||||
@@ -617,6 +654,17 @@ export function FileViewer({
|
||||
<Code size={13} />
|
||||
</IconButton>
|
||||
)}
|
||||
{canCopyContents && (
|
||||
<IconButton
|
||||
size="small"
|
||||
data-tip={copiedContents ? m.common_copied() : m.file_viewer_copy_contents()}
|
||||
data-tip-align="end"
|
||||
aria-label={copiedContents ? m.common_copied() : m.file_viewer_copy_contents()}
|
||||
onClick={() => void copyContents()}
|
||||
>
|
||||
{copiedContents ? <Check size={13} /> : <Copy size={13} />}
|
||||
</IconButton>
|
||||
)}
|
||||
{data != null && (
|
||||
<>
|
||||
<IconButton
|
||||
@@ -747,6 +795,7 @@ export function FileViewer({
|
||||
<div
|
||||
ref={bodyRef}
|
||||
className="file-view-body flex-1 min-h-0 overflow-auto bg-background"
|
||||
tabIndex={canSelectContents ? 0 : undefined}
|
||||
onScroll={(event) => {
|
||||
const position = {
|
||||
top: Math.max(0, event.currentTarget.scrollTop),
|
||||
@@ -820,7 +869,7 @@ export function FileViewer({
|
||||
downloadBar={false}
|
||||
/>
|
||||
) : isMarkdown && !showSource ? (
|
||||
<div className="file-view-md max-w-readable pt-4.5 px-5 pb-8 [&_.md]:text-base [&_.md_h1]:text-2xl [&_.md_h1]:mt-4.5 [&_.md_h1]:mx-0 [&_.md_h1]:mb-2 [&_.md_h2]:text-xl [&_.md_h2]:mt-4 [&_.md_h2]:mx-0 [&_.md_h2]:mb-2 [&_.md_h3]:text-lg">
|
||||
<div ref={selectableContentRef} className="file-view-md max-w-readable pt-4.5 px-5 pb-8 [&_.md]:text-base [&_.md_h1]:text-2xl [&_.md_h1]:mt-4.5 [&_.md_h1]:mx-0 [&_.md_h1]:mb-2 [&_.md_h2]:text-xl [&_.md_h2]:mt-4 [&_.md_h2]:mx-0 [&_.md_h2]:mb-2 [&_.md_h3]:text-lg">
|
||||
{artifactsMode ? (
|
||||
<ArtifactMarkdown
|
||||
projectId={projectId}
|
||||
@@ -851,13 +900,15 @@ export function FileViewer({
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<CodeView
|
||||
text={data.content}
|
||||
path={path}
|
||||
highlightLine={line}
|
||||
scrollRequest={lineScrollRequest}
|
||||
onScrollRequestHandled={onLineScrollRequestHandled}
|
||||
/>
|
||||
<div ref={selectableContentRef}>
|
||||
<CodeView
|
||||
text={data.content}
|
||||
path={path}
|
||||
highlightLine={line}
|
||||
scrollRequest={lineScrollRequest}
|
||||
onScrollRequestHandled={onLineScrollRequestHandled}
|
||||
/>
|
||||
</div>
|
||||
{data.truncated && (
|
||||
<div className="file-view-note py-2.5 px-4 text-sm text-muted">{m.file_viewer_file_truncated_showing_the_first_512_kb()}</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user