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 +109 -80 3.10.0-beta.2 → trunk View file →
@@ -1,7 +1,7 @@
1 1 import { __, sprintf } from '@wordpress/i18n'
2 2 import React, { useState } from 'react'
3 -import { getSnippetType , isCloudSnippetDownloadable } 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'
@@ -10,75 +10,18 @@
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 - {isPreviewOpen && (
54 - <CloudSnippetPreviewModal snippet={snippet} setIsOpen={setIsPreviewOpen} onDownloaded={doSearch} />)}
55 - </>
56 - )
57 -}
58 -
59 -const CloudSnippetRow: React.FC<CloudSnippetRowProps> = ({ snippet, selected, setSelected }) => {
60 - const [isPreviewOpen, setIsPreviewOpen] = useState(false)
61 -
62 - return (
63 - <tr>
64 - {selected && setSelected
65 - ? <th scope="row" className="check-column">
66 - {isCloudSnippetDownloadable(snippet) && <input
67 - id={`cb-select-${snippet.id}`}
68 - type="checkbox"
69 - name="checked[]"
70 - checked={selected.has(snippet.id)}
71 - aria-label={sprintf(
72 - // translators: %s: snippet name.
73 - __('Select %s', 'code-snippets'),
74 - snippet.name
75 - )}
76 - onChange={event => updateSelection(setSelected, [snippet.id], event.target.checked)}
77 - />}
78 - </th>
79 - : null}
80 -
81 24 <td className="column-name column-primary">
82 25 <strong>
83 26 <button
84 27 type="button"
@@ -99,18 +42,105 @@
99 42 <div className="cloud-table-description">{truncateChars(stripTags(snippet.description))}</div>
100 43 </td>
101 44
102 45 <td className="column-actions">
103 - <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} />)}
104 56 </td>
105 - </tr>
57 + </>
106 58 )
107 59 }
108 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 +
109 139 export interface CloudSnippetsTableProps {
110 140 snippets: CloudSnippetSchema[]
111 - selected?: Set<CloudSnippetId>
112 - setSelected?: SetSelected
141 + selected?: Set<CloudSnippetSchema['id']>
142 + setSelected?: Dispatch<SetStateAction<Set<CloudSnippetSchema['id']>>>
113 143 }
114 144
115 145 /**
116 146 * Table view for lists of cloud snippets (community search results and
@@ -122,25 +152,21 @@
122 152 export const CloudSnippetsTable: React.FC<CloudSnippetsTableProps> = ({
123 153 snippets,
124 154 selected,
125 155 setSelected
126 -}) => {
127 - const downloadable = snippets.filter(snippet => isCloudSnippetDownloadable(snippet))
128 -
129 - return <table className="wp-list-table widefat fixed striped cloud-snippets-table">
156 +}) =>
157 + <table className="wp-list-table widefat fixed striped cloud-snippets-table">
130 158 <thead>
131 159 <tr>
132 160 {selected && setSelected && (
133 - <td className="column-cb check-column">
134 - <input
135 - id="cb-select-all-cloud-snippets"
136 - type="checkbox"
137 - checked={0 < downloadable.length && downloadable.every(snippet => selected.has(snippet.id))}
138 - aria-label={__('Select all snippets', 'code-snippets')}
139 - onChange={event =>
140 - updateSelection(setSelected, downloadable.map(snippet => snippet.id), event.target.checked)}
141 - />
142 - </td>)}
161 + <TableHeadingCheckbox
162 + selectedIds={selected}
163 + setSelectedIds={setSelected}
164 + availableIds={snippets
165 + .filter(snippet => isCloudSnippetDownloadable(snippet))
166 + .map(snippet => snippet.id)}
167 + />)}
168 +
143 169 <th scope="col" className="column-name column-primary">{__('Name', 'code-snippets')}</th>
144 170 <th scope="col" className="column-type">{__('Type', 'code-snippets')}</th>
145 171 <th scope="col" className="column-status">{__('Status', 'code-snippets')}</th>
146 172 <th scope="col" className="column-desc">{__('Description', 'code-snippets')}</th>
@@ -150,8 +176,11 @@
150 176 </tr>
151 177 </thead>
152 178 <tbody>
153 179 {snippets.map(snippet =>
154 - <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 + )}
155 185 </tbody>
156 186 </table>
157 -}