mirror of
https://github.com/suitenumerique/drive.git
synced 2026-04-25 17:15:19 +02:00
♻️(frontend) unify preview messages into a shared component
The error, not-supported, suspicious and WOPI placeholder viewers all rendered the same "icon + title + description + action" layout with near-duplicate scss. Extract a PreviewMessage component so styling and backdrop-close behaviour live in one place, and update the e2e selectors to match the new markup.
This commit is contained in:
@@ -193,6 +193,23 @@
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
gap: var(--c--globals--spacings--xs);
|
||||
|
||||
&__title {
|
||||
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
.file-preview-error {
|
||||
.preview-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--c--globals--spacings--sm);
|
||||
height: 100%;
|
||||
gap: 8px;
|
||||
|
||||
&__title {
|
||||
color: var(--c--contextuals--content--semantic--error--primary);
|
||||
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 22px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__description {
|
||||
@@ -22,9 +23,14 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__download-button {
|
||||
margin-top: var(--c--globals--spacings--xs);
|
||||
&__action {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&--error &__title {
|
||||
color: var(--c--contextuals--content--semantic--error--primary);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
interface PreviewMessageProps {
|
||||
icon: React.ReactNode;
|
||||
title: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
action?: React.ReactNode;
|
||||
variant?: "neutral" | "error";
|
||||
closeOnBackdrop?: boolean;
|
||||
}
|
||||
|
||||
export const PreviewMessage = ({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
variant = "neutral",
|
||||
closeOnBackdrop = true,
|
||||
}: PreviewMessageProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"preview-message",
|
||||
variant === "error" && "preview-message--error",
|
||||
)}
|
||||
data-preview-backdrop={closeOnBackdrop ? "true" : undefined}
|
||||
>
|
||||
<div className="preview-message__icon">{icon}</div>
|
||||
<p className="preview-message__title">{title}</p>
|
||||
{description && (
|
||||
<p className="preview-message__description">{description}</p>
|
||||
)}
|
||||
{action && <div className="preview-message__action">{action}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,10 +2,8 @@
|
||||
@use "./components/duration-bar/DurationBar.scss";
|
||||
@use "./components/volume-bar/VolumeBar.scss";
|
||||
@use "./components/controls/PreviewControls.scss";
|
||||
@use "./viewers/not-supported/NotSupportedPreview.scss";
|
||||
@use "./viewers/suspicious/SuspiciousPreview.scss";
|
||||
@use "./components/preview-message/PreviewMessage.scss";
|
||||
@use "./viewers/wopi/WopiEditorFrame.scss";
|
||||
@use "./viewers/error/ErrorPreview.scss";
|
||||
@use "./viewers/audio-player/AudioPlayer.scss";
|
||||
@use "./viewers/image-viewer/ImageViewer.scss";
|
||||
@use "./viewers/video-player/VideoPlayer.scss";
|
||||
|
||||
@@ -6,6 +6,7 @@ import { FileIcon } from "@/features/explorer/components/icons/ItemIcon";
|
||||
import { downloadFile } from "@/features/items/utils";
|
||||
import { useCallback } from "react";
|
||||
import { FilePreviewType } from "../../FilesPreview";
|
||||
import { PreviewMessage } from "../../components/preview-message/PreviewMessage";
|
||||
|
||||
interface ErrorPreviewProps {
|
||||
file: FilePreviewType;
|
||||
@@ -19,25 +20,22 @@ export const ErrorPreview = ({ file }: ErrorPreviewProps) => {
|
||||
}, [file]);
|
||||
|
||||
return (
|
||||
<div className="file-preview-error">
|
||||
<div className="file-preview-error__icon">
|
||||
<FileIcon file={file} size="xlarge" />
|
||||
</div>
|
||||
<div className="file-preview-error__title">
|
||||
{t("file_preview.error.title")}
|
||||
</div>
|
||||
<div className="file-preview-error__description">
|
||||
{t("file_preview.error.description")}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="bordered"
|
||||
className="file-preview-error__download-button"
|
||||
icon={<Icon name="file_download" type={IconType.OUTLINED} size={16} />}
|
||||
onClick={handleDownload}
|
||||
>
|
||||
{t("file_preview.unsupported.download")}
|
||||
</Button>
|
||||
</div>
|
||||
<PreviewMessage
|
||||
variant="error"
|
||||
icon={<FileIcon file={file} size="xlarge" />}
|
||||
title={t("file_preview.error.title")}
|
||||
description={t("file_preview.error.description")}
|
||||
action={
|
||||
<Button
|
||||
variant="bordered"
|
||||
icon={
|
||||
<Icon name="file_download" type={IconType.OUTLINED} size={16} />
|
||||
}
|
||||
onClick={handleDownload}
|
||||
>
|
||||
{t("file_preview.unsupported.download")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
.file-preview-unsupported {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--c--globals--spacings--xs);
|
||||
|
||||
&__title {
|
||||
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
&__download-button {
|
||||
margin-top: var(--c--globals--spacings--md);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { Button } from "@gouvfr-lasuite/cunningham-react";
|
||||
import { Icon, IconType } from "@gouvfr-lasuite/ui-kit";
|
||||
import { FileIcon } from "@/features/explorer/components/icons/ItemIcon";
|
||||
import { useCallback } from "react";
|
||||
import { PreviewMessage } from "../../components/preview-message/PreviewMessage";
|
||||
|
||||
interface NotSupportedPreviewProps {
|
||||
file: FilePreviewType;
|
||||
@@ -24,33 +25,31 @@ export const NotSupportedPreview = ({
|
||||
}, [onDownload, file]);
|
||||
|
||||
return (
|
||||
<div className="file-preview-unsupported" data-preview-backdrop="true">
|
||||
<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">
|
||||
{title || (
|
||||
<PreviewMessage
|
||||
icon={<FileIcon file={file} size="xlarge" />}
|
||||
title={file.title}
|
||||
description={
|
||||
title ?? (
|
||||
<>
|
||||
<strong>{t("file_preview.unsupported.disclaimer")}</strong>{" "}
|
||||
{t("file_preview.unsupported.description")}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
{onDownload && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
color="neutral"
|
||||
className="file-preview-unsupported__download-button"
|
||||
icon={
|
||||
<Icon name="file_download" type={IconType.OUTLINED} size={16} />
|
||||
}
|
||||
onClick={handleDownload}
|
||||
>
|
||||
{t("file_preview.suspicious.download")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
action={
|
||||
onDownload && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
color="neutral"
|
||||
icon={
|
||||
<Icon name="file_download" type={IconType.OUTLINED} size={16} />
|
||||
}
|
||||
onClick={handleDownload}
|
||||
>
|
||||
{t("file_preview.suspicious.download")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
.file-preview-suspicious {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: var(--c--globals--spacings--sm);
|
||||
|
||||
&__title {
|
||||
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@gouvfr-lasuite/cunningham-react";
|
||||
import { Icon, IconType } from "@gouvfr-lasuite/ui-kit";
|
||||
import mimeSuspicious from "@/assets/files/icons/suspicious_file.svg";
|
||||
import { PreviewMessage } from "../../components/preview-message/PreviewMessage";
|
||||
|
||||
interface SuspiciousPreviewProps {
|
||||
handleDownload?: () => void;
|
||||
@@ -14,30 +15,26 @@ export const SuspiciousPreview = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="file-preview-suspicious" data-preview-backdrop="true">
|
||||
<div className="file-preview-suspicious__icon">
|
||||
<PreviewMessage
|
||||
icon={
|
||||
<img src={mimeSuspicious.src} alt="" className="item-icon xlarge" />
|
||||
</div>
|
||||
<span className="file-preview-suspicious__title">
|
||||
{t("file_preview.suspicious.title")}
|
||||
</span>
|
||||
<span className="file-preview-suspicious__description">
|
||||
{t("file_preview.suspicious.description")}
|
||||
</span>
|
||||
|
||||
{handleDownload && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
color="warning"
|
||||
className="file-preview-suspicious__download-button"
|
||||
icon={
|
||||
<Icon name="file_download" type={IconType.OUTLINED} size={16} />
|
||||
}
|
||||
onClick={handleDownload}
|
||||
>
|
||||
{t("file_preview.unsupported.download")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
title={t("file_preview.suspicious.title")}
|
||||
description={t("file_preview.suspicious.description")}
|
||||
action={
|
||||
handleDownload && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
color="warning"
|
||||
icon={
|
||||
<Icon name="file_download" type={IconType.OUTLINED} size={16} />
|
||||
}
|
||||
onClick={handleDownload}
|
||||
>
|
||||
{t("file_preview.unsupported.download")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Icon, IconType } from "@gouvfr-lasuite/ui-kit";
|
||||
import { FileIcon } from "@/features/explorer/components/icons/ItemIcon";
|
||||
import { FilePreviewType } from "../../FilesPreview";
|
||||
import { openWopiInNewTab } from "./openWopi";
|
||||
import { PreviewMessage } from "../../components/preview-message/PreviewMessage";
|
||||
|
||||
interface WopiOpenInEditorProps {
|
||||
file: FilePreviewType;
|
||||
@@ -13,22 +14,18 @@ export const WopiOpenInEditor = ({ file }: WopiOpenInEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="file-preview-unsupported" data-preview-backdrop="true">
|
||||
<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>
|
||||
<PreviewMessage
|
||||
icon={<FileIcon file={file} size="xlarge" />}
|
||||
title={file.title}
|
||||
description={t("file_preview.wopi.open_in_editor_description")}
|
||||
action={
|
||||
<Button
|
||||
icon={<Icon name="open_in_new" type={IconType.OUTLINED} size={16} />}
|
||||
onClick={() => openWopiInNewTab(file.id)}
|
||||
>
|
||||
{t("file_preview.wopi.open_in_editor")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -34,20 +34,20 @@ test.describe("Unsupported File Preview", () => {
|
||||
test("Renders the NotSupportedPreview for an unknown binary file", async ({
|
||||
page,
|
||||
}) => {
|
||||
const unsupported = page.locator(".file-preview-unsupported");
|
||||
const unsupported = page.locator(".preview-message");
|
||||
await expect(unsupported).toBeVisible();
|
||||
|
||||
await expect(
|
||||
unsupported.locator(".file-preview-unsupported__title"),
|
||||
unsupported.locator(".preview-message__title"),
|
||||
).toHaveText("test-unsupported.bin");
|
||||
});
|
||||
|
||||
test("Download button in the unsupported view triggers a download", async ({
|
||||
page,
|
||||
}) => {
|
||||
const downloadButton = page.locator(
|
||||
".file-preview-unsupported__download-button",
|
||||
);
|
||||
const downloadButton = page
|
||||
.locator(".preview-message__action")
|
||||
.getByRole("button");
|
||||
await expect(downloadButton).toBeVisible();
|
||||
|
||||
const downloadPromise = page.waitForEvent("download");
|
||||
|
||||
@@ -69,10 +69,10 @@ test("Navigating the previewer onto a WOPI file shows the Open in editor placeho
|
||||
// test-image.png, so ArrowLeft navigates from the image to the docx.
|
||||
await page.keyboard.press("ArrowLeft");
|
||||
|
||||
const unsupported = filePreview.locator(".file-preview-unsupported");
|
||||
const unsupported = filePreview.locator(".preview-message");
|
||||
await expect(unsupported).toBeVisible();
|
||||
await expect(
|
||||
unsupported.locator(".file-preview-unsupported__title"),
|
||||
unsupported.locator(".preview-message__title"),
|
||||
).toHaveText("empty_doc.docx");
|
||||
|
||||
const openButton = unsupported.getByRole("button", {
|
||||
|
||||
Reference in New Issue
Block a user