PluginProbe
Code Snippets / 3.10.0-beta.2
Code Snippets v3.10.0-beta.2
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 3.10.0-beta.2, at js/components/ManageMenu/CommunityCloud/CloudSearch.tsx

253 lines 7.7 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 { CloudSnippetsTable } from './CloudSnippetsTable'
12 import { CloudSnippetAuthor, SearchResult } from './SearchResult'
13 import { useCloudSearch } from './WithCloudSearchContext'
14 import { SearchFilters } from './SearchFilters'
15 import type { CloudSnippetSchema } from '../../../types/schema/CloudSnippetSchema'
16 import type { SnippetView } from '../../../types/SnippetView'
17 import type { ListTableAction } from '../../common/ListTable'
18 import type { Dispatch, FormEventHandler, SetStateAction } from 'react'
19
20 const SearchBox = () => {
21 const { searchParams, updateSearchParams, isSearching, doSearch } = useCloudSearch()
22 const searchMethodId = useId()
23 const [query, setQuery] = useState(searchParams.query)
24
25 const handleSubmit: FormEventHandler<HTMLFormElement> = event => {
26 event.preventDefault()
27 doSearch({ query })
28 }
29
30 return (
31 <form className="cloud-search-form" onSubmit={handleSubmit}>
32 <label htmlFor={searchMethodId} className="screen-reader-text">
33 {__('Search method', 'code-snippets')}
34 </label>
35 <select
36 id={searchMethodId}
37 value={searchParams.method}
38 onChange={event =>
39 updateSearchParams({ method: 'codevault' === event.target.value ? 'codevault' : 'term' })}
40 >
41 <option value="term">{__('Search by keyword', 'code-snippets')}</option>
42 <option value="codevault">{__('Name of library', 'code-snippets')}</option>
43 </select>
44
45 <div className="cloud-search-query">
46 <input
47 id="cloud-search-query"
48 type="search"
49 value={query}
50 aria-label={__('Search query', 'code-snippets')}
51 onChange={event => setQuery(event.target.value)}
52 placeholder={__('e.g. Remove unused JavaScript…', 'code-snippets')}
53 />
54 {isSearching && (
55 <span role="status" aria-live="polite">
56 <span className="screen-reader-text">{__('Searching…', 'code-snippets')}</span>
57 </span>)}
58 </div>
59
60 <button
61 type="submit"
62 className="button button-primary cloud-search-submit"
63 disabled={isSearching}
64 >
65 {isSearching ? <Spinner /> : __('Search Cloud Library', 'code-snippets')}
66 </button>
67 </form>
68 )
69 }
70
71 interface SearchResultsGridProps {
72 snippets: CloudSnippetSchema[]
73 selected: Set<CloudSnippetSchema['id']>
74 setSelected: Dispatch<SetStateAction<Set<CloudSnippetSchema['id']>>>
75 }
76
77 const SearchResultsGrid: React.FC<SearchResultsGridProps> = ({ snippets, selected, setSelected }) =>
78 <ul
79 className={classnames('cloud-search-results', 'code-snippets-cards', {
80 'has-selection': snippets.some(snippet => selected.has(snippet.id))
81 })}
82 >
83 {snippets.map(result =>
84 <SearchResult
85 key={result.id}
86 snippet={result}
87 author={<CloudSnippetAuthor codevaultSlug={result.codevault} />}
88 isSelected={selected.has(result.id)}
89 onSelectedChange={isCloudSnippetDownloadable(result)
90 ? isSelected => {
91 setSelected(previous => {
92 const updated = new Set(previous)
93
94 if (isSelected) {
95 updated.add(result.id)
96 } else {
97 updated.delete(result.id)
98 }
99
100 return updated
101 })
102 }
103 : undefined}
104 />)}
105 </ul>
106
107 interface SearchResultsViewProps {
108 snippetView: SnippetView
109 setSnippetView: (view: SnippetView) => void
110 }
111
112 type CloudSearchAction = 'download'
113
114 const CLOUD_BULK_ACTIONS: ListTableAction<CloudSearchAction>[] = [
115 { key: 'download', label: __('Download', 'code-snippets') }
116 ]
117
118 const useSearchResultsSelection = () => {
119 const { api } = useRestAPI()
120 const { searchResults, isSearching, doSearch } = useCloudSearch()
121 const [selected, setSelected] = useState<Set<CloudSnippetSchema['id']>>(new Set())
122
123 useEffect(() => {
124 setSelected(new Set())
125 }, [searchResults?.snippets])
126
127 const doAction = async (
128 action: CloudSearchAction | undefined,
129 selectedIds: Set<CloudSnippetSchema['id']>
130 ): Promise<void> => {
131 if ('download' === action) {
132 await Promise.all((searchResults?.snippets ?? [])
133 .filter(snippet => selectedIds.has(snippet.id) && isCloudSnippetDownloadable(snippet))
134 .map(({ id }) => api.post(`${REST_BASES.cloud.snippets}/${id}/download`)))
135
136 doSearch()
137 }
138 }
139
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 return (
164 <div className="snippets-list-view">
165 <TableNav
166 which="top"
167 actions={CLOUD_BULK_ACTIONS}
168 doAction={doAction}
169 selectAllKeys={'card' === snippetView
170 ? searchResults.snippets
171 .filter(snippet => isCloudSnippetDownloadable(snippet))
172 .map(snippet => snippet.id)
173 : undefined}
174 extraTableNav={() => <SearchFilters />}
175 endTableNav={which =>
176 'top' === which
177 ? <SnippetViewToggle snippetView={snippetView} setSnippetView={setSnippetView} />
178 : null}
179 {...navProps}
180 />
181
182 {'card' === snippetView
183 ? <SearchResultsGrid
184 snippets={searchResults.snippets}
185 selected={selected}
186 setSelected={setSelected}
187 />
188 : <CloudSnippetsTable
189 snippets={searchResults.snippets}
190 selected={selected}
191 setSelected={setSelected}
192 />}
193
194 <TableNav which="bottom" {...navProps} />
195 </div>
196 )
197 }
198
199 const SearchResults: React.FC<SearchResultsViewProps> = ({ snippetView, setSnippetView }) => {
200 const { searchResults, searchParams } = useCloudSearch()
201
202 if (!searchResults) {
203 return null
204 }
205
206 if (0 < searchResults.page && 0 === searchResults.snippets.length) {
207 return (
208 <div className="banner banner-neutral no-results">
209 <p>{'codevault' === searchParams.method
210 ? __('Could not find a codevault with that name. Please try again.', 'code-snippets')
211 : __('No snippets could be found with that search term. Please try again.', 'code-snippets')
212 }</p>
213 </div>
214 )
215 }
216
217 return searchResults.snippets.length
218 ? <>
219 {searchResults.isFeatured
220 ? <h2 className="cloud-featured-heading">{__('Featured Snippets', 'code-snippets')}</h2>
221 : <h2 className="cloud-snippets-heading">{__('Search Results', 'code-snippets')}</h2>}
222 <SearchResultsTable snippetView={snippetView} setSnippetView={setSnippetView} />
223 </>
224 : null
225 }
226
227 const SearchStatus = () => {
228 const { isErrored, isLoading } = useCloudSearch()
229
230 return isErrored || isLoading
231 ? <LoadingStatusNotices
232 isLoading={isLoading}
233 errorMessage={isErrored
234 ? __('An error occurred while fetching search results. Please try again.', 'code-snippets')
235 : undefined}
236 loadingNotice={__('Loading snippets from cloud…', 'code-snippets')}
237 noticeLabel={__('Community snippets status', 'code-snippets')}
238 />
239 : null
240 }
241
242 export interface CloudSearchProps {
243 snippetView: SnippetView
244 setSnippetView: (view: SnippetView) => void
245 }
246
247 export const CloudSearch: React.FC<CloudSearchProps> = ({ snippetView, setSnippetView }) =>
248 <div className="cloud-search">
249 <SearchBox />
250 <SearchStatus />
251 <SearchResults snippetView={snippetView} setSnippetView={setSnippetView} />
252 </div>
253