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 / WithCloudSearchContext.tsx

WithCloudSearchContext.tsx in Code Snippets 3.10.2, at js/components/ManageMenu/CommunityCloud/WithCloudSearchContext.tsx

262 lines 8.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useCallback, useEffect, useRef, useState } from 'react'
2 import { createContextHook } from '../../../utils/bootstrap'
3 import { useRestAPI } from '../../../hooks/useRestAPI'
4 import { REST_BASES } from '../../../utils/restAPI'
5 import { buildUrl, fetchQueryParam, updateQueryParams } from '../../../utils/urls'
6 import type { CloudSnippetsSchema } from '../../../types/schema/CloudSnippetsSchema'
7 import type { PropsWithChildren } from 'react'
8 import type { CloudSnippetSchema } from '../../../types/schema/CloudSnippetSchema'
9 import type { AxiosResponse } from 'axios'
10
11 const DEFAULT_SNIPPETS_PER_PAGE = 10
12 const MAX_CLOUD_RESULTS_PER_PAGE = 100
13 const SNIPPETS_PER_PAGE = Math.min(
14 window.CODE_SNIPPETS_MANAGE?.cloudSearchPerPage ?? window.CODE_SNIPPETS_MANAGE?.snippetsPerPage ?? DEFAULT_SNIPPETS_PER_PAGE,
15 MAX_CLOUD_RESULTS_PER_PAGE
16 )
17
18 const SEARCH_URLS = {
19 SEARCH_QUERY: REST_BASES.cloud.snippets,
20 FEATURED: `${REST_BASES.cloud.snippets}/featured`
21 } as const
22
23 export interface AvailableCloudFilters {
24 types?: CloudFilterOption[]
25 statuses?: CloudFilterOption[]
26 categories?: CloudFilterOption[]
27 }
28
29 const SEARCH_PARAM_VARS: Record<keyof CloudSearchParams, string> = {
30 page: 'paged',
31 query: 's',
32 method: 'by',
33 type: 'type',
34 status: 'status',
35 category: 'category'
36 }
37
38 export interface CloudSearchParams {
39 page: number
40 query: string
41 method: 'term' | 'codevault'
42 type: string
43 status: number
44 category: string
45 }
46
47 export interface CloudSearchResults {
48 page: number
49 snippets: CloudSnippetSchema[]
50 totalItems: number
51 totalPages: number
52 isFeatured: boolean
53 }
54
55 export interface CloudFilterOption {
56 id: number
57 name: string
58 }
59
60 const fetchSearchQueryParams = (): CloudSearchParams => {
61 const page = Number(fetchQueryParam(SEARCH_PARAM_VARS.page) ?? 1)
62 const query = fetchQueryParam(SEARCH_PARAM_VARS.query) ?? ''
63 const type = fetchQueryParam(SEARCH_PARAM_VARS.type) ?? ''
64 const status = fetchQueryParam(SEARCH_PARAM_VARS.status) ?? 0
65 const method = fetchQueryParam(SEARCH_PARAM_VARS.method)
66 const category = fetchQueryParam(SEARCH_PARAM_VARS.category) ?? ''
67
68 return {
69 page: isNaN(page) ? 1 : page,
70 type,
71 query,
72 status: Number(status),
73 method: 'codevault' === method ? 'codevault' : 'term',
74 category
75 }
76 }
77
78 const updateSearchQueryParams = (params: CloudSearchParams) => {
79 updateQueryParams({
80 [SEARCH_PARAM_VARS.page]: params.page,
81 [SEARCH_PARAM_VARS.type]: params.type || undefined,
82 [SEARCH_PARAM_VARS.query]: params.query || undefined,
83 [SEARCH_PARAM_VARS.status]: params.status || undefined,
84 [SEARCH_PARAM_VARS.method]: 'codevault' === params.method ? 'codevault' : undefined,
85 [SEARCH_PARAM_VARS.category]: params.category || undefined
86 })
87 }
88
89 const buildSearchUrl = (
90 baseUrl: string,
91 { query, type, method, status, category, page }: CloudSearchParams
92 ) =>
93 buildUrl(baseUrl, {
94 query,
95 searchByCodevault: 'codevault' === method ? true : undefined,
96 per_page: SNIPPETS_PER_PAGE,
97 type: type || undefined,
98 category: category || undefined,
99 status: status || undefined,
100 page
101 })
102
103 const processSearchParams = (params: CloudSearchParams, previous: CloudSearchParams): CloudSearchParams => {
104 const queryDiff = params.query.trim() !== previous.query.trim()
105 const methodDiff = params.method !== previous.method
106
107 const typeDiff = params.type !== previous.type
108 const statusDiff = params.status !== previous.status
109 const categoryDiff = params.category !== previous.category
110
111 const filterDiff = typeDiff || statusDiff || categoryDiff
112
113 if (queryDiff || methodDiff) {
114 return { ...params, page: 1, type: '', status: 0, category: '' }
115 } else if (filterDiff) {
116 return { ...params, page: 1 }
117 }
118
119 return params
120 }
121
122 const unpackSearchResponse = ({ data }: AxiosResponse<CloudSnippetsSchema>, baseUrl: string) => ({
123 page: data.page,
124 isFeatured: baseUrl === SEARCH_URLS.FEATURED,
125 snippets: data.snippets,
126 totalItems: data.total_snippets,
127 totalPages: data.total_pages
128 })
129
130 const isFilterOption = (value: unknown): value is CloudFilterOption =>
131 'object' === typeof value && null !== value &&
132 'id' in value && 'number' === typeof value.id &&
133 'name' in value && 'string' === typeof value.name
134
135 const unpackFilterOptions = (data: unknown): CloudFilterOption[] | undefined =>
136 Array.isArray(data)
137 ? data.filter(isFilterOption)
138 : undefined
139
140 const unpackFiltersFromResponse = (
141 { data: { available_filters } }: AxiosResponse<CloudSnippetsSchema>
142 ): AvailableCloudFilters | undefined =>
143 'object' === typeof available_filters && available_filters
144 ? {
145 ...'types' in available_filters && { types: unpackFilterOptions(available_filters.types) },
146 ...'statuses' in available_filters && { statuses: unpackFilterOptions(available_filters.statuses) },
147 ...'categories' in available_filters && { categories: unpackFilterOptions(available_filters.categories) }
148 }
149 : undefined
150
151 const useRequestIds = () => {
152 const activeRequestRef = useRef(0)
153
154 const nextRequestId = useCallback(() => {
155 activeRequestRef.current += 1
156 return activeRequestRef.current
157 }, [])
158
159 const isCurrentRequest = useCallback((requestId: number) => requestId === activeRequestRef.current, [])
160
161 return { isCurrentRequest, nextRequestId }
162 }
163
164 export interface CloudSearchContext {
165 isErrored: boolean
166 doSearch: (paramsDelta?: Partial<CloudSearchParams>) => void
167 isLoading: boolean
168 isSearching: boolean
169 searchParams: CloudSearchParams
170 searchResults: CloudSearchResults | undefined
171 availableFilters: AvailableCloudFilters
172 updateSearchParams: (params: Partial<CloudSearchParams>) => void
173 }
174
175 const useSearchApi = () => {
176 const { api } = useRestAPI()
177 const { isCurrentRequest, nextRequestId } = useRequestIds()
178 const [currentSearch, setCurrentSearch] = useState(false)
179 const [isLoading, setIsLoading] = useState(false)
180 const [searchResults, setSearchResults] = useState<CloudSearchResults | false | undefined>()
181 const [availableFilters, setAvailableFilters] = useState<AvailableCloudFilters>({})
182
183 const makeSearchRequest = useCallback((params: CloudSearchParams) => {
184 const requestId = nextRequestId()
185 const isFeaturedSearch = '' === params.query.trim()
186
187 setIsLoading(true)
188 setCurrentSearch(!isFeaturedSearch)
189 const baseUrl = isFeaturedSearch ? SEARCH_URLS.FEATURED : SEARCH_URLS.SEARCH_QUERY
190
191 api.getResponse<CloudSnippetsSchema>(buildSearchUrl(baseUrl, params))
192 .then(response => {
193 if (isCurrentRequest(requestId)) {
194 setSearchResults(unpackSearchResponse(response, baseUrl))
195 setAvailableFilters(previous => unpackFiltersFromResponse(response) ?? previous)
196 }
197 })
198 .catch(() => {
199 if (isCurrentRequest(requestId)) {
200 setSearchResults(false)
201 }
202 })
203 .finally(() => {
204 if (isCurrentRequest(requestId)) {
205 setCurrentSearch(false)
206 setIsLoading(false)
207 }
208 })
209 }, [api, nextRequestId, isCurrentRequest])
210
211 return {
212 isLoading,
213 isSearching: currentSearch,
214 searchResults,
215 availableFilters,
216 makeSearchRequest
217 }
218 }
219
220 const [Context, useCloudSearch] = createContextHook<CloudSearchContext>('useCloudSearch')
221
222 export const WithCloudSearchContext: React.FC<PropsWithChildren> = ({ children }) => {
223 const { isLoading, isSearching, makeSearchRequest, availableFilters, searchResults } = useSearchApi()
224 const [searchParams, setSearchParams] = useState<CloudSearchParams>(fetchSearchQueryParams)
225 const [madeInitialRequest, setMadeInitialRequest] = useState(false)
226
227 const updateSearchParams = useCallback(
228 (delta: Partial<CloudSearchParams>) => setSearchParams(previous => ({ ...previous, ...delta })),
229 [])
230
231 const doSearch = useCallback((paramsDelta?: Partial<CloudSearchParams>) => {
232 // An empty query is the featured view: makeSearchRequest routes it to the featured endpoint.
233 const params = processSearchParams({ ...searchParams, ...paramsDelta }, searchParams)
234 updateSearchQueryParams(params)
235 setSearchParams(params)
236 makeSearchRequest(params)
237 }, [makeSearchRequest, searchParams])
238
239 useEffect(() => {
240 if (!madeInitialRequest) {
241 setMadeInitialRequest(true)
242 makeSearchRequest(searchParams)
243 }
244 }, [makeSearchRequest, searchParams, madeInitialRequest])
245
246 const value: CloudSearchContext = {
247 doSearch,
248 isLoading,
249 isSearching,
250 searchParams,
251 availableFilters,
252 updateSearchParams,
253 ...false === searchResults
254 ? { isErrored: true, searchResults: undefined }
255 : { isErrored: false, searchResults }
256 }
257
258 return <Context.Provider value={value}>{children}</Context.Provider>
259 }
260
261 export { useCloudSearch }
262