From 8b40960fec09e21db732ec9ea01f3cc431b4bcc5 Mon Sep 17 00:00:00 2001 From: stdavis Date: Tue, 10 Sep 2024 09:26:04 -0600 Subject: [PATCH] fix(sherlock): make error message visible and guard against fetch errors --- .../src/components/Sherlock.jsx | 223 ++++++++++-------- .../src/components/Sherlock.stories.jsx | 16 ++ 2 files changed, 146 insertions(+), 93 deletions(-) diff --git a/packages/utah-design-system/src/components/Sherlock.jsx b/packages/utah-design-system/src/components/Sherlock.jsx index 254de0dd..63253b4f 100644 --- a/packages/utah-design-system/src/components/Sherlock.jsx +++ b/packages/utah-design-system/src/components/Sherlock.jsx @@ -8,15 +8,15 @@ import { ListBoxItem, Popover, ComboBox, - FieldError, Button, Group, + Form, } from 'react-aria-components'; import { useAsyncList } from 'react-stately'; import { search } from '@ugrc/utilities'; import { tv } from 'tailwind-variants'; import { focusRing } from './utils.ts'; -import { Label, fieldBorderStyles } from './Field.tsx'; +import { FieldError, Label, fieldBorderStyles } from './Field.tsx'; const defaultSymbols = { polygon: { @@ -59,6 +59,30 @@ let defaultItems = [ { id: 5, name: '84111' }, ]; +async function safeFetch(url, options) { + let responseJson; + try { + const response = await fetch(url, options); + + if (!response.ok) { + throw new Error( + `non-200 status code returned from ${url}: ${response.status} - ${response.statusText}`, + ); + } + + responseJson = await response.json(); + } catch (error) { + throw new Error(`error thrown during fetch to ${url}: ${error.message}`); + } + + // handle esri response errors which return a 200 status code + if (responseJson.error) { + throw new Error(`${url} returned an error: ${responseJson.error.message}`); + } + + return responseJson; +} + export const ugrcApiProvider = ( apiKey, table, @@ -169,8 +193,7 @@ export const masqueradeProvider = (url, wkid) => { } const suggestUrl = `${url}/suggest?text=${filterText}&maxSuggestions=${maxResults}`; - let response = await fetch(suggestUrl, { signal }); - const responseJson = await response.json(); + const responseJson = await safeFetch(suggestUrl, { signal }); const features = responseJson.suggestions.map((suggestion) => { return { name: suggestion.text, key: suggestion.magicKey }; @@ -181,8 +204,8 @@ export const masqueradeProvider = (url, wkid) => { getFeature: async (magicKey) => { const getFeatureUrl = `${url}/findAddressCandidates?magicKey=${magicKey}&outSR={"wkid":${wkid}}`; - const response = await fetch(getFeatureUrl); - const responseJson = await response.json(); + const responseJson = await safeFetch(getFeatureUrl); + const candidate = responseJson.candidates[0]; candidate.geometry = { ...candidate.location, @@ -212,8 +235,7 @@ export const featureServiceProvider = ( let oidField; const init = async (signal) => { // get oid field name, and validate searchField and contextField - const serviceResponse = await fetch(`${url}?f=json`, { signal }); - const serviceJson = await serviceResponse.json(); + const serviceJson = await safeFetch(`${url}?f=json`, { signal }); let searchFieldValidated = false; let contextFieldValidated = false; @@ -267,10 +289,12 @@ export const featureServiceProvider = ( resultRecordCount: maxResults, }); - const response = await fetch(`${url}/query?${searchParams.toString()}`, { - signal, - }); - const responseJson = await response.json(); + const responseJson = await safeFetch( + `${url}/query?${searchParams.toString()}`, + { + signal, + }, + ); const suggestions = responseJson.features.map((feature) => { return { @@ -290,8 +314,10 @@ export const featureServiceProvider = ( returnGeometry: true, }); - const response = await fetch(`${url}/query?${searchParams.toString()}`); - const responseJson = await response.json(); + const responseJson = await safeFetch( + `${url}/query?${searchParams.toString()}`, + ); + const feature = responseJson.features[0]; feature.geometry.type = { esriGeometryPolyline: 'polyline', @@ -361,91 +387,102 @@ export const Sherlock = (props) => { props.onSherlockMatch(graphics); }; + if (list.error) { + // send this to the console since we are displaying a generic error message in the UI + console.error(list.error); + } + return ( - - {props.label && } -
- - - list.setFilterText(event.target.value)} - className="block w-full appearance-none bg-transparent pl-9 pr-3 leading-5 text-zinc-900 caret-primary-800 placeholder:text-zinc-400 focus:outline-none dark:text-white dark:caret-accent-500 dark:ring-zinc-200/20 dark:placeholder:text-zinc-300 dark:focus:ring-accent-700 sm:text-sm" - /> - {(list.loadingState === 'loading' || - list.loadingState === 'filtering') && ( - - - - )} - - -
- {list.error} - - { - if ( - event.state.inputValue.length >= 3 && - list.loadingState === 'idle' - ) { - return ( -
- No items found matching your search -
- ); - } - return ( -
- Type 2 or more characters to begin searching -
- ); - }} - > - {(item) => ( - + + + )} + + + + {list.error ? ( + There was an error with the search process + ) : ( + + { + if ( + event.state.inputValue.length >= 3 && + list.loadingState === 'idle' + ) { + return ( +
+ No items found matching your search +
+ ); + } + return ( +
+ Type 2 or more characters to begin searching +
+ ); + }} > - {({ isSelected }) => ( - <> - - {isSelected && } - - - {item.context && ( - {item.context} + {(item) => ( + + {({ isSelected }) => ( + <> + + {isSelected && } + + + {item.context && ( + {item.context} + )} + )} - + )} -
- )} -
-
-
+ + + )} + + ); }; diff --git a/packages/utah-design-system/src/components/Sherlock.stories.jsx b/packages/utah-design-system/src/components/Sherlock.stories.jsx index e694d2ea..9d2b57d2 100644 --- a/packages/utah-design-system/src/components/Sherlock.stories.jsx +++ b/packages/utah-design-system/src/components/Sherlock.stories.jsx @@ -87,3 +87,19 @@ export const HasMultiProvider = { /> ), }; + +export const HasError = { + render: (args) => ( + + ), +};