fix(mod multiboard): show account import empty state

This commit is contained in:
Tommaso Casaburi
2026-05-21 17:37:18 +07:00
parent a31c3b44d3
commit 56c4fdf3c1
42 changed files with 198 additions and 74 deletions
+1
View File
@@ -0,0 +1 @@
export { default as ModEmptyState } from './mod-empty-state';
@@ -0,0 +1,28 @@
.modEmptyState {
padding: 1em 0;
text-align: center;
width: 100%;
}
.modEmptyStateTitle {
color: red;
display: block;
margin: 0;
font-size: x-large;
font-weight: bold;
line-height: normal;
}
.modEmptyStateAction {
color: var(--post-mobile-abbr-text-color);
margin-top: 0.5em;
}
.modEmptyStateAction a {
text-decoration: var(--button-text-decoration);
color: var(--button-desktop-text-color);
}
.modEmptyStateAction a:hover {
color: var(--button-desktop-text-color-hover);
}
@@ -0,0 +1,22 @@
import { Link } from 'react-router-dom';
import { Trans, useTranslation } from 'react-i18next';
import styles from './mod-empty-state.module.css';
const MOD_ACCOUNT_SETTINGS_LINK_COMPONENTS = {
1: <Link to='/mod/settings#account-settings' />,
};
const ModEmptyState = () => {
const { t } = useTranslation();
return (
<div className={styles.modEmptyState} role='status'>
<div className={styles.modEmptyStateTitle}>{t('not_mod_of_any_board')}</div>
<div className={styles.modEmptyStateAction}>
<Trans i18nKey='go_to_settings_to_import_mod_account' components={MOD_ACCOUNT_SETTINGS_LINK_COMPONENTS} />
</div>
</div>
);
};
export default ModEmptyState;
+2
View File
@@ -616,6 +616,8 @@ describe('Board', () => {
});
expect(container.textContent).toContain('not_mod_of_any_board');
expect(container.textContent).toContain('go_to_settings_to_import_mod_account');
expect(container.querySelector('[role="status"]')?.className).toContain('modEmptyState');
expect(container.querySelector('a')?.getAttribute('href')).toBe('/mod/settings#account-settings');
});
+2 -4
View File
@@ -32,6 +32,7 @@ import LoadingEllipsis from '../../components/loading-ellipsis';
import BoardPagination from '../../components/board-pagination';
import { CatalogButton } from '../../components/board-buttons/board-buttons';
import { PageFooterDesktop, PageFooterMobile } from '../../components/footer';
import { ModEmptyState } from '../../components/mod-empty-state';
import { Post } from '../post';
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
@@ -41,9 +42,6 @@ const MONTH_IN_SECONDS = 30 * 24 * 60 * 60;
const YEAR_IN_SECONDS = 365 * 24 * 60 * 60;
// Keep the hook on its indexed fast path when this view should not inject local posts.
const EMPTY_ACCOUNT_COMMENT_LOOKUP = { commentIndices: [-1] };
const MOD_ACCOUNT_IMPORT_LINK_COMPONENTS = {
1: <Link to='/mod/settings#account-settings' />,
};
/** Board feed always uses 'active' sort; catalog dropdown does not affect board ordering. */
const BOARD_SORT_TYPE = 'active' as const;
@@ -137,7 +135,7 @@ const BoardFooter = ({
) : isInSubscriptionsView && subscriptionsLength === 0 ? (
<span className='red'>{t('not_subscribed_to_any_board')}</span>
) : isInModView && accountCommunityAddressesLength === 0 ? (
<Trans i18nKey='not_mod_of_any_board' components={MOD_ACCOUNT_IMPORT_LINK_COMPONENTS} />
<ModEmptyState />
) : (
showLoadingEllipsis && (hasMore || isEmptyBoardLoading) && <LoadingEllipsis string={loadingStateString} />
)}
@@ -909,6 +909,22 @@ describe('Catalog', () => {
expect(container.querySelector('[data-testid="catalog-first-row"]')?.textContent).toBe('music-posting.eth');
});
it('shows mod empty state with settings link in mod catalog', async () => {
testState.accountCommunityAddresses = [];
await renderCatalog({
catalogProps: { viewType: 'mod' },
initialEntry: '/mod/catalog',
routePath: '/mod/*',
});
expect(container.textContent).toContain('not_mod_of_any_board');
expect(container.textContent).toContain('go_to_settings_to_import_mod_account');
expect(container.querySelector('[role="status"]')?.className).toContain('modEmptyState');
expect(container.querySelector('a')?.getAttribute('href')).toBe('/mod/settings#account-settings');
expect(container.querySelectorAll('[data-testid="catalog-row"]')).toHaveLength(0);
});
it('shows centered nothing found when catalog search has no matches', async () => {
testState.feed = [{ cid: 'network-post', title: 'cats on stage', communityAddress: 'music-posting.eth' }];
testState.searchText = 'asddasd';
+23 -1
View File
@@ -30,6 +30,7 @@ import { ReturnButton, ArchiveButton, TopButton, RefreshButton } from '../../com
import mobileFooterStyles from '../../components/footer/footer.module.css';
import LoadingEllipsis from '../../components/loading-ellipsis';
import ErrorDisplay from '../../components/error-display/error-display';
import { ModEmptyState } from '../../components/mod-empty-state';
import styles from './catalog.module.css';
import { commentMatchesPattern } from '../../lib/utils/pattern-utils';
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
@@ -695,8 +696,14 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
);
const isFeedLoaded = feed.length > 0 || hiddenThreadsCount > 0 || state === 'failed';
const hasActiveSearch = searchText.trim().length > 0;
const showModEmptyState = isInModView && accountCommunityAddresses.length === 0;
const showSearchNothingFound =
hasActiveSearch && !footerHasMore && footerCombinedFeedLength === 0 && state !== 'failed' && !(isInSubscriptionsView && (subscriptions?.length || 0) === 0);
hasActiveSearch &&
!footerHasMore &&
footerCombinedFeedLength === 0 &&
state !== 'failed' &&
!(isInSubscriptionsView && (subscriptions?.length || 0) === 0) &&
!showModEmptyState;
// Process the feed to move "top" posts to the top (applied after display sort)
const processedFeed = useMemo(() => {
@@ -901,6 +908,21 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
</>
)}
</>
) : showModEmptyState ? (
<>
<ModEmptyState />
<hr />
{catalogFooterFirstRow}
<PageFooterDesktop variant='catalog' styleRow={catalogFooterStyleRow} />
<PageFooterMobile>
<div className={mobileFooterStyles.mobileFooterButtons}>
<ReturnButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
<ArchiveButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
<TopButton />
<RefreshButton />
</div>
</PageFooterMobile>
</>
) : showSearchNothingFound ? (
<>
<CatalogSearchNothingFound />