PluginProbe
Code Snippets / 4.0.0-beta.1
Code Snippets v4.0.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 / WithCloudSearchContext.tsx

WithCloudSearchContext.tsx in Code Snippets 4.0.0-beta.1, at js/components/ManageMenu/CommunityCloud/WithCloudSearchContext.tsx

256 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 export const clearSearchQueryParams = () => {
61 updateQueryParams(Object.fromEntries(Object.values(SEARCH_PARAM_VARS).map(param => [param, undefined])))
62 }
63
64 const fetchSearchQueryParams = (): CloudSearchParams => {
65 const page = Number(fetchQueryParam(SEARCH_PARAM_VARS.page) ?? 1)
66 const query = fetchQueryParam(SEARCH_PARAM_VARS.query) ?? ''
67 const type = fetchQueryParam(SEARCH_PARAM_VARS.type) ?? ''
68 const status = fetchQueryParam(SEARCH_PARAM_VARS.status) ?? 0
69 const method = fetchQueryParam(SEARCH_PARAM_VARS.method)
70 const category = fetchQueryParam(SEARCH_PARAM_VARS.category) ?? ''
71
72 return {
73 page: isNaN(page) ? 1 : page,
74 type,
75 query,
76 status: Number(status),
77 method: 'codevault' === method ? 'codevault' : 'term',
78 category
79 }
80 }
81
82 const updateSearchQueryParams = (params: CloudSearchParams) => {
83 updateQueryParams({
84 [SEARCH_PARAM_VARS.page]: params.page,
85 [SEARCH_PARAM_VARS.type]: params.type || undefined,
86 [SEARCH_PARAM_VARS.query]: params.query || undefined,
87 [SEARCH_PARAM_VARS.status]: params.status || undefined,
88 [SEARCH_PARAM_VARS.method]: 'codevault' === params.method ? 'codevault' : undefined,
89 [SEARCH_PARAM_VARS.category]: params.category || undefined
90 })
91 }
92
93 const buildSearchUrl = (
94 baseUrl: string,
95 { query, type, method, status, category, page }: CloudSearchParams
96 ) =>
97 buildUrl(baseUrl, {
98 query,
99 searchByCodevault: 'codevault' === method ? true : undefined,
100 per_page: SNIPPETS_PER_PAGE,
101 type: type || undefined,
102 category: category || undefined,
103 status: status || undefined,
104 page
105 })
106
107 const processSearchParams = (params: CloudSearchParams, previous: CloudSearchParams): CloudSearchParams => {
108 const queryDiff = params.query.trim() !== previous.query.trim()
109 const methodDiff = params.method !== previous.method
110
111 const typeDiff = params.type !== previous.type
112 const statusDiff = params.status !== previous.status
113 const categoryDiff = params.category !== previous.category
114
115 const filterDiff = typeDiff || statusDiff || categoryDiff
116
117 if (queryDiff || methodDiff) {
118 return { ...params, page: 1, type: '', status: 0, category: '' }
119 } else if (filterDiff) {
120 return { ...params, page: 1 }
121 }
122
123 return params
124 }
125
126 const unpackSearchResponse = ({ data }: AxiosResponse<CloudSnippetsSchema>, baseUrl: string) => ({
127 page: data.page,
128 isFeatured: baseUrl === SEARCH_URLS.FEATURED,
129 snippets: data.snippets,
130 totalItems: data.total_snippets,
131 totalPages: data.total_pages
132 })
133
134 const isFilterOption = (value: unknown): value is CloudFilterOption =>
135 'object' === typeof value && null !== value &&
136 'id' in value && 'number' === typeof value.id &&
137 'name' in value && 'string' === typeof value.name
138
139 const unpackFilterOptions = (data: unknown): CloudFilterOption[] | undefined =>
140 Array.isArray(data)
141 ? data.filter(isFilterOption)
142 : undefined
143
144 const unpackFiltersFromResponse = (
145 { data: { available_filters } }: AxiosResponse<CloudSnippetsSchema>
146 ): AvailableCloudFilters | undefined =>
147 'object' === typeof available_filters && available_filters
148 ? {
149 ...'types' in available_filters && { types: unpackFilterOptions(available_filters.types) },
150 ...'statuses' in available_filters && { statuses: unpackFilterOptions(available_filters.statuses) },
151 ...'categories' in available_filters && { categories: unpackFilterOptions(available_filters.categories) }
152 }
153 : undefined
154
155 const useRequestIds = () => {
156 const activeRequestRef = useRef(0)
157
158 const nextRequestId = useCallback(() => {
159 activeRequestRef.current += 1
160 return activeRequestRef.current
161 }, [])
162
163 const isCurrentRequest = useCallback((requestId: number) => requestId === activeRequestRef.current, [])
164
165 return { isCurrentRequest, nextRequestId }
166 }
167
168 export interface CloudSearchContext {
169 isErrored: boolean
170 doSearch: (paramsDelta?: Partial<CloudSearchParams>) => void
171 isSearching: boolean
172 searchParams: CloudSearchParams
173 searchResults: CloudSearchResults | undefined
174 availableFilters: AvailableCloudFilters
175 updateSearchParams: (params: Partial<CloudSearchParams>) => void
176 }
177
178 const useSearchApi = () => {
179 const { api } = useRestAPI()
180 const { isCurrentRequest, nextRequestId } = useRequestIds()
181 const [currentSearch, setCurrentSearch] = useState(false)
182 const [searchResults, setSearchResults] = useState<CloudSearchResults | false | undefined>()
183 const [availableFilters, setAvailableFilters] = useState<AvailableCloudFilters>({})
184
185 const makeSearchRequest = useCallback((params: CloudSearchParams) => {
186 const requestId = nextRequestId()
187 const isFeaturedSearch = '' === params.query.trim()
188
189 setCurrentSearch(!isFeaturedSearch)
190 const baseUrl = isFeaturedSearch ? SEARCH_URLS.FEATURED : SEARCH_URLS.SEARCH_QUERY
191
192 api.getResponse<CloudSnippetsSchema>(buildSearchUrl(baseUrl, params))
193 .then(response => {
194 if (isCurrentRequest(requestId)) {
195 setSearchResults(unpackSearchResponse(response, baseUrl))
196 setAvailableFilters(previous => unpackFiltersFromResponse(response) ?? previous)
197 }
198 })
199 .catch(() => {
200 if (isCurrentRequest(requestId)) {
201 setSearchResults(false)
202 }
203 })
204 .finally(() => setCurrentSearch(false))
205 }, [api, nextRequestId, isCurrentRequest])
206
207 return {
208 isSearching: currentSearch,
209 searchResults,
210 availableFilters,
211 makeSearchRequest
212 }
213 }
214
215 const [Context, useCloudSearch] = createContextHook<CloudSearchContext>('useCloudSearch')
216
217 export const WithCloudSearchContext: React.FC<PropsWithChildren> = ({ children }) => {
218 const { isSearching, makeSearchRequest, availableFilters, searchResults } = useSearchApi()
219 const [searchParams, setSearchParams] = useState<CloudSearchParams>(fetchSearchQueryParams)
220 const [madeInitialRequest, setMadeInitialRequest] = useState(false)
221
222 const updateSearchParams = useCallback(
223 (delta: Partial<CloudSearchParams>) => setSearchParams(previous => ({ ...previous, ...delta })),
224 [])
225
226 const doSearch = useCallback((paramsDelta?: Partial<CloudSearchParams>) => {
227 // An empty query is the featured view: makeSearchRequest routes it to the featured endpoint.
228 const params = processSearchParams({ ...searchParams, ...paramsDelta }, searchParams)
229 updateSearchQueryParams(params)
230 setSearchParams(params)
231 makeSearchRequest(params)
232 }, [makeSearchRequest, searchParams])
233
234 useEffect(() => {
235 if (!madeInitialRequest) {
236 makeSearchRequest(searchParams)
237 setMadeInitialRequest(true)
238 }
239 }, [makeSearchRequest, searchParams, madeInitialRequest])
240
241 const value: CloudSearchContext = {
242 doSearch,
243 isSearching,
244 searchParams,
245 availableFilters,
246 updateSearchParams,
247 ...false === searchResults
248 ? { isErrored: true, searchResults: undefined }
249 : { isErrored: false, searchResults }
250 }
251
252 return <Context.Provider value={value}>{children}</Context.Provider>
253 }
254
255 export { useCloudSearch }
256