diff --git a/src/js/components/ManageMenu/SnippetsTable/SnippetsListTable.tsx b/src/js/components/ManageMenu/SnippetsTable/SnippetsListTable.tsx index b0a3a70da..77d50aa56 100644 --- a/src/js/components/ManageMenu/SnippetsTable/SnippetsListTable.tsx +++ b/src/js/components/ManageMenu/SnippetsTable/SnippetsListTable.tsx @@ -122,8 +122,7 @@ const SnippetsView: React.FC = ({ }) => { const { activeByCondition } = useFilteredSnippets() const columns = useMemo(() => getTableColumns(hiddenColumns), [hiddenColumns]) - const itemsPerPage = window.CODE_SNIPPETS_MANAGE?.snippetsPerPage - const pageCount = itemsPerPage && Math.ceil(snippets.length / itemsPerPage) + const itemsPerPage = Number(window.CODE_SNIPPETS_MANAGE?.snippetsPerPage) || undefined const endTableNav = (which: 'top' | 'bottom') => 'top' === which ? @@ -148,7 +147,7 @@ const SnippetsView: React.FC = ({ columns={columns} actions={actions} doAction={doAction} - totalPages={pageCount} + itemsPerPage={itemsPerPage} extraTableNav={extraTableNav} selectAllControl endTableNav={endTableNav} diff --git a/src/js/components/ManageMenu/SnippetsTable/SnippetsTable.tsx b/src/js/components/ManageMenu/SnippetsTable/SnippetsTable.tsx index 72146c3ae..82e878ef6 100644 --- a/src/js/components/ManageMenu/SnippetsTable/SnippetsTable.tsx +++ b/src/js/components/ManageMenu/SnippetsTable/SnippetsTable.tsx @@ -12,6 +12,7 @@ import { isLicensed } from '../../../utils/screen' import { SNIPPET_TYPE_LABELS, getSnippetAddNewUrl, getSnippetType, isProType } from '../../../utils/snippets/snippets' import { buildUrl } from '../../../utils/urls' import { Badge } from '../../common/Badge' +import { Button } from '../../common/Button' import { Notice } from '../../common/Notice' import { ScreenMetaSlot } from '../../common/ScreenMetaSlot' import { UpsellPage } from '../../common/UpsellDialog' @@ -99,6 +100,26 @@ const SafeModeNotice = () => : null +const SnippetsLoadErrorNotice = () => { + const { hasLoadError, refreshSnippetsList } = useSnippetsList() + + return ( + <> + {hasLoadError && + +

+ {__('Some of your snippets could not be loaded, so this list may be incomplete.', 'code-snippets')}{' '} + +

+
} + + ) +} + // Counts render immediately from localized values, then switch to live values. const useSnippetTypeCounts = () => { const { snippetsList } = useSnippetsList() @@ -174,6 +195,7 @@ const SnippetsTableInner = () => { )} + {isUpsell ? diff --git a/src/js/components/common/ListTable/ListTable.tsx b/src/js/components/common/ListTable/ListTable.tsx index 9a310d5ed..dba0e7c4b 100644 --- a/src/js/components/common/ListTable/ListTable.tsx +++ b/src/js/components/common/ListTable/ListTable.tsx @@ -42,7 +42,8 @@ export interface ListTableRowsProps { } export interface ListTablePaginationProps { - totalPages?: number + /** Items on each page; every item is on one page when omitted. */ + itemsPerPage?: number pageSearchParam?: string } @@ -78,17 +79,11 @@ export const sortTableItems = ( const pageItems = ( items: T[], - { currentPage, totalPages }: { currentPage: number; totalPages?: number } -): T[] => { - if (totalPages) { - const itemsPerPage = Math.ceil(items.length / totalPages) - const start = (currentPage - 1) * itemsPerPage - const end = start + itemsPerPage - return items.slice(start, end) - } else { - return items - } -} + { currentPage, itemsPerPage }: { currentPage: number; itemsPerPage?: number } +): T[] => + itemsPerPage + ? items.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage) + : items const getVisibleSelected = ( visibleItems: T[], @@ -137,7 +132,7 @@ export interface ListTableProps extends List export const ListTable = ({ items, getKey, - totalPages, + itemsPerPage, pageSearchParam = 'paged', initialSort, ...tableProps @@ -154,9 +149,11 @@ export const ListTable = ({ () => initialSort?.direction ?? 'asc' ) + const totalPages = itemsPerPage ? Math.ceil(items.length / itemsPerPage) : undefined + const visibleItems: T[] = useMemo( - () => pageItems(sortTableItems(items, sortColumn, sortDirection), { currentPage, totalPages }), - [items, sortColumn, sortDirection, currentPage, totalPages]) + () => pageItems(sortTableItems(items, sortColumn, sortDirection), { currentPage, itemsPerPage }), + [items, sortColumn, sortDirection, currentPage, itemsPerPage]) return ( ListTableNavProps { sortColumn: ListTableColumn | undefined totalItems: number + totalPages?: number currentPage: number visibleItems: T[] beforeTable?: ReactNode diff --git a/src/js/hooks/useSnippetsList.tsx b/src/js/hooks/useSnippetsList.tsx index 999004730..6e3773621 100644 --- a/src/js/hooks/useSnippetsList.tsx +++ b/src/js/hooks/useSnippetsList.tsx @@ -8,6 +8,8 @@ import type { Snippet } from '../types/Snippet' export interface SnippetsListContext { snippetsList: readonly Snippet[] | undefined + /** Whether the last attempt to load the full list failed, leaving only the snippets sent with the page. */ + hasLoadError: boolean refreshSnippetsList: () => Promise } @@ -18,14 +20,17 @@ export const WithSnippetsListContext: React.FC = ({ children const [snippetsList, setSnippetsList] = useState( () => window.CODE_SNIPPETS_MANAGE?.snippetsList?.map(parseSnippetObject) ) + const [hasLoadError, setHasLoadError] = useState(false) const refreshSnippetsList = useCallback(async (): Promise => { try { console.info('Fetching snippets list') const response = await fetchAll(isNetworkAdmin()) setSnippetsList(response) + setHasLoadError(false) } catch (error: unknown) { console.error('Error fetching snippets list', error) + setHasLoadError(true) } }, [fetchAll]) @@ -36,6 +41,7 @@ export const WithSnippetsListContext: React.FC = ({ children const value: SnippetsListContext = { snippetsList, + hasLoadError, refreshSnippetsList } diff --git a/src/php/Admin/Menus/Manage/Manage_Menu_Assets.php b/src/php/Admin/Menus/Manage/Manage_Menu_Assets.php index 2ffedaced..11fb11636 100644 --- a/src/php/Admin/Menus/Manage/Manage_Menu_Assets.php +++ b/src/php/Admin/Menus/Manage/Manage_Menu_Assets.php @@ -125,7 +125,7 @@ function ( Snippet $snippet ) { get_snippets() ); - $inline_limit = apply_filters( 'code_snippets/manage/inline_snippets_limit', min( $snippets_per_page, 100 ) ); + $inline_limit = apply_filters( 'code_snippets/manage/inline_snippets_limit', $snippets_per_page ); $inline_limit = max( 0, intval( $inline_limit ) ); if ( $inline_limit > 0 ) { diff --git a/tests/unit/Admin/Menus/Manage/Manage_Menu_Assets_Test.php b/tests/unit/Admin/Menus/Manage/Manage_Menu_Assets_Test.php index 507b000e3..46c2124a3 100644 --- a/tests/unit/Admin/Menus/Manage/Manage_Menu_Assets_Test.php +++ b/tests/unit/Admin/Menus/Manage/Manage_Menu_Assets_Test.php @@ -126,6 +126,28 @@ public function test_enqueue_applies_disabled_inline_snippets_limit(): void { $this->assertArrayNotHasKey( 'snippetsList', $localized ); } + /** + * As many snippets as the user shows on a page are sent with the page, even beyond 100. + * + * @return void + */ + public function test_enqueue_localizes_a_page_of_snippets_beyond_one_hundred(): void { + $existing = count( get_snippets() ); + + for ( $i = $existing; $i < 120; $i++ ) { + save_snippet( new Snippet( [ 'name' => "Per-page fixture $i" ] ) ); + } + + update_user_option( get_current_user_id(), 'snippets_per_page', 110 ); + + $this->enqueue_assets(); + $localized = $this->get_localized_data(); + + delete_user_option( get_current_user_id(), 'snippets_per_page' ); + + $this->assertCount( 110, $localized['snippetsList'] ); + } + /** * Snippet lists at or below the configured limit are localized inline. * @@ -162,12 +184,13 @@ public static function provide_inline_limit_offsets(): array { } /** - * Snippet lists above the configured limit are omitted. + * A library larger than the inline limit is cut to the limit; the rest load after the page. * * @return void */ - public function test_enqueue_omits_snippets_above_inline_limit(): void { + public function test_enqueue_cuts_snippets_above_inline_limit(): void { save_snippet( new Snippet( [ 'name' => 'Oversized Inline Snippet Fixture' ] ) ); + save_snippet( new Snippet( [ 'name' => 'Second Oversized Inline Snippet Fixture' ] ) ); $inline_limit = count( get_snippets() ) - 1; $filter = static fn() => $inline_limit; add_filter( 'code_snippets/manage/inline_snippets_limit', $filter ); @@ -177,7 +200,7 @@ public function test_enqueue_omits_snippets_above_inline_limit(): void { remove_filter( 'code_snippets/manage/inline_snippets_limit', $filter ); - $this->assertArrayNotHasKey( 'snippetsList', $localized ); + $this->assertCount( $inline_limit, $localized['snippetsList'] ); } /**