← All changes
|
js/components/ManageMenu/SnippetsTable/SnippetsListTable.tsx
+136
-306
4.0.0-beta.1
→
trunk
View file →
| @@ -1,166 +1,24 @@ | ||
| 1 | -import { __, _n, _x, sprintf } from '@wordpress/i18n' | |
| 2 | -import React, { Fragment, useEffect, useMemo, useState } from 'react' | |
| 1 | +import { __ } from '@wordpress/i18n' | |
| 2 | +import React, { useEffect, useMemo, useState } from 'react' | |
| 3 | 3 | import classnames from 'classnames' |
| 4 | -import { createInterpolateElement } from '@wordpress/element' | |
| 5 | -import { useRestAPI } from '../../../hooks/useRestAPI' | |
| 6 | -import { useSnippetsAPI } from '../../../hooks/useSnippetsAPI' | |
| 7 | -import { useSnippetsList } from '../../../hooks/useSnippetsList' | |
| 8 | -import { handleUnknownError } from '../../../utils/errors' | |
| 9 | -import { downloadBulkSnippetExportFile } from '../../../utils/files' | |
| 10 | -import { REST_BASES } from '../../../utils/restAPI' | |
| 11 | -import { cloneSnippetObject, getSnippetType } from '../../../utils/snippets/snippets' | |
| 4 | +import { getSnippetType, isSnippetActive } from '../../../utils/snippets/snippets' | |
| 12 | 5 | import { buildUrl } from '../../../utils/urls' |
| 13 | 6 | import { ListTable } from '../../common/ListTable' |
| 14 | -import { SubmitButton } from '../../common/SubmitButton' | |
| 7 | +import { SnippetViewToggle } from '../../common/SnippetViewToggle' | |
| 8 | +import { SnippetsCardGrid } from './SnippetsCardGrid' | |
| 9 | +import { SnippetsTableNavigation, SnippetsTableToolbar } from './SnippetsTableControls' | |
| 10 | +import { SearchResultsIndicator } from './SnippetsTableSearch' | |
| 11 | +import { getTableColumns } from './TableColumns' | |
| 12 | +import { useFilteredSnippets } from './WithFilteredSnippetsContext' | |
| 15 | 13 | import { INDEX_STATUS, useSnippetsFilters } from './WithSnippetsTableFilters' |
| 16 | -import { useFilteredSnippets } from './WithFilteredSnippetsContext' | |
| 17 | -import { getTableColumns } from './TableColumns' | |
| 18 | -import type { ListTableAction } from '../../common/ListTable' | |
| 19 | -import type { SnippetStatus } from './WithSnippetsTableFilters' | |
| 14 | +import { BULK_ACTIONS, TRASHED_BULK_ACTIONS, useApplyBulkAction } from './useApplyBulkAction' | |
| 15 | +import type { ListTableAction, ListTableSortDirection } from '../../common/ListTable' | |
| 16 | +import type { SnippetsTableAction } from './useApplyBulkAction' | |
| 20 | 17 | import type { Snippet } from '../../../types/Snippet' |
| 18 | +import type { SnippetView } from '../../../types/SnippetView' | |
| 19 | +import type { ReactNode } from 'react' | |
| 21 | 20 | |
| 22 | -const STATUS_LABELS: [SnippetStatus, string][] = [ | |
| 23 | - ['all', __('All', 'code-snippets')], | |
| 24 | - ['active', __('Active', 'code-snippets')], | |
| 25 | - ['inactive', __('Inactive', 'code-snippets')], | |
| 26 | - ['recently_active', __('Recently Active', 'code-snippets')], | |
| 27 | - ['locked', __('Locked', 'code-snippets')], | |
| 28 | - ['unlocked', __('Unlocked', 'code-snippets')], | |
| 29 | - ['trashed', __('Trashed', 'code-snippets')] | |
| 30 | -] | |
| 31 | - | |
| 32 | -type SnippetsTableAction = | |
| 33 | - 'activate' | 'deactivate' | | |
| 34 | - 'clone' | 'export' | 'download' | | |
| 35 | - 'trash' | 'restore' | 'delete' | |
| 36 | - | |
| 37 | -const BULK_ACTIONS: ListTableAction<SnippetsTableAction>[] = [ | |
| 38 | - { key: 'activate', label: __('Activate', 'code-snippets') }, | |
| 39 | - { key: 'deactivate', label: __('Deactivate', 'code-snippets') }, | |
| 40 | - { key: 'clone', label: __('Clone', 'code-snippets') }, | |
| 41 | - { key: 'export', label: __('Export', 'code-snippets') }, | |
| 42 | - { key: 'download', label: __('Download', 'code-snippets') }, | |
| 43 | - { key: 'trash', label: __('Trash', 'code-snippets') } | |
| 44 | -] | |
| 45 | - | |
| 46 | -const TRASHED_BULK_ACTIONS: ListTableAction<SnippetsTableAction>[] = [ | |
| 47 | - { key: 'restore', label: __('Restore', 'code-snippets') }, | |
| 48 | - { key: 'delete', label: __('Delete Permanently', 'code-snippets') } | |
| 49 | -] | |
| 50 | - | |
| 51 | -const BULK_DOWNLOAD_ACTION = 'bulk-download' | |
| 52 | -const INDIVIDUAL_DOWNLOAD_DELAY_MS = 200 | |
| 53 | - | |
| 54 | -const submitBulkSnippetDownload = (snippets: readonly Snippet[]): Promise<void> => { | |
| 55 | - if (0 === snippets.length) { | |
| 56 | - return Promise.resolve() | |
| 57 | - } | |
| 58 | - | |
| 59 | - const form = document.createElement('form') | |
| 60 | - | |
| 61 | - const appendHiddenField = (name: string, value: string) => { | |
| 62 | - const input = document.createElement('input') | |
| 63 | - input.type = 'hidden' | |
| 64 | - input.name = name | |
| 65 | - input.value = value | |
| 66 | - form.appendChild(input) | |
| 67 | - } | |
| 68 | - | |
| 69 | - form.method = 'post' | |
| 70 | - form.action = window.location.href | |
| 71 | - form.hidden = true | |
| 72 | - | |
| 73 | - appendHiddenField('code_snippets_action', BULK_DOWNLOAD_ACTION) | |
| 74 | - appendHiddenField('code_snippets_bulk_download_nonce', window.CODE_SNIPPETS_MANAGE?.bulkDownloadNonce ?? '') | |
| 75 | - appendHiddenField( | |
| 76 | - 'snippets', | |
| 77 | - JSON.stringify(snippets.map(({ id, network }) => ({ id, network }))) | |
| 78 | - ) | |
| 79 | - | |
| 80 | - document.body.appendChild(form) | |
| 81 | - form.submit() | |
| 82 | - | |
| 83 | - window.setTimeout(() => { | |
| 84 | - form.remove() | |
| 85 | - }, 0) | |
| 86 | - | |
| 87 | - return Promise.resolve() | |
| 88 | -} | |
| 89 | - | |
| 90 | -const submitBulkSnippetDownloadsIndividually = (snippets: readonly Snippet[]): Promise<void> => | |
| 91 | - snippets.reduce( | |
| 92 | - (promise, snippet) => | |
| 93 | - promise.then( | |
| 94 | - () => | |
| 95 | - new Promise<void>(resolve => { | |
| 96 | - void submitBulkSnippetDownload([snippet]) | |
| 97 | - window.setTimeout(resolve, INDIVIDUAL_DOWNLOAD_DELAY_MS) | |
| 98 | - }) | |
| 99 | - ), | |
| 100 | - Promise.resolve() | |
| 101 | - ) | |
| 102 | - | |
| 103 | -const SnippetStatusCounts = () => { | |
| 104 | - const { currentStatus, setCurrentStatus } = useSnippetsFilters() | |
| 105 | - const { snippetsByStatus } = useFilteredSnippets() | |
| 106 | - | |
| 107 | - const visibleStatuses = STATUS_LABELS.filter(([status]) => | |
| 108 | - snippetsByStatus.has(status) && ('unlocked' !== status || snippetsByStatus.has('locked'))) | |
| 109 | - | |
| 110 | - return ( | |
| 111 | - <ul className="subsubsub"> | |
| 112 | - {visibleStatuses.map(([status, label], index) => | |
| 113 | - <Fragment key={status}> | |
| 114 | - <li className={status}> | |
| 115 | - <a | |
| 116 | - href={buildUrl(window.location.href, { status: INDEX_STATUS === status ? undefined : status })} | |
| 117 | - className={currentStatus === status ? 'current' : undefined} | |
| 118 | - aria-current={currentStatus === status ? 'page' : undefined} | |
| 119 | - onClick={event => { | |
| 120 | - event.preventDefault() | |
| 121 | - setCurrentStatus(status) | |
| 122 | - }} | |
| 123 | - > | |
| 124 | - {`${label} `} | |
| 125 | - <span className="count">{ | |
| 126 | - // translators: %d: number of snippets in the current view. | |
| 127 | - sprintf(_x('(%d)', 'table view count', 'code-snippets'), snippetsByStatus.get(status)?.length ?? 0) | |
| 128 | - }</span> | |
| 129 | - </a> | |
| 130 | - {index < visibleStatuses.length - 1 && ' | '} | |
| 131 | - </li> | |
| 132 | - </Fragment>)} | |
| 133 | - </ul> | |
| 134 | - ) | |
| 135 | -} | |
| 136 | - | |
| 137 | -const ClearRecentlyActiveButton: React.FC = () => { | |
| 138 | - const { api } = useRestAPI() | |
| 139 | - const { refreshSnippetsList } = useSnippetsList() | |
| 140 | - const { currentStatus } = useSnippetsFilters() | |
| 141 | - | |
| 142 | - return 'recently_active' === currentStatus | |
| 143 | - ? <div className="alignleft actions"> | |
| 144 | - <SubmitButton | |
| 145 | - secondary | |
| 146 | - name="clear-recent-list" | |
| 147 | - text={__('Clear List', 'code-snippets')} | |
| 148 | - onClick={event => { | |
| 149 | - event.preventDefault() | |
| 150 | - api.del(REST_BASES.recentlyActive) | |
| 151 | - .then(refreshSnippetsList) | |
| 152 | - .catch(handleUnknownError) | |
| 153 | - }} | |
| 154 | - /> | |
| 155 | - </div> | |
| 156 | - : null | |
| 157 | -} | |
| 158 | - | |
| 159 | -interface ExtraTableNavProps { | |
| 160 | - visibleSnippets: Snippet[] | |
| 161 | -} | |
| 162 | - | |
| 163 | 21 | interface ManageTableSettings { |
| 164 | 22 | hiddenColumns: Set<string> |
| 165 | 23 | truncateRowValues: boolean |
| 166 | 24 | } |
| @@ -165,9 +23,11 @@ | ||
| 165 | 23 | truncateRowValues: boolean |
| 166 | 24 | } |
| 167 | 25 | |
| 168 | 26 | const useManageTableSettings = (): ManageTableSettings => { |
| 169 | - const [hiddenColumns, setHiddenColumns] = useState(() => new Set(window.CODE_SNIPPETS_MANAGE?.hiddenColumns ?? [])) | |
| 27 | + const [hiddenColumns, setHiddenColumns] = useState( | |
| 28 | + () => new Set(window.CODE_SNIPPETS_MANAGE?.hiddenColumns ?? []) | |
| 29 | + ) | |
| 170 | 30 | const [truncateRowValues, setTruncateRowValues] = useState( |
| 171 | 31 | () => 0 !== Number(window.CODE_SNIPPETS_MANAGE?.truncateRowValues ?? 1) |
| 172 | 32 | ) |
| 173 | 33 | |
| @@ -179,14 +39,18 @@ | ||
| 179 | 39 | } |
| 180 | 40 | |
| 181 | 41 | const updateHiddenColumns = () => { |
| 182 | 42 | setHiddenColumns( |
| 183 | - new Set(Array.from(screenOptions.querySelectorAll<HTMLInputElement>('.hide-column-tog:not(:checked)')) | |
| 43 | + new Set(Array.from(screenOptions.querySelectorAll<HTMLInputElement>( | |
| 44 | + '.hide-column-tog:not(:checked)' | |
| 45 | + )) | |
| 184 | 46 | .map(toggle => toggle.value)) |
| 185 | 47 | ) |
| 186 | 48 | |
| 187 | 49 | setTruncateRowValues( |
| 188 | - screenOptions.querySelector<HTMLInputElement>('#snippets-table-truncate-row-values')?.checked ?? true | |
| 50 | + screenOptions.querySelector<HTMLInputElement>( | |
| 51 | + '#snippets-table-truncate-row-values' | |
| 52 | + )?.checked ?? true | |
| 189 | 53 | ) |
| 190 | 54 | } |
| 191 | 55 | |
| 192 | 56 | updateHiddenColumns() |
| @@ -199,60 +63,13 @@ | ||
| 199 | 63 | |
| 200 | 64 | return { hiddenColumns, truncateRowValues } |
| 201 | 65 | } |
| 202 | 66 | |
| 203 | -const FilterByTagControl: React.FC<ExtraTableNavProps> = ({ visibleSnippets }) => { | |
| 204 | - const { currentTag, setCurrentTag } = useSnippetsFilters() | |
| 205 | - | |
| 206 | - const tagsList: Set<string> = useMemo( | |
| 207 | - () => visibleSnippets.reduce((tags, snippet) => { | |
| 208 | - snippet.tags.forEach(tag => tags.add(tag)) | |
| 209 | - return tags | |
| 210 | - }, new Set<string>()), | |
| 211 | - [visibleSnippets]) | |
| 212 | - | |
| 213 | - return 0 < tagsList.size | |
| 214 | - ? <div className="alignleft actions"> | |
| 215 | - <label htmlFor="snippets-tag-filter" className="screen-reader-text"> | |
| 216 | - {__('Filter snippets by tag', 'code-snippets')} | |
| 217 | - </label> | |
| 218 | - <select | |
| 219 | - id="snippets-tag-filter" | |
| 220 | - name="tag" | |
| 221 | - value={currentTag} | |
| 222 | - aria-label={__('Filter snippets by tag', 'code-snippets')} | |
| 223 | - onChange={event => setCurrentTag(event.target.value)} | |
| 224 | - > | |
| 225 | - <option value="">{__('Show all tags', 'code-snippets')}</option> | |
| 226 | - {[...tagsList].map(tag => | |
| 227 | - <option key={tag} value={tag}>{tag}</option>)} | |
| 228 | - </select> | |
| 229 | - </div> | |
| 230 | - : null | |
| 231 | -} | |
| 232 | - | |
| 233 | -const SearchBox = () => { | |
| 234 | - const { searchQuery, setSearchQuery } = useSnippetsFilters() | |
| 235 | - | |
| 236 | - return ( | |
| 237 | - <search aria-label={__('Search Snippets', 'code-snippets')}> | |
| 238 | - <p className="search-box"> | |
| 239 | - <input | |
| 240 | - type="search" | |
| 241 | - id="snippets_search" | |
| 242 | - name="s" | |
| 243 | - value={searchQuery ?? ''} | |
| 244 | - aria-label={__('Search Snippets:', 'code-snippets')} | |
| 245 | - onChange={event => setSearchQuery(event.target.value)} | |
| 246 | - placeholder={__('Search snippets', 'code-snippets')} | |
| 247 | - /> | |
| 248 | - </p> | |
| 249 | - </search> | |
| 250 | - ) | |
| 251 | -} | |
| 252 | - | |
| 253 | 67 | const NoItemsMessage = () => { |
| 254 | 68 | const { currentType, currentTag, searchQuery } = useSnippetsFilters() |
| 69 | + const emptyMessage = currentType | |
| 70 | + ? __("You don't have snippets of this type yet.", 'code-snippets') | |
| 71 | + : __("You don't have any snippets yet.", 'code-snippets') | |
| 255 | 72 | |
| 256 | 73 | return searchQuery || currentTag |
| 257 | 74 | ? <> |
| 258 | 75 | {__('No snippets were found matching the current search query.', 'code-snippets')} |
| @@ -257,87 +74,115 @@ | ||
| 257 | 74 | ? <> |
| 258 | 75 | {__('No snippets were found matching the current search query.', 'code-snippets')} |
| 259 | 76 | {__(' Please enter a new query or use the "Clear Filters" button above.', 'code-snippets')} |
| 260 | 77 | </> |
| 261 | - : <>{createInterpolateElement( | |
| 262 | - currentType | |
| 263 | - ? __("It looks like you don't have any snippets of this type. <a>Perhaps you would like to add a new one?</a>", 'code-snippets') | |
| 264 | - : __("It looks like you don't have any snippets. <a>Perhaps you would like to add a new one?</a>", 'code-snippets'), | |
| 265 | - { | |
| 266 | - // eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label | |
| 267 | - a: <a href={buildUrl(window.CODE_SNIPPETS?.urls.addNew, { type: currentType })} /> | |
| 268 | - } | |
| 269 | - )} | |
| 78 | + : <> | |
| 79 | + {emptyMessage}{' '} | |
| 80 | + <a href={buildUrl(window.CODE_SNIPPETS?.urls.addNew, { type: currentType })}> | |
| 81 | + {__('Add a new snippet.', 'code-snippets')} | |
| 82 | + </a> | |
| 270 | 83 | </> |
| 271 | 84 | } |
| 272 | 85 | |
| 273 | -const applyAndRefresh = async ( | |
| 274 | - targets: Snippet[], | |
| 275 | - action: (snippet: Snippet) => Promise<Snippet> | Promise<void>, | |
| 276 | - refresh: () => Promise<void> | |
| 277 | -): Promise<void> => { | |
| 278 | - if (0 < targets.length) { | |
| 279 | - for (const snippet of targets) { | |
| 280 | - await action(snippet).catch(handleUnknownError) | |
| 86 | +const getRowClassName = ( | |
| 87 | + snippet: Snippet, | |
| 88 | + activeByCondition: Map<Snippet['id'], Snippet[]> | |
| 89 | +): string => | |
| 90 | + classnames( | |
| 91 | + 'snippet', | |
| 92 | + `snippet ${isSnippetActive(snippet, activeByCondition) ? 'active' : 'inactive'}-snippet`, | |
| 93 | + `${getSnippetType(snippet)}-snippet`, | |
| 94 | + `${snippet.scope}-snippet`, | |
| 95 | + { | |
| 96 | + 'trashed-snippet': snippet.trashed | |
| 281 | 97 | } |
| 98 | + ) | |
| 282 | 99 | |
| 283 | - await refresh() | |
| 284 | - } | |
| 100 | +interface SnippetsViewProps { | |
| 101 | + snippetView: SnippetView | |
| 102 | + setSnippetView: (view: SnippetView) => void | |
| 103 | + snippets: Snippet[] | |
| 104 | + actions: ListTableAction<SnippetsTableAction>[] | |
| 105 | + doAction: (action: SnippetsTableAction | undefined, selected: Set<Snippet['id']>) => Promise<void> | |
| 106 | + extraTableNav: (which: 'top' | 'bottom') => ReactNode | |
| 107 | + hiddenColumns: Set<string> | |
| 108 | + truncateRowValues: boolean | |
| 285 | 109 | } |
| 286 | 110 | |
| 287 | -const useApplyBulkAction = ( | |
| 288 | - allSnippets: Snippet[] | |
| 289 | -): (action: SnippetsTableAction, selected: Set<Snippet['id']>) => Promise<void> => { | |
| 290 | - const api = useSnippetsAPI() | |
| 291 | - const { refreshSnippetsList } = useSnippetsList() | |
| 111 | +const SnippetsView: React.FC<SnippetsViewProps> = ({ | |
| 112 | + snippetView, | |
| 113 | + setSnippetView, | |
| 114 | + snippets, | |
| 115 | + actions, | |
| 116 | + doAction, | |
| 117 | + extraTableNav, | |
| 118 | + hiddenColumns, | |
| 119 | + truncateRowValues | |
| 120 | +}) => { | |
| 121 | + const { activeByCondition } = useFilteredSnippets() | |
| 122 | + const columns = useMemo(() => getTableColumns(hiddenColumns), [hiddenColumns]) | |
| 123 | + const itemsPerPage = window.CODE_SNIPPETS_MANAGE?.snippetsPerPage | |
| 124 | + const pageCount = itemsPerPage && Math.ceil(snippets.length / itemsPerPage) | |
| 125 | + const endTableNav = (which: 'top' | 'bottom') => | |
| 126 | + 'top' === which | |
| 127 | + ? <SnippetViewToggle snippetView={snippetView} setSnippetView={setSnippetView} /> | |
| 128 | + : null | |
| 292 | 129 | |
| 293 | - return async (action, selected) => { | |
| 294 | - switch (action) { | |
| 295 | - case 'activate': | |
| 296 | - await applyAndRefresh( | |
| 297 | - allSnippets.filter(snippet => selected.has(snippet.id) && !snippet.active), | |
| 298 | - snippet => api.activate({ id: snippet.id, network: snippet.network }), | |
| 299 | - refreshSnippetsList) | |
| 300 | - break | |
| 130 | + return 'card' === snippetView | |
| 131 | + ? <SnippetsCardGrid | |
| 132 | + snippets={snippets} | |
| 133 | + actions={actions} | |
| 134 | + doAction={doAction} | |
| 135 | + itemsPerPage={itemsPerPage} | |
| 136 | + extraTableNav={extraTableNav} | |
| 137 | + endTableNav={endTableNav} | |
| 138 | + noItems={<NoItemsMessage />} | |
| 139 | + beforeGrid={<SearchResultsIndicator />} | |
| 140 | + /> | |
| 141 | + : <ListTable | |
| 142 | + items={snippets} | |
| 143 | + getKey={snippet => snippet.id} | |
| 144 | + initialSort={getInitialSort()} | |
| 145 | + className={classnames({ 'truncate-row-values': truncateRowValues })} | |
| 146 | + columns={columns} | |
| 147 | + actions={actions} | |
| 148 | + doAction={doAction} | |
| 149 | + totalPages={pageCount} | |
| 150 | + extraTableNav={extraTableNav} | |
| 151 | + selectAllControl | |
| 152 | + endTableNav={endTableNav} | |
| 153 | + rowClassName={snippet => getRowClassName(snippet, activeByCondition)} | |
| 154 | + noItems={<NoItemsMessage />} | |
| 155 | + beforeTable={<SearchResultsIndicator />} | |
| 156 | + /> | |
| 157 | +} | |
| 301 | 158 | |
| 302 | - case 'deactivate': | |
| 303 | - await applyAndRefresh( | |
| 304 | - allSnippets.filter(snippet => selected.has(snippet.id) && snippet.active), | |
| 305 | - snippet => api.deactivate({ id: snippet.id, network: snippet.network }), | |
| 306 | - refreshSnippetsList) | |
| 307 | - break | |
| 159 | +/** | |
| 160 | + * The "Snippets List Order" setting names a default ordering for the table, | |
| 161 | + * which sorting is now expressed through columns. Map one to the other so the | |
| 162 | + * setting still decides how the list opens; clicking a heading overrides it for | |
| 163 | + * the rest of the visit, as it does for any other starting order. | |
| 164 | + */ | |
| 165 | +const LIST_ORDER_SORTS: Record<string, { columnId: string, direction: ListTableSortDirection }> = { | |
| 166 | + 'priority-asc': { columnId: 'priority', direction: 'asc' }, | |
| 167 | + 'name-asc': { columnId: 'name', direction: 'asc' }, | |
| 168 | + 'name-desc': { columnId: 'name', direction: 'desc' }, | |
| 169 | + 'modified-desc': { columnId: 'date', direction: 'desc' }, | |
| 170 | + 'modified-asc': { columnId: 'date', direction: 'asc' } | |
| 171 | +} | |
| 308 | 172 | |
| 309 | - case 'clone': | |
| 310 | - await applyAndRefresh( | |
| 311 | - allSnippets.filter(snippet => selected.has(snippet.id) && !snippet.trashed), | |
| 312 | - snippet => api.create(cloneSnippetObject(snippet)), | |
| 313 | - refreshSnippetsList) | |
| 314 | - break | |
| 173 | +const getInitialSort = () => | |
| 174 | + LIST_ORDER_SORTS[window.CODE_SNIPPETS_MANAGE?.listOrder ?? ''] ?? undefined | |
| 315 | 175 | |
| 316 | - case 'export': | |
| 317 | - downloadBulkSnippetExportFile(allSnippets.filter(snippet => selected.has(snippet.id))) | |
| 318 | - break | |
| 319 | - | |
| 320 | - case 'download': { | |
| 321 | - const selectedSnippets = allSnippets.filter(snippet => selected.has(snippet.id)) | |
| 322 | - | |
| 323 | - return 1 < selectedSnippets.length && !window.CODE_SNIPPETS_MANAGE?.supportsZipDownloads | |
| 324 | - ? submitBulkSnippetDownloadsIndividually(selectedSnippets) | |
| 325 | - : submitBulkSnippetDownload(selectedSnippets) | |
| 326 | - } | |
| 327 | - | |
| 328 | - case 'trash': | |
| 329 | - case 'delete': | |
| 330 | - await applyAndRefresh( | |
| 331 | - allSnippets.filter(snippet => selected.has(snippet.id) && snippet.trashed), | |
| 332 | - snippet => api.delete({ id: snippet.id, network: snippet.network }), | |
| 333 | - refreshSnippetsList) | |
| 334 | - break | |
| 335 | - } | |
| 336 | - } | |
| 176 | +export interface SnippetsListTableProps { | |
| 177 | + snippetView: SnippetView | |
| 178 | + setSnippetView: (view: SnippetView) => void | |
| 337 | 179 | } |
| 338 | 180 | |
| 339 | -export const SnippetsListTable: React.FC = () => { | |
| 181 | +export const SnippetsListTable: React.FC<SnippetsListTableProps> = ({ | |
| 182 | + snippetView, | |
| 183 | + setSnippetView | |
| 184 | +}) => { | |
| 340 | 185 | const { snippetsByStatus } = useFilteredSnippets() |
| 341 | 186 | const { currentStatus, setCurrentStatus } = useSnippetsFilters() |
| 342 | 187 | const { hiddenColumns, truncateRowValues } = useManageTableSettings() |
| 343 | 188 | |
| @@ -344,12 +189,8 @@ | ||
| 344 | 189 | const currentSnippets = useMemo( |
| 345 | 190 | () => snippetsByStatus.get(currentStatus) ?? [], |
| 346 | 191 | [snippetsByStatus, currentStatus] |
| 347 | 192 | ) |
| 348 | - const totalItems = currentSnippets.length | |
| 349 | - const itemsPerPage = window.CODE_SNIPPETS_MANAGE?.snippetsPerPage | |
| 350 | - | |
| 351 | - const columns = useMemo(() => getTableColumns(hiddenColumns), [hiddenColumns]) | |
| 352 | 193 | const applyBulkAction = useApplyBulkAction(currentSnippets) |
| 353 | 194 | |
| 354 | 195 | useEffect(() => { |
| 355 | 196 | if (INDEX_STATUS !== currentStatus && !snippetsByStatus.has(currentStatus)) { |
| @@ -356,37 +197,26 @@ | ||
| 356 | 197 | setCurrentStatus(INDEX_STATUS) |
| 357 | 198 | } |
| 358 | 199 | }, [currentStatus, setCurrentStatus, snippetsByStatus]) |
| 359 | 200 | |
| 201 | + const extraTableNav = (which: 'top' | 'bottom') => | |
| 202 | + <SnippetsTableNavigation which={which} visibleSnippets={snippetsByStatus.get('all') ?? []} /> | |
| 203 | + | |
| 360 | 204 | return ( |
| 361 | 205 | <> |
| 362 | - <SnippetStatusCounts /> | |
| 363 | - <SearchBox /> | |
| 206 | + <SnippetsTableToolbar /> | |
| 364 | 207 | |
| 365 | - <p className="screen-reader-text" role="status" aria-live="polite"> | |
| 366 | - {sprintf( | |
| 367 | - // translators: %d: number of snippets matching current filters. | |
| 368 | - _n('%d snippet found.', '%d snippets found.', totalItems), | |
| 369 | - totalItems | |
| 370 | - )} | |
| 371 | - </p> | |
| 372 | - | |
| 373 | - <ListTable | |
| 374 | - items={currentSnippets} | |
| 375 | - getKey={snippet => snippet.id} | |
| 376 | - className={classnames({ 'truncate-row-values': truncateRowValues })} | |
| 377 | - columns={columns} | |
| 378 | - actions={'trashed' === currentStatus ? TRASHED_BULK_ACTIONS : BULK_ACTIONS} | |
| 379 | - doAction={applyBulkAction} | |
| 380 | - totalPages={itemsPerPage && Math.ceil(totalItems / itemsPerPage)} | |
| 381 | - extraTableNav={which => | |
| 382 | - <> | |
| 383 | - {'top' === which && <FilterByTagControl visibleSnippets={snippetsByStatus.get('all') ?? []} />} | |
| 384 | - <ClearRecentlyActiveButton /> | |
| 385 | - </>} | |
| 386 | - rowClassName={snippet => | |
| 387 | - `snippet ${snippet.active ? 'active' : 'inactive'}-snippet ${getSnippetType(snippet)}-snippet ${snippet.scope}-snippet`} | |
| 388 | - noItems={<NoItemsMessage />} | |
| 389 | - /> | |
| 208 | + <div className="snippets-list-view"> | |
| 209 | + <SnippetsView | |
| 210 | + snippetView={snippetView} | |
| 211 | + setSnippetView={setSnippetView} | |
| 212 | + snippets={currentSnippets} | |
| 213 | + actions={'trashed' === currentStatus ? TRASHED_BULK_ACTIONS : BULK_ACTIONS} | |
| 214 | + doAction={applyBulkAction} | |
| 215 | + extraTableNav={extraTableNav} | |
| 216 | + hiddenColumns={hiddenColumns} | |
| 217 | + truncateRowValues={truncateRowValues} | |
| 218 | + /> | |
| 219 | + </div> | |
| 390 | 220 | </> |
| 391 | 221 | ) |
| 392 | 222 | } |