PluginProbe
Code Snippets / trunk
Code Snippets vtrunk
4.0.0-beta.2 3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 All 65 releases
← 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">&nbsp;</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 : <>&#8212;</>
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">&#8212;</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>[] =>