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

267 lines 8.3 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, { 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 { 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 <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={isCloudSnippetDownloadable(result)
92 ? isSelected => {
93 setSelected(previous => {
94 const updated = new Set(previous)
95
96 if (isSelected) {
97 updated.add(result.id)
98 } else {
99 updated.delete(result.id)
100 }
101
102 return updated
103 })
104 }
105 : undefined}
106 />)}
107 </ul>
108
109 interface SearchResultsViewProps {
110 snippetView: SnippetView
111 setSnippetView: (view: SnippetView) => void
112 }
113
114 type CloudSearchAction = 'download'
115
116 const CLOUD_BULK_ACTIONS: ListTableAction<CloudSearchAction>[] = [
117 { key: 'download', label: __('Download', 'code-snippets') }
118 ]
119
120 interface CloudSearchSnippetsProps {
121 snippetView: SnippetView
122 snippets: CloudSnippetSchema[]
123 selected: Set<CloudSnippetSchema['id']>
124 setSelected: Dispatch<SetStateAction<Set<CloudSnippetSchema['id']>>>
125 }
126
127 const CloudSearchSnippets: React.FC<CloudSearchSnippetsProps> = ({ snippetView, ...props }) =>
128 'card' === snippetView
129 ? <SearchResultsGrid {...props} />
130 : <CloudSnippetsTable {...props} />
131
132 interface SearchResultsTableNavProps extends Omit<TableNavProps<CloudSnippetSchema['id'], CloudSearchAction>,
133 'totalItems' | 'totalPages' | 'currentPage' | 'setCurrentPage'> {
134 which: 'top' | 'bottom'
135 doSearch: ReturnType<typeof useCloudSearch>['doSearch']
136 isSearching: boolean
137 searchResults: CloudSearchResults
138 }
139
140 const SearchResultsTableNav: React.FC<SearchResultsTableNavProps> = ({
141 which,
142 doSearch,
143 isSearching,
144 searchResults,
145 ...props
146 }) =>
147 <TableNav
148 which={which}
149 disabled={isSearching}
150 totalItems={searchResults.totalItems}
151 totalPages={searchResults.totalPages}
152 currentPage={searchResults.page}
153 setCurrentPage={page => doSearch({ page })}
154 {...props}
155 />
156
157 const SearchResultsTable: React.FC<SearchResultsViewProps> = ({ snippetView, setSnippetView }) => {
158 const { api } = useRestAPI()
159 const { searchResults, isSearching, doSearch } = useCloudSearch()
160 const [selected, setSelected] = useState<Set<CloudSnippetSchema['id']>>(new Set())
161
162 if (!searchResults) {
163 return null
164 }
165
166 const doAction = async (
167 action: CloudSearchAction | undefined,
168 selectedIds: Set<CloudSnippetSchema['id']>
169 ): Promise<void> => {
170 if ('download' === action) {
171 await Promise.all(searchResults.snippets
172 .filter(snippet => selectedIds.has(snippet.id) && isCloudSnippetDownloadable(snippet))
173 .map(({ id }) => api.post(`${REST_BASES.cloud.snippets}/${id}/download`)))
174
175 doSearch()
176 }
177 }
178
179 return (
180 <div className="snippets-list-view">
181 <SearchResultsTableNav
182 which="top"
183 {...{ isSearching, searchResults, selected, setSelected, doSearch }}
184 actions={CLOUD_BULK_ACTIONS}
185 doAction={doAction}
186 selectAllKeys={'card' === snippetView
187 ? searchResults.snippets
188 .filter(snippet => isCloudSnippetDownloadable(snippet))
189 .map(snippet => snippet.id)
190 : undefined}
191 extraTableNav={() => <SearchFilters />}
192 endTableNav={which =>
193 'top' === which
194 ? <SnippetViewToggle snippetView={snippetView} setSnippetView={setSnippetView} />
195 : null}
196 />
197
198 <CloudSearchSnippets
199 snippets={searchResults.snippets}
200 snippetView={snippetView}
201 selected={selected}
202 setSelected={setSelected}
203 />
204
205 <SearchResultsTableNav
206 which="bottom"
207 {...{ isSearching, searchResults, selected, setSelected, doSearch }}
208 />
209 </div>
210 )
211 }
212
213 const SearchResults: React.FC<SearchResultsViewProps> = ({ snippetView, setSnippetView }) => {
214 const { searchResults, searchParams } = useCloudSearch()
215
216 if (!searchResults) {
217 return null
218 }
219
220 if (0 < searchResults.page && 0 === searchResults.snippets.length) {
221 return (
222 <div className="banner banner-neutral no-results">
223 <p>{'codevault' === searchParams.method
224 ? __('Could not find a codevault with that name. Please try again.', 'code-snippets')
225 : __('No snippets could be found with that search term. Please try again.', 'code-snippets')
226 }</p>
227 </div>
228 )
229 }
230
231 return searchResults.snippets.length
232 ? <>
233 {searchResults.isFeatured
234 ? <h2 className="cloud-featured-heading">{__('Featured Snippets', 'code-snippets')}</h2>
235 : <h2 className="cloud-snippets-heading">{__('Search Results', 'code-snippets')}</h2>}
236 <SearchResultsTable snippetView={snippetView} setSnippetView={setSnippetView} />
237 </>
238 : null
239 }
240
241 const SearchStatus = () => {
242 const { isErrored, isLoading } = useCloudSearch()
243
244 return isErrored || isLoading
245 ? <LoadingStatusNotices
246 isLoading={isLoading}
247 errorMessage={isErrored
248 ? __('An error occurred while fetching search results. Please try again.', 'code-snippets')
249 : undefined}
250 loadingNotice={__('Loading snippets from cloud…', 'code-snippets')}
251 noticeLabel={__('Community snippets status', 'code-snippets')}
252 />
253 : null
254 }
255
256 export interface CloudSearchProps {
257 snippetView: SnippetView
258 setSnippetView: (view: SnippetView) => void
259 }
260
261 export const CloudSearch: React.FC<CloudSearchProps> = ({ snippetView, setSnippetView }) =>
262 <div className="cloud-search">
263 <SearchBox />
264 <SearchStatus />
265 <SearchResults snippetView={snippetView} setSnippetView={setSnippetView} />
266 </div>
267