Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitbe7eaac

Browse files
committed
Handle filter form errors
1 parentacbd54a commitbe7eaac

File tree

8 files changed

+202
-83
lines changed

8 files changed

+202
-83
lines changed

‎site/src/api/errors.test.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import{isApiError,mapApiErrorToFieldErrors}from"./errors"
1+
import{getValidationErrorMessage,isApiError,mapApiErrorToFieldErrors}from"./errors"
22

33
describe("isApiError",()=>{
44
it("returns true when the object is an API Error",()=>{
@@ -36,3 +36,57 @@ describe("mapApiErrorToFieldErrors", () => {
3636
})
3737
})
3838
})
39+
40+
describe("getValidationErrorMessage",()=>{
41+
it("returns multiple validation messages",()=>{
42+
expect(
43+
getValidationErrorMessage({
44+
response:{
45+
data:{
46+
message:"Invalid user search query.",
47+
validations:[
48+
{
49+
field:"status",
50+
detail:`Query param "status" has invalid value: "inactive" is not a valid user status`,
51+
},
52+
{
53+
field:"q",
54+
detail:`Query element "role:a:e" can only contain 1 ':'`,
55+
},
56+
],
57+
},
58+
},
59+
isAxiosError:true,
60+
}),
61+
).toEqual(
62+
`Query param "status" has invalid value: "inactive" is not a valid user status\nQuery element "role:a:e" can only contain 1 ':'`,
63+
)
64+
})
65+
66+
it("non-API error returns empty validation message",()=>{
67+
expect(
68+
getValidationErrorMessage({
69+
response:{
70+
data:{
71+
error:"Invalid user search query.",
72+
},
73+
},
74+
isAxiosError:true,
75+
}),
76+
).toEqual("")
77+
})
78+
79+
it("no validations field returns empty validation message",()=>{
80+
expect(
81+
getValidationErrorMessage({
82+
response:{
83+
data:{
84+
message:"Invalid user search query.",
85+
detail:`Query element "role:a:e" can only contain 1 ':'`,
86+
},
87+
},
88+
isAxiosError:true,
89+
}),
90+
).toEqual("")
91+
})
92+
})

‎site/src/api/errors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,9 @@ export const getErrorMessage = (
7171
:errorinstanceofError
7272
?error.message
7373
:defaultMessage
74+
75+
exportconstgetValidationErrorMessage=(error:Error|ApiError|unknown):string=>{
76+
constvalidationErrors=
77+
isApiError(error)&&error.response.data.validations ?error.response.data.validations :[]
78+
returnvalidationErrors.map((error)=>error.detail).join("\n")
79+
}

‎site/src/components/SearchBarWithFilter/SearchBarWithFilter.stories.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,21 @@ WithPresetFilters.args = {
2323
{query:"random query",name:"Random query"},
2424
],
2525
}
26+
27+
exportconstWithError=Template.bind({})
28+
WithError.args={
29+
presetFilters:[
30+
{query:workspaceFilterQuery.me,name:"Your workspaces"},
31+
{query:"random query",name:"Random query"},
32+
],
33+
error:{
34+
response:{
35+
data:{
36+
validations:{
37+
field:"status",
38+
detail:`Query param "status" has invalid value: "inactive" is not a valid user status`,
39+
},
40+
},
41+
},
42+
},
43+
}

‎site/src/components/SearchBarWithFilter/SearchBarWithFilter.tsx

Lines changed: 67 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import TextField from "@material-ui/core/TextField"
88
importSearchIconfrom"@material-ui/icons/Search"
99
import{FormikErrors,useFormik}from"formik"
1010
import{useState}from"react"
11+
import{getValidationErrorMessage}from"../../api/errors"
1112
import{getFormHelpers,onChangeTrimmed}from"../../util/formUtils"
1213
import{CloseDropdown,OpenDropdown}from"../DropdownArrows/DropdownArrows"
1314
import{Stack}from"../Stack/Stack"
@@ -20,6 +21,7 @@ export interface SearchBarWithFilterProps {
2021
filter?:string
2122
onFilter:(query:string)=>void
2223
presetFilters?:PresetFilter[]
24+
error?:unknown
2325
}
2426

2527
exportinterfacePresetFilter{
@@ -37,6 +39,7 @@ export const SearchBarWithFilter: React.FC<SearchBarWithFilterProps> = ({
3739
filter,
3840
onFilter,
3941
presetFilters,
42+
error,
4043
})=>{
4144
conststyles=useStyles()
4245

@@ -68,69 +71,76 @@ export const SearchBarWithFilter: React.FC<SearchBarWithFilterProps> = ({
6871
handleClose()
6972
}
7073

74+
consterrorMessage=getValidationErrorMessage(error)
75+
7176
return(
72-
<Stackdirection="row"spacing={0}className={styles.filterContainer}>
73-
{presetFilters&&presetFilters.length>0&&(
74-
<Button
75-
aria-controls="filter-menu"
76-
aria-haspopup="true"
77-
onClick={handleClick}
78-
className={styles.buttonRoot}
79-
>
80-
{Language.filterName}{anchorEl ?<CloseDropdown/> :<OpenDropdown/>}
81-
</Button>
82-
)}
83-
84-
<formonSubmit={form.handleSubmit}className={styles.filterForm}>
85-
<TextField
86-
{...getFieldHelpers("query")}
87-
className={styles.textFieldRoot}
88-
onChange={onChangeTrimmed(form)}
89-
fullWidth
90-
variant="outlined"
91-
InputProps={{
92-
startAdornment:(
93-
<InputAdornmentposition="start">
94-
<SearchIconfontSize="small"/>
95-
</InputAdornment>
96-
),
97-
}}
98-
/>
99-
</form>
100-
101-
{presetFilters&&presetFilters.length>0&&(
102-
<Menu
103-
id="filter-menu"
104-
anchorEl={anchorEl}
105-
keepMounted
106-
open={Boolean(anchorEl)}
107-
onClose={handleClose}
108-
TransitionComponent={Fade}
109-
anchorOrigin={{
110-
vertical:"bottom",
111-
horizontal:"left",
112-
}}
113-
transformOrigin={{
114-
vertical:"top",
115-
horizontal:"left",
116-
}}
117-
>
118-
{presetFilters.map((presetFilter)=>(
119-
<MenuItemkey={presetFilter.name}onClick={setPresetFilter(presetFilter.query)}>
120-
{presetFilter.name}
121-
</MenuItem>
122-
))}
123-
</Menu>
124-
)}
77+
<Stackspacing={1}className={styles.root}>
78+
<Stackdirection="row"spacing={0}className={styles.filterContainer}>
79+
{presetFilters&&presetFilters.length>0&&(
80+
<Button
81+
aria-controls="filter-menu"
82+
aria-haspopup="true"
83+
onClick={handleClick}
84+
className={styles.buttonRoot}
85+
>
86+
{Language.filterName}{anchorEl ?<CloseDropdown/> :<OpenDropdown/>}
87+
</Button>
88+
)}
89+
90+
<formonSubmit={form.handleSubmit}className={styles.filterForm}>
91+
<TextField
92+
{...getFieldHelpers("query")}
93+
className={styles.textFieldRoot}
94+
onChange={onChangeTrimmed(form)}
95+
fullWidth
96+
variant="outlined"
97+
InputProps={{
98+
startAdornment:(
99+
<InputAdornmentposition="start">
100+
<SearchIconfontSize="small"/>
101+
</InputAdornment>
102+
),
103+
}}
104+
/>
105+
</form>
106+
107+
{presetFilters&&presetFilters.length>0&&(
108+
<Menu
109+
id="filter-menu"
110+
anchorEl={anchorEl}
111+
keepMounted
112+
open={Boolean(anchorEl)}
113+
onClose={handleClose}
114+
TransitionComponent={Fade}
115+
anchorOrigin={{
116+
vertical:"bottom",
117+
horizontal:"left",
118+
}}
119+
transformOrigin={{
120+
vertical:"top",
121+
horizontal:"left",
122+
}}
123+
>
124+
{presetFilters.map((presetFilter)=>(
125+
<MenuItemkey={presetFilter.name}onClick={setPresetFilter(presetFilter.query)}>
126+
{presetFilter.name}
127+
</MenuItem>
128+
))}
129+
</Menu>
130+
)}
131+
</Stack>
132+
{errorMessage&&<StackclassName={styles.errorRoot}>{errorMessage}</Stack>}
125133
</Stack>
126134
)
127135
}
128136

129137
constuseStyles=makeStyles((theme)=>({
138+
root:{
139+
marginBottom:theme.spacing(2),
140+
},
130141
filterContainer:{
131142
border:`1px solid${theme.palette.divider}`,
132143
borderRadius:theme.shape.borderRadius,
133-
marginBottom:theme.spacing(2),
134144
},
135145
filterForm:{
136146
width:"100%",
@@ -146,4 +156,7 @@ const useStyles = makeStyles((theme) => ({
146156
border:"none",
147157
},
148158
},
159+
errorRoot:{
160+
color:theme.palette.error.dark,
161+
},
149162
}))

‎site/src/components/UsersTable/UsersTable.stories.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,19 @@ Empty.args = {
2828
users:[],
2929
roles:MockSiteRoles,
3030
}
31+
32+
exportconstError=Template.bind({})
33+
Error.args={
34+
users:[MockUser,MockUser2],
35+
roles:MockSiteRoles,
36+
canEditUsers:true,
37+
error:{
38+
message:"Invalid user search query.",
39+
validations:[
40+
{
41+
field:"status",
42+
detail:`Query param "status" has invalid value: "inactive" is not a valid user status`,
43+
},
44+
],
45+
},
46+
}

‎site/src/components/UsersTable/UsersTable.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export interface UsersTableProps {
3636
onActivateUser:(user:TypesGen.User)=>void
3737
onResetUserPassword:(user:TypesGen.User)=>void
3838
onUpdateUserRoles:(user:TypesGen.User,roles:TypesGen.Role["name"][])=>void
39+
error?:unknown
3940
}
4041

4142
exportconstUsersTable:FC<UsersTableProps>=({
@@ -48,6 +49,7 @@ export const UsersTable: FC<UsersTableProps> = ({
4849
isUpdatingUserRoles,
4950
canEditUsers,
5051
isLoading,
52+
error,
5153
})=>{
5254
conststyles=useStyles()
5355

@@ -63,8 +65,9 @@ export const UsersTable: FC<UsersTableProps> = ({
6365
</TableRow>
6466
</TableHead>
6567
<TableBody>
66-
{isLoading&&<TableLoader/>}
68+
{isLoading&&!error&&<TableLoader/>}
6769
{!isLoading&&
70+
!error&&
6871
users&&
6972
users.map((user)=>{
7073
// When the user has no role we want to show they are a Member
@@ -134,15 +137,18 @@ export const UsersTable: FC<UsersTableProps> = ({
134137
)
135138
})}
136139

137-
{users&&users.length===0&&(
138-
<TableRow>
139-
<TableCellcolSpan={999}>
140-
<Boxp={4}>
141-
<EmptyStatemessage={Language.emptyMessage}/>
142-
</Box>
143-
</TableCell>
144-
</TableRow>
145-
)}
140+
{
141+
// Default behavior for error state and empty list
142+
(error||(users&&users.length===0))&&(
143+
<TableRow>
144+
<TableCellcolSpan={999}>
145+
<Boxp={4}>
146+
<EmptyStatemessage={Language.emptyMessage}/>
147+
</Box>
148+
</TableCell>
149+
</TableRow>
150+
)
151+
}
146152
</TableBody>
147153
</Table>
148154
)

‎site/src/pages/UsersPage/UsersPageView.tsx

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import Button from "@material-ui/core/Button"
22
importAddCircleOutlinefrom"@material-ui/icons/AddCircleOutline"
33
import{FC}from"react"
44
import*asTypesGenfrom"../../api/typesGenerated"
5-
import{ErrorSummary}from"../../components/ErrorSummary/ErrorSummary"
65
import{Margins}from"../../components/Margins/Margins"
76
import{PageHeader,PageHeaderTitle}from"../../components/PageHeader/PageHeader"
87
import{SearchBarWithFilter}from"../../components/SearchBarWithFilter/SearchBarWithFilter"
@@ -68,23 +67,25 @@ export const UsersPageView: FC<UsersPageViewProps> = ({
6867
<PageHeaderTitle>Users</PageHeaderTitle>
6968
</PageHeader>
7069

71-
<SearchBarWithFilterfilter={filter}onFilter={onFilter}presetFilters={presetFilters}/>
70+
<SearchBarWithFilter
71+
filter={filter}
72+
onFilter={onFilter}
73+
presetFilters={presetFilters}
74+
error={error}
75+
/>
7276

73-
{error ?(
74-
<ErrorSummaryerror={error}/>
75-
) :(
76-
<UsersTable
77-
users={users}
78-
roles={roles}
79-
onSuspendUser={onSuspendUser}
80-
onActivateUser={onActivateUser}
81-
onResetUserPassword={onResetUserPassword}
82-
onUpdateUserRoles={onUpdateUserRoles}
83-
isUpdatingUserRoles={isUpdatingUserRoles}
84-
canEditUsers={canEditUsers}
85-
isLoading={isLoading}
86-
/>
87-
)}
77+
<UsersTable
78+
users={users}
79+
roles={roles}
80+
onSuspendUser={onSuspendUser}
81+
onActivateUser={onActivateUser}
82+
onResetUserPassword={onResetUserPassword}
83+
onUpdateUserRoles={onUpdateUserRoles}
84+
isUpdatingUserRoles={isUpdatingUserRoles}
85+
canEditUsers={canEditUsers}
86+
isLoading={isLoading}
87+
error={error}
88+
/>
8889
</Margins>
8990
)
9091
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp