From e4a06f842a2976f41aa42b5f54801b896f57d792 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 17 Dec 2025 20:29:34 +0000 Subject: [PATCH] feat: add workspace share button and dialog (#21299) resolves coder/internal#1074 Screenshot 2025-12-16 at 20 31 33 Screenshot 2025-12-16 at 20 31 16 --- .../AddWorkspaceUserOrGroup.tsx | 80 +++++ .../UserOrGroupAutocomplete.tsx | 0 .../WorkspaceSharingForm.tsx | 276 ++++++++++-------- .../WorkspaceActions/ShareButton.tsx | 62 ++++ .../WorkspaceActions/WorkspaceActions.tsx | 6 + .../WorkspaceSharingPageView.tsx | 78 +---- 6 files changed, 302 insertions(+), 200 deletions(-) create mode 100644 site/src/modules/workspaces/WorkspaceSharingForm/AddWorkspaceUserOrGroup.tsx rename site/src/{pages/WorkspaceSettingsPage/WorkspaceSharingPage => modules/workspaces/WorkspaceSharingForm}/UserOrGroupAutocomplete.tsx (100%) create mode 100644 site/src/pages/WorkspacePage/WorkspaceActions/ShareButton.tsx diff --git a/site/src/modules/workspaces/WorkspaceSharingForm/AddWorkspaceUserOrGroup.tsx b/site/src/modules/workspaces/WorkspaceSharingForm/AddWorkspaceUserOrGroup.tsx new file mode 100644 index 0000000000..e8e0efda04 --- /dev/null +++ b/site/src/modules/workspaces/WorkspaceSharingForm/AddWorkspaceUserOrGroup.tsx @@ -0,0 +1,80 @@ +import type { + Group, + User, + WorkspaceACL, + WorkspaceRole, + WorkspaceUser, +} from "api/typesGenerated"; +import { + UserOrGroupAutocomplete, + type UserOrGroupAutocompleteValue, +} from "modules/workspaces/WorkspaceSharingForm/UserOrGroupAutocomplete"; +import { + AddWorkspaceMemberForm, + RoleSelectField, +} from "modules/workspaces/WorkspaceSharingForm/WorkspaceSharingForm"; +import { type FC, useState } from "react"; + +type AddWorkspaceUserOrGroupProps = { + organizationID: string; + isLoading: boolean; + workspaceACL: WorkspaceACL | undefined; + onSubmit: ( + value: WorkspaceUser | Group | ({ role: WorkspaceRole } & User), + role: WorkspaceRole, + reset: () => void, + ) => void; +}; + +export const AddWorkspaceUserOrGroup: FC = ({ + organizationID, + isLoading, + workspaceACL, + onSubmit, +}) => { + const [selectedOption, setSelectedOption] = + useState(null); + const [selectedRole, setSelectedRole] = useState("use"); + const excludeFromAutocomplete = workspaceACL + ? [...workspaceACL.group, ...workspaceACL.users] + : []; + + const resetValues = () => { + setSelectedOption(null); + setSelectedRole("use"); + }; + + return ( + { + if (selectedOption && selectedRole) { + onSubmit( + { + ...selectedOption, + role: selectedRole, + }, + selectedRole, + resetValues, + ); + } + }} + > + { + setSelectedOption(newValue); + }} + /> + + + + ); +}; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/UserOrGroupAutocomplete.tsx b/site/src/modules/workspaces/WorkspaceSharingForm/UserOrGroupAutocomplete.tsx similarity index 100% rename from site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/UserOrGroupAutocomplete.tsx rename to site/src/modules/workspaces/WorkspaceSharingForm/UserOrGroupAutocomplete.tsx diff --git a/site/src/modules/workspaces/WorkspaceSharingForm/WorkspaceSharingForm.tsx b/site/src/modules/workspaces/WorkspaceSharingForm/WorkspaceSharingForm.tsx index 2529b2df56..a66f82000c 100644 --- a/site/src/modules/workspaces/WorkspaceSharingForm/WorkspaceSharingForm.tsx +++ b/site/src/modules/workspaces/WorkspaceSharingForm/WorkspaceSharingForm.tsx @@ -148,6 +148,7 @@ interface WorkspaceSharingFormProps { updatingGroupId?: WorkspaceGroup["id"] | undefined; onRemoveGroup: (group: Group) => void; addMemberForm?: ReactNode; + isCompact?: boolean; } export const WorkspaceSharingForm: FC = ({ @@ -161,6 +162,7 @@ export const WorkspaceSharingForm: FC = ({ onUpdateGroup, onRemoveGroup, addMemberForm, + isCompact, }) => { const isEmpty = Boolean( workspaceACL && @@ -168,136 +170,160 @@ export const WorkspaceSharingForm: FC = ({ workspaceACL.group.length === 0, ); + const tableHeader = ( + + + Member + Role + + + + ); + + const tableBody = ( + + {!workspaceACL ? ( + + ) : isEmpty ? ( + + + + + + ) : ( + <> + {workspaceACL.group.map((group) => ( + + + + } + title={group.display_name || group.name} + subtitle={getGroupSubtitle(group)} + /> + + + {canUpdatePermissions ? ( + onUpdateGroup(group, value)} + /> + ) : ( +
{group.role}
+ )} +
+ + + {canUpdatePermissions && ( + + + + + + onRemoveGroup(group)} + > + Remove + + + + )} + +
+ ))} + + {workspaceACL.users.map((user) => ( + + + + + + {canUpdatePermissions ? ( + onUpdateUser(user, value)} + /> + ) : ( +
{user.role}
+ )} +
+ + + {canUpdatePermissions && ( + + + + + + onRemoveUser(user)} + > + Remove + + + + )} + +
+ ))} + + )} +
+ ); + + if (isCompact) { + return ( +
+ {Boolean(error) && } + {canUpdatePermissions && addMemberForm} +
+ {tableHeader}
+
+ {tableBody}
+
+
+
+ ); + } + return (
{Boolean(error) && } {canUpdatePermissions && addMemberForm} - - - Member - Role - - - - - {!workspaceACL ? ( - - ) : isEmpty ? ( - - - - - - ) : ( - <> - {workspaceACL.group.map((group) => ( - - - - } - title={group.display_name || group.name} - subtitle={getGroupSubtitle(group)} - /> - - - {canUpdatePermissions ? ( - onUpdateGroup(group, value)} - /> - ) : ( -
{group.role}
- )} -
- - - {canUpdatePermissions && ( - - - - - - onRemoveGroup(group)} - > - Remove - - - - )} - -
- ))} - - {workspaceACL.users.map((user) => ( - - - - - - {canUpdatePermissions ? ( - onUpdateUser(user, value)} - /> - ) : ( -
{user.role}
- )} -
- - - {canUpdatePermissions && ( - - - - - - onRemoveUser(user)} - > - Remove - - - - )} - -
- ))} - - )} -
+ {tableHeader} + {tableBody}
); diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/ShareButton.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/ShareButton.tsx new file mode 100644 index 0000000000..485f208cfc --- /dev/null +++ b/site/src/pages/WorkspacePage/WorkspaceActions/ShareButton.tsx @@ -0,0 +1,62 @@ +import type { Workspace } from "api/typesGenerated"; +import { TopbarButton } from "components/FullPageLayout/Topbar"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "components/Popover/Popover"; +import { UsersIcon } from "lucide-react"; +import { isGroup } from "modules/groups"; +import { AddWorkspaceUserOrGroup } from "modules/workspaces/WorkspaceSharingForm/AddWorkspaceUserOrGroup"; +import { useWorkspaceSharing } from "modules/workspaces/WorkspaceSharingForm/useWorkspaceSharing"; +import { WorkspaceSharingForm } from "modules/workspaces/WorkspaceSharingForm/WorkspaceSharingForm"; +import type { FC } from "react"; + +interface ShareButtonProps { + workspace: Workspace; + canUpdatePermissions: boolean; +} + +export const ShareButton: FC = ({ + workspace, + canUpdatePermissions, +}) => { + const sharing = useWorkspaceSharing(workspace); + + return ( + + + + + Share + + + + + isGroup(value) + ? sharing.addGroup(value, role, resetAutocomplete) + : sharing.addUser(value, role, resetAutocomplete) + } + /> + } + /> + + + ); +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index f46589a0a6..6ebb8001b5 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -22,6 +22,7 @@ import { } from "./Buttons"; import { DebugButton } from "./DebugButton"; import { RetryButton } from "./RetryButton"; +import { ShareButton } from "./ShareButton"; interface WorkspaceActionsProps { workspace: Workspace; @@ -188,6 +189,11 @@ export const WorkspaceActions: FC = ({ onToggle={handleToggleFavorite} /> + + ); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPageView.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPageView.tsx index 0a0d79bd1f..8573ca1863 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPageView.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSharingPage/WorkspaceSharingPageView.tsx @@ -1,6 +1,5 @@ import type { Group, - User, Workspace, WorkspaceACL, WorkspaceGroup, @@ -8,80 +7,9 @@ import type { WorkspaceUser, } from "api/typesGenerated"; import { isGroup } from "modules/groups"; -import { - AddWorkspaceMemberForm, - RoleSelectField, - WorkspaceSharingForm, -} from "modules/workspaces/WorkspaceSharingForm/WorkspaceSharingForm"; -import { type FC, useState } from "react"; -import { - UserOrGroupAutocomplete, - type UserOrGroupAutocompleteValue, -} from "./UserOrGroupAutocomplete"; - -type AddWorkspaceUserOrGroupProps = { - organizationID: string; - isLoading: boolean; - workspaceACL: WorkspaceACL | undefined; - onSubmit: ( - value: WorkspaceUser | Group | ({ role: WorkspaceRole } & User), - role: WorkspaceRole, - reset: () => void, - ) => void; -}; - -const AddWorkspaceUserOrGroup: FC = ({ - organizationID, - isLoading, - workspaceACL, - onSubmit, -}) => { - const [selectedOption, setSelectedOption] = - useState(null); - const [selectedRole, setSelectedRole] = useState("use"); - const excludeFromAutocomplete = workspaceACL - ? [...workspaceACL.group, ...workspaceACL.users] - : []; - - const resetValues = () => { - setSelectedOption(null); - setSelectedRole("use"); - }; - - return ( - { - if (selectedOption && selectedRole) { - onSubmit( - { - ...selectedOption, - role: selectedRole, - }, - selectedRole, - resetValues, - ); - } - }} - > - { - setSelectedOption(newValue); - }} - /> - - - - ); -}; +import { AddWorkspaceUserOrGroup } from "modules/workspaces/WorkspaceSharingForm/AddWorkspaceUserOrGroup"; +import { WorkspaceSharingForm } from "modules/workspaces/WorkspaceSharingForm/WorkspaceSharingForm"; +import type { FC } from "react"; interface WorkspaceSharingPageViewProps { workspace: Workspace;