| 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 { useHorizontalScrollOverflow } from '../../../hooks/useHorizontalScrollOverflow' |
| 6 |
import { WithRestAPIContext } from '../../../hooks/useRestAPI' |
| 7 |
import { useSnippetView } from '../../../hooks/useSnippetView' |
| 8 |
import { WithSnippetsAPIContext } from '../../../hooks/useSnippetsAPI' |
| 9 |
import { WithSnippetsListContext, useSnippetsList } from '../../../hooks/useSnippetsList' |
| 10 |
import { SNIPPET_TYPES } from '../../../types/Snippet' |
| 11 |
import { isLicensed } from '../../../utils/screen' |
| 12 |
import { SNIPPET_TYPE_LABELS, getSnippetAddNewUrl, getSnippetType, isProType } from '../../../utils/snippets/snippets' |
| 13 |
import { buildUrl } from '../../../utils/urls' |
| 14 |
import { Badge } from '../../common/Badge' |
| 15 |
import { Notice } from '../../common/Notice' |
| 16 |
import { ScreenMetaSlot } from '../../common/ScreenMetaSlot' |
| 17 |
import { UpsellPage } from '../../common/UpsellDialog' |
| 18 |
import { WithSnippetsTableFilters, useSnippetsFilters } from './WithSnippetsTableFilters' |
| 19 |
import { WithFilteredSnippetsContext } from './WithFilteredSnippetsContext' |
| 20 |
import { SnippetsListTable } from './SnippetsListTable' |
| 21 |
import type { SnippetType } from '../../../types/Snippet' |
| 22 |
|
| 23 |
interface SnippetTypeTabProps { |
| 24 |
type?: SnippetType |
| 25 |
count?: number |
| 26 |
} |
| 27 |
|
| 28 |
const SnippetTypeTab: React.FC<SnippetTypeTabProps> = ({ type, count }) => { |
| 29 |
const { currentType, setCurrentType } = useSnippetsFilters() |
| 30 |
const tabName = type ?? 'all' |
| 31 |
|
| 32 |
const isActive = type === currentType |
| 33 |
const isProLocked = type && isProType(type) && !isLicensed() |
| 34 |
|
| 35 |
return ( |
| 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() |
| 46 |
setCurrentType(type) |
| 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 && !isProLocked && <span className="subnav-count">{count}</span>} |
| 59 |
{isProLocked && <span className="pro-chip">{__('Pro', 'code-snippets')}</span>} |
| 60 |
</a> |
| 61 |
</li> |
| 62 |
) |
| 63 |
} |
| 64 |
|
| 65 |
const SafeModeNotice = () => |
| 66 |
window.CODE_SNIPPETS_MANAGE?.isSafeModeActive |
| 67 |
? <Notice type="error"> |
| 68 |
<p> |
| 69 |
<strong>{__('Warning:', 'code-snippets')}</strong>{'\n'} |
| 70 |
{__('Safe mode is active and snippets will not execute!', 'code-snippets')}{'\n'} |
| 71 |
|
| 72 |
{createInterpolateElement( |
| 73 |
__('Remove the <code>CODE_SNIPPETS_SAFE_MODE</code> constant from <code>wp-config.php</code> file to turn off safe mode.', 'code-snippets'), |
| 74 |
{ |
| 75 |
code: <code /> |
| 76 |
} |
| 77 |
)}{'\n'} |
| 78 |
|
| 79 |
<a href="https://codesnippets.pro/doc/safe-mode" target="_blank" rel="noreferrer"> |
| 80 |
{__('Read more', 'code-snippets')} |
| 81 |
</a> |
| 82 |
</p> |
| 83 |
</Notice> |
| 84 |
: null |
| 85 |
|
| 86 |
// Counts render immediately from the values localized with the page, then |
| 87 |
// switch to live values derived from the snippets list once it has loaded. |
| 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 |
|
| 116 |
const SnippetsTableInner = () => { |
| 117 |
const { snippetView, setSnippetView } = useSnippetView() |
| 118 |
const { currentType } = useSnippetsFilters() |
| 119 |
const { getCount } = useSnippetTypeCounts() |
| 120 |
const { atStart, atEnd, scrollRef } = useHorizontalScrollOverflow() |
| 121 |
|
| 122 |
return ( |
| 123 |
<> |
| 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> |
| 141 |
|
| 142 |
<ScreenMetaSlot /> |
| 143 |
|
| 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> |
| 154 |
|
| 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>} |
| 164 |
</> |
| 165 |
) |
| 166 |
} |
| 167 |
|
| 168 |
export const SnippetsTable: React.FC = () => |
| 169 |
<WithRestAPIContext> |
| 170 |
<WithSnippetsAPIContext> |
| 171 |
<WithSnippetsListContext> |
| 172 |
<WithSnippetsTableFilters> |
| 173 |
<SnippetsTableInner /> |
| 174 |
</WithSnippetsTableFilters> |
| 175 |
</WithSnippetsListContext> |
| 176 |
</WithSnippetsAPIContext> |
| 177 |
</WithRestAPIContext> |
| 178 |
|