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/CommunityCloud/CloudSnippetsTable.tsx +117 -80 3.10.0-beta.1 → trunk View file →
@@ -1,86 +1,27 @@
1 1 import { __, sprintf } from '@wordpress/i18n'
2 2 import React, { useState } from 'react'
3 -import { getSnippetType } from '../../../utils/snippets/snippets'
3 +import { getSnippetType, isCloudSnippetDownloadable } from '../../../utils/snippets/snippets'
4 4 import { stripTags, truncateChars } from '../../../utils/text'
5 5 import { Badge } from '../../common/Badge'
6 6 import { Button } from '../../common/Button'
7 7 import { CloudSnippetDownloadButton } from '../../common/cloud/CloudSnippetDownloadButton'
8 -import { CloudSnippetPreviewModal } from '../../common/cloud/CloudSnippetPreviewModal'
9 8 import { CloudStatusBadge } from '../../common/cloud/CloudStatusBadge'
9 +import { CloudSnippetPreviewModal } from '../../common/snippets/SnippetPreviewModal'
10 10 import { useCloudSearch } from './WithCloudSearchContext'
11 11 import type { CloudSnippetSchema } from '../../../types/schema/CloudSnippetSchema'
12 12 import type { Dispatch, SetStateAction } from 'react'
13 13
14 -type CloudSnippetId = CloudSnippetSchema['id']
15 -type SetSelected = Dispatch<SetStateAction<Set<CloudSnippetId>>>
16 -
17 -const updateSelection = (setSelected: SetSelected, ids: CloudSnippetId[], isSelected: boolean) => {
18 - setSelected(previous => {
19 - const updated = new Set(previous)
20 - ids.forEach(id => isSelected ? updated.add(id) : updated.delete(id))
21 - return updated
22 - })
23 -}
24 -
25 14 interface CloudSnippetRowProps {
26 15 snippet: CloudSnippetSchema
27 - selected: Set<CloudSnippetId>
28 - setSelected: SetSelected
29 16 }
30 17
31 -interface CloudSnippetActionsProps extends Pick<CloudSnippetRowProps, 'snippet'> {
32 - isPreviewOpen: boolean
33 - setIsPreviewOpen: (isOpen: boolean) => void
34 -}
35 -
36 -const CloudSnippetActions: React.FC<CloudSnippetActionsProps> = ({
37 - snippet,
38 - isPreviewOpen,
39 - setIsPreviewOpen
40 -}) => {
18 +const CloudSnippetRow: React.FC<CloudSnippetRowProps> = ({ snippet }) => {
41 19 const { doSearch } = useCloudSearch()
20 + const [isPreviewOpen, setIsPreviewOpen] = useState(false)
42 21
43 22 return (
44 23 <>
45 - <div className="cloud-snippet-action-buttons">
46 - <Button secondary onClick={() => setIsPreviewOpen(true)}>
47 - {__('Preview', 'code-snippets')}
48 - </Button>
49 -
50 - <CloudSnippetDownloadButton snippet={snippet} onDownloaded={doSearch} />
51 - </div>
52 -
53 - <CloudSnippetPreviewModal
54 - snippet={snippet}
55 - isOpen={isPreviewOpen}
56 - setIsOpen={setIsPreviewOpen}
57 - onDownloaded={doSearch}
58 - />
59 - </>
60 - )
61 -}
62 -
63 -const CloudSnippetRow: React.FC<CloudSnippetRowProps> = ({ snippet, selected, setSelected }) => {
64 - const [isPreviewOpen, setIsPreviewOpen] = useState(false)
65 -
66 - return (
67 - <tr>
68 - <th scope="row" className="check-column">
69 - <input
70 - id={`cb-select-${snippet.id}`}
71 - type="checkbox"
72 - name="checked[]"
73 - checked={selected.has(snippet.id)}
74 - aria-label={sprintf(
75 - // translators: %s: snippet name.
76 - __('Select %s', 'code-snippets'),
77 - snippet.name
78 - )}
79 - onChange={event => updateSelection(setSelected, [snippet.id], event.target.checked)}
80 - />
81 - </th>
82 -
83 24 <td className="column-name column-primary">
84 25 <strong>
85 26 <button
86 27 type="button"
@@ -101,39 +42,131 @@
101 42 <div className="cloud-table-description">{truncateChars(stripTags(snippet.description))}</div>
102 43 </td>
103 44
104 45 <td className="column-actions">
105 - <CloudSnippetActions {...{ snippet, isPreviewOpen, setIsPreviewOpen }} />
46 + <div className="cloud-snippet-action-buttons">
47 + <Button secondary onClick={() => setIsPreviewOpen(true)}>
48 + {__('Preview', 'code-snippets')}
49 + </Button>
50 +
51 + <CloudSnippetDownloadButton snippet={snippet} onDownloaded={doSearch} />
52 + </div>
53 +
54 + {isPreviewOpen && (
55 + <CloudSnippetPreviewModal snippet={snippet} setIsOpen={setIsPreviewOpen} onDownloaded={doSearch} />)}
106 56 </td>
107 - </tr>
57 + </>
108 58 )
109 59 }
110 60
61 +interface TableHeadingCheckboxProps {
62 + availableIds: CloudSnippetSchema['id'][]
63 + selectedIds: Set<CloudSnippetSchema['id']>
64 + setSelectedIds: Dispatch<SetStateAction<Set<CloudSnippetSchema['id']>>>
65 +}
66 +
67 +/**
68 + * Selects every snippet on the page that can be downloaded. With nothing
69 + * downloadable the box is disabled and unticked: "all of nothing" must not
70 + * read as a selection.
71 + */
72 +const TableHeadingCheckbox: React.FC<TableHeadingCheckboxProps> = ({ availableIds, selectedIds, setSelectedIds }) =>
73 + <td className="column-cb check-column">
74 + <input
75 + id="cb-select-all-cloud-snippets"
76 + type="checkbox"
77 + checked={0 < availableIds.length && availableIds.every(snippetId => selectedIds.has(snippetId))}
78 + disabled={0 === availableIds.length}
79 + title={0 === availableIds.length ? __('Nothing on this page can be downloaded.', 'code-snippets') : undefined}
80 + onChange={event =>
81 + setSelectedIds(previous =>
82 + new Set(event.target.checked
83 + ? [...previous, ...availableIds]
84 + : [...previous].filter(snippetId => !availableIds.includes(snippetId)))
85 + )}
86 + aria-label={__('Select all downloadable snippets', 'code-snippets')}
87 + />
88 + </td>
89 +
90 +/** Why a snippet cannot be selected for download, or undefined when it can. */
91 +const unavailableReason = (snippet: CloudSnippetSchema): string | undefined => {
92 + if (snippet.local_id) {
93 + return __('Already in your library.', 'code-snippets')
94 + }
95 +
96 + return isCloudSnippetDownloadable(snippet)
97 + ? undefined
98 + : __('Requires Code Snippets Pro.', 'code-snippets')
99 +}
100 +
101 +interface TableRowCheckboxProps {
102 + snippet: CloudSnippetSchema
103 + selected: Set<CloudSnippetSchema['id']>
104 + setSelected: Dispatch<SetStateAction<Set<CloudSnippetSchema['id']>>>
105 +}
106 +
107 +/**
108 + * Every row shows a box, so the column reads as one control: a snippet that
109 + * cannot be downloaded gets a disabled box that says why.
110 + */
111 +const TableRowCheckbox: React.FC<TableRowCheckboxProps> = ({ snippet, selected, setSelected }) => {
112 + const reason = unavailableReason(snippet)
113 +
114 + return (
115 + <th scope="row" className="check-column">
116 + <input
117 + id={`cb-select-${snippet.id}`}
118 + type="checkbox"
119 + name="checked[]"
120 + checked={!reason && selected.has(snippet.id)}
121 + disabled={!!reason}
122 + title={reason}
123 + aria-label={reason
124 + // translators: 1: snippet name, 2: why it cannot be selected.
125 + ? sprintf(__('%1$s cannot be selected: %2$s', 'code-snippets'), snippet.name, reason)
126 + // translators: %s: snippet name.
127 + : sprintf(__('Select %s', 'code-snippets'), snippet.name)}
128 + onChange={event =>
129 + setSelected(previous =>
130 + new Set(event.target.checked
131 + ? [...previous, snippet.id]
132 + : [...previous].filter(snippetId => snippetId !== snippet.id))
133 + )}
134 + />
135 + </th>
136 + )
137 +}
138 +
111 139 export interface CloudSnippetsTableProps {
112 140 snippets: CloudSnippetSchema[]
113 - selected: Set<CloudSnippetId>
114 - setSelected: SetSelected
141 + selected?: Set<CloudSnippetSchema['id']>
142 + setSelected?: Dispatch<SetStateAction<Set<CloudSnippetSchema['id']>>>
115 143 }
116 144
117 145 /**
118 146 * Table view for lists of cloud snippets (community search results and
119 147 * bundle contents), mirroring the card actions. Descriptions are clamped
120 - * so all rows stay the same height.
148 + * so all rows stay the same height. Selection is only offered when the
149 + * containing view provides selection state, as bundle contents have no
150 + * bulk actions.
121 151 */
122 -export const CloudSnippetsTable: React.FC<CloudSnippetsTableProps> = ({ snippets, selected, setSelected }) =>
152 +export const CloudSnippetsTable: React.FC<CloudSnippetsTableProps> = ({
153 + snippets,
154 + selected,
155 + setSelected
156 +}) =>
123 157 <table className="wp-list-table widefat fixed striped cloud-snippets-table">
124 158 <thead>
125 159 <tr>
126 - <td className="column-cb check-column">
127 - <input
128 - id="cb-select-all-cloud-snippets"
129 - type="checkbox"
130 - checked={0 < snippets.length && snippets.every(snippet => selected.has(snippet.id))}
131 - aria-label={__('Select all snippets', 'code-snippets')}
132 - onChange={event =>
133 - updateSelection(setSelected, snippets.map(snippet => snippet.id), event.target.checked)}
134 - />
135 - </td>
160 + {selected && setSelected && (
161 + <TableHeadingCheckbox
162 + selectedIds={selected}
163 + setSelectedIds={setSelected}
164 + availableIds={snippets
165 + .filter(snippet => isCloudSnippetDownloadable(snippet))
166 + .map(snippet => snippet.id)}
167 + />)}
168 +
136 169 <th scope="col" className="column-name column-primary">{__('Name', 'code-snippets')}</th>
137 170 <th scope="col" className="column-type">{__('Type', 'code-snippets')}</th>
138 171 <th scope="col" className="column-status">{__('Status', 'code-snippets')}</th>
139 172 <th scope="col" className="column-desc">{__('Description', 'code-snippets')}</th>
@@ -143,7 +176,11 @@
143 176 </tr>
144 177 </thead>
145 178 <tbody>
146 179 {snippets.map(snippet =>
147 - <CloudSnippetRow key={snippet.id} {...{ snippet, selected, setSelected }} />)}
180 + <tr key={snippet.id}>
181 + {selected && setSelected && <TableRowCheckbox {...{ snippet, selected, setSelected }} />}
182 + <CloudSnippetRow snippet={snippet} />
183 + </tr>
184 + )}
148 185 </tbody>
149 186 </table>