Skip to content

Commit a193adb

Browse files
committed
Add stories for organization members page
Needed to break it out into a separate view to do this.
1 parent 956a341 commit a193adb

File tree

3 files changed

+308
-202
lines changed

3 files changed

+308
-202
lines changed
Lines changed: 34 additions & 202 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,6 @@
1-
import type { Interpolation, Theme } from "@emotion/react";
2-
import PersonAdd from "@mui/icons-material/PersonAdd";
3-
import LoadingButton from "@mui/lab/LoadingButton";
4-
import Table from "@mui/material/Table";
5-
import TableBody from "@mui/material/TableBody";
6-
import TableCell from "@mui/material/TableCell";
7-
import TableContainer from "@mui/material/TableContainer";
8-
import TableHead from "@mui/material/TableHead";
9-
import TableRow from "@mui/material/TableRow";
10-
import { type FC, useState } from "react";
1+
import type { FC } from "react";
112
import { useMutation, useQuery, useQueryClient } from "react-query";
123
import { useParams } from "react-router-dom";
13-
import { getErrorMessage } from "api/errors";
144
import {
155
addOrganizationMember,
166
organizationMembers,
@@ -19,26 +9,11 @@ import {
199
updateOrganizationMemberRoles,
2010
} from "api/queries/organizations";
2111
import { organizationRoles } from "api/queries/roles";
22-
import type { User } from "api/typesGenerated";
23-
import { ErrorAlert } from "components/Alert/ErrorAlert";
24-
import { AvatarData } from "components/AvatarData/AvatarData";
25-
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
12+
import type { OrganizationMemberWithUserData, User } from "api/typesGenerated";
2613
import { Loader } from "components/Loader/Loader";
27-
import {
28-
MoreMenu,
29-
MoreMenuTrigger,
30-
MoreMenuContent,
31-
MoreMenuItem,
32-
ThreeDotsButton,
33-
} from "components/MoreMenu/MoreMenu";
34-
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
35-
import { Stack } from "components/Stack/Stack";
36-
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
37-
import { UserAvatar } from "components/UserAvatar/UserAvatar";
3814
import { useAuthenticated } from "contexts/auth/RequireAuth";
3915
import { useOrganizationSettings } from "./ManagementSettingsLayout";
40-
import { TableColumnHelpTooltip } from "./UserTable/TableColumnHelpTooltip";
41-
import { UserRoleCell } from "./UserTable/UserRoleCell";
16+
import { OrganizationMembersPageView } from "./OrganizationMembersPageView";
4217

4318
const OrganizationMembersPage: FC = () => {
4419
const queryClient = useQueryClient();
@@ -71,182 +46,39 @@ const OrganizationMembersPage: FC = () => {
7146
return <Loader />;
7247
}
7348

74-
const error =
75-
membersQuery.error ?? addMemberMutation.error ?? removeMemberMutation.error;
76-
const members = membersQuery.data;
77-
78-
return (
79-
<div>
80-
<PageHeader>
81-
<PageHeaderTitle>Organization members</PageHeaderTitle>
82-
</PageHeader>
83-
84-
<Stack>
85-
{Boolean(error) && <ErrorAlert error={error} />}
86-
87-
{permissions.editMembers && (
88-
<AddOrganizationMember
89-
isLoading={addMemberMutation.isLoading}
90-
onSubmit={async (user) => {
91-
await addMemberMutation.mutateAsync(user.id);
92-
void membersQuery.refetch();
93-
}}
94-
/>
95-
)}
96-
97-
<TableContainer>
98-
<Table>
99-
<TableHead>
100-
<TableRow>
101-
<TableCell width="50%">User</TableCell>
102-
<TableCell width="49%">
103-
<Stack direction="row" spacing={1} alignItems="center">
104-
<span>Roles</span>
105-
<TableColumnHelpTooltip variant="roles" />
106-
</Stack>
107-
</TableCell>
108-
<TableCell width="1%"></TableCell>
109-
</TableRow>
110-
</TableHead>
111-
<TableBody>
112-
{members?.map((member) => (
113-
<TableRow key={member.user_id}>
114-
<TableCell>
115-
<AvatarData
116-
avatar={
117-
<UserAvatar
118-
username={member.username}
119-
avatarURL={member.avatar_url}
120-
/>
121-
}
122-
title={member.name || member.username}
123-
subtitle={member.email}
124-
/>
125-
</TableCell>
126-
<UserRoleCell
127-
inheritedRoles={member.global_roles}
128-
roles={member.roles}
129-
allAvailableRoles={organizationRolesQuery.data}
130-
oidcRoleSyncEnabled={false}
131-
isLoading={updateMemberRolesMutation.isLoading}
132-
canEditUsers={permissions.editMembers}
133-
onEditRoles={async (newRoleNames) => {
134-
try {
135-
await updateMemberRolesMutation.mutateAsync({
136-
userId: member.user_id,
137-
roles: newRoleNames,
138-
});
139-
displaySuccess("Roles updated successfully.");
140-
} catch (e) {
141-
displayError(
142-
getErrorMessage(e, "Failed to update roles."),
143-
);
144-
}
145-
}}
146-
/>
147-
<TableCell>
148-
{member.user_id !== me.id && permissions.editMembers && (
149-
<MoreMenu>
150-
<MoreMenuTrigger>
151-
<ThreeDotsButton />
152-
</MoreMenuTrigger>
153-
<MoreMenuContent>
154-
<MoreMenuItem
155-
danger
156-
onClick={async () => {
157-
try {
158-
await removeMemberMutation.mutateAsync(
159-
member.user_id,
160-
);
161-
void membersQuery.refetch();
162-
displaySuccess("Member removed.");
163-
} catch (e) {
164-
displayError(
165-
getErrorMessage(
166-
e,
167-
"Failed to remove member.",
168-
),
169-
);
170-
}
171-
}}
172-
>
173-
Remove
174-
</MoreMenuItem>
175-
</MoreMenuContent>
176-
</MoreMenu>
177-
)}
178-
</TableCell>
179-
</TableRow>
180-
))}
181-
</TableBody>
182-
</Table>
183-
</TableContainer>
184-
</Stack>
185-
</div>
186-
);
187-
};
188-
189-
export default OrganizationMembersPage;
190-
191-
interface AddOrganizationMemberProps {
192-
isLoading: boolean;
193-
onSubmit: (user: User) => Promise<void>;
194-
}
195-
196-
const AddOrganizationMember: FC<AddOrganizationMemberProps> = ({
197-
isLoading,
198-
onSubmit,
199-
}) => {
200-
const [selectedUser, setSelectedUser] = useState<User | null>(null);
201-
20249
return (
203-
<form
204-
onSubmit={async (e) => {
205-
e.preventDefault();
206-
207-
if (selectedUser) {
208-
try {
209-
await onSubmit(selectedUser);
210-
setSelectedUser(null);
211-
} catch (error) {
212-
displayError(getErrorMessage(error, "Failed to add member."));
213-
}
214-
}
50+
<OrganizationMembersPageView
51+
allAvailableRoles={organizationRolesQuery.data}
52+
canEditMembers={permissions.editMembers}
53+
error={
54+
membersQuery.error ??
55+
addMemberMutation.error ??
56+
removeMemberMutation.error ??
57+
updateMemberRolesMutation.error
58+
}
59+
isAddingMember={addMemberMutation.isLoading}
60+
isUpdatingMemberRoles={updateMemberRolesMutation.isLoading}
61+
me={me}
62+
members={membersQuery.data}
63+
addMember={async (user: User) => {
64+
await addMemberMutation.mutateAsync(user.id);
65+
void membersQuery.refetch();
21566
}}
216-
>
217-
<Stack direction="row" alignItems="center" spacing={1}>
218-
<UserAutocomplete
219-
css={styles.autoComplete}
220-
value={selectedUser}
221-
onChange={(newValue) => {
222-
setSelectedUser(newValue);
223-
}}
224-
/>
225-
226-
<LoadingButton
227-
loadingPosition="start"
228-
disabled={!selectedUser}
229-
type="submit"
230-
startIcon={<PersonAdd />}
231-
loading={isLoading}
232-
>
233-
Add user
234-
</LoadingButton>
235-
</Stack>
236-
</form>
67+
removeMember={async (member: OrganizationMemberWithUserData) => {
68+
await removeMemberMutation.mutateAsync(member.user_id);
69+
void membersQuery.refetch();
70+
}}
71+
updateMemberRoles={async (
72+
member: OrganizationMemberWithUserData,
73+
newRoles: string[],
74+
) => {
75+
await updateMemberRolesMutation.mutateAsync({
76+
userId: member.user_id,
77+
roles: newRoles,
78+
});
79+
}}
80+
/>
23781
);
23882
};
23983

240-
const styles = {
241-
role: (theme) => ({
242-
backgroundColor: theme.roles.info.background,
243-
borderColor: theme.roles.info.outline,
244-
}),
245-
globalRole: (theme) => ({
246-
backgroundColor: theme.roles.inactive.background,
247-
borderColor: theme.roles.inactive.outline,
248-
}),
249-
autoComplete: {
250-
width: 300,
251-
},
252-
} satisfies Record<string, Interpolation<Theme>>;
84+
export default OrganizationMembersPage;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import {
3+
MockUser,
4+
MockOrganizationMember,
5+
MockOrganizationMember2,
6+
} from "testHelpers/entities";
7+
import { OrganizationMembersPageView } from "./OrganizationMembersPageView";
8+
9+
const meta: Meta<typeof OrganizationMembersPageView> = {
10+
title: "pages/OrganizationMembersPageView",
11+
component: OrganizationMembersPageView,
12+
args: {
13+
canEditMembers: true,
14+
error: undefined,
15+
isAddingMember: false,
16+
isUpdatingMemberRoles: false,
17+
me: MockUser,
18+
members: [MockOrganizationMember, MockOrganizationMember2],
19+
addMember: () => Promise.resolve(),
20+
removeMember: () => Promise.resolve(),
21+
updateMemberRoles: () => Promise.resolve(),
22+
},
23+
};
24+
25+
export default meta;
26+
type Story = StoryObj<typeof OrganizationMembersPageView>;
27+
28+
export const Default: Story = {};
29+
30+
export const NoMembers: Story = {
31+
args: {
32+
members: [],
33+
},
34+
};
35+
36+
export const Error: Story = {
37+
args: {
38+
error: "Something went wrong",
39+
},
40+
};
41+
42+
export const NoEdit: Story = {
43+
args: {
44+
canEditMembers: false,
45+
},
46+
};
47+
48+
export const AddingMember: Story = {
49+
args: {
50+
isAddingMember: true,
51+
},
52+
};
53+
54+
export const UpdatingMember: Story = {
55+
args: {
56+
isUpdatingMemberRoles: true,
57+
},
58+
};

0 commit comments

Comments
 (0)