mirror of
https://github.com/suitenumerique/drive.git
synced 2026-04-25 17:15:19 +02:00
✨(frontend) open WOPI files in a new tab
WOPI editors (OnlyOffice / Collabora) work better as a full-page experience than embedded inside the preview modal. Double-clicking a WOPI file in the grid or activating one from the search modal now opens /wopi/:id in a new tab. The preview modal still shows an "Open in editor" placeholder when a user navigates onto a WOPI file via keyboard, so the preview flow stays consistent. The former WopiEditor component is renamed WopiEditorFrame and hosted on the new page. The inline rename-sync path is removed because the editor no longer lives inside the items list.
This commit is contained in:
@@ -305,7 +305,6 @@ export const GlobalExplorerProvider = ({
|
||||
currentItem={previewItem}
|
||||
items={previewItems}
|
||||
setPreviewItem={setPreviewItem}
|
||||
onItemsChange={setPreviewItems}
|
||||
/>
|
||||
</GlobalExplorerContext.Provider>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@ import { DefaultRoute, getDefaultRouteId } from "@/utils/defaultRoutes";
|
||||
import { useMemo } from "react";
|
||||
import { canCreateChildren } from "@/features/items/utils";
|
||||
import { Spinner } from "@gouvfr-lasuite/ui-kit";
|
||||
import { openWopiInNewTab } from "@/features/ui/preview/viewers/wopi/openWopi";
|
||||
|
||||
/**
|
||||
* Wrapper around EmbeddedExplorerGrid to display a list of items in a table.
|
||||
@@ -46,6 +47,10 @@ export const AppExplorerGrid = () => {
|
||||
const effectiveOnNavigate = appExplorer.onNavigate ?? onNavigate;
|
||||
|
||||
const handleFileClick = appExplorer.onFileClick ?? ((item: Item) => {
|
||||
if (item.is_wopi_supported) {
|
||||
openWopiInNewTab(item.id);
|
||||
return;
|
||||
}
|
||||
if (item.url) {
|
||||
// We need to ensure the preview items list is updated when clicking on a file from the grid. Because this list
|
||||
// can be updated when clicking on a file from the search modal which sets the preview items to a list of one item.
|
||||
|
||||
@@ -31,6 +31,7 @@ import { Key } from "react-aria-components";
|
||||
import { clearFromRoute, getItemTitle } from "@/features/explorer/utils/utils";
|
||||
import { messageModalTrashNavigate } from "../../trash/utils";
|
||||
import { useIsMinimalLayout } from "@/utils/useLayout";
|
||||
import { openWopiInNewTab } from "@/features/ui/preview/viewers/wopi/openWopi";
|
||||
|
||||
type ExplorerSearchModalProps = Pick<ModalProps, "isOpen" | "onClose"> & {
|
||||
defaultFilters?: ItemFilters;
|
||||
@@ -99,6 +100,11 @@ export const ExplorerSearchModal = (props: ExplorerSearchModalProps) => {
|
||||
props.onClose();
|
||||
}
|
||||
} else {
|
||||
if (item.is_wopi_supported) {
|
||||
openWopiInNewTab(item.id);
|
||||
props.onClose();
|
||||
return;
|
||||
}
|
||||
setPreviewItems([item]);
|
||||
setPreviewItem(item);
|
||||
inputTextSelected.current = false;
|
||||
|
||||
@@ -20,7 +20,7 @@ import { AudioPlayer } from "./viewers/audio-player/AudioPlayer";
|
||||
import { ImageViewer } from "./viewers/image-viewer/ImageViewer";
|
||||
import { SuspiciousPreview } from "./viewers/suspicious/SuspiciousPreview";
|
||||
import { NotSupportedPreview } from "./viewers/not-supported/NotSupportedPreview";
|
||||
import { WopiEditor } from "./viewers/wopi/WopiEditor";
|
||||
import { WopiOpenInEditor } from "./viewers/wopi/WopiOpenInEditor";
|
||||
import { OPEN_DELAY } from "./viewers/pdf-preview/pdfConsts";
|
||||
import { FileIcon } from "@/features/explorer/components/icons/ItemIcon";
|
||||
|
||||
@@ -69,7 +69,6 @@ interface FilePreviewProps {
|
||||
onChangeFile?: (file?: FilePreviewType) => void;
|
||||
handleDownloadFile?: (file?: FilePreviewType) => void;
|
||||
hideCloseButton?: boolean;
|
||||
onFileRename?: (file: FilePreviewType, newName: string) => void;
|
||||
}
|
||||
|
||||
export const FilePreview = ({
|
||||
@@ -84,7 +83,6 @@ export const FilePreview = ({
|
||||
onChangeFile,
|
||||
handleDownloadFile,
|
||||
hideCloseButton,
|
||||
onFileRename,
|
||||
}: FilePreviewProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [currentIndex, setCurrentIndex] = useState(initialIndexFile);
|
||||
@@ -134,7 +132,7 @@ export const FilePreview = ({
|
||||
return <SuspiciousPreview handleDownload={handleDownload} />;
|
||||
}
|
||||
if (currentFile.is_wopi_supported) {
|
||||
return <WopiEditor item={currentFile} onFileRename={onFileRename} />;
|
||||
return <WopiOpenInEditor file={currentFile} />;
|
||||
}
|
||||
|
||||
switch (currentFile.category) {
|
||||
|
||||
@@ -7,21 +7,17 @@ import { useDownloadItem } from "@/features/items/hooks/useDownloadItem";
|
||||
import { ItemInfo } from "@/features/items/components/ItemInfo";
|
||||
import { Button, useModal } from "@gouvfr-lasuite/cunningham-react";
|
||||
import { ItemShareModal } from "@/features/explorer/components/modals/share/ItemShareModal";
|
||||
import { useRefreshItemCache } from "@/features/explorer/hooks/useRefreshItems";
|
||||
|
||||
type CustomFilesPreviewProps = {
|
||||
currentItem?: Item;
|
||||
items: Item[];
|
||||
setPreviewItem?: (item?: Item) => void;
|
||||
/** Used for optimistic updates only ( when the file is renamed in the preview ) */
|
||||
onItemsChange?: (items: Item[]) => void;
|
||||
};
|
||||
|
||||
export const CustomFilesPreview = ({
|
||||
currentItem,
|
||||
items,
|
||||
setPreviewItem,
|
||||
onItemsChange,
|
||||
}: CustomFilesPreviewProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -42,15 +38,6 @@ export const CustomFilesPreview = ({
|
||||
setPreviewItem?.(item);
|
||||
};
|
||||
|
||||
|
||||
const refreshItemCache = useRefreshItemCache();
|
||||
const handleFileRename = (file: FilePreviewType, newName: string) => {
|
||||
// Optimistic update of the items in the preview.
|
||||
onItemsChange?.(items.map((item) => item.id === file.id ? { ...item, title: newName } : item));
|
||||
// Update the item in the explorer if needed.
|
||||
refreshItemCache(file.id, { title: newName });
|
||||
};
|
||||
|
||||
return (
|
||||
<FilePreview
|
||||
isOpen={!!currentItem}
|
||||
@@ -64,7 +51,6 @@ export const CustomFilesPreview = ({
|
||||
<CustomFilesPreviewRightHeader currentItem={currentItem} />
|
||||
}
|
||||
sidebarContent={currentItem && <ItemInfo item={currentItem} />}
|
||||
onFileRename={handleFileRename}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
@use "./components/controls/PreviewControls.scss";
|
||||
@use "./viewers/not-supported/NotSupportedPreview.scss";
|
||||
@use "./viewers/suspicious/SuspiciousPreview.scss";
|
||||
@use "./viewers/wopi/WopiEditor.scss";
|
||||
@use "./viewers/wopi/WopiEditorFrame.scss";
|
||||
@use "./viewers/error/ErrorPreview.scss";
|
||||
@use "./viewers/audio-player/AudioPlayer.scss";
|
||||
@use "./viewers/image-viewer/ImageViewer.scss";
|
||||
|
||||
@@ -3,3 +3,7 @@
|
||||
height: calc(100vh - 52px);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.wopi-page .wopi-editor-iframe {
|
||||
height: 100vh;
|
||||
}
|
||||
@@ -5,12 +5,12 @@ import { getDriver } from "@/features/config/Config";
|
||||
import { ErrorPreview } from "../error/ErrorPreview";
|
||||
import { FilePreviewType } from "../../FilesPreview";
|
||||
|
||||
interface WopiEditorProps {
|
||||
interface WopiEditorFrameProps {
|
||||
item: FilePreviewType;
|
||||
onFileRename?: (file: FilePreviewType, newName: string) => void;
|
||||
}
|
||||
|
||||
export const WopiEditor = ({ item, onFileRename }: WopiEditorProps) => {
|
||||
export const WopiEditorFrame = ({ item, onFileRename }: WopiEditorFrameProps) => {
|
||||
const { t } = useTranslation();
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const queryClient = useQueryClient();
|
||||
@@ -49,7 +49,6 @@ export const WopiEditor = ({ item, onFileRename }: WopiEditorProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle rename notifications from the WOPI editor
|
||||
if (data.MessageId === "File_Rename") {
|
||||
onFileRename?.(item, data.Values.NewName);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@gouvfr-lasuite/cunningham-react";
|
||||
import { Icon, IconType } from "@gouvfr-lasuite/ui-kit";
|
||||
import { FileIcon } from "@/features/explorer/components/icons/ItemIcon";
|
||||
import { FilePreviewType } from "../../FilesPreview";
|
||||
import { openWopiInNewTab } from "./openWopi";
|
||||
|
||||
interface WopiOpenInEditorProps {
|
||||
file: FilePreviewType;
|
||||
}
|
||||
|
||||
export const WopiOpenInEditor = ({ file }: WopiOpenInEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="file-preview-unsupported">
|
||||
<div className="file-preview-unsupported__icon">
|
||||
<FileIcon file={file} size="xlarge" />
|
||||
</div>
|
||||
<p className="file-preview-unsupported__title">{file.title}</p>
|
||||
<p className="file-preview-unsupported__description">
|
||||
{t("file_preview.wopi.open_in_editor_description")}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
className="file-preview-unsupported__download-button"
|
||||
icon={<Icon name="open_in_new" type={IconType.OUTLINED} size={16} />}
|
||||
onClick={() => openWopiInNewTab(file.id)}
|
||||
>
|
||||
{t("file_preview.wopi.open_in_editor")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export const WOPI_TAB_PATH = "/wopi";
|
||||
|
||||
export const openWopiInNewTab = (itemId: string): void => {
|
||||
window.open(
|
||||
`${WOPI_TAB_PATH}/${encodeURIComponent(itemId)}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
};
|
||||
61
src/frontend/apps/drive/src/pages/wopi/[id].tsx
Normal file
61
src/frontend/apps/drive/src/pages/wopi/[id].tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Icon } from "@gouvfr-lasuite/ui-kit";
|
||||
import { Button } from "@gouvfr-lasuite/cunningham-react";
|
||||
|
||||
import { GenericDisclaimer } from "@/features/ui/components/generic-disclaimer/GenericDisclaimer";
|
||||
import { SpinnerPage } from "@/features/ui/components/spinner/SpinnerPage";
|
||||
import { useItem } from "@/features/explorer/hooks/useQueries";
|
||||
import { useRefreshItemCache } from "@/features/explorer/hooks/useRefreshItems";
|
||||
import { itemToPreviewFile } from "@/features/explorer/utils/utils";
|
||||
import { WopiEditorFrame } from "@/features/ui/preview/viewers/wopi/WopiEditorFrame";
|
||||
import { FilePreviewType } from "@/features/ui/preview/FilesPreview";
|
||||
|
||||
export default function WopiPage() {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const itemId = router.query.id as string;
|
||||
|
||||
const { data: item, isLoading, error } = useItem(itemId);
|
||||
const refreshItemCache = useRefreshItemCache();
|
||||
|
||||
useEffect(() => {
|
||||
if (!item) return;
|
||||
const previousTitle = document.title;
|
||||
document.title = `${item.title} - ${t("app_title")}`;
|
||||
return () => {
|
||||
document.title = previousTitle;
|
||||
};
|
||||
}, [item, t]);
|
||||
|
||||
if (isLoading || (error && [401, 403].includes(error.code))) {
|
||||
return <SpinnerPage />;
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
return (
|
||||
<GenericDisclaimer
|
||||
message={t("explorer.files.not_found.description")}
|
||||
imageSrc="/assets/403-background.png"
|
||||
>
|
||||
<Button href="/" icon={<Icon name="home" />}>
|
||||
{t("403.button")}
|
||||
</Button>
|
||||
</GenericDisclaimer>
|
||||
);
|
||||
}
|
||||
|
||||
const handleRename = (file: FilePreviewType, newName: string) => {
|
||||
refreshItemCache(file.id, { title: newName });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="wopi-page">
|
||||
<WopiEditorFrame
|
||||
item={itemToPreviewFile(item)}
|
||||
onFileRename={handleRename}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user