diff --git a/src/js/components/ManageMenu/CommunityCloud/CloudSnippetsTable.tsx b/src/js/components/ManageMenu/CommunityCloud/CloudSnippetsTable.tsx index 5539a8197..8d78dec74 100644 --- a/src/js/components/ManageMenu/CommunityCloud/CloudSnippetsTable.tsx +++ b/src/js/components/ManageMenu/CommunityCloud/CloudSnippetsTable.tsx @@ -64,46 +64,77 @@ interface TableHeadingCheckboxProps { setSelectedIds: Dispatch>> } +/** + * Selects every snippet on the page that can be downloaded. With nothing + * downloadable the box is disabled and unticked: "all of nothing" must not + * read as a selection. + */ const TableHeadingCheckbox: React.FC = ({ availableIds, selectedIds, setSelectedIds }) => selectedIds.has(snippetId))} + checked={0 < availableIds.length && availableIds.every(snippetId => selectedIds.has(snippetId))} + disabled={0 === availableIds.length} + title={0 === availableIds.length ? __('Nothing on this page can be downloaded.', 'code-snippets') : undefined} onChange={event => setSelectedIds(previous => new Set(event.target.checked ? [...previous, ...availableIds] : [...previous].filter(snippetId => !availableIds.includes(snippetId))) )} - aria-label={__('Select all snippets', 'code-snippets')} + aria-label={__('Select all downloadable snippets', 'code-snippets')} /> +/** Why a snippet cannot be selected for download, or undefined when it can. */ +const unavailableReason = (snippet: CloudSnippetSchema): string | undefined => { + if (snippet.local_id) { + return __('Already in your library.', 'code-snippets') + } + + return isCloudSnippetDownloadable(snippet) + ? undefined + : __('Requires Code Snippets Pro.', 'code-snippets') +} + interface TableRowCheckboxProps { snippet: CloudSnippetSchema selected: Set setSelected: Dispatch>> } -const TableRowCheckbox: React.FC = ({ snippet, selected, setSelected }) => - - {isCloudSnippetDownloadable(snippet) && ( +/** + * Every row shows a box, so the column reads as one control: a snippet that + * cannot be downloaded gets a disabled box that says why. + */ +const TableRowCheckbox: React.FC = ({ snippet, selected, setSelected }) => { + const reason = unavailableReason(snippet) + + return ( + setSelected(previous => new Set(event.target.checked ? [...previous, snippet.id] : [...previous].filter(snippetId => snippetId !== snippet.id)) )} - />)} - + /> + + ) +} export interface CloudSnippetsTableProps { snippets: CloudSnippetSchema[] diff --git a/tests/e2e/cloud-download-eligibility.spec.ts b/tests/e2e/cloud-download-eligibility.spec.ts index 8d9eecc0d..32b91b0e1 100644 --- a/tests/e2e/cloud-download-eligibility.spec.ts +++ b/tests/e2e/cloud-download-eligibility.spec.ts @@ -87,11 +87,15 @@ test.describe('Cloud bulk download eligibility', () => { const table = page.locator('.cloud-snippets-table') await expect(table.getByRole('checkbox', { name: 'Select Eligible Alpha' })).toBeVisible() - await expect(table.getByRole('checkbox', { name: 'Select Linked Beta' })).toHaveCount(0) - await expect(table.getByRole('checkbox', { name: 'Select Pro Gamma' })).toHaveCount(0) + // Rows that cannot be downloaded keep a box, disabled, that says why. + await expect(table.getByRole('checkbox', { name: 'Linked Beta cannot be selected: Already in your library.' })).toBeDisabled() + await expect(table.getByRole('checkbox', { name: 'Pro Gamma cannot be selected: Requires Code Snippets Pro.' })).toBeDisabled() - await table.getByRole('checkbox', { name: 'Select all snippets' }).check() + const selectAll = table.getByRole('checkbox', { name: 'Select all downloadable snippets' }) + await expect(selectAll).not.toBeChecked() + await selectAll.check() await expect(table.getByRole('checkbox', { name: 'Select Eligible Alpha' })).toBeChecked() + await expect(table.getByRole('checkbox', { name: 'Linked Beta cannot be selected: Already in your library.' })).not.toBeChecked() await applyBulkDownload(page) await expect.poll(() => state.downloads).toEqual([ELIGIBLE.id]) @@ -123,13 +127,34 @@ test.describe('Cloud bulk download eligibility', () => { const table = page.locator('.cloud-snippets-table') await expect(table.getByRole('checkbox', { name: 'Select Pro Gamma' })).toBeVisible() - await expect(table.getByRole('checkbox', { name: 'Select Linked Beta' })).toHaveCount(0) + await expect(table.getByRole('checkbox', { name: 'Linked Beta cannot be selected: Already in your library.' })).toBeDisabled() - await table.getByRole('checkbox', { name: 'Select all snippets' }).check() + await table.getByRole('checkbox', { name: 'Select all downloadable snippets' }).check() await applyBulkDownload(page) await expect.poll(() => [...state.downloads].sort((a, b) => a - b)).toEqual([ELIGIBLE.id, PRO_LOCKED.id]) }) + test('a page with nothing downloadable offers no selection at all', async ({ page }) => { + const state: CloudRoutesState = { + snippets: [ + cloudSnippet({ id: 201, name: 'Owned One', local_id: 11 }), + cloudSnippet({ id: 202, name: 'Owned Two', local_id: 12 }) + ], + downloads: [] + } + await forceLicenseState(page, true) + await routeCloudSnippets(page, state) + await openCommunityCloud(page, 'table') + + const table = page.locator('.cloud-snippets-table') + const selectAll = table.getByRole('checkbox', { name: 'Select all downloadable snippets' }) + // "All of nothing" must not read as a selection: the header box is unticked and disabled. + await expect(selectAll).toBeDisabled() + await expect(selectAll).not.toBeChecked() + await expect(table.getByRole('checkbox', { name: 'Owned One cannot be selected: Already in your library.' })).toBeDisabled() + await expect(table.getByRole('checkbox', { name: 'Owned Two cannot be selected: Already in your library.' })).toBeDisabled() + }) + test('selections hidden by a new search are not downloaded', async ({ page }) => { const state: CloudRoutesState = { snippets: [ELIGIBLE, LINKED, PRO_LOCKED], downloads: [] } await forceLicenseState(page, false) diff --git a/tests/e2e/code-snippets-community-featured.spec.ts b/tests/e2e/code-snippets-community-featured.spec.ts index 1087e7c74..92c2d3bac 100644 --- a/tests/e2e/code-snippets-community-featured.spec.ts +++ b/tests/e2e/code-snippets-community-featured.spec.ts @@ -262,7 +262,7 @@ test.describe('Community Cloud Featured Snippets', () => { const table = page.locator('.cloud-snippets-table') await expect(table).toBeVisible({ timeout: TIMEOUTS.DEFAULT }) - const headerCheckbox = table.locator('thead').getByRole('checkbox', { name: 'Select all snippets' }) + const headerCheckbox = table.locator('thead').getByRole('checkbox', { name: 'Select all downloadable snippets' }) const rowCheckbox = table.locator('tbody').getByRole('checkbox', { name: 'Select Mock Cloud Snippet' }) // The table owns the only select-all control; the toolbar checkbox is