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/CloudSearch.tsx +127 -88 3.10.0-beta.1 → trunk View file →
@@ -1,19 +1,20 @@
1 1 import { __ } from '@wordpress/i18n'
2 -import React, { useEffect, useState } from 'react'
2 +import React, { useEffect, useId, useState } from 'react'
3 3 import classnames from 'classnames'
4 4 import { Spinner } from '@wordpress/components'
5 5 import { useRestAPI } from '../../../hooks/useRestAPI'
6 6 import { REST_BASES } from '../../../utils/restAPI'
7 -import { isLicensed } from '../../../utils/screen'
8 -import { isProSnippet } from '../../../utils/snippets/snippets'
7 +import { isCloudSnippetDownloadable } from '../../../utils/snippets/snippets'
9 8 import { TableNav } from '../../common/ListTable/TableNavigation'
10 9 import { LoadingStatusNotices } from '../../common/LoadingStatusNotices'
11 10 import { SnippetViewToggle } from '../../common/SnippetViewToggle'
11 +import { CloudSnippetAuthor, CloudSnippetCard } from '../../common/cloud/CloudSnippetCard'
12 12 import { CloudSnippetsTable } from './CloudSnippetsTable'
13 -import { CloudSnippetAuthor, SearchResult } from './SearchResult'
14 13 import { useCloudSearch } from './WithCloudSearchContext'
15 14 import { SearchFilters } from './SearchFilters'
15 +import type { CloudSearchResults } from './WithCloudSearchContext'
16 +import type { TableNavProps } from '../../common/ListTable/TableNavigation'
16 17 import type { CloudSnippetSchema } from '../../../types/schema/CloudSnippetSchema'
17 18 import type { SnippetView } from '../../../types/SnippetView'
18 19 import type { ListTableAction } from '../../common/ListTable'
19 20 import type { Dispatch, FormEventHandler, SetStateAction } from 'react'
@@ -19,21 +20,23 @@
19 20 import type { Dispatch, FormEventHandler, SetStateAction } from 'react'
20 21
21 22 const SearchBox = () => {
22 23 const { searchParams, updateSearchParams, isSearching, doSearch } = useCloudSearch()
24 + const searchMethodId = useId()
25 + const [query, setQuery] = useState(searchParams.query)
23 26
24 27 const handleSubmit: FormEventHandler<HTMLFormElement> = event => {
25 28 event.preventDefault()
26 - doSearch()
29 + doSearch({ query })
27 30 }
28 31
29 32 return (
30 33 <form className="cloud-search-form" onSubmit={handleSubmit}>
31 - <label className="screen-reader-text" htmlFor="cloud-search-method">
34 + <label htmlFor={searchMethodId} className="screen-reader-text">
32 35 {__('Search method', 'code-snippets')}
33 36 </label>
34 37 <select
35 - id="cloud-search-method"
38 + id={searchMethodId}
36 39 value={searchParams.method}
37 40 onChange={event =>
38 41 updateSearchParams({ method: 'codevault' === event.target.value ? 'codevault' : 'term' })}
39 42 >
@@ -44,17 +47,17 @@
44 47 <div className="cloud-search-query">
45 48 <input
46 49 id="cloud-search-query"
47 50 type="search"
48 - value={searchParams.query}
51 + value={query}
49 52 aria-label={__('Search query', 'code-snippets')}
50 - onChange={event => updateSearchParams({ query: event.target.value })}
53 + onChange={event => setQuery(event.target.value)}
51 54 placeholder={__('e.g. Remove unused JavaScript…', 'code-snippets')}
52 55 />
53 - <span role="status" aria-live="polite">
54 - {isSearching &&
55 - <span className="screen-reader-text">{__('Searching…', 'code-snippets')}</span>}
56 - </span>
56 + {isSearching && (
57 + <span role="status" aria-live="polite">
58 + <span className="screen-reader-text">{__('Searching…', 'code-snippets')}</span>
59 + </span>)}
57 60 </div>
58 61
59 62 <button
60 63 type="submit"
@@ -66,11 +69,8 @@
66 69 </form>
67 70 )
68 71 }
69 72
70 -const isSnippetDownloadable = (snippet: CloudSnippetSchema): boolean =>
71 - !snippet.local_id && (isLicensed() || !isProSnippet(snippet))
72 -
73 73 interface SearchResultsGridProps {
74 74 snippets: CloudSnippetSchema[]
75 75 selected: Set<CloudSnippetSchema['id']>
76 76 setSelected: Dispatch<SetStateAction<Set<CloudSnippetSchema['id']>>>
@@ -75,36 +75,44 @@
75 75 selected: Set<CloudSnippetSchema['id']>
76 76 setSelected: Dispatch<SetStateAction<Set<CloudSnippetSchema['id']>>>
77 77 }
78 78
79 -const SearchResultsGrid: React.FC<SearchResultsGridProps> = ({ snippets, selected, setSelected }) =>
80 - <ul
81 - className={classnames('cloud-search-results', 'code-snippets-cards', {
82 - 'has-selection': snippets.some(snippet => selected.has(snippet.id))
83 - })}
84 - >
85 - {snippets.map(result =>
86 - <SearchResult
87 - key={result.id}
88 - snippet={result}
89 - author={<CloudSnippetAuthor codevaultSlug={result.codevault} />}
90 - isSelected={selected.has(result.id)}
91 - onSelectedChange={isSelected => {
92 - setSelected(previous => {
93 - const updated = new Set(previous)
79 +const SearchResultsGrid: React.FC<SearchResultsGridProps> = ({ snippets, selected, setSelected }) => {
80 + const { doSearch } = useCloudSearch()
94 81
95 - if (isSelected) {
96 - updated.add(result.id)
97 - } else {
98 - updated.delete(result.id)
82 + return (
83 + <ul
84 + className={classnames('cloud-search-results', 'code-snippets-cards', {
85 + 'has-selection': snippets.some(snippet => selected.has(snippet.id))
86 + })}
87 + >
88 + {snippets.map(result =>
89 + <CloudSnippetCard
90 + key={result.id}
91 + snippet={result}
92 + author={<CloudSnippetAuthor codevaultSlug={result.codevault} />}
93 + isSelected={selected.has(result.id)}
94 + onSnippetDownloaded={() => doSearch()}
95 + onSelectedChange={isCloudSnippetDownloadable(result)
96 + ? isSelected => {
97 + setSelected(previous => {
98 + const updated = new Set(previous)
99 +
100 + if (isSelected) {
101 + updated.add(result.id)
102 + } else {
103 + updated.delete(result.id)
104 + }
105 +
106 + return updated
107 + })
99 108 }
109 + : undefined}
110 + />)}
111 + </ul>
112 + )
113 +}
100 114
101 - return updated
102 - })
103 - }}
104 - />)}
105 - </ul>
106 -
107 115 interface SearchResultsViewProps {
108 116 snippetView: SnippetView
109 117 setSnippetView: (view: SnippetView) => void
110 118 }
@@ -114,24 +122,76 @@
114 122 const CLOUD_BULK_ACTIONS: ListTableAction<CloudSearchAction>[] = [
115 123 { key: 'download', label: __('Download', 'code-snippets') }
116 124 ]
117 125
118 -const useSearchResultsSelection = () => {
119 - const { api } = useRestAPI()
120 - const { searchResults, isSearching, doSearch } = useCloudSearch()
126 +interface CloudSearchSnippetsProps {
127 + snippetView: SnippetView
128 + snippets: CloudSnippetSchema[]
129 + selected: Set<CloudSnippetSchema['id']>
130 + setSelected: Dispatch<SetStateAction<Set<CloudSnippetSchema['id']>>>
131 +}
132 +
133 +const CloudSearchSnippets: React.FC<CloudSearchSnippetsProps> = ({ snippetView, ...props }) =>
134 + 'card' === snippetView
135 + ? <SearchResultsGrid {...props} />
136 + : <CloudSnippetsTable {...props} />
137 +
138 +interface SearchResultsTableNavProps extends Omit<TableNavProps<CloudSnippetSchema['id'], CloudSearchAction>,
139 + 'totalItems' | 'totalPages' | 'currentPage' | 'setCurrentPage'> {
140 + which: 'top' | 'bottom'
141 + doSearch: ReturnType<typeof useCloudSearch>['doSearch']
142 + isSearching: boolean
143 + searchResults: CloudSearchResults
144 +}
145 +
146 +const SearchResultsTableNav: React.FC<SearchResultsTableNavProps> = ({
147 + which,
148 + doSearch,
149 + isSearching,
150 + searchResults,
151 + ...props
152 +}) =>
153 + <TableNav
154 + which={which}
155 + disabled={isSearching}
156 + totalItems={searchResults.totalItems}
157 + totalPages={searchResults.totalPages}
158 + currentPage={searchResults.page}
159 + setCurrentPage={page => doSearch({ page })}
160 + {...props}
161 + />
162 +
163 +/**
164 + * Bulk selection for the current page of results. A new set of results replaces
165 + * the rows the selection referred to, so nothing carries over: a stale
166 + * selection would let a bulk action act on snippets no longer on screen.
167 + */
168 +const useResultsSelection = (snippets: CloudSnippetSchema[] | undefined) => {
121 169 const [selected, setSelected] = useState<Set<CloudSnippetSchema['id']>>(new Set())
122 170
123 171 useEffect(() => {
124 172 setSelected(new Set())
125 - }, [searchResults?.snippets])
173 + }, [snippets])
126 174
175 + return { selected, setSelected }
176 +}
177 +
178 +const SearchResultsTable: React.FC<SearchResultsViewProps> = ({ snippetView, setSnippetView }) => {
179 + const { api } = useRestAPI()
180 + const { searchResults, isSearching, doSearch } = useCloudSearch()
181 + const { selected, setSelected } = useResultsSelection(searchResults?.snippets)
182 +
183 + if (!searchResults) {
184 + return null
185 + }
186 +
127 187 const doAction = async (
128 188 action: CloudSearchAction | undefined,
129 189 selectedIds: Set<CloudSnippetSchema['id']>
130 190 ): Promise<void> => {
131 191 if ('download' === action) {
132 - await Promise.all((searchResults?.snippets ?? [])
133 - .filter(snippet => selectedIds.has(snippet.id) && isSnippetDownloadable(snippet))
192 + await Promise.all(searchResults.snippets
193 + .filter(snippet => selectedIds.has(snippet.id) && isCloudSnippetDownloadable(snippet))
134 194 .map(({ id }) => api.post(`${REST_BASES.cloud.snippets}/${id}/download`)))
135 195
136 196 doSearch()
137 197 }
@@ -136,59 +196,38 @@
136 196 doSearch()
137 197 }
138 198 }
139 199
140 - return { doAction, doSearch, isSearching, searchResults, selected, setSelected }
141 -}
142 -
143 -const SearchResultsTable: React.FC<SearchResultsViewProps> = ({ snippetView, setSnippetView }) => {
144 - const { doAction, doSearch, isSearching, searchResults, selected, setSelected } =
145 - useSearchResultsSelection()
146 -
147 - if (!searchResults) {
148 - return null
149 - }
150 -
151 - const { totalItems, totalPages, page } = searchResults
152 -
153 - const navProps = {
154 - totalItems,
155 - totalPages,
156 - selected,
157 - setSelected,
158 - disabled: isSearching,
159 - currentPage: page,
160 - setCurrentPage: (newPage: number) => doSearch({ page: newPage })
161 - }
162 -
163 200 return (
164 201 <div className="snippets-list-view">
165 - <TableNav
202 + <SearchResultsTableNav
166 203 which="top"
204 + {...{ isSearching, searchResults, selected, setSelected, doSearch }}
167 205 actions={CLOUD_BULK_ACTIONS}
168 206 doAction={doAction}
169 - selectAllKeys={searchResults.snippets.map(snippet => snippet.id)}
207 + selectAllKeys={'card' === snippetView
208 + ? searchResults.snippets
209 + .filter(snippet => isCloudSnippetDownloadable(snippet))
210 + .map(snippet => snippet.id)
211 + : undefined}
170 212 extraTableNav={() => <SearchFilters />}
171 213 endTableNav={which =>
172 214 'top' === which
173 215 ? <SnippetViewToggle snippetView={snippetView} setSnippetView={setSnippetView} />
174 216 : null}
175 - {...navProps}
176 217 />
177 218
178 - {'card' === snippetView
179 - ? <SearchResultsGrid
180 - snippets={searchResults.snippets}
181 - selected={selected}
182 - setSelected={setSelected}
183 - />
184 - : <CloudSnippetsTable
185 - snippets={searchResults.snippets}
186 - selected={selected}
187 - setSelected={setSelected}
188 - />}
219 + <CloudSearchSnippets
220 + snippets={searchResults.snippets}
221 + snippetView={snippetView}
222 + selected={selected}
223 + setSelected={setSelected}
224 + />
189 225
190 - <TableNav which="bottom" {...navProps} />
226 + <SearchResultsTableNav
227 + which="bottom"
228 + {...{ isSearching, searchResults, selected, setSelected, doSearch }}
229 + />
191 230 </div>
192 231 )
193 232 }
194 233
@@ -212,10 +251,10 @@
212 251
213 252 return searchResults.snippets.length
214 253 ? <>
215 254 {searchResults.isFeatured
216 - ? <h3 className="cloud-featured-heading">{__('Featured Snippets', 'code-snippets')}</h3>
217 - : <h3 className="cloud-snippets-heading">{__('Search Results', 'code-snippets')}</h3>}
255 + ? <h2 className="cloud-featured-heading">{__('Featured Snippets', 'code-snippets')}</h2>
256 + : <h2 className="cloud-snippets-heading">{__('Search Results', 'code-snippets')}</h2>}
218 257 <SearchResultsTable snippetView={snippetView} setSnippetView={setSnippetView} />
219 258 </>
220 259 : null
221 260 }
@@ -228,9 +267,9 @@
228 267 isLoading={isLoading}
229 268 errorMessage={isErrored
230 269 ? __('An error occurred while fetching search results. Please try again.', 'code-snippets')
231 270 : undefined}
232 - loadingNotice={__('Loading community snippets…', 'code-snippets')}
271 + loadingNotice={__('Loading snippets from cloud…', 'code-snippets')}
233 272 noticeLabel={__('Community snippets status', 'code-snippets')}
234 273 />
235 274 : null
236 275 }