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
code-snippets / js / components / ManageMenu / CommunityCloud / CloudSearch.tsx

CloudSearch.tsx in Code Snippets trunk, at js/components/ManageMenu/CommunityCloud/CloudSearch.tsx

288 lines 9.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { __ } from '@wordpress/i18n'
2 import React, { useEffect, useId, useState } from 'react'
3 import classnames from 'classnames'
4 import { Spinner } from '@wordpress/components'
5 import { useRestAPI } from '../../../hooks/useRestAPI'
6 import { REST_BASES } from '../../../utils/restAPI'
7 import { isCloudSnippetDownloadable } from '../../../utils/snippets/snippets'
8 import { TableNav } from '../../common/ListTable/TableNavigation'
9 import { LoadingStatusNotices } from '../../common/LoadingStatusNotices'
10 import { SnippetViewToggle } from '../../common/SnippetViewToggle'
11 import { CloudSnippetAuthor, CloudSnippetCard } from '../../common/cloud/CloudSnippetCard'
12 import { CloudSnippetsTable } from './CloudSnippetsTable'
13 import { useCloudSearch } from './WithCloudSearchContext'
14 import { SearchFilters } from './SearchFilters'
15 import type { CloudSearchResults } from './WithCloudSearchContext'
16 import type { TableNavProps } from '../../common/ListTable/TableNavigation'
17 import type { CloudSnippetSchema } from '../../../types/schema/CloudSnippetSchema'
18 import type { SnippetView } from '../../../types/SnippetView'
19 import type { ListTableAction } from '../../common/ListTable'
20 import type { Dispatch, FormEventHandler, SetStateAction } from 'react'
21
22 const SearchBox = () => {
23 const { searchParams, updateSearchParams, isSearching, doSearch } = useCloudSearch()
24 const searchMethodId = useId()
25 const [query, setQuery] = useState(searchParams.query)
26
27 const handleSubmit: FormEventHandler<HTMLFormElement> = event => {
28 event.preventDefault()
29 doSearch({ query })
30 }
31
32 return (
33 <form className="cloud-search-form" onSubmit={handleSubmit}>
34 <label htmlFor={searchMethodId} className="screen-reader-text">
35 {__('Search method', 'code-snippets')}
36 </label>
37 <select
38 id={searchMethodId}
39 value={searchParams.method}
40 onChange={event =>
41 updateSearchParams({ method: 'codevault' === event.target.value ? 'codevault' : 'term' })}
42 >
43 <option value="term">{__('Search by keyword', 'code-snippets')}</option>
44 <option value="codevault">{__('Name of library', 'code-snippets')}</option>
45 </select>
46
47 <div className="cloud-search-query">
48 <input
49 id="cloud-search-query"
50 type="search"
51 value={query}
52 aria-label={__('Search query', 'code-snippets')}
53 onChange={event => setQuery(event.target.value)}
54 placeholder={__('e.g. Remove unused JavaScript…', 'code-snippets')}
55 />
56 {isSearching && (
57 <span role="status" aria-live="polite">
58 <span className="screen-reader-text">{__('Searching…', 'code-snippets')}</span>
59 </span>)}
60 </div>
61
62 <button
63 type="submit"
64 className="button button-primary cloud-search-submit"
65 disabled={isSearching}
66 >
67 {isSearching ? <Spinner /> : __('Search Cloud Library', 'code-snippets')}
68 </button>
69 </form>
70 )
71 }
72
73 interface SearchResultsGridProps {
74 snippets: CloudSnippetSchema[]
75 selected: Set<CloudSnippetSchema['id']>
76 setSelected: Dispatch<SetStateAction<Set<CloudSnippetSchema['id']>>>
77 }
78
79 const SearchResultsGrid: React.FC<SearchResultsGridProps> = ({ snippets, selected, setSelected }) => {
80 const { doSearch } = useCloudSearch()
81
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 })
108 }
109 : undefined}
110 />)}
111 </ul>
112 )
113 }
114
115 interface SearchResultsViewProps {
116 snippetView: SnippetView
117 setSnippetView: (view: SnippetView) => void
118 }
119
120 type CloudSearchAction = 'download'
121
122 const CLOUD_BULK_ACTIONS: ListTableAction<CloudSearchAction>[] = [
123 { key: 'download', label: __('Download', 'code-snippets') }
124 ]
125
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) => {
169 const [selected, setSelected] = useState<Set<CloudSnippetSchema['id']>>(new Set())
170
171 useEffect(() => {
172 setSelected(new Set())
173 }, [snippets])
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
187 const doAction = async (
188 action: CloudSearchAction | undefined,
189 selectedIds: Set<CloudSnippetSchema['id']>
190 ): Promise<void> => {
191 if ('download' === action) {
192 await Promise.all(searchResults.snippets
193 .filter(snippet => selectedIds.has(snippet.id) && isCloudSnippetDownloadable(snippet))
194 .map(({ id }) => api.post(`${REST_BASES.cloud.snippets}/${id}/download`)))
195
196 doSearch()
197 }
198 }
199
200 return (
201 <div className="snippets-list-view">
202 <SearchResultsTableNav
203 which="top"
204 {...{ isSearching, searchResults, selected, setSelected, doSearch }}
205 actions={CLOUD_BULK_ACTIONS}
206 doAction={doAction}
207 selectAllKeys={'card' === snippetView
208 ? searchResults.snippets
209 .filter(snippet => isCloudSnippetDownloadable(snippet))
210 .map(snippet => snippet.id)
211 : undefined}
212 extraTableNav={() => <SearchFilters />}
213 endTableNav={which =>
214 'top' === which
215 ? <SnippetViewToggle snippetView={snippetView} setSnippetView={setSnippetView} />
216 : null}
217 />
218
219 <CloudSearchSnippets
220 snippets={searchResults.snippets}
221 snippetView={snippetView}
222 selected={selected}
223 setSelected={setSelected}
224 />
225
226 <SearchResultsTableNav
227 which="bottom"
228 {...{ isSearching, searchResults, selected, setSelected, doSearch }}
229 />
230 </div>
231 )
232 }
233
234 const SearchResults: React.FC<SearchResultsViewProps> = ({ snippetView, setSnippetView }) => {
235 const { searchResults, searchParams } = useCloudSearch()
236
237 if (!searchResults) {
238 return null
239 }
240
241 if (0 < searchResults.page && 0 === searchResults.snippets.length) {
242 return (
243 <div className="banner banner-neutral no-results">
244 <p>{'codevault' === searchParams.method
245 ? __('Could not find a codevault with that name. Please try again.', 'code-snippets')
246 : __('No snippets could be found with that search term. Please try again.', 'code-snippets')
247 }</p>
248 </div>
249 )
250 }
251
252 return searchResults.snippets.length
253 ? <>
254 {searchResults.isFeatured
255 ? <h2 className="cloud-featured-heading">{__('Featured Snippets', 'code-snippets')}</h2>
256 : <h2 className="cloud-snippets-heading">{__('Search Results', 'code-snippets')}</h2>}
257 <SearchResultsTable snippetView={snippetView} setSnippetView={setSnippetView} />
258 </>
259 : null
260 }
261
262 const SearchStatus = () => {
263 const { isErrored, isLoading } = useCloudSearch()
264
265 return isErrored || isLoading
266 ? <LoadingStatusNotices
267 isLoading={isLoading}
268 errorMessage={isErrored
269 ? __('An error occurred while fetching search results. Please try again.', 'code-snippets')
270 : undefined}
271 loadingNotice={__('Loading snippets from cloud…', 'code-snippets')}
272 noticeLabel={__('Community snippets status', 'code-snippets')}
273 />
274 : null
275 }
276
277 export interface CloudSearchProps {
278 snippetView: SnippetView
279 setSnippetView: (view: SnippetView) => void
280 }
281
282 export const CloudSearch: React.FC<CloudSearchProps> = ({ snippetView, setSnippetView }) =>
283 <div className="cloud-search">
284 <SearchBox />
285 <SearchStatus />
286 <SearchResults snippetView={snippetView} setSnippetView={setSnippetView} />
287 </div>
288