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

249 lines 7.6 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, 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 { isLicensed } from '../../../utils/screen'
8 import { isProSnippet } from '../../../utils/snippets/snippets'
9 import { TableNav } from '../../common/ListTable/TableNavigation'
10 import { LoadingStatusNotices } from '../../common/LoadingStatusNotices'
11 import { SnippetViewToggle } from '../../common/SnippetViewToggle'
12 import { CloudSnippetsTable } from './CloudSnippetsTable'
13 import { CloudSnippetAuthor, SearchResult } from './SearchResult'
14 import { useCloudSearch } from './WithCloudSearchContext'
15 import { SearchFilters } from './SearchFilters'
16 import type { CloudSnippetSchema } from '../../../types/schema/CloudSnippetSchema'
17 import type { SnippetView } from '../../../types/SnippetView'
18 import type { ListTableAction } from '../../common/ListTable'
19 import type { Dispatch, FormEventHandler, SetStateAction } from 'react'
20
21 const SearchBox = () => {
22 const { searchParams, updateSearchParams, isSearching, doSearch } = useCloudSearch()
23
24 const handleSubmit: FormEventHandler<HTMLFormElement> = event => {
25 event.preventDefault()
26 doSearch()
27 }
28
29 return (
30 <form className="cloud-search-form" onSubmit={handleSubmit}>
31 <label className="screen-reader-text" htmlFor="cloud-search-method">
32 {__('Search method', 'code-snippets')}
33 </label>
34 <select
35 id="cloud-search-method"
36 value={searchParams.method}
37 onChange={event =>
38 updateSearchParams({ method: 'codevault' === event.target.value ? 'codevault' : 'term' })}
39 >
40 <option value="term">{__('Search by keyword', 'code-snippets')}</option>
41 <option value="codevault">{__('Name of library', 'code-snippets')}</option>
42 </select>
43
44 <div className="cloud-search-query">
45 <input
46 id="cloud-search-query"
47 type="search"
48 value={searchParams.query}
49 aria-label={__('Search query', 'code-snippets')}
50 onChange={event => updateSearchParams({ query: event.target.value })}
51 placeholder={__('e.g. Remove unused JavaScript…', 'code-snippets')}
52 />
53 <span role="status" aria-live="polite">
54 {isSearching &&
55 <span className="screen-reader-text">{__('Searching…', 'code-snippets')}</span>}
56 </span>
57 </div>
58
59 <button
60 type="submit"
61 className="button button-primary cloud-search-submit"
62 disabled={isSearching}
63 >
64 {isSearching ? <Spinner /> : __('Search Cloud Library', 'code-snippets')}
65 </button>
66 </form>
67 )
68 }
69
70 const isSnippetDownloadable = (snippet: CloudSnippetSchema): boolean =>
71 !snippet.local_id && (isLicensed() || !isProSnippet(snippet))
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={isSelected => {
92 setSelected(previous => {
93 const updated = new Set(previous)
94
95 if (isSelected) {
96 updated.add(result.id)
97 } else {
98 updated.delete(result.id)
99 }
100
101 return updated
102 })
103 }}
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) && isSnippetDownloadable(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={searchResults.snippets.map(snippet => snippet.id)}
170 extraTableNav={() => <SearchFilters />}
171 endTableNav={which =>
172 'top' === which
173 ? <SnippetViewToggle snippetView={snippetView} setSnippetView={setSnippetView} />
174 : null}
175 {...navProps}
176 />
177
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 />}
189
190 <TableNav which="bottom" {...navProps} />
191 </div>
192 )
193 }
194
195 const SearchResults: React.FC<SearchResultsViewProps> = ({ snippetView, setSnippetView }) => {
196 const { searchResults, searchParams } = useCloudSearch()
197
198 if (!searchResults) {
199 return null
200 }
201
202 if (0 < searchResults.page && 0 === searchResults.snippets.length) {
203 return (
204 <div className="banner banner-neutral no-results">
205 <p>{'codevault' === searchParams.method
206 ? __('Could not find a codevault with that name. Please try again.', 'code-snippets')
207 : __('No snippets could be found with that search term. Please try again.', 'code-snippets')
208 }</p>
209 </div>
210 )
211 }
212
213 return searchResults.snippets.length
214 ? <>
215 {searchResults.isFeatured
216 ? <h3 className="cloud-featured-heading">{__('Featured Snippets', 'code-snippets')}</h3>
217 : <h3 className="cloud-snippets-heading">{__('Search Results', 'code-snippets')}</h3>}
218 <SearchResultsTable snippetView={snippetView} setSnippetView={setSnippetView} />
219 </>
220 : null
221 }
222
223 const SearchStatus = () => {
224 const { isErrored, isLoading } = useCloudSearch()
225
226 return isErrored || isLoading
227 ? <LoadingStatusNotices
228 isLoading={isLoading}
229 errorMessage={isErrored
230 ? __('An error occurred while fetching search results. Please try again.', 'code-snippets')
231 : undefined}
232 loadingNotice={__('Loading community snippets…', 'code-snippets')}
233 noticeLabel={__('Community snippets status', 'code-snippets')}
234 />
235 : null
236 }
237
238 export interface CloudSearchProps {
239 snippetView: SnippetView
240 setSnippetView: (view: SnippetView) => void
241 }
242
243 export const CloudSearch: React.FC<CloudSearchProps> = ({ snippetView, setSnippetView }) =>
244 <div className="cloud-search">
245 <SearchBox />
246 <SearchStatus />
247 <SearchResults snippetView={snippetView} setSnippetView={setSnippetView} />
248 </div>
249