mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
fix: allow users with workspace:create for any owner to list users (#21947)
## Summary Custom roles that can create workspaces on behalf of other users need to be able to list users to populate the owner dropdown in the workspace creation UI. Previously, this required a separate `user:read` permission, causing the dropdown to fail for custom roles. ## Changes - Modified `GetUsers` in `dbauthz` to check if the user can create workspaces for any owner (`workspace:create` with `owner_id: *`) - If the user has this permission, they can list all users without needing explicit `user:read` permission - Added tests to verify the new behavior ## Testing - Updated mock tests to assert the new authorization check - Added integration tests for both positive and negative cases Fixes #18203
This commit is contained in:
Generated
+63
@@ -3745,6 +3745,69 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations/{organization}/members/{user}/workspaces/available-users": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Workspaces"
|
||||
],
|
||||
"summary": "Get users available for workspace creation",
|
||||
"operationId": "get-users-available-for-workspace-creation",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Organization ID",
|
||||
"name": "organization",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID, name, or me",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Search query",
|
||||
"name": "q",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Limit results",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Offset for pagination",
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.MinimalUser"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations/{organization}/paginated-members": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
||||
Generated
+59
@@ -3296,6 +3296,65 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations/{organization}/members/{user}/workspaces/available-users": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Workspaces"],
|
||||
"summary": "Get users available for workspace creation",
|
||||
"operationId": "get-users-available-for-workspace-creation",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Organization ID",
|
||||
"name": "organization",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID, name, or me",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Search query",
|
||||
"name": "q",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Limit results",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Offset for pagination",
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.MinimalUser"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations/{organization}/paginated-members": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
||||
+4
-1
@@ -1232,7 +1232,10 @@ func New(options *Options) *API {
|
||||
r.Get("/", api.organizationMember)
|
||||
r.Delete("/", api.deleteOrganizationMember)
|
||||
r.Put("/roles", api.putMemberRoles)
|
||||
r.Post("/workspaces", api.postWorkspacesByOrganization)
|
||||
r.Route("/workspaces", func(r chi.Router) {
|
||||
r.Post("/", api.postWorkspacesByOrganization)
|
||||
r.Get("/available-users", api.workspaceAvailableUsers)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -265,6 +265,16 @@ func TestRolePermissions(t *testing.T) {
|
||||
false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "CreateWorkspaceForMembers",
|
||||
// When creating the WithID won't be set, but it does not change the result.
|
||||
Actions: []policy.Action{policy.ActionCreate},
|
||||
Resource: rbac.ResourceWorkspace.InOrg(orgID).WithOwner(policy.WildcardSymbol),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin},
|
||||
false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, userAdmin, templateAdmin, orgTemplateAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "MyWorkspaceInOrgExecution",
|
||||
// When creating the WithID won't be set, but it does not change the result.
|
||||
|
||||
@@ -2952,3 +2952,48 @@ func convertToWorkspaceRole(actions []policy.Action) codersdk.WorkspaceRole {
|
||||
|
||||
return codersdk.WorkspaceRoleDeleted
|
||||
}
|
||||
|
||||
// @Summary Get users available for workspace creation
|
||||
// @ID get-users-available-for-workspace-creation
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Workspaces
|
||||
// @Param organization path string true "Organization ID" format(uuid)
|
||||
// @Param user path string true "User ID, name, or me"
|
||||
// @Param q query string false "Search query"
|
||||
// @Param limit query int false "Limit results"
|
||||
// @Param offset query int false "Offset for pagination"
|
||||
// @Success 200 {array} codersdk.MinimalUser
|
||||
// @Router /organizations/{organization}/members/{user}/workspaces/available-users [get]
|
||||
func (api *API) workspaceAvailableUsers(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
|
||||
// This endpoint requires the user to be able to create workspaces for other
|
||||
// users in this organization. We check if they can create a workspace with
|
||||
// a wildcard owner.
|
||||
if !api.Authorize(r, policy.ActionCreate, rbac.ResourceWorkspace.InOrg(organization.ID).WithOwner(policy.WildcardSymbol)) {
|
||||
httpapi.Forbidden(rw)
|
||||
return
|
||||
}
|
||||
|
||||
// Use system context to list all users. The authorization check above
|
||||
// ensures only users who can create workspaces for others can access this.
|
||||
//nolint:gocritic // System context needed to list users for workspace owner selection.
|
||||
users, _, ok := api.GetUsers(rw, r.WithContext(dbauthz.AsSystemRestricted(ctx)))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
minimalUsers := make([]codersdk.MinimalUser, 0, len(users))
|
||||
for _, user := range users {
|
||||
minimalUsers = append(minimalUsers, codersdk.MinimalUser{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Name: user.Name,
|
||||
AvatarURL: user.AvatarURL,
|
||||
})
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, minimalUsers)
|
||||
}
|
||||
|
||||
@@ -5625,6 +5625,54 @@ func TestWorkspaceSharingDisabled(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspaceAvailableUsers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OrgAdminCanListUsers", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
// Create an org admin and additional users
|
||||
orgAdminClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID))
|
||||
_, user1 := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
_, user2 := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
|
||||
// Org admin should be able to list available users
|
||||
users, err := orgAdminClient.WorkspaceAvailableUsers(ctx, owner.OrganizationID, "me")
|
||||
require.NoError(t, err)
|
||||
require.GreaterOrEqual(t, len(users), 4) // owner + orgAdmin + 2 users
|
||||
|
||||
// Verify the users we created are in the list
|
||||
usernames := make([]string, 0, len(users))
|
||||
for _, u := range users {
|
||||
usernames = append(usernames, u.Username)
|
||||
}
|
||||
require.Contains(t, usernames, user1.Username)
|
||||
require.Contains(t, usernames, user2.Username)
|
||||
})
|
||||
|
||||
t.Run("MemberCannotListUsers", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
// Create a regular member
|
||||
memberClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
|
||||
// Regular member should not be able to list available users
|
||||
_, err := memberClient.WorkspaceAvailableUsers(ctx, owner.OrganizationID, "me")
|
||||
require.Error(t, err)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusForbidden, apiErr.StatusCode())
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspaceCreateWithImplicitPreset(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -787,3 +787,19 @@ func (c *Client) WorkspaceExternalAgentCredentials(ctx context.Context, workspac
|
||||
var credentials ExternalAgentCredentials
|
||||
return credentials, json.NewDecoder(res.Body).Decode(&credentials)
|
||||
}
|
||||
|
||||
// WorkspaceAvailableUsers returns users available for workspace creation.
|
||||
// This is used to populate the owner dropdown when creating workspaces for
|
||||
// other users.
|
||||
func (c *Client) WorkspaceAvailableUsers(ctx context.Context, organizationID uuid.UUID, userID string) ([]MinimalUser, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/members/%s/workspaces/available-users", organizationID, userID), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, ReadBodyAsError(res)
|
||||
}
|
||||
var users []MinimalUser
|
||||
return users, json.NewDecoder(res.Body).Decode(&users)
|
||||
}
|
||||
|
||||
Generated
+58
@@ -331,6 +331,64 @@ of the template will be used.
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get users available for workspace creation
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members/{user}/workspaces/available-users \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /organizations/{organization}/members/{user}/workspaces/available-users`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|----------------|-------|--------------|----------|-----------------------|
|
||||
| `organization` | path | string(uuid) | true | Organization ID |
|
||||
| `user` | path | string | true | User ID, name, or me |
|
||||
| `q` | query | string | false | Search query |
|
||||
| `limit` | query | integer | false | Limit results |
|
||||
| `offset` | query | integer | false | Offset for pagination |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"avatar_url": "http://example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"name": "string",
|
||||
"username": "string"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|---------------------------------------------------------|-------------|-----------------------------------------------------------------|
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.MinimalUser](schemas.md#codersdkminimaluser) |
|
||||
|
||||
<h3 id="get-users-available-for-workspace-creation-responseschema">Response Schema</h3>
|
||||
|
||||
Status Code **200**
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|----------------|--------------|----------|--------------|-------------|
|
||||
| `[array item]` | array | false | | |
|
||||
| `» avatar_url` | string(uri) | false | | |
|
||||
| `» id` | string(uuid) | true | | |
|
||||
| `» name` | string | false | | |
|
||||
| `» username` | string | true | | |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get workspace metadata by user and workspace name
|
||||
|
||||
### Code samples
|
||||
|
||||
@@ -569,6 +569,28 @@ class ApiMethods {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get users for workspace owner selection. Requires
|
||||
* permission to create workspaces for other users in the
|
||||
* organization. Returns minimal user data (no email, roles,
|
||||
* etc.).
|
||||
*/
|
||||
getWorkspaceAvailableUsers = async (
|
||||
organizationId: string,
|
||||
options: TypesGen.UsersRequest,
|
||||
signal?: AbortSignal,
|
||||
): Promise<TypesGen.MinimalUser[]> => {
|
||||
const url = getURLWithSearchParams(
|
||||
`/api/v2/organizations/${organizationId}/members/me/workspaces/available-users`,
|
||||
options,
|
||||
);
|
||||
const response = await this.axios.get<TypesGen.MinimalUser[]>(
|
||||
url.toString(),
|
||||
{ signal },
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
createOrganization = async (params: TypesGen.CreateOrganizationRequest) => {
|
||||
const response = await this.axios.post<TypesGen.Organization>(
|
||||
"/api/v2/organizations",
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
AuthorizationRequest,
|
||||
GenerateAPIKeyResponse,
|
||||
GetUsersResponse,
|
||||
MinimalUser,
|
||||
RequestOneTimePasscodeRequest,
|
||||
UpdateUserAppearanceSettingsRequest,
|
||||
UpdateUserPasswordRequest,
|
||||
@@ -58,6 +59,18 @@ export const users = (req: UsersRequest): UseQueryOptions<GetUsersResponse> => {
|
||||
};
|
||||
};
|
||||
|
||||
export const workspaceAvailableUsers = (
|
||||
organizationId: string,
|
||||
req: UsersRequest,
|
||||
): UseQueryOptions<MinimalUser[]> => {
|
||||
return {
|
||||
queryKey: ["workspaceAvailableUsers", organizationId, req],
|
||||
queryFn: ({ signal }) =>
|
||||
API.getWorkspaceAvailableUsers(organizationId, req, signal),
|
||||
gcTime: 5 * 1000 * 60,
|
||||
};
|
||||
};
|
||||
|
||||
export const updatePassword = () => {
|
||||
return {
|
||||
mutationFn: ({
|
||||
|
||||
@@ -4,8 +4,12 @@ import CircularProgress from "@mui/material/CircularProgress";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { getErrorMessage } from "api/errors";
|
||||
import { organizationMembers } from "api/queries/organizations";
|
||||
import { users } from "api/queries/users";
|
||||
import type { OrganizationMemberWithUserData, User } from "api/typesGenerated";
|
||||
import { users, workspaceAvailableUsers } from "api/queries/users";
|
||||
import type {
|
||||
MinimalUser,
|
||||
OrganizationMemberWithUserData,
|
||||
User,
|
||||
} from "api/typesGenerated";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import { AvatarData } from "components/Avatar/AvatarData";
|
||||
import { useDebouncedFunction } from "hooks/debounce";
|
||||
@@ -21,7 +25,7 @@ import { prepareQuery } from "utils/filters";
|
||||
// The common properties between users and org members that we need.
|
||||
type SelectedUser = {
|
||||
avatar_url?: string;
|
||||
email: string;
|
||||
email?: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
@@ -85,6 +89,35 @@ export const MemberAutocomplete: FC<MemberAutocompleteProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
type WorkspaceUserAutocompleteProps = CommonAutocompleteProps<MinimalUser> & {
|
||||
organizationId: string;
|
||||
};
|
||||
|
||||
export const WorkspaceUserAutocomplete: FC<WorkspaceUserAutocompleteProps> = ({
|
||||
organizationId,
|
||||
...props
|
||||
}) => {
|
||||
const [filter, setFilter] = useState<string>();
|
||||
|
||||
const availableUsersQuery = useQuery({
|
||||
...workspaceAvailableUsers(organizationId, {
|
||||
q: prepareQuery(encodeURI(filter ?? "")),
|
||||
limit: 25,
|
||||
}),
|
||||
enabled: filter !== undefined,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
return (
|
||||
<InnerAutocomplete<MinimalUser>
|
||||
error={availableUsersQuery.error}
|
||||
isFetching={availableUsersQuery.isFetching}
|
||||
setFilter={setFilter}
|
||||
users={availableUsersQuery.data}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type InnerAutocompleteProps<T extends SelectedUser> =
|
||||
CommonAutocompleteProps<T> & {
|
||||
/** The error is null if not loaded or no error. */
|
||||
@@ -131,10 +164,10 @@ const InnerAutocomplete = <T extends SelectedUser>({
|
||||
data-testid="user-autocomplete"
|
||||
open={open}
|
||||
isOptionEqualToValue={(a, b) => a.username === b.username}
|
||||
getOptionLabel={(option) => option.email}
|
||||
getOptionLabel={(option) => option.email ?? option.username}
|
||||
onOpen={() => {
|
||||
setOpen(true);
|
||||
setFilter(value?.email ?? "");
|
||||
setFilter(value?.email ?? value?.username ?? "");
|
||||
}}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
@@ -159,7 +192,7 @@ const InnerAutocomplete = <T extends SelectedUser>({
|
||||
fullWidth
|
||||
size={size}
|
||||
label={label}
|
||||
placeholder="User email or username"
|
||||
placeholder="Username or email"
|
||||
css={{
|
||||
"&:not(:has(label))": {
|
||||
margin: 0,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { autoCreateWorkspace, createWorkspace } from "api/queries/workspaces";
|
||||
import type {
|
||||
DynamicParametersRequest,
|
||||
DynamicParametersResponse,
|
||||
MinimalUser,
|
||||
PreviewParameter,
|
||||
Workspace,
|
||||
} from "api/typesGenerated";
|
||||
@@ -63,8 +64,8 @@ const CreateWorkspacePage: FC = () => {
|
||||
const [autoCreateConsented, setAutoCreateConsented] = useState(false);
|
||||
const [autoCreateError, setAutoCreateError] =
|
||||
useState<ApiErrorResponse | null>(null);
|
||||
const defaultOwner = me;
|
||||
const [owner, setOwner] = useState(defaultOwner);
|
||||
const defaultOwner: MinimalUser = me;
|
||||
const [owner, setOwner] = useState<MinimalUser>(defaultOwner);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const autoCreateWorkspaceMutation = useMutation(
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
|
||||
import { WorkspaceUserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
|
||||
import { type FormikContextType, useFormik } from "formik";
|
||||
import { useDebouncedFunction } from "hooks/debounce";
|
||||
import type { ExternalAuthPollingState } from "hooks/useExternalAuth";
|
||||
@@ -57,7 +57,7 @@ interface CreateWorkspacePageViewProps {
|
||||
canUpdateTemplate?: boolean;
|
||||
creatingWorkspace: boolean;
|
||||
defaultName?: string | null;
|
||||
defaultOwner: TypesGen.User;
|
||||
defaultOwner: TypesGen.MinimalUser;
|
||||
diagnostics: readonly FriendlyDiagnostic[];
|
||||
disabledParams?: string[];
|
||||
error: unknown;
|
||||
@@ -74,13 +74,13 @@ interface CreateWorkspacePageViewProps {
|
||||
onCancel: () => void;
|
||||
onSubmit: (
|
||||
req: TypesGen.CreateWorkspaceRequest,
|
||||
owner: TypesGen.User,
|
||||
owner: TypesGen.MinimalUser,
|
||||
) => void;
|
||||
resetMutation: () => void;
|
||||
sendMessage: (message: Record<string, string>, ownerId?: string) => void;
|
||||
startPollingExternalAuth: () => void;
|
||||
owner: TypesGen.User;
|
||||
setOwner: (user: TypesGen.User) => void;
|
||||
owner: TypesGen.MinimalUser;
|
||||
setOwner: (user: TypesGen.MinimalUser) => void;
|
||||
}
|
||||
|
||||
export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
@@ -313,7 +313,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
sendDynamicParamsRequest,
|
||||
]);
|
||||
|
||||
const handleOwnerChange = (user: TypesGen.User) => {
|
||||
const handleOwnerChange = (user: TypesGen.MinimalUser) => {
|
||||
setOwner(user);
|
||||
sendDynamicParamsRequest([], user.id);
|
||||
};
|
||||
@@ -503,7 +503,8 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
<Label className="text-sm" htmlFor={`${id}-workspace-name`}>
|
||||
Owner
|
||||
</Label>
|
||||
<UserAutocomplete
|
||||
<WorkspaceUserAutocomplete
|
||||
organizationId={template.organization_id}
|
||||
value={owner}
|
||||
onChange={(user) => {
|
||||
handleOwnerChange(user ?? defaultOwner);
|
||||
|
||||
Reference in New Issue
Block a user