← All changes
|
js/components/ManageMenu/SnippetsTable/SnippetsTable.tsx
+107
-84
4.0.0-beta.1
→
trunk
View file →
| @@ -1,19 +1,21 @@ | ||
| 1 | 1 | import { __, sprintf } from '@wordpress/i18n' |
| 2 | 2 | import { createInterpolateElement } from '@wordpress/element' |
| 3 | -import React, { useState } from 'react' | |
| 3 | +import React, { useCallback, useMemo } from 'react' | |
| 4 | 4 | import classnames from 'classnames' |
| 5 | +import { useHorizontalScrollOverflow } from '../../../hooks/useHorizontalScrollOverflow' | |
| 5 | 6 | import { WithRestAPIContext } from '../../../hooks/useRestAPI' |
| 7 | +import { useSnippetView } from '../../../hooks/useSnippetView' | |
| 6 | 8 | import { WithSnippetsAPIContext } from '../../../hooks/useSnippetsAPI' |
| 7 | -import { WithSnippetsListContext } from '../../../hooks/useSnippetsList' | |
| 9 | +import { WithSnippetsListContext, useSnippetsList } from '../../../hooks/useSnippetsList' | |
| 8 | 10 | import { SNIPPET_TYPES } from '../../../types/Snippet' |
| 9 | 11 | import { isLicensed } from '../../../utils/screen' |
| 10 | -import { SNIPPET_TYPE_LABELS, getSnippetEditUrl, isProType } from '../../../utils/snippets/snippets' | |
| 12 | +import { SNIPPET_TYPE_LABELS, getSnippetAddNewUrl, getSnippetType, isProType } from '../../../utils/snippets/snippets' | |
| 11 | 13 | import { buildUrl } from '../../../utils/urls' |
| 12 | 14 | import { Badge } from '../../common/Badge' |
| 13 | -import { Button } from '../../common/Button' | |
| 14 | 15 | import { Notice } from '../../common/Notice' |
| 15 | -import { UpsellDialog } from '../../common/UpsellDialog' | |
| 16 | +import { ScreenMetaSlot } from '../../common/ScreenMetaSlot' | |
| 17 | +import { UpsellPage } from '../../common/UpsellDialog' | |
| 16 | 18 | import { WithSnippetsTableFilters, useSnippetsFilters } from './WithSnippetsTableFilters' |
| 17 | 19 | import { WithFilteredSnippetsContext } from './WithFilteredSnippetsContext' |
| 18 | 20 | import { SnippetsListTable } from './SnippetsListTable' |
| 19 | 21 | import type { SnippetType } from '../../../types/Snippet' |
| @@ -19,84 +21,49 @@ | ||
| 19 | 21 | import type { SnippetType } from '../../../types/Snippet' |
| 20 | 22 | |
| 21 | 23 | interface SnippetTypeTabProps { |
| 22 | 24 | type?: SnippetType |
| 23 | - setIsUpgradeDialogOpen: (isOpen: boolean) => void | |
| 25 | + count?: number | |
| 24 | 26 | } |
| 25 | 27 | |
| 26 | -const SnippetTypeTab: React.FC<SnippetTypeTabProps> = ({ type, setIsUpgradeDialogOpen }) => { | |
| 28 | +const SnippetTypeTab: React.FC<SnippetTypeTabProps> = ({ type, count }) => { | |
| 27 | 29 | const { currentType, setCurrentType } = useSnippetsFilters() |
| 28 | 30 | const tabName = type ?? 'all' |
| 29 | 31 | |
| 32 | + const isActive = type === currentType | |
| 33 | + const isProLocked = type && isProType(type) && !isLicensed() | |
| 34 | + | |
| 30 | 35 | return ( |
| 31 | - <a | |
| 32 | - href={buildUrl(window.location.href, { type: tabName })} | |
| 33 | - className={classnames('nav-tab', `${tabName}-tab`, { | |
| 34 | - 'nav-tab-active': type === currentType, | |
| 35 | - 'nav-tab-inactive': type && type !== currentType && !isLicensed() && isProType(type) | |
| 36 | - })} | |
| 37 | - aria-current={type === currentType ? 'page' : undefined} | |
| 38 | - onClick={event => { | |
| 39 | - event.preventDefault() | |
| 40 | - | |
| 41 | - if (type && !isLicensed() && isProType(type)) { | |
| 42 | - setIsUpgradeDialogOpen(true) | |
| 43 | - } else { | |
| 36 | + <li> | |
| 37 | + <a | |
| 38 | + href={buildUrl(window.location.href, { type: tabName })} | |
| 39 | + className={classnames('snippet-type-link', `${tabName}-type-link`, { | |
| 40 | + 'active-type': isActive, | |
| 41 | + 'pro-locked-type': isProLocked && !isActive | |
| 42 | + })} | |
| 43 | + aria-current={isActive ? 'page' : undefined} | |
| 44 | + onClick={event => { | |
| 45 | + event.preventDefault() | |
| 44 | 46 | setCurrentType(type) |
| 45 | - } | |
| 46 | - }} | |
| 47 | - > | |
| 48 | - <span className={`${tabName}-label`}> | |
| 49 | - {type ? SNIPPET_TYPE_LABELS[type] : __('All Snippets', 'code-snippets')} | |
| 50 | - </span> | |
| 51 | - {type && <Badge name={type} /> | |
| 52 | - } | |
| 53 | - </a> | |
| 47 | + }} | |
| 48 | + > | |
| 49 | + {type && <Badge name={type} />} | |
| 50 | + <span className={classnames(`${tabName}-label`, 'snippet-type-name')}> | |
| 51 | + {type | |
| 52 | + ? SNIPPET_TYPE_LABELS[type] | |
| 53 | + : <> | |
| 54 | + <span className="snippet-type-name-full">{__('All Snippets', 'code-snippets')}</span> | |
| 55 | + <span className="snippet-type-name-short">{__('All', 'code-snippets')}</span> | |
| 56 | + </>} | |
| 57 | + </span> | |
| 58 | + {count !== undefined && !isProLocked && <span className="subnav-count">{count}</span>} | |
| 59 | + {isProLocked && <span className="pro-chip">{__('Pro', 'code-snippets')}</span>} | |
| 60 | + </a> | |
| 61 | + </li> | |
| 54 | 62 | ) |
| 55 | 63 | } |
| 56 | 64 | |
| 57 | -const PageHeading = () => { | |
| 58 | - const { searchQueryText, searchLineNumber, currentTag, setSearchQuery, setCurrentTag } = useSnippetsFilters() | |
| 59 | - return ( | |
| 60 | - <> | |
| 61 | - <a href={getSnippetEditUrl()} className="button button-primary button-large create-snippet-button"> | |
| 62 | - {__('Create new snippet', 'code-snippets')} | |
| 63 | - </a> | |
| 64 | 65 | |
| 65 | - <h2> | |
| 66 | - {__('Manage Code Snippets', 'code-snippets')} | |
| 67 | - | |
| 68 | - {searchQueryText || currentTag | |
| 69 | - ? <span className="subtitle"> | |
| 70 | - {__('Search results', 'code-snippets')} | |
| 71 | - | |
| 72 | - {/* translators: %s: search query. */} | |
| 73 | - {searchQueryText && sprintf(__(' for “%s”', 'code-snippets'), searchQueryText)} | |
| 74 | - | |
| 75 | - {/* translators: %d: code line number. */} | |
| 76 | - {searchLineNumber && sprintf(__(' on line “%d”', 'code-snippets'), searchLineNumber)} | |
| 77 | - | |
| 78 | - {/* translators: %s: tag name. */} | |
| 79 | - {currentTag && sprintf(__(' in tag “%s”', 'code-snippets'), currentTag)} | |
| 80 | - | |
| 81 | - {' '} | |
| 82 | - <Button small className="clear-filters" onClick={() => { | |
| 83 | - setSearchQuery() | |
| 84 | - setCurrentTag() | |
| 85 | - }}> | |
| 86 | - {__('Clear Filters', 'code-snippets')} | |
| 87 | - </Button> | |
| 88 | - </span> | |
| 89 | - : null} | |
| 90 | - </h2> | |
| 91 | - | |
| 92 | - <hr className="wp-header-end" /> | |
| 93 | - | |
| 94 | - <SafeModeNotice /> | |
| 95 | - </> | |
| 96 | - ) | |
| 97 | -} | |
| 98 | - | |
| 99 | 66 | const SafeModeNotice = () => |
| 100 | 67 | window.CODE_SNIPPETS_MANAGE?.isSafeModeActive |
| 101 | 68 | ? <Notice type="error"> |
| 102 | 69 | <p> |
| @@ -116,29 +83,85 @@ | ||
| 116 | 83 | </p> |
| 117 | 84 | </Notice> |
| 118 | 85 | : null |
| 119 | 86 | |
| 87 | +// Counts render immediately from localized values, then switch to live values. | |
| 88 | +const useSnippetTypeCounts = () => { | |
| 89 | + const { snippetsList } = useSnippetsList() | |
| 90 | + | |
| 91 | + const countedSnippets = useMemo( | |
| 92 | + () => snippetsList?.filter(snippet => !snippet.trashed), | |
| 93 | + [snippetsList] | |
| 94 | + ) | |
| 95 | + | |
| 96 | + const typeCounts = useMemo( | |
| 97 | + () => countedSnippets?.reduce((counts, snippet) => { | |
| 98 | + const type = getSnippetType(snippet) | |
| 99 | + return counts.set(type, (counts.get(type) ?? 0) + 1) | |
| 100 | + }, new Map<SnippetType, number>()), | |
| 101 | + [countedSnippets] | |
| 102 | + ) | |
| 103 | + | |
| 104 | + const localized = window.CODE_SNIPPETS_MANAGE?.typeCounts | |
| 105 | + | |
| 106 | + const getCount = useCallback( | |
| 107 | + (type?: SnippetType) => countedSnippets | |
| 108 | + ? type ? typeCounts?.get(type) ?? 0 : countedSnippets.length | |
| 109 | + : localized?.[type ?? 'all'], | |
| 110 | + [countedSnippets, typeCounts, localized] | |
| 111 | + ) | |
| 112 | + | |
| 113 | + return { getCount } | |
| 114 | +} | |
| 115 | + | |
| 120 | 116 | const SnippetsTableInner = () => { |
| 121 | - const [isUpgradeDialogOpen, setIsUpgradeDialogOpen] = useState(false) | |
| 117 | + const { snippetView, setSnippetView } = useSnippetView() | |
| 118 | + const { currentType } = useSnippetsFilters() | |
| 119 | + const { getCount } = useSnippetTypeCounts() | |
| 120 | + const { atStart, atEnd, scrollRef } = useHorizontalScrollOverflow() | |
| 122 | 121 | |
| 123 | 122 | return ( |
| 124 | 123 | <> |
| 125 | - <PageHeading /> | |
| 124 | + <div | |
| 125 | + className={classnames('snippet-type-nav-wrapper', { | |
| 126 | + 'has-scroll-start': !atStart, | |
| 127 | + 'has-scroll-end': !atEnd | |
| 128 | + })} | |
| 129 | + > | |
| 130 | + <nav | |
| 131 | + ref={scrollRef} | |
| 132 | + className="snippet-type-nav" | |
| 133 | + aria-label={__('Snippet types', 'code-snippets')} | |
| 134 | + > | |
| 135 | + <ul> | |
| 136 | + <SnippetTypeTab count={getCount()} /> | |
| 137 | + {SNIPPET_TYPES.map(type => <SnippetTypeTab key={type} type={type} count={getCount(type)} />)} | |
| 138 | + </ul> | |
| 139 | + </nav> | |
| 140 | + </div> | |
| 126 | 141 | |
| 127 | - <nav | |
| 128 | - className="nav-tab-wrapper snippet-type-tabs" | |
| 129 | - aria-label={__('Snippet types', 'code-snippets')} | |
| 130 | - > | |
| 131 | - <SnippetTypeTab setIsUpgradeDialogOpen={setIsUpgradeDialogOpen} /> | |
| 132 | - {SNIPPET_TYPES.map(type => | |
| 133 | - <SnippetTypeTab key={type} type={type} setIsUpgradeDialogOpen={setIsUpgradeDialogOpen} />)} | |
| 134 | - </nav> | |
| 142 | + <ScreenMetaSlot /> | |
| 135 | 143 | |
| 136 | - <WithFilteredSnippetsContext> | |
| 137 | - <SnippetsListTable /> | |
| 138 | - </WithFilteredSnippetsContext> | |
| 144 | + <div className="snippets-page-header"> | |
| 145 | + <h1>{sprintf( | |
| 146 | + // translators: %s: label of the currently selected snippet type. | |
| 147 | + __('Local Snippets: %s', 'code-snippets'), | |
| 148 | + currentType ? SNIPPET_TYPE_LABELS[currentType] : __('All Snippets', 'code-snippets') | |
| 149 | + )}</h1> | |
| 150 | + <a href={getSnippetAddNewUrl(currentType)} className="button button-primary"> | |
| 151 | + {__('Add Snippet', 'code-snippets')} | |
| 152 | + </a> | |
| 153 | + </div> | |
| 139 | 154 | |
| 140 | - <UpsellDialog isOpen={isUpgradeDialogOpen} setIsOpen={setIsUpgradeDialogOpen} /> | |
| 155 | + <hr className="wp-header-end" /> | |
| 156 | + | |
| 157 | + <SafeModeNotice /> | |
| 158 | + | |
| 159 | + {currentType && !isLicensed() && isProType(currentType) | |
| 160 | + ? <UpsellPage /> | |
| 161 | + : <WithFilteredSnippetsContext> | |
| 162 | + <SnippetsListTable snippetView={snippetView} setSnippetView={setSnippetView} /> | |
| 163 | + </WithFilteredSnippetsContext>} | |
| 141 | 164 | </> |
| 142 | 165 | ) |
| 143 | 166 | } |
| 144 | 167 | |