Refactor and Move TileErrorBoundary to Shared Components (#32793)
* creation of stories and view in shared-components * migrate EventTile error fallback to shared TileErrorView MVVM * Fix lint errors and unused import * Update tests because of the refactoring * Update snapshots + stories * removal of mxEvent since it never changes in timeline * Update packages/shared-components/src/message-body/TileErrorView/TileErrorView.stories.tsx Co-authored-by: Florian Duros <florian.duros@ormaz.fr> * Update apps/web/src/viewmodels/message-body/TileErrorViewModel.ts Co-authored-by: Florian Duros <florian.duros@ormaz.fr> * Update apps/web/src/viewmodels/message-body/TileErrorViewModel.ts Co-authored-by: Florian Duros <florian.duros@ormaz.fr> * docs: add TileErrorView tsdoc * docs: add TileErrorViewModel tsdoc * docs: add view source label tsdoc * refactor: move tile error layout into vm * docs: add TileErrorView story view docs * docs: move tile error story list wrapper * refactor: remove unused tile error event setter * Update packages/shared-components/src/message-body/TileErrorView/TileErrorView.stories.tsx Co-authored-by: Florian Duros <florian.duros@ormaz.fr> * docs: add tsdoc for event tile error fallback props * refactor: rely on snapshot merge no-op checks * remove unessecery if statment * test: restore EventTile mocks in afterEach * test(shared-components): move TileErrorView baselines --------- Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
This commit is contained in:
BIN
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
@@ -17,6 +17,7 @@ export * from "./room/timeline/event-tile/body/EventContentBodyView";
|
||||
export * from "./room/timeline/event-tile/body/RedactedBodyView";
|
||||
export * from "./room/timeline/event-tile/body/MFileBodyView";
|
||||
export * from "./room/timeline/event-tile/body/MVideoBodyView";
|
||||
export * from "./room/timeline/event-tile/EventTileView/TileErrorView";
|
||||
export * from "./core/pill-input/Pill";
|
||||
export * from "./core/pill-input/PillInput";
|
||||
export * from "./room/RoomStatusBar";
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
.tileErrorView {
|
||||
color: var(--cpd-color-text-critical-primary);
|
||||
list-style: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--cpd-space-2x);
|
||||
}
|
||||
|
||||
.bubble .line {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: var(--cpd-space-1x) var(--cpd-space-4x);
|
||||
}
|
||||
|
||||
.viewSourceButton {
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: var(--cpd-color-text-action-primary);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.viewSourceButton:focus-visible {
|
||||
outline: 2px solid var(--cpd-color-border-focused);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--cpd-space-1x);
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type ComponentProps, type JSX } from "react";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { withViewDocs } from "../../../../../../.storybook/withViewDocs";
|
||||
import { useMockedViewModel } from "../../../../../core/viewmodel";
|
||||
import { TileErrorView, type TileErrorViewActions, type TileErrorViewSnapshot } from "./TileErrorView";
|
||||
|
||||
type WrapperProps = TileErrorViewSnapshot &
|
||||
Partial<TileErrorViewActions> &
|
||||
Omit<ComponentProps<typeof TileErrorView>, "vm">;
|
||||
|
||||
const TileErrorViewWrapperImpl = ({
|
||||
className,
|
||||
onBugReportClick = fn(),
|
||||
onViewSourceClick = fn(),
|
||||
...snapshotProps
|
||||
}: WrapperProps): JSX.Element => {
|
||||
const vm = useMockedViewModel(snapshotProps, {
|
||||
onBugReportClick,
|
||||
onViewSourceClick,
|
||||
});
|
||||
|
||||
return <TileErrorView vm={vm} className={className} />;
|
||||
};
|
||||
|
||||
const TileErrorViewWrapper = withViewDocs(TileErrorViewWrapperImpl, TileErrorView);
|
||||
|
||||
const meta = {
|
||||
title: "MessageBody/TileErrorView",
|
||||
component: TileErrorViewWrapper,
|
||||
tags: ["autodocs"],
|
||||
decorators: [
|
||||
(Story): JSX.Element => (
|
||||
<ul>
|
||||
<Story />
|
||||
</ul>
|
||||
),
|
||||
],
|
||||
args: {
|
||||
message: "Can't load this message",
|
||||
eventType: "m.room.message",
|
||||
bugReportCtaLabel: "Submit debug logs",
|
||||
viewSourceCtaLabel: "View source",
|
||||
layout: "group",
|
||||
},
|
||||
} satisfies Meta<typeof TileErrorViewWrapper>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const BubbleLayout: Story = {
|
||||
args: {
|
||||
layout: "bubble",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutActions: Story = {
|
||||
args: {
|
||||
bugReportCtaLabel: undefined,
|
||||
viewSourceCtaLabel: undefined,
|
||||
},
|
||||
};
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type MouseEventHandler } from "react";
|
||||
import { composeStories } from "@storybook/react-vite";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { render, screen } from "@test-utils";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { MockViewModel } from "../../../../../core/viewmodel";
|
||||
import {
|
||||
TileErrorView,
|
||||
type TileErrorViewActions,
|
||||
type TileErrorViewModel,
|
||||
type TileErrorViewSnapshot,
|
||||
} from "./TileErrorView";
|
||||
import * as stories from "./TileErrorView.stories";
|
||||
|
||||
const { Default, BubbleLayout, WithoutActions } = composeStories(stories);
|
||||
|
||||
describe("TileErrorView", () => {
|
||||
it("renders the default tile error state", () => {
|
||||
const { container } = render(<Default />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders the bubble layout variant", () => {
|
||||
const { container } = render(<BubbleLayout />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders the fallback text without actions", () => {
|
||||
render(<WithoutActions />);
|
||||
|
||||
expect(screen.getByText("Can't load this message (m.room.message)")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Submit debug logs" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "View source" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("invokes bug-report and view-source actions", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onBugReportClick = vi.fn();
|
||||
const onViewSourceClick = vi.fn();
|
||||
|
||||
class TestTileErrorViewModel extends MockViewModel<TileErrorViewSnapshot> implements TileErrorViewActions {
|
||||
public onBugReportClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
public onViewSourceClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
|
||||
public constructor(snapshot: TileErrorViewSnapshot, actions: TileErrorViewActions) {
|
||||
super(snapshot);
|
||||
Object.assign(this, actions);
|
||||
}
|
||||
}
|
||||
|
||||
const vm = new TestTileErrorViewModel(
|
||||
{
|
||||
layout: "group",
|
||||
message: "Can't load this message",
|
||||
eventType: "m.room.message",
|
||||
bugReportCtaLabel: "Submit debug logs",
|
||||
viewSourceCtaLabel: "View source",
|
||||
},
|
||||
{
|
||||
onBugReportClick,
|
||||
onViewSourceClick,
|
||||
},
|
||||
) as TileErrorViewModel;
|
||||
|
||||
render(<TileErrorView vm={vm} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Submit debug logs" }));
|
||||
await user.click(screen.getByRole("button", { name: "View source" }));
|
||||
|
||||
expect(onBugReportClick).toHaveBeenCalledTimes(1);
|
||||
expect(onViewSourceClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("applies a custom className to the root element", () => {
|
||||
const vm = new MockViewModel<TileErrorViewSnapshot>({
|
||||
layout: "group",
|
||||
message: "Can't load this message",
|
||||
}) as TileErrorViewModel;
|
||||
|
||||
render(<TileErrorView vm={vm} className="custom-tile-error" />);
|
||||
|
||||
expect(screen.getByRole("status").closest("li")).toHaveClass("custom-tile-error");
|
||||
});
|
||||
});
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX, type MouseEventHandler } from "react";
|
||||
import classNames from "classnames";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
|
||||
import { type ViewModel, useViewModel } from "../../../../../core/viewmodel";
|
||||
import styles from "./TileErrorView.module.css";
|
||||
|
||||
export type TileErrorViewLayout = "bubble" | "group" | "irc";
|
||||
|
||||
export interface TileErrorViewSnapshot {
|
||||
/**
|
||||
* Layout variant used by the host timeline.
|
||||
*/
|
||||
layout?: TileErrorViewLayout;
|
||||
/**
|
||||
* Primary fallback text shown when a tile fails to render.
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Optional event type appended to the fallback text.
|
||||
*/
|
||||
eventType?: string;
|
||||
/**
|
||||
* Optional label for the bug-report action button.
|
||||
*/
|
||||
bugReportCtaLabel?: string;
|
||||
/**
|
||||
* Optional label for the view-source action.
|
||||
*/
|
||||
viewSourceCtaLabel?: string;
|
||||
}
|
||||
|
||||
export interface TileErrorViewActions {
|
||||
/**
|
||||
* Invoked when the bug-report button is clicked.
|
||||
*/
|
||||
onBugReportClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
/**
|
||||
* Invoked when the view-source action is clicked.
|
||||
*/
|
||||
onViewSourceClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
export type TileErrorViewModel = ViewModel<TileErrorViewSnapshot, TileErrorViewActions>;
|
||||
|
||||
interface TileErrorViewProps {
|
||||
/**
|
||||
* The view model for the tile error fallback.
|
||||
*/
|
||||
vm: TileErrorViewModel;
|
||||
/**
|
||||
* Optional host-level class names.
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a timeline tile fallback when message content cannot be displayed.
|
||||
*
|
||||
* The component shows the fallback error message from the view model, optionally
|
||||
* appends the event type in parentheses, and can render bug-report and view-source
|
||||
* actions when their labels are provided. The layout in the view-model snapshot
|
||||
* selects the timeline presentation variant.
|
||||
*/
|
||||
export function TileErrorView({ vm, className }: Readonly<TileErrorViewProps>): JSX.Element {
|
||||
const { message, eventType, bugReportCtaLabel, viewSourceCtaLabel, layout = "group" } = useViewModel(vm);
|
||||
|
||||
return (
|
||||
<li
|
||||
className={classNames(styles.tileErrorView, className, { [styles.bubble]: layout === "bubble" })}
|
||||
data-layout={layout}
|
||||
>
|
||||
<div className={styles.line} role="status">
|
||||
<span className={styles.message}>
|
||||
{message}
|
||||
{eventType && ` (${eventType})`}
|
||||
</span>
|
||||
{bugReportCtaLabel && (
|
||||
<Button kind="secondary" size="sm" onClick={vm.onBugReportClick}>
|
||||
{bugReportCtaLabel}
|
||||
</Button>
|
||||
)}
|
||||
{viewSourceCtaLabel && (
|
||||
<button type="button" className={styles.viewSourceButton} onClick={vm.onViewSourceClick}>
|
||||
{viewSourceCtaLabel}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`TileErrorView > renders the bubble layout variant 1`] = `
|
||||
<div>
|
||||
<ul>
|
||||
<li
|
||||
class="tileErrorView bubble"
|
||||
data-layout="bubble"
|
||||
>
|
||||
<div
|
||||
class="line"
|
||||
role="status"
|
||||
>
|
||||
<span
|
||||
class="message"
|
||||
>
|
||||
Can't load this message
|
||||
(m.room.message)
|
||||
</span>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Submit debug logs
|
||||
</button>
|
||||
<button
|
||||
class="viewSourceButton"
|
||||
type="button"
|
||||
>
|
||||
View source
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`TileErrorView > renders the default tile error state 1`] = `
|
||||
<div>
|
||||
<ul>
|
||||
<li
|
||||
class="tileErrorView"
|
||||
data-layout="group"
|
||||
>
|
||||
<div
|
||||
class="line"
|
||||
role="status"
|
||||
>
|
||||
<span
|
||||
class="message"
|
||||
>
|
||||
Can't load this message
|
||||
(m.room.message)
|
||||
</span>
|
||||
<button
|
||||
class="_button_13vu4_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Submit debug logs
|
||||
</button>
|
||||
<button
|
||||
class="viewSourceButton"
|
||||
type="button"
|
||||
>
|
||||
View source
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
export {
|
||||
TileErrorView,
|
||||
type TileErrorViewActions,
|
||||
type TileErrorViewLayout,
|
||||
type TileErrorViewModel,
|
||||
type TileErrorViewSnapshot,
|
||||
} from "./TileErrorView";
|
||||
Reference in New Issue
Block a user