| 1 |
import { __, sprintf } from '@wordpress/i18n' |
| 2 |
import { createInterpolateElement } from '@wordpress/element' |
| 3 |
import React, { useCallback, useMemo } from 'react' |
| 4 |
import classnames from 'classnames' |
| 5 |
import { WithRestAPIContext } from '../../../hooks/useRestAPI' |
| 6 |
import { useSnippetView } from '../../../hooks/useSnippetView' |
| 7 |
import { WithSnippetsAPIContext } from '../../../hooks/useSnippetsAPI' |
| 8 |
import { WithSnippetsListContext, useSnippetsList } from '../../../hooks/useSnippetsList' |
| 9 |
import { SNIPPET_TYPES } from '../../../types/Snippet' |
| 10 |
import { isLicensed } from '../../../utils/screen' |
| 11 |
import { SNIPPET_TYPE_LABELS, getSnippetAddNewUrl, getSnippetType, isProType } from '../../../utils/snippets/snippets' |
| 12 |
import { buildUrl } from '../../../utils/urls' |
| 13 |
import { Badge } from '../../common/Badge' |
| 14 |
import { Notice } from '../../common/Notice' |
| 15 |
import { ScreenMetaSlot } from '../../common/ScreenMetaSlot' |
| 16 |
import { UpsellPage } from '../../common/UpsellDialog' |
| 17 |
import { WithSnippetsTableFilters, useSnippetsFilters } from './WithSnippetsTableFilters' |
| 18 |
import { WithFilteredSnippetsContext } from './WithFilteredSnippetsContext' |
| 19 |
import { SnippetsListTable } from './SnippetsListTable' |
| 20 |
import type { SnippetType } from '../../../types/Snippet' |
| 21 |
|
| 22 |
interface SnippetTypeTabProps { |
| 23 |
type?: SnippetType |
| 24 |
count?: number |
| 25 |
} |
| 26 |
|
| 27 |
const SnippetTypeTab: React.FC<SnippetTypeTabProps> = ({ type, count }) => { |
| 28 |
const { currentType, setCurrentType } = useSnippetsFilters() |
| 29 |
const tabName = type ?? 'all' |
| 30 |
|
| 31 |
return ( |
| 32 |
<li> |
| 33 |
<a |
| 34 |
href={buildUrl(window.location.href, { type: tabName })} |
| 35 |
className={classnames('snippet-type-link', `${tabName}-type-link`, { |
| 36 |
'active-type': type === currentType, |
| 37 |
'pro-locked-type': type && type !== currentType && !isLicensed() && isProType(type) |
| 38 |
})} |
| 39 |
aria-current={type === currentType ? 'page' : undefined} |
| 40 |
onClick={event => { |
| 41 |
event.preventDefault() |
| 42 |
setCurrentType(type) |
| 43 |
}} |
| 44 |
> |
| 45 |
{type && <Badge name={type} />} |
| 46 |
<span className={classnames(`${tabName}-label`, 'snippet-type-name')}> |
| 47 |
{type |
| 48 |
? SNIPPET_TYPE_LABELS[type] |
| 49 |
: <> |
| 50 |
<span className="snippet-type-name-full">{__('All Snippets', 'code-snippets')}</span> |
| 51 |
<span className="snippet-type-name-short">{__('All', 'code-snippets')}</span> |
| 52 |
</>} |
| 53 |
</span> |
| 54 |
{count && <span className="subnav-count">{count}</span>} |
| 55 |
{type && isProType(type) && !isLicensed() && <span className="pro-chip">{__('Pro', 'code-snippets')}</span>} |
| 56 |
</a> |
| 57 |
</li> |
| 58 |
) |
| 59 |
} |
| 60 |
|
| 61 |
const SafeModeNotice = () => |
| 62 |
window.CODE_SNIPPETS_MANAGE?.isSafeModeActive |
| 63 |
? <Notice type="error"> |
| 64 |
<p> |
| 65 |
<strong>{__('Warning:', 'code-snippets')}</strong>{'\n'} |
| 66 |
{__('Safe mode is active and snippets will not execute!', 'code-snippets')}{'\n'} |
| 67 |
|
| 68 |
{createInterpolateElement( |
| 69 |
__('Remove the <code>CODE_SNIPPETS_SAFE_MODE</code> constant from <code>wp-config.php</code> file to turn off safe mode.', 'code-snippets'), |
| 70 |
{ |
| 71 |
code: <code /> |
| 72 |
} |
| 73 |
)}{'\n'} |
| 74 |
|
| 75 |
<a href="https://codesnippets.pro/doc/safe-mode" target="_blank" rel="noreferrer"> |
| 76 |
{__('Read more', 'code-snippets')} |
| 77 |
</a> |
| 78 |
</p> |
| 79 |
</Notice> |
| 80 |
: null |
| 81 |
|
| 82 |
// Counts render immediately from the values localized with the page, then |
| 83 |
// switch to live values derived from the snippets list once it has loaded. |
| 84 |
const useSnippetTypeCounts = () => { |
| 85 |
const { snippetsList } = useSnippetsList() |
| 86 |
|
| 87 |
const countedSnippets = useMemo( |
| 88 |
() => snippetsList?.filter(snippet => !snippet.trashed), |
| 89 |
[snippetsList] |
| 90 |
) |
| 91 |
|
| 92 |
const typeCounts = useMemo( |
| 93 |
() => countedSnippets?.reduce((counts, snippet) => { |
| 94 |
const type = getSnippetType(snippet) |
| 95 |
return counts.set(type, (counts.get(type) ?? 0) + 1) |
| 96 |
}, new Map<SnippetType, number>()), |
| 97 |
[countedSnippets] |
| 98 |
) |
| 99 |
|
| 100 |
const localized = window.CODE_SNIPPETS_MANAGE?.typeCounts |
| 101 |
|
| 102 |
const getCount = useCallback( |
| 103 |
(type?: SnippetType) => countedSnippets |
| 104 |
? type ? typeCounts?.get(type) ?? 0 : countedSnippets.length |
| 105 |
: localized?.[type ?? 'all'], |
| 106 |
[countedSnippets, typeCounts, localized] |
| 107 |
) |
| 108 |
|
| 109 |
return { getCount } |
| 110 |
} |
| 111 |
|
| 112 |
const SnippetsTableInner = () => { |
| 113 |
const { snippetView, setSnippetView } = useSnippetView() |
| 114 |
const { currentType } = useSnippetsFilters() |
| 115 |
const { getCount } = useSnippetTypeCounts() |
| 116 |
|
| 117 |
return ( |
| 118 |
<> |
| 119 |
<nav |
| 120 |
className="snippet-type-nav" |
| 121 |
aria-label={__('Snippet types', 'code-snippets')} |
| 122 |
> |
| 123 |
<ul> |
| 124 |
<SnippetTypeTab count={getCount()} /> |
| 125 |
{SNIPPET_TYPES.map(type => |
| 126 |
<SnippetTypeTab key={type} type={type} count={getCount(type)} />)} |
| 127 |
</ul> |
| 128 |
</nav> |
| 129 |
|
| 130 |
<ScreenMetaSlot /> |
| 131 |
|
| 132 |
<div className="snippets-page-header"> |
| 133 |
<h1>{sprintf( |
| 134 |
// translators: %s: label of the currently selected snippet type. |
| 135 |
__('Local Snippets: %s', 'code-snippets'), |
| 136 |
currentType ? SNIPPET_TYPE_LABELS[currentType] : __('All Snippets', 'code-snippets') |
| 137 |
)}</h1> |
| 138 |
<a href={getSnippetAddNewUrl(currentType)} className="button button-primary"> |
| 139 |
{__('Create new Snippet', 'code-snippets')} |
| 140 |
</a> |
| 141 |
</div> |
| 142 |
|
| 143 |
<hr className="wp-header-end" /> |
| 144 |
|
| 145 |
<SafeModeNotice /> |
| 146 |
|
| 147 |
{currentType && !isLicensed() && isProType(currentType) |
| 148 |
? <UpsellPage /> |
| 149 |
: <WithFilteredSnippetsContext> |
| 150 |
<SnippetsListTable snippetView={snippetView} setSnippetView={setSnippetView} /> |
| 151 |
</WithFilteredSnippetsContext>} |
| 152 |
</> |
| 153 |
) |
| 154 |
} |
| 155 |
|
| 156 |
export const SnippetsTable: React.FC = () => |
| 157 |
<WithRestAPIContext> |
| 158 |
<WithSnippetsAPIContext> |
| 159 |
<WithSnippetsListContext> |
| 160 |
<WithSnippetsTableFilters> |
| 161 |
<SnippetsTableInner /> |
| 162 |
</WithSnippetsTableFilters> |
| 163 |
</WithSnippetsListContext> |
| 164 |
</WithSnippetsAPIContext> |
| 165 |
</WithRestAPIContext> |
| 166 |
|