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

Commit88d7181

Browse files
authored
fix: filter "add group member" by organization (#14404)
This is accomplished by using the members endpoint instead of the usersendpoint, and to that end the UserAutocomplete component has beenreworked to support either endpoint as separate components with a sharedbase.* Add Storybook for groups pageThis ensures it is using the right endpoint for the add member dropdown.* Add ability to mock react-query errors
1 parent83f9ea1 commit88d7181

File tree

8 files changed

+306
-59
lines changed

8 files changed

+306
-59
lines changed

‎site/.storybook/preview.jsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ThemeProvider as EmotionThemeProvider } from "@emotion/react";
77
import{DecoratorHelpers}from"@storybook/addon-themes";
88
import{withRouter}from"storybook-addon-remix-react-router";
99
import{StrictMode}from"react";
10-
import{QueryClient,QueryClientProvider}from"react-query";
10+
import{parseQueryArgs,QueryClient,QueryClientProvider}from"react-query";
1111
import{HelmetProvider}from"react-helmet-async";
1212
importthemesfrom"theme";
1313
import"theme/globalFonts";
@@ -93,7 +93,18 @@ function withQuery(Story, { parameters }) {
9393

9494
if(parameters.queries){
9595
parameters.queries.forEach((query)=>{
96-
queryClient.setQueryData(query.key,query.data);
96+
if(query.datainstanceofError){
97+
// This is copied from setQueryData() but sets the error.
98+
constcache=queryClient.getQueryCache();
99+
constparsedOptions=parseQueryArgs(query.key)
100+
constdefaultedOptions=queryClient.defaultQueryOptions(parsedOptions)
101+
constcachedQuery=cache.build(queryClient,defaultedOptions);
102+
// Set manual data so react-query will not try to refetch.
103+
cachedQuery.setData(undefined,{manual:true});
104+
cachedQuery.setState({error:query.data});
105+
}else{
106+
queryClient.setQueryData(query.key,query.data);
107+
}
97108
});
98109
}
99110

‎site/src/api/queries/groups.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const groups = (organization: string) => {
2121
}satisfiesUseQueryOptions<Group[]>;
2222
};
2323

24-
constgetGroupQueryKey=(organization:string,groupName:string)=>[
24+
exportconstgetGroupQueryKey=(organization:string,groupName:string)=>[
2525
"organization",
2626
organization,
2727
"group",
@@ -77,9 +77,15 @@ export function groupsForUser(organization: string, userId: string) {
7777
}asconstsatisfiesUseQueryOptions<Group[],unknown,readonlyGroup[]>;
7878
}
7979

80+
exportconstgroupPermissionsKey=(groupId:string)=>[
81+
"group",
82+
groupId,
83+
"permissions",
84+
];
85+
8086
exportconstgroupPermissions=(groupId:string)=>{
8187
return{
82-
queryKey:["group",groupId,"permissions"],
88+
queryKey:groupPermissionsKey(groupId),
8389
queryFn:()=>
8490
API.checkAuthorization({
8591
checks:{

‎site/src/api/queries/organizations.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,16 @@ export const deleteOrganization = (queryClient: QueryClient) => {
4747
};
4848
};
4949

50+
exportconstorganizationMembersKey=(id:string)=>[
51+
"organization",
52+
id,
53+
"members",
54+
];
55+
5056
exportconstorganizationMembers=(id:string)=>{
5157
return{
5258
queryFn:()=>API.getOrganizationMembers(id),
53-
queryKey:["organization",id,"members"],
59+
queryKey:organizationMembersKey(id),
5460
};
5561
};
5662

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
importtype{Meta,StoryObj}from"@storybook/react";
2+
import{MockOrganizationMember}from"testHelpers/entities";
3+
import{MemberAutocomplete}from"./UserAutocomplete";
4+
5+
constmeta:Meta<typeofMemberAutocomplete>={
6+
title:"components/MemberAutocomplete",
7+
component:MemberAutocomplete,
8+
};
9+
10+
exportdefaultmeta;
11+
typeStory=StoryObj<typeofMemberAutocomplete>;
12+
13+
exportconstWithLabel:Story={
14+
args:{
15+
value:MockOrganizationMember,
16+
organizationId:MockOrganizationMember.organization_id,
17+
label:"Member",
18+
},
19+
};
20+
21+
exportconstNoLabel:Story={
22+
args:{
23+
value:MockOrganizationMember,
24+
organizationId:MockOrganizationMember.organization_id,
25+
},
26+
};

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const meta: Meta<typeof UserAutocomplete> = {
1010
exportdefaultmeta;
1111
typeStory=StoryObj<typeofUserAutocomplete>;
1212

13-
exportconstExample:Story={
13+
exportconstWithLabel:Story={
1414
args:{
1515
value:MockUser,
1616
label:"User",

‎site/src/components/UserAutocomplete/UserAutocomplete.tsx

Lines changed: 99 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { css } from "@emotion/css";
22
importAutocompletefrom"@mui/material/Autocomplete";
33
importCircularProgressfrom"@mui/material/CircularProgress";
44
importTextFieldfrom"@mui/material/TextField";
5+
import{getErrorMessage}from"api/errors";
6+
import{organizationMembers}from"api/queries/organizations";
57
import{users}from"api/queries/users";
6-
importtype{User}from"api/typesGenerated";
8+
importtype{OrganizationMemberWithUserData,User}from"api/typesGenerated";
79
import{Avatar}from"components/Avatar/Avatar";
810
import{AvatarData}from"components/AvatarData/AvatarData";
911
import{useDebouncedFunction}from"hooks/debounce";
@@ -16,71 +18,128 @@ import {
1618
import{useQuery}from"react-query";
1719
import{prepareQuery}from"utils/filters";
1820

19-
exporttypeUserAutocompleteProps={
20-
value:User|null;
21-
onChange:(user:User|null)=>void;
22-
label?:string;
21+
// The common properties between users and org members that we need.
22+
exporttypeSelectedUser={
23+
avatar_url:string;
24+
email:string;
25+
username:string;
26+
};
27+
28+
exporttypeCommonAutocompleteProps<TextendsSelectedUser>={
2329
className?:string;
24-
size?:ComponentProps<typeofTextField>["size"];
30+
label?:string;
31+
onChange:(user:T|null)=>void;
2532
required?:boolean;
33+
size?:ComponentProps<typeofTextField>["size"];
34+
value:T|null;
2635
};
2736

28-
exportconstUserAutocomplete:FC<UserAutocompleteProps>=({
29-
value,
30-
onChange,
31-
label,
32-
className,
33-
size="small",
34-
required,
35-
})=>{
36-
const[autoComplete,setAutoComplete]=useState<{
37-
value:string;
38-
open:boolean;
39-
}>({
40-
value:value?.email??"",
41-
open:false,
42-
});
37+
exporttypeUserAutocompleteProps=CommonAutocompleteProps<User>;
38+
39+
exportconstUserAutocomplete:FC<UserAutocompleteProps>=(props)=>{
40+
const[filter,setFilter]=useState<string>();
41+
4342
constusersQuery=useQuery({
4443
...users({
45-
q:prepareQuery(encodeURI(autoComplete.value)),
44+
q:prepareQuery(encodeURI(filter??"")),
4645
limit:25,
4746
}),
48-
enabled:autoComplete.open,
47+
enabled:filter!==undefined,
48+
keepPreviousData:true,
49+
});
50+
return(
51+
<InnerAutocomplete<User>
52+
error={usersQuery.error}
53+
isFetching={usersQuery.isFetching}
54+
setFilter={setFilter}
55+
users={usersQuery.data?.users}
56+
{...props}
57+
/>
58+
);
59+
};
60+
61+
exporttypeMemberAutocompleteProps=
62+
CommonAutocompleteProps<OrganizationMemberWithUserData>&{
63+
organizationId:string;
64+
};
65+
66+
exportconstMemberAutocomplete:FC<MemberAutocompleteProps>=({
67+
organizationId,
68+
...props
69+
})=>{
70+
const[filter,setFilter]=useState<string>();
71+
72+
// Currently this queries all members, as there is no pagination.
73+
constmembersQuery=useQuery({
74+
...organizationMembers(organizationId),
75+
enabled:filter!==undefined,
4976
keepPreviousData:true,
5077
});
78+
return(
79+
<InnerAutocomplete<OrganizationMemberWithUserData>
80+
error={membersQuery.error}
81+
isFetching={membersQuery.isFetching}
82+
setFilter={setFilter}
83+
users={membersQuery.data}
84+
{...props}
85+
/>
86+
);
87+
};
88+
89+
typeInnerAutocompleteProps<TextendsSelectedUser>=
90+
CommonAutocompleteProps<T>&{
91+
/** The error is null if not loaded or no error. */
92+
error:unknown;
93+
isFetching:boolean;
94+
/** Filter is undefined if the autocomplete is closed. */
95+
setFilter:(filter:string|undefined)=>void;
96+
/** Users are undefined if not loaded or errored. */
97+
users:readonlyT[]|undefined;
98+
};
99+
100+
constInnerAutocomplete=<TextendsSelectedUser>({
101+
className,
102+
error,
103+
isFetching,
104+
label,
105+
onChange,
106+
required,
107+
setFilter,
108+
size="small",
109+
users,
110+
value,
111+
}:InnerAutocompleteProps<T>)=>{
112+
const[open,setOpen]=useState(false);
51113

52114
const{debounced:debouncedInputOnChange}=useDebouncedFunction(
53115
(event:ChangeEvent<HTMLInputElement>)=>{
54-
setAutoComplete((state)=>({
55-
...state,
56-
value:event.target.value,
57-
}));
116+
setFilter(event.target.value??"");
58117
},
59118
750,
60119
);
61120

62121
return(
63122
<Autocomplete
64-
noOptionsText="No users found"
123+
noOptionsText={
124+
error
125+
?getErrorMessage(error,"Unable to fetch users")
126+
:"No users found"
127+
}
65128
className={className}
66-
options={usersQuery.data?.users??[]}
67-
loading={usersQuery.isLoading}
129+
options={users??[]}
130+
loading={!users&&!error}
68131
value={value}
69132
data-testid="user-autocomplete"
70-
open={autoComplete.open}
133+
open={open}
71134
isOptionEqualToValue={(a,b)=>a.username===b.username}
72135
getOptionLabel={(option)=>option.email}
73136
onOpen={()=>{
74-
setAutoComplete((state)=>({
75-
...state,
76-
open:true,
77-
}));
137+
setOpen(true);
138+
setFilter(value?.email??"");
78139
}}
79140
onClose={()=>{
80-
setAutoComplete({
81-
value:value?.email??"",
82-
open:false,
83-
});
141+
setOpen(false);
142+
setFilter(undefined);
84143
}}
85144
onChange={(_,newValue)=>{
86145
onChange(newValue);
@@ -117,9 +176,7 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
117176
),
118177
endAdornment:(
119178
<>
120-
{usersQuery.isFetching&&autoComplete.open&&(
121-
<CircularProgresssize={16}/>
122-
)}
179+
{isFetching&&open&&<CircularProgresssize={16}/>}
123180
{params.InputProps.endAdornment}
124181
</>
125182
),

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp