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

Commit32c36d5

Browse files
authored
feat: allow selecting the initial organization for new users (#16829)
1 parentdb064ed commit32c36d5

File tree

7 files changed

+151
-77
lines changed

7 files changed

+151
-77
lines changed

‎site/e2e/helpers.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,6 +1062,7 @@ type UserValues = {
10621062
exportasyncfunctioncreateUser(
10631063
page:Page,
10641064
userValues:Partial<UserValues>={},
1065+
orgName=defaultOrganizationName,
10651066
):Promise<UserValues>{
10661067
constreturnTo=page.url();
10671068

@@ -1082,6 +1083,16 @@ export async function createUser(
10821083
awaitpage.getByLabel("Full name").fill(name);
10831084
}
10841085
awaitpage.getByLabel("Email").fill(email);
1086+
1087+
// If the organization picker is present on the page, select the default
1088+
// organization.
1089+
constorgPicker=page.getByLabel("Organization *");
1090+
constorganizationsEnabled=awaitorgPicker.isVisible();
1091+
if(organizationsEnabled){
1092+
awaitorgPicker.click();
1093+
awaitpage.getByText(orgName,{exact:true}).click();
1094+
}
1095+
10851096
awaitpage.getByLabel("Login Type").click();
10861097
awaitpage.getByRole("option",{name:"Password",exact:false}).click();
10871098
// Using input[name=password] due to the select element utilizing 'password'

‎site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx

Lines changed: 14 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,10 @@ import { organizations } from "api/queries/organizations";
77
importtype{AuthorizationCheck,Organization}from"api/typesGenerated";
88
import{Avatar}from"components/Avatar/Avatar";
99
import{AvatarData}from"components/Avatar/AvatarData";
10-
import{useDebouncedFunction}from"hooks/debounce";
11-
import{
12-
typeChangeEvent,
13-
typeComponentProps,
14-
typeFC,
15-
useState,
16-
}from"react";
10+
import{typeComponentProps,typeFC,useState}from"react";
1711
import{useQuery}from"react-query";
1812

1913
exporttypeOrganizationAutocompleteProps={
20-
value:Organization|null;
2114
onChange:(organization:Organization|null)=>void;
2215
label?:string;
2316
className?:string;
@@ -27,21 +20,16 @@ export type OrganizationAutocompleteProps = {
2720
};
2821

2922
exportconstOrganizationAutocomplete:FC<OrganizationAutocompleteProps>=({
30-
value,
3123
onChange,
3224
label,
3325
className,
3426
size="small",
3527
required,
3628
check,
3729
})=>{
38-
const[autoComplete,setAutoComplete]=useState<{
39-
value:string;
40-
open:boolean;
41-
}>({
42-
value:value?.name??"",
43-
open:false,
44-
});
30+
const[open,setOpen]=useState(false);
31+
const[selected,setSelected]=useState<Organization|null>(null);
32+
4533
constorganizationsQuery=useQuery(organizations());
4634

4735
constpermissionsQuery=useQuery(
@@ -60,16 +48,6 @@ export const OrganizationAutocomplete: FC<OrganizationAutocompleteProps> = ({
6048
:{enabled:false},
6149
);
6250

63-
const{debounced:debouncedInputOnChange}=useDebouncedFunction(
64-
(event:ChangeEvent<HTMLInputElement>)=>{
65-
setAutoComplete((state)=>({
66-
...state,
67-
value:event.target.value,
68-
}));
69-
},
70-
750,
71-
);
72-
7351
// If an authorization check was provided, filter the organizations based on
7452
// the results of that check.
7553
letoptions=organizationsQuery.data??[];
@@ -85,24 +63,18 @@ export const OrganizationAutocomplete: FC<OrganizationAutocompleteProps> = ({
8563
className={className}
8664
options={options}
8765
loading={organizationsQuery.isLoading}
88-
value={value}
8966
data-testid="organization-autocomplete"
90-
open={autoComplete.open}
91-
isOptionEqualToValue={(a,b)=>a.name===b.name}
67+
open={open}
68+
isOptionEqualToValue={(a,b)=>a.id===b.id}
9269
getOptionLabel={(option)=>option.display_name}
9370
onOpen={()=>{
94-
setAutoComplete((state)=>({
95-
...state,
96-
open:true,
97-
}));
71+
setOpen(true);
9872
}}
9973
onClose={()=>{
100-
setAutoComplete({
101-
value:value?.name??"",
102-
open:false,
103-
});
74+
setOpen(false);
10475
}}
10576
onChange={(_,newValue)=>{
77+
setSelected(newValue);
10678
onChange(newValue);
10779
}}
10880
renderOption={({ key, ...props},option)=>(
@@ -130,13 +102,12 @@ export const OrganizationAutocomplete: FC<OrganizationAutocompleteProps> = ({
130102
}}
131103
InputProps={{
132104
...params.InputProps,
133-
onChange:debouncedInputOnChange,
134-
startAdornment:value&&(
135-
<Avatarsize="sm"src={value.icon}fallback={value.name}/>
105+
startAdornment:selected&&(
106+
<Avatarsize="sm"src={selected.icon}fallback={selected.name}/>
136107
),
137108
endAdornment:(
138109
<>
139-
{organizationsQuery.isFetching&&autoComplete.open&&(
110+
{organizationsQuery.isFetching&&open&&(
140111
<CircularProgresssize={16}/>
141112
)}
142113
{params.InputProps.endAdornment}
@@ -154,6 +125,6 @@ export const OrganizationAutocomplete: FC<OrganizationAutocompleteProps> = ({
154125
};
155126

156127
constroot=css`
157-
padding-left:14px!important; // Same padding left as input
158-
gap:4px;
128+
padding-left:14px!important; // Same padding left as input
129+
gap:4px;
159130
`;

‎site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,6 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
266266
{...getFieldHelpers("organization")}
267267
required
268268
label="Belongs to"
269-
value={selectedOrg}
270269
onChange={(newValue)=>{
271270
setSelectedOrg(newValue);
272271
voidform.setFieldValue("organization",newValue?.name||"");

‎site/src/pages/CreateUserPage/CreateUserForm.stories.tsx

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import{action}from"@storybook/addon-actions";
22
importtype{Meta,StoryObj}from"@storybook/react";
3-
import{mockApiError}from"testHelpers/entities";
3+
import{userEvent,within}from"@storybook/test";
4+
import{organizationsKey}from"api/queries/organizations";
5+
importtype{Organization}from"api/typesGenerated";
6+
import{
7+
MockOrganization,
8+
MockOrganization2,
9+
mockApiError,
10+
}from"testHelpers/entities";
411
import{CreateUserForm}from"./CreateUserForm";
512

613
constmeta:Meta<typeofCreateUserForm>={
@@ -18,6 +25,48 @@ type Story = StoryObj<typeof CreateUserForm>;
1825

1926
exportconstReady:Story={};
2027

28+
constpermissionCheckQuery=(organizations:Organization[])=>{
29+
return{
30+
key:[
31+
"authorization",
32+
{
33+
checks:Object.fromEntries(
34+
organizations.map((org)=>[
35+
org.id,
36+
{
37+
action:"create",
38+
object:{
39+
resource_type:"organization_member",
40+
organization_id:org.id,
41+
},
42+
},
43+
]),
44+
),
45+
},
46+
],
47+
data:Object.fromEntries(organizations.map((org)=>[org.id,true])),
48+
};
49+
};
50+
51+
exportconstWithOrganizations:Story={
52+
parameters:{
53+
queries:[
54+
{
55+
key:organizationsKey,
56+
data:[MockOrganization,MockOrganization2],
57+
},
58+
permissionCheckQuery([MockOrganization,MockOrganization2]),
59+
],
60+
},
61+
args:{
62+
showOrganizations:true,
63+
},
64+
play:async({ canvasElement})=>{
65+
constcanvas=within(canvasElement);
66+
awaituserEvent.click(canvas.getByLabelText("Organization *"));
67+
},
68+
};
69+
2170
exportconstFormError:Story={
2271
args:{
2372
error:mockApiError({

‎site/src/pages/CreateUserPage/CreateUserForm.tsx

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import { ErrorAlert } from "components/Alert/ErrorAlert";
77
import{Button}from"components/Button/Button";
88
import{FormFooter}from"components/Form/Form";
99
import{FullPageForm}from"components/FullPageForm/FullPageForm";
10+
import{OrganizationAutocomplete}from"components/OrganizationAutocomplete/OrganizationAutocomplete";
1011
import{PasswordField}from"components/PasswordField/PasswordField";
1112
import{Spinner}from"components/Spinner/Spinner";
1213
import{Stack}from"components/Stack/Stack";
13-
import{typeFormikContextType,useFormik}from"formik";
14+
import{useFormik}from"formik";
1415
importtype{FC}from"react";
1516
import{
1617
displayNameValidator,
@@ -52,14 +53,6 @@ export const authMethodLanguage = {
5253
},
5354
};
5455

55-
exportinterfaceCreateUserFormProps{
56-
onSubmit:(user:TypesGen.CreateUserRequestWithOrgs)=>void;
57-
onCancel:()=>void;
58-
error?:unknown;
59-
isLoading:boolean;
60-
authMethods?:TypesGen.AuthMethods;
61-
}
62-
6356
constvalidationSchema=Yup.object({
6457
email:Yup.string()
6558
.trim()
@@ -75,27 +68,51 @@ const validationSchema = Yup.object({
7568
login_type:Yup.string().oneOf(Object.keys(authMethodLanguage)),
7669
});
7770

71+
typeCreateUserFormData={
72+
readonlyusername:string;
73+
readonlyname:string;
74+
readonlyemail:string;
75+
readonlyorganization:string;
76+
readonlylogin_type:TypesGen.LoginType;
77+
readonlypassword:string;
78+
};
79+
80+
exportinterfaceCreateUserFormProps{
81+
error?:unknown;
82+
isLoading:boolean;
83+
onSubmit:(user:CreateUserFormData)=>void;
84+
onCancel:()=>void;
85+
authMethods?:TypesGen.AuthMethods;
86+
showOrganizations:boolean;
87+
}
88+
7889
exportconstCreateUserForm:FC<
7990
React.PropsWithChildren<CreateUserFormProps>
80-
>=({ onSubmit, onCancel, error, isLoading, authMethods})=>{
81-
constform:FormikContextType<TypesGen.CreateUserRequestWithOrgs>=
82-
useFormik<TypesGen.CreateUserRequestWithOrgs>({
83-
initialValues:{
84-
email:"",
85-
password:"",
86-
username:"",
87-
name:"",
88-
organization_ids:["00000000-0000-0000-0000-000000000000"],
89-
login_type:"",
90-
user_status:null,
91-
},
92-
validationSchema,
93-
onSubmit,
94-
});
95-
constgetFieldHelpers=getFormHelpers<TypesGen.CreateUserRequestWithOrgs>(
96-
form,
97-
error,
98-
);
91+
>=({
92+
error,
93+
isLoading,
94+
onSubmit,
95+
onCancel,
96+
showOrganizations,
97+
authMethods,
98+
})=>{
99+
constform=useFormik<CreateUserFormData>({
100+
initialValues:{
101+
email:"",
102+
password:"",
103+
username:"",
104+
name:"",
105+
// If organizations aren't enabled, use the fallback ID to add the user to
106+
// the default organization.
107+
organization:showOrganizations
108+
?""
109+
:"00000000-0000-0000-0000-000000000000",
110+
login_type:"",
111+
},
112+
validationSchema,
113+
onSubmit,
114+
});
115+
constgetFieldHelpers=getFormHelpers(form,error);
99116

100117
constmethods=[
101118
authMethods?.password.enabled&&"password",
@@ -132,6 +149,20 @@ export const CreateUserForm: FC<
132149
fullWidth
133150
label={Language.emailLabel}
134151
/>
152+
{showOrganizations&&(
153+
<OrganizationAutocomplete
154+
{...getFieldHelpers("organization")}
155+
required
156+
label="Organization"
157+
onChange={(newValue)=>{
158+
voidform.setFieldValue("organization",newValue?.id??"");
159+
}}
160+
check={{
161+
object:{resource_type:"organization_member"},
162+
action:"create",
163+
}}
164+
/>
165+
)}
135166
<TextField
136167
{...getFieldHelpers("login_type",{
137168
helperText:"Authentication method for this user",

‎site/src/pages/CreateUserPage/CreateUserPage.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import { Language as FormLanguage } from "./Language";
99

1010
constrenderCreateUserPage=async()=>{
1111
renderWithAuth(<CreateUserPage/>,{
12-
extraRoutes:[{path:"/users",element:<div>Users Page</div>}],
12+
extraRoutes:[
13+
{path:"/deployment/users",element:<div>Users Page</div>},
14+
],
1315
});
1416
awaitwaitForLoaderToBeRemoved();
1517
};

‎site/src/pages/CreateUserPage/CreateUserPage.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import{authMethods,createUser}from"api/queries/users";
22
import{displaySuccess}from"components/GlobalSnackbar/utils";
33
import{Margins}from"components/Margins/Margins";
4+
import{useDashboard}from"modules/dashboard/useDashboard";
45
importtype{FC}from"react";
56
import{Helmet}from"react-helmet-async";
67
import{useMutation,useQuery,useQueryClient}from"react-query";
@@ -17,6 +18,7 @@ export const CreateUserPage: FC = () => {
1718
constqueryClient=useQueryClient();
1819
constcreateUserMutation=useMutation(createUser(queryClient));
1920
constauthMethodsQuery=useQuery(authMethods());
21+
const{ showOrganizations}=useDashboard();
2022

2123
return(
2224
<Margins>
@@ -26,16 +28,25 @@ export const CreateUserPage: FC = () => {
2628

2729
<CreateUserForm
2830
error={createUserMutation.error}
29-
authMethods={authMethodsQuery.data}
31+
isLoading={createUserMutation.isLoading}
3032
onSubmit={async(user)=>{
31-
awaitcreateUserMutation.mutateAsync(user);
33+
awaitcreateUserMutation.mutateAsync({
34+
username:user.username,
35+
name:user.name,
36+
email:user.email,
37+
organization_ids:[user.organization],
38+
login_type:user.login_type,
39+
password:user.password,
40+
user_status:null,
41+
});
3242
displaySuccess("Successfully created user.");
3343
navigate("..",{relative:"path"});
3444
}}
3545
onCancel={()=>{
3646
navigate("..",{relative:"path"});
3747
}}
38-
isLoading={createUserMutation.isLoading}
48+
authMethods={authMethodsQuery.data}
49+
showOrganizations={showOrganizations}
3950
/>
4051
</Margins>
4152
);

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp