← All changes
|
js/components/ManageMenu/SnippetsTable/TableColumns.tsx
+88
-122
4.0.0-beta.1
→
trunk
View file →
| @@ -1,23 +1,22 @@ | ||
| 1 | -import classnames from 'classnames' | |
| 2 | -import React, { Fragment, useState } from 'react' | |
| 3 | -import { __ } from '@wordpress/i18n' | |
| 4 | 1 | import { humanTimeDiff } from '@wordpress/date' |
| 5 | 2 | import { RawHTML } from '@wordpress/element' |
| 3 | +import { __, sprintf } from '@wordpress/i18n' | |
| 4 | +import React, { Fragment } from 'react' | |
| 6 | 5 | import { useSnippetsAPI } from '../../../hooks/useSnippetsAPI' |
| 7 | 6 | import { useSnippetsList } from '../../../hooks/useSnippetsList' |
| 8 | 7 | import { handleUnknownError } from '../../../utils/errors' |
| 9 | -import { downloadSnippetExportFile } from '../../../utils/files' | |
| 10 | 8 | import { isNetworkAdmin } from '../../../utils/screen' |
| 11 | -import { cloneSnippetObject, getSnippetDisplayName, getSnippetEditUrl, getSnippetType } from '../../../utils/snippets/snippets' | |
| 9 | +import { getSnippetDisplayName, getSnippetEditUrl, getSnippetType } from '../../../utils/snippets/snippets' | |
| 10 | +import { getRunOnceNonce } from '../../../utils/restAPI' | |
| 12 | 11 | import { buildUrl } from '../../../utils/urls' |
| 13 | 12 | import { Badge } from '../../common/Badge' |
| 14 | -import { Button } from '../../common/Button' | |
| 15 | -import { DeleteButton } from '../../common/DeleteButton' | |
| 13 | +import { SnippetPriorityInput } from '../../common/snippets/SnippetPriorityInput' | |
| 16 | 14 | import { Tooltip } from '../../common/Tooltip' |
| 15 | +import { RowActions } from './RowActions' | |
| 16 | +import { useFilteredSnippets } from './WithFilteredSnippetsContext' | |
| 17 | 17 | import { useSnippetsFilters } from './WithSnippetsTableFilters' |
| 18 | -import { useFilteredSnippets } from './WithFilteredSnippetsContext' | |
| 19 | -import type { Key, ReactNode } from 'react' | |
| 18 | +import type { Key } from 'react' | |
| 20 | 19 | import type { Snippet } from '../../../types/Snippet' |
| 21 | 20 | import type { ListTableColumn } from '../../common/ListTable' |
| 22 | 21 | |
| 23 | 22 | interface ColumnProps { |
| @@ -23,13 +22,32 @@ | ||
| 23 | 22 | interface ColumnProps { |
| 24 | 23 | snippet: Snippet |
| 25 | 24 | } |
| 26 | 25 | |
| 26 | +const runOnceUrl = (snippet: Snippet, nonce: string): string => | |
| 27 | + buildUrl(window.location.href, { | |
| 28 | + action: 'run-once', | |
| 29 | + snippet: snippet.id, | |
| 30 | + network: snippet.network ? 'true' : 'false', | |
| 31 | + _wpnonce: nonce | |
| 32 | + }) | |
| 33 | + | |
| 34 | +// The rendered link carries the nonce from page load. Before any navigation | |
| 35 | +// starts, whether a click, a middle-click or "open in new tab", the href is | |
| 36 | +// rebuilt from the nonce the Heartbeat has refreshed since, so a page left open | |
| 37 | +// still works. | |
| 27 | 38 | const RunOnceButton: React.FC<ColumnProps> = ({ snippet }) => |
| 28 | 39 | <a |
| 29 | 40 | className="snippet-execution-button" |
| 30 | 41 | title={__('Run Once', 'code-snippets')} |
| 31 | - href={buildUrl(window.location.href, { action: 'run-once', snippet: snippet.id })} | |
| 42 | + href={runOnceUrl(snippet, window.CODE_SNIPPETS_MANAGE?.runOnceNonce ?? '')} | |
| 43 | + onMouseDown={event => { | |
| 44 | + event.currentTarget.href = runOnceUrl(snippet, getRunOnceNonce()) | |
| 45 | + }} | |
| 46 | + onClick={event => { | |
| 47 | + event.preventDefault() | |
| 48 | + window.location.assign(runOnceUrl(snippet, getRunOnceNonce())) | |
| 49 | + }} | |
| 32 | 50 | > |
| 33 | 51 | <span className="screen-reader-text">{__('Run Once', 'code-snippets')}</span> |
| 34 | 52 | <span aria-hidden="true"> </span> |
| 35 | 53 | </a> |
| @@ -60,9 +78,9 @@ | ||
| 60 | 78 | /> |
| 61 | 79 | ) |
| 62 | 80 | } |
| 63 | 81 | |
| 64 | -const ActivateColumn: React.FC<ColumnProps> = ({ snippet }) => { | |
| 82 | +export const ActivateColumn: React.FC<ColumnProps> = ({ snippet }) => { | |
| 65 | 83 | const { activeByCondition } = useFilteredSnippets() |
| 66 | 84 | |
| 67 | 85 | if (snippet.trashed) { |
| 68 | 86 | return '' |
| @@ -83,83 +101,10 @@ | ||
| 83 | 101 | return <ActivationSwitch snippet={snippet} /> |
| 84 | 102 | } |
| 85 | 103 | } |
| 86 | 104 | |
| 87 | -const ActionLinks = ({ snippet }: { snippet: Snippet }) => { | |
| 88 | - const api = useSnippetsAPI() | |
| 89 | - const { refreshSnippetsList } = useSnippetsList() | |
| 90 | - | |
| 91 | - const Edit = !snippet.trashed && (() => | |
| 92 | - <a href={getSnippetEditUrl(snippet)}> | |
| 93 | - {snippet.locked ? __('View', 'code-snippets') : __('Edit', 'code-snippets')} | |
| 94 | - </a>) | |
| 95 | - | |
| 96 | - const Clone = !snippet.trashed && (() => | |
| 97 | - <Button link onClick={() => { | |
| 98 | - api.create(cloneSnippetObject(snippet)) | |
| 99 | - .then(refreshSnippetsList) | |
| 100 | - .catch(handleUnknownError) | |
| 101 | - }}> | |
| 102 | - {__('Clone', 'code-snippets')} | |
| 103 | - </Button>) | |
| 104 | - | |
| 105 | - const Export = !snippet.trashed && (() => | |
| 106 | - <Button link onClick={() => { | |
| 107 | - api.export(snippet) | |
| 108 | - .then(response => downloadSnippetExportFile(response, snippet)) | |
| 109 | - .catch(handleUnknownError) | |
| 110 | - }}> | |
| 111 | - {__('Export', 'code-snippets')} | |
| 112 | - </Button>) | |
| 113 | - | |
| 114 | - const Restore = snippet.trashed && (() => | |
| 115 | - <Button link onClick={() => { | |
| 116 | - api.restore(snippet) | |
| 117 | - .then(refreshSnippetsList) | |
| 118 | - .catch(handleUnknownError) | |
| 119 | - }}> | |
| 120 | - {__('Restore', 'code-snippets')} | |
| 121 | - </Button>) | |
| 122 | - | |
| 123 | - const Delete = (!snippet.locked || snippet.trashed) && (() => | |
| 124 | - <DeleteButton link className="delete" snippet={snippet} onSuccess={refreshSnippetsList} />) | |
| 125 | - | |
| 126 | - return ( | |
| 127 | - <> | |
| 128 | - {[Edit, Clone, Restore, Export, Delete] | |
| 129 | - .filter(Action => false !== Action) | |
| 130 | - .reduce<ReactNode>( | |
| 131 | - (Actions, Action) => | |
| 132 | - null === Actions ? <Action /> : <>{Actions} | <Action /></>, | |
| 133 | - null)} | |
| 134 | - </> | |
| 135 | - ) | |
| 136 | -} | |
| 137 | - | |
| 138 | -const RowActions: React.FC<ColumnProps> = ({ snippet }) => { | |
| 139 | - if (!isNetworkAdmin() && snippet.network && !snippet.shared_network) { | |
| 140 | - return ( | |
| 141 | - <div className="row-actions visible"> | |
| 142 | - {snippet.active | |
| 143 | - ? <span className="network-active">{__('Network Active', 'code-snippets')}</span> | |
| 144 | - : <span className="network-only">{__('Network Only', 'code-snippets')}</span>} | |
| 145 | - </div> | |
| 146 | - ) | |
| 147 | - } | |
| 148 | - | |
| 149 | - if (snippet.shared_network && !window.CODE_SNIPPETS_MANAGE?.hasNetworkCap) { | |
| 150 | - return undefined | |
| 151 | - } | |
| 152 | - | |
| 153 | - return ( | |
| 154 | - <div className={classnames('row-actions', { visible: !snippet.trashed })}> | |
| 155 | - <ActionLinks snippet={snippet} /> | |
| 156 | - </div> | |
| 157 | - ) | |
| 158 | -} | |
| 159 | - | |
| 160 | -const NameColumn: React.FC<ColumnProps> = ({ snippet }) => | |
| 161 | - <> | |
| 105 | +export const SnippetExtraIcons: React.FC<ColumnProps> = ({ snippet }) => | |
| 106 | + <div className="extra-icons"> | |
| 162 | 107 | {snippet.locked && ( |
| 163 | 108 | <Tooltip |
| 164 | 109 | inline |
| 165 | 110 | end |
| @@ -167,19 +112,35 @@ | ||
| 167 | 112 | icon={<span className="dashicons dashicons-lock" aria-hidden="true"></span>} |
| 168 | 113 | > |
| 169 | 114 | {__('This snippet is locked and cannot be modified.', 'code-snippets')} |
| 170 | 115 | </Tooltip>)} |
| 116 | + </div> | |
| 171 | 117 | |
| 118 | +export const SnippetName: React.FC<ColumnProps> = ({ snippet }) => | |
| 119 | + <span className="snippet-name-group"> | |
| 172 | 120 | {!snippet.trashed && (isNetworkAdmin() || !snippet.network || window.CODE_SNIPPETS_MANAGE?.hasNetworkCap) |
| 173 | - ? <a href={getSnippetEditUrl(snippet)} className="snippet-name">{getSnippetDisplayName(snippet)}</a> | |
| 174 | - : getSnippetDisplayName(snippet)} | |
| 121 | + ? <a | |
| 122 | + href={getSnippetEditUrl(snippet)} | |
| 123 | + className="snippet-name" | |
| 124 | + title={getSnippetDisplayName(snippet)} | |
| 125 | + > | |
| 126 | + {getSnippetDisplayName(snippet)} | |
| 127 | + </a> | |
| 128 | + : <span className="snippet-name" title={getSnippetDisplayName(snippet)}> | |
| 129 | + {getSnippetDisplayName(snippet)} | |
| 130 | + </span>} | |
| 175 | 131 | |
| 176 | 132 | {snippet.shared_network && <span className="badge">{__('Shared on Network', 'code-snippets')}</span>} |
| 133 | + </span> | |
| 177 | 134 | |
| 135 | +const NameColumn: React.FC<ColumnProps> = ({ snippet }) => | |
| 136 | + <> | |
| 137 | + <SnippetExtraIcons snippet={snippet} /> | |
| 138 | + <SnippetName snippet={snippet} /> | |
| 178 | 139 | <RowActions snippet={snippet} /> |
| 179 | 140 | </> |
| 180 | 141 | |
| 181 | -const TypeColumn: React.FC<ColumnProps> = ({ snippet }) => { | |
| 142 | +export const TypeColumn: React.FC<ColumnProps> = ({ snippet }) => { | |
| 182 | 143 | const { setCurrentType } = useSnippetsFilters() |
| 183 | 144 | const type = getSnippetType(snippet) |
| 184 | 145 | |
| 185 | 146 | return ( |
| @@ -194,9 +155,9 @@ | ||
| 194 | 155 | </a> |
| 195 | 156 | ) |
| 196 | 157 | } |
| 197 | 158 | |
| 198 | -const TagsColumn: React.FC<ColumnProps> = ({ snippet }) => | |
| 159 | +export const TagsColumn: React.FC<ColumnProps> = ({ snippet }) => | |
| 199 | 160 | snippet.tags.map((tag, index) => |
| 200 | 161 | <Fragment key={tag}> |
| 201 | 162 | <a key={tag} href={buildUrl(window.location.href, { tag })}> |
| 202 | 163 | {tag} |
| @@ -203,9 +164,9 @@ | ||
| 203 | 164 | </a> |
| 204 | 165 | {index < snippet.tags.length - 1 ? ', ' : ''} |
| 205 | 166 | </Fragment>) |
| 206 | 167 | |
| 207 | -const DateColumn: React.FC<ColumnProps> = ({ snippet }) => | |
| 168 | +export const DateColumn: React.FC<ColumnProps> = ({ snippet }) => | |
| 208 | 169 | snippet.modified |
| 209 | 170 | ? <span className="modified-column-content" title={snippet.modified}> |
| 210 | 171 | <time dateTime={snippet.modified}> |
| 211 | 172 | {humanTimeDiff(snippet.modified, undefined)} |
| @@ -212,42 +173,35 @@ | ||
| 212 | 173 | </time> |
| 213 | 174 | </span> |
| 214 | 175 | : <>—</> |
| 215 | 176 | |
| 216 | -const PriorityColumn: React.FC<ColumnProps> = ({ snippet }) => { | |
| 217 | - const [value, setValue] = useState(snippet.priority) | |
| 218 | - const snippetsAPI = useSnippetsAPI() | |
| 219 | - const { refreshSnippetsList } = useSnippetsList() | |
| 220 | - const id = `snippet-${snippet.id}-priority` | |
| 177 | +export const PriorityColumn: React.FC<ColumnProps> = ({ snippet }) => | |
| 178 | + <SnippetPriorityInput snippet={snippet} /> | |
| 221 | 179 | |
| 222 | - const handleUpdate = () => { | |
| 223 | - snippetsAPI.update({ ...snippet, priority: value }) | |
| 224 | - .then(response => { | |
| 225 | - if (response.id === snippet.id) { | |
| 226 | - setValue(response.priority) | |
| 227 | - } | |
| 228 | - }) | |
| 229 | - .then(refreshSnippetsList) | |
| 230 | - .catch(handleUnknownError) | |
| 180 | +export const getAuthorDisplayName = (snippet: Snippet): string => | |
| 181 | + (snippet.updatedBy ?? snippet.createdBy)?.displayName ?? '' | |
| 182 | + | |
| 183 | +const AuthorColumn: React.FC<ColumnProps> = ({ snippet }) => { | |
| 184 | + const creator = snippet.createdBy | |
| 185 | + const updater = snippet.updatedBy | |
| 186 | + const primary = updater ?? creator | |
| 187 | + | |
| 188 | + if (!primary) { | |
| 189 | + return <span className="snippet-author is-unknown">—</span> | |
| 231 | 190 | } |
| 232 | 191 | |
| 192 | + const tooltip = creator && updater && creator.id !== updater.id | |
| 193 | + ? sprintf(__('Created by %1$s ยท Last edited by %2$s', 'code-snippets'), creator.displayName, updater.displayName) | |
| 194 | + : creator | |
| 195 | + ? sprintf(__('Created by %s', 'code-snippets'), creator.displayName) | |
| 196 | + : sprintf(__('Last edited by %s', 'code-snippets'), primary.displayName) | |
| 197 | + | |
| 233 | 198 | return ( |
| 234 | - <form onSubmit={event => { | |
| 235 | - event.preventDefault() | |
| 236 | - handleUpdate() | |
| 237 | - }}> | |
| 238 | - <input | |
| 239 | - id={id} | |
| 240 | - type="number" | |
| 241 | - className="snippet-priority" | |
| 242 | - value={value} | |
| 243 | - step="1" | |
| 244 | - onBlur={handleUpdate} | |
| 245 | - aria-label={__('Snippet priority', 'code-snippets')} | |
| 246 | - onChange={event => setValue(Number(event.target.value))} | |
| 247 | - disabled={snippet.locked || snippet.trashed} | |
| 248 | - /> | |
| 249 | - </form> | |
| 199 | + <span className="snippet-author" title={tooltip}> | |
| 200 | + {primary.avatarUrl && | |
| 201 | + <img className="snippet-author__avatar" src={primary.avatarUrl} alt="" width={20} height={20} />} | |
| 202 | + <span className="snippet-author__name">{primary.displayName}</span> | |
| 203 | + </span> | |
| 250 | 204 | ) |
| 251 | 205 | } |
| 252 | 206 | |
| 253 | 207 | const baseTableColumns: ListTableColumn<Snippet>[] = [ |
| @@ -256,8 +210,14 @@ | ||
| 256 | 210 | title: <span className="screen-reader-text">{__('Activate', 'code-snippets')}</span>, |
| 257 | 211 | render: snippet => <ActivateColumn snippet={snippet} /> |
| 258 | 212 | }, |
| 259 | 213 | { |
| 214 | + id: 'id', | |
| 215 | + title: __('ID', 'code-snippets'), | |
| 216 | + sortedValue: snippet => snippet.id, | |
| 217 | + render: snippet => snippet.id | |
| 218 | + }, | |
| 219 | + { | |
| 260 | 220 | id: 'name', |
| 261 | 221 | title: __('Name', 'code-snippets'), |
| 262 | 222 | isPrimary: true, |
| 263 | 223 | sortedValue: snippet => getSnippetDisplayName(snippet).toLowerCase(), |
| @@ -288,9 +248,15 @@ | ||
| 288 | 248 | { |
| 289 | 249 | id: 'priority', |
| 290 | 250 | title: __('Priority', 'code-snippets'), |
| 291 | 251 | sortedValue: snippet => snippet.priority, |
| 292 | - render: snippet => <PriorityColumn snippet={snippet} /> | |
| 252 | + render: snippet => <SnippetPriorityInput snippet={snippet} /> | |
| 253 | + }, | |
| 254 | + { | |
| 255 | + id: 'author', | |
| 256 | + title: __('Author', 'code-snippets'), | |
| 257 | + sortedValue: snippet => getAuthorDisplayName(snippet).toLowerCase(), | |
| 258 | + render: snippet => <AuthorColumn snippet={snippet} /> | |
| 293 | 259 | } |
| 294 | 260 | ] |
| 295 | 261 | |
| 296 | 262 | export const getTableColumns = (hiddenColumns: Set<Key>): ListTableColumn<Snippet>[] => |