PluginProbe
Code Snippets / 4.0.0-beta.2
Code Snippets v4.0.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 / hooks / useFeedbackReport.ts

useFeedbackReport.ts in Code Snippets 4.0.0-beta.2, at js/hooks/useFeedbackReport.ts

156 lines 4.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { useCallback, useMemo, useRef, useState } from 'react'
2 import { __ } from '@wordpress/i18n'
3 import { useDuplicateReports } from './useDuplicateReports'
4 import { useRestAPI } from './useRestAPI'
5 import type {
6 DuplicateReport,
7 FeedbackConfig,
8 FeedbackDraft,
9 FeedbackReportRequest,
10 FeedbackReportResponse
11 } from '../types/Feedback'
12
13 /** Most captured errors to attach to a report. */
14 const MAX_JS_ERRORS = 10
15
16 /** Shortest title that summarises anything. */
17 const MIN_TITLE_LENGTH = 8
18
19 /** Shortest free-text answer that describes anything. */
20 const MIN_TEXT_LENGTH = 20
21
22 interface AxiosLikeError {
23 response?: {
24 data?: {
25 message?: string
26 }
27 }
28 }
29
30 export interface FeedbackValidity {
31 title: boolean
32 description: boolean
33 steps: boolean
34 }
35
36 export interface FeedbackReport {
37 draft: FeedbackDraft
38 duplicates: DuplicateReport[]
39 errorMessage: string
40 invalidFields: FeedbackValidity
41 isSending: boolean
42 result: FeedbackReportResponse | undefined
43 updateDraft: (changes: Partial<FeedbackDraft>) => void
44 submit: VoidFunction
45 }
46
47 const emptyDraft = (config: FeedbackConfig): FeedbackDraft => ({
48 type: '',
49 title: '',
50 description: '',
51 steps: '',
52 comments: '',
53 name: config.user.name,
54 email: config.user.email,
55 isolation: {
56 plugin_only: false,
57 blank_theme: false,
58 reproducible: false
59 }
60 })
61
62 const describeBrowser = (): FeedbackReportRequest['browser'] => ({
63 userAgent: navigator.userAgent,
64 viewport: `${window.innerWidth}×${window.innerHeight}`,
65 screen: `${window.screen.width}×${window.screen.height}`,
66 language: navigator.language
67 })
68
69 /**
70 * The message an error carries, whether it came from WordPress, from the cloud, or from
71 * the request never arriving.
72 */
73 const describeError = (error: unknown): string =>
74 (<AxiosLikeError>error).response?.data?.message ??
75 __('The report could not be sent. Check the connection and try again.', 'code-snippets')
76
77 /** The first message describing what is still missing from a report. */
78 const firstValidationMessage = (invalid: FeedbackValidity): string => {
79 if (invalid.title) {
80 return __('Give the report a one-line title.', 'code-snippets')
81 }
82
83 if (invalid.description) {
84 return __('Add a little more detail to the description.', 'code-snippets')
85 }
86
87 if (invalid.steps) {
88 return __('List the steps that reproduce the bug.', 'code-snippets')
89 }
90
91 return ''
92 }
93
94 export const useFeedbackReport = (config: FeedbackConfig): FeedbackReport => {
95 const { api } = useRestAPI()
96
97 const [draft, setDraft] = useState<FeedbackDraft>(() => emptyDraft(config))
98 const [errorMessage, setErrorMessage] = useState('')
99 const [isSending, setIsSending] = useState(false)
100 const [result, setResult] = useState<FeedbackReportResponse>()
101
102 const idempotencyKey = useRef(window.crypto.randomUUID())
103
104 const invalidFields = useMemo(() => ({
105 title: MIN_TITLE_LENGTH > draft.title.trim().length,
106 description: MIN_TEXT_LENGTH > draft.description.trim().length,
107 steps: 'bug' === draft.type && MIN_TEXT_LENGTH > draft.steps.trim().length
108 }), [draft.type, draft.title, draft.description, draft.steps])
109
110 const updateDraft = useCallback((changes: Partial<FeedbackDraft>) => {
111 setDraft(current => ({ ...current, ...changes }))
112 }, [])
113
114 const duplicates = useDuplicateReports(config.searchUrl, draft.title)
115
116 const submit = useCallback(() => {
117 const type = draft.type
118
119 if (!type) {
120 return
121 }
122
123 const message = firstValidationMessage(invalidFields)
124
125 if (message) {
126 setErrorMessage(message)
127 return
128 }
129
130 setErrorMessage('')
131 setIsSending(true)
132
133 const request: FeedbackReportRequest = {
134 type,
135 idempotency_key: idempotencyKey.current,
136 title: draft.title.trim(),
137 description: draft.description.trim(),
138 steps: 'bug' === type ? draft.steps.trim() : '',
139 comments: draft.comments.trim(),
140 isolation: draft.isolation,
141 name: draft.name.trim(),
142 email: draft.email.trim(),
143 page_url: window.location.href,
144 js_errors: (window.codeSnippetsErrors ?? []).slice(0, MAX_JS_ERRORS),
145 browser: describeBrowser()
146 }
147
148 api.post<FeedbackReportResponse, FeedbackReportRequest>(config.restUrl, request)
149 .then(setResult)
150 .catch((error: unknown) => setErrorMessage(describeError(error)))
151 .finally(() => setIsSending(false))
152 }, [api, config.restUrl, draft, invalidFields])
153
154 return { draft, duplicates, errorMessage, invalidFields, isSending, result, updateDraft, submit }
155 }
156