From b5879fd61fe6c6e570920fca0501ba7fa2568a72 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Wed, 27 May 2026 12:33:52 +0200 Subject: [PATCH] Refactor `BundleColumnError` to TS (#39177) --- .../empty_state/empty_state.module.scss | 23 ++-- .../empty_state/empty_state.stories.tsx | 8 ++ .../mastodon/components/empty_state/index.tsx | 9 +- app/javascript/mastodon/components/gif.tsx | 2 +- .../account_edit/components/column.tsx | 2 +- .../features/account_featured/index.tsx | 2 +- .../features/account_gallery/index.tsx | 2 +- .../features/account_timeline/index.tsx | 2 +- .../features/followers/components/list.tsx | 2 +- .../features/terms_of_service/index.tsx | 2 +- .../ui/components/bundle_column_error.jsx | 116 ----------------- .../components/bundle_column_error/index.tsx | 122 ++++++++++++++++++ .../bundle_column_error/styles.module.scss | 12 ++ .../features/ui/components/columns_area.tsx | 2 +- .../styles/mastodon/components.scss | 48 +------ 15 files changed, 173 insertions(+), 181 deletions(-) delete mode 100644 app/javascript/mastodon/features/ui/components/bundle_column_error.jsx create mode 100644 app/javascript/mastodon/features/ui/components/bundle_column_error/index.tsx create mode 100644 app/javascript/mastodon/features/ui/components/bundle_column_error/styles.module.scss diff --git a/app/javascript/mastodon/components/empty_state/empty_state.module.scss b/app/javascript/mastodon/components/empty_state/empty_state.module.scss index 64d9a4e584..cb2332aec6 100644 --- a/app/javascript/mastodon/components/empty_state/empty_state.module.scss +++ b/app/javascript/mastodon/components/empty_state/empty_state.module.scss @@ -13,8 +13,7 @@ .content { max-width: 370px; - svg, - img { + :where(svg, img) { width: 200px; aspect-ratio: 1; object-fit: contain; @@ -22,16 +21,10 @@ margin-bottom: 16px; } - h3 { - font-size: 17px; - font-weight: 500; - text-wrap: balance; - line-height: 1.2; - } - p { margin-top: 8px; font-size: 15px; + line-height: 1.4; color: var(--color-text-secondary); text-wrap: pretty; } @@ -41,6 +34,18 @@ } } +.heading { + font-size: 17px; + font-weight: 500; + text-wrap: balance; + line-height: 1.2; +} + +.errorImage { + width: 280px; + margin: -10% 0; +} + [data-color-scheme='dark'] .defaultImage { --color-skin-1: #3a3a50; --color-skin-2: #67678e; diff --git a/app/javascript/mastodon/components/empty_state/empty_state.stories.tsx b/app/javascript/mastodon/components/empty_state/empty_state.stories.tsx index 83fce03468..c1faaf6f39 100644 --- a/app/javascript/mastodon/components/empty_state/empty_state.stories.tsx +++ b/app/javascript/mastodon/components/empty_state/empty_state.stories.tsx @@ -29,6 +29,14 @@ export const Default: Story = { }, }; +export const Error: Story = { + args: { + image: 'error', + title: 'Error', + message: 'Something went wrong loading the page.', + }, +}; + export const WithAction: Story = { args: { ...Default.args, diff --git a/app/javascript/mastodon/components/empty_state/index.tsx b/app/javascript/mastodon/components/empty_state/index.tsx index e332aaedb5..3f0738ce5a 100644 --- a/app/javascript/mastodon/components/empty_state/index.tsx +++ b/app/javascript/mastodon/components/empty_state/index.tsx @@ -4,10 +4,15 @@ import classNames from 'classnames'; import ElephantImage from '@/images/elephant_ui.svg?react'; +import { GIF } from '../gif'; + import classes from './empty_state.module.scss'; const images = { default: , + error: ( + + ), }; /** @@ -21,6 +26,7 @@ export const EmptyState: React.FC<{ title?: React.ReactNode; message?: React.ReactNode; children?: React.ReactNode; + headingLevel?: 'h2' | 'h3' | 'h4'; className?: string; }> = ({ image = 'default', @@ -29,6 +35,7 @@ export const EmptyState: React.FC<{ ), message, children, + headingLevel: Heading = 'h2', className, }) => { const imageToRender = typeof image === 'string' ? images[image] : image; @@ -38,7 +45,7 @@ export const EmptyState: React.FC<{ {(title || message || imageToRender) && (
{imageToRender} - {!!title &&

{title}

} + {!!title && {title}} {!!message &&

{message}

}
)} diff --git a/app/javascript/mastodon/components/gif.tsx b/app/javascript/mastodon/components/gif.tsx index 1cc0881a5a..7bce985784 100644 --- a/app/javascript/mastodon/components/gif.tsx +++ b/app/javascript/mastodon/components/gif.tsx @@ -4,7 +4,7 @@ import { autoPlayGif } from 'mastodon/initial_state'; export const GIF: React.FC<{ src: string; staticSrc: string; - className: string; + className?: string; animate?: boolean; }> = ({ src, staticSrc, className, animate = autoPlayGif }) => { const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate); diff --git a/app/javascript/mastodon/features/account_edit/components/column.tsx b/app/javascript/mastodon/features/account_edit/components/column.tsx index a9b0f8cbd5..2718aab7f2 100644 --- a/app/javascript/mastodon/features/account_edit/components/column.tsx +++ b/app/javascript/mastodon/features/account_edit/components/column.tsx @@ -9,7 +9,7 @@ import { Helmet } from '@unhead/react/helmet'; import { Column } from '@/mastodon/components/column'; import { ColumnHeader } from '@/mastodon/components/column_header'; import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; -import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error'; +import { BundleColumnError } from '@/mastodon/features/ui/components/bundle_column_error'; import { useColumnsContext } from '../../ui/util/columns_context'; import classes from '../styles.module.scss'; diff --git a/app/javascript/mastodon/features/account_featured/index.tsx b/app/javascript/mastodon/features/account_featured/index.tsx index 3082f19abc..bccdddeda8 100644 --- a/app/javascript/mastodon/features/account_featured/index.tsx +++ b/app/javascript/mastodon/features/account_featured/index.tsx @@ -19,7 +19,7 @@ import { } from '@/mastodon/components/scrollable_list/components'; import type { TruncatedListItemInfo } from '@/mastodon/components/truncated_list'; import { TruncatedListItems } from '@/mastodon/components/truncated_list'; -import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error'; +import { BundleColumnError } from '@/mastodon/features/ui/components/bundle_column_error'; import Column from '@/mastodon/features/ui/components/column'; import { useAccount } from '@/mastodon/hooks/useAccount'; import { useAccountId } from '@/mastodon/hooks/useAccountId'; diff --git a/app/javascript/mastodon/features/account_gallery/index.tsx b/app/javascript/mastodon/features/account_gallery/index.tsx index feba6f454d..25ad8c88c1 100644 --- a/app/javascript/mastodon/features/account_gallery/index.tsx +++ b/app/javascript/mastodon/features/account_gallery/index.tsx @@ -11,7 +11,7 @@ import { ColumnBackButton } from '@/mastodon/components/column_back_button'; import { LimitedAccountHint } from '@/mastodon/components/limited_account_hint'; import { RemoteHint } from '@/mastodon/components/remote_hint'; import ScrollableList from '@/mastodon/components/scrollable_list'; -import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error'; +import { BundleColumnError } from '@/mastodon/features/ui/components/bundle_column_error'; import Column from '@/mastodon/features/ui/components/column'; import { useAccountId } from '@/mastodon/hooks/useAccountId'; import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility'; diff --git a/app/javascript/mastodon/features/account_timeline/index.tsx b/app/javascript/mastodon/features/account_timeline/index.tsx index ce1e99a45f..450236386a 100644 --- a/app/javascript/mastodon/features/account_timeline/index.tsx +++ b/app/javascript/mastodon/features/account_timeline/index.tsx @@ -19,7 +19,7 @@ import { LimitedAccountHint } from '@/mastodon/components/limited_account_hint'; import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; import { RemoteHint } from '@/mastodon/components/remote_hint'; import StatusList from '@/mastodon/components/status_list'; -import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error'; +import { BundleColumnError } from '@/mastodon/features/ui/components/bundle_column_error'; import { useAccountId, useCurrentAccountId, diff --git a/app/javascript/mastodon/features/followers/components/list.tsx b/app/javascript/mastodon/features/followers/components/list.tsx index 6c600d570f..3c593e10b8 100644 --- a/app/javascript/mastodon/features/followers/components/list.tsx +++ b/app/javascript/mastodon/features/followers/components/list.tsx @@ -6,7 +6,7 @@ import { Column } from '@/mastodon/components/column'; import { ColumnBackButton } from '@/mastodon/components/column_back_button'; import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; import ScrollableList from '@/mastodon/components/scrollable_list'; -import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error'; +import { BundleColumnError } from '@/mastodon/features/ui/components/bundle_column_error'; import { useAccount } from '@/mastodon/hooks/useAccount'; import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility'; import { useLayout } from '@/mastodon/hooks/useLayout'; diff --git a/app/javascript/mastodon/features/terms_of_service/index.tsx b/app/javascript/mastodon/features/terms_of_service/index.tsx index 669fb18b92..0a7c2d5669 100644 --- a/app/javascript/mastodon/features/terms_of_service/index.tsx +++ b/app/javascript/mastodon/features/terms_of_service/index.tsx @@ -14,7 +14,7 @@ import { Helmet } from '@unhead/react/helmet'; import { apiGetTermsOfService } from 'mastodon/api/instance'; import type { ApiTermsOfServiceJSON } from 'mastodon/api_types/instance'; import { Column } from 'mastodon/components/column'; -import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; +import { BundleColumnError } from 'mastodon/features/ui/components/bundle_column_error'; const messages = defineMessages({ title: { id: 'terms_of_service.title', defaultMessage: 'Terms of Service' }, diff --git a/app/javascript/mastodon/features/ui/components/bundle_column_error.jsx b/app/javascript/mastodon/features/ui/components/bundle_column_error.jsx deleted file mode 100644 index 81fb8f48c7..0000000000 --- a/app/javascript/mastodon/features/ui/components/bundle_column_error.jsx +++ /dev/null @@ -1,116 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedMessage } from 'react-intl'; - -import classNames from 'classnames'; -import { Helmet } from '@unhead/react/helmet'; -import { Link } from 'react-router-dom'; - -import { Button } from 'mastodon/components/button'; -import Column from 'mastodon/components/column'; -import { injectIntl } from '@/mastodon/components/intl'; -import { GIF } from 'mastodon/components/gif'; - -class CopyButton extends PureComponent { - - static propTypes = { - children: PropTypes.node.isRequired, - value: PropTypes.string.isRequired, - }; - - state = { - copied: false, - }; - - handleClick = () => { - const { value } = this.props; - navigator.clipboard.writeText(value); - this.setState({ copied: true }); - this.timeout = setTimeout(() => this.setState({ copied: false }), 700); - }; - - componentWillUnmount () { - if (this.timeout) clearTimeout(this.timeout); - } - - render () { - const { children } = this.props; - const { copied } = this.state; - - return ( - - ); - } - -} - -class BundleColumnError extends PureComponent { - - static propTypes = { - errorType: PropTypes.oneOf(['routing', 'network', 'error']), - onRetry: PropTypes.func, - intl: PropTypes.object.isRequired, - multiColumn: PropTypes.bool, - stacktrace: PropTypes.string, - }; - - static defaultProps = { - errorType: 'routing', - }; - - handleRetry = () => { - const { onRetry } = this.props; - - if (onRetry) { - onRetry(); - } - }; - - render () { - const { errorType, multiColumn, stacktrace } = this.props; - - let title, body; - - switch(errorType) { - case 'routing': - title = ; - body = ; - break; - case 'network': - title = ; - body = ; - break; - case 'error': - title = ; - body = ; - break; - } - - return ( - -
- - -
-

{title}

-

{body}

- -
- {errorType === 'network' && } - {errorType === 'error' && } - -
-
-
- - - - -
- ); - } - -} - -export default injectIntl(BundleColumnError); diff --git a/app/javascript/mastodon/features/ui/components/bundle_column_error/index.tsx b/app/javascript/mastodon/features/ui/components/bundle_column_error/index.tsx new file mode 100644 index 0000000000..f4acb3059f --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/bundle_column_error/index.tsx @@ -0,0 +1,122 @@ +import { FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; +import { Link } from 'react-router-dom'; + +import { Helmet } from '@unhead/react/helmet'; + +import { CopyButton } from '@/mastodon/components/copy_button'; +import { EmptyState } from '@/mastodon/components/empty_state'; +import { Button } from 'mastodon/components/button'; +import { Column } from 'mastodon/components/column'; + +import classes from './styles.module.scss'; + +interface BundleColumnErrorProps { + errorType?: 'routing' | 'network' | 'error'; + onRetry?: () => void; + multiColumn?: boolean; + stacktrace?: string; +} + +export const BundleColumnError: React.FC = ({ + errorType = 'routing', + onRetry, + multiColumn, + stacktrace, +}) => { + let title, body; + + switch (errorType) { + case 'routing': + title = ( + + ); + body = ( + + ); + break; + case 'network': + title = ( + + ); + body = ( + + ); + break; + case 'error': + title = ( + + ); + body = ( + + ); + break; + } + + return ( + + +
+ {errorType === 'network' && onRetry && ( + + )} + {errorType === 'error' && stacktrace && ( + + + + )} + + + +
+
+ + + + +
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export default BundleColumnError; diff --git a/app/javascript/mastodon/features/ui/components/bundle_column_error/styles.module.scss b/app/javascript/mastodon/features/ui/components/bundle_column_error/styles.module.scss new file mode 100644 index 0000000000..62549efffc --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/bundle_column_error/styles.module.scss @@ -0,0 +1,12 @@ +.error { + flex-grow: 1; + border: 1px solid var(--color-border-primary); + border-radius: 4px; +} + +.actions { + display: flex; + flex-wrap: wrap; + gap: 8px 12px; + justify-content: center; +} diff --git a/app/javascript/mastodon/features/ui/components/columns_area.tsx b/app/javascript/mastodon/features/ui/components/columns_area.tsx index f5ab3de849..cd94f34ec0 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.tsx +++ b/app/javascript/mastodon/features/ui/components/columns_area.tsx @@ -33,7 +33,7 @@ import { import { useColumnsContext } from '../util/columns_context'; import Bundle from './bundle'; -import BundleColumnError from './bundle_column_error'; +import { BundleColumnError } from './bundle_column_error'; import { ColumnLoading } from './column_loading'; import { ComposePanel, RedirectToMobileComposeIfNeeded } from './compose_panel'; import DrawerLoading from './drawer_loading'; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 809634e8f5..a2771815ab 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3374,8 +3374,7 @@ a.account__display-name { .column-header, .column-back-button, - .scrollable, - .error-column { + .scrollable { border-radius: 0 !important; } @@ -5288,51 +5287,6 @@ a.status-card { margin-bottom: 0; } -.error-column { - padding: 20px; - border: 1px solid var(--color-border-primary); - border-radius: 4px; - display: flex; - flex: 1 1 auto; - align-items: center; - justify-content: center; - flex-direction: column; - cursor: default; - - &__image { - width: 70%; - max-width: 350px; - margin-top: -50px; - } - - &__message { - text-align: center; - color: var(--color-text-secondary); - font-size: 15px; - line-height: 22px; - - h1 { - font-size: 28px; - line-height: 33px; - font-weight: 700; - margin-bottom: 15px; - color: var(--color-text-primary); - } - - p { - max-width: 48ch; - } - - &__actions { - margin-top: 30px; - display: flex; - gap: 10px; - align-items: center; - justify-content: center; - } - } -} - @keyframes heartbeat { 0% { transform: scale(1);