Skip to content

Commit 6f5952e

Browse files
committed
🫣
1 parent de2585b commit 6f5952e

17 files changed

+1137
-39
lines changed

site/src/api/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,9 +1455,9 @@ class ApiMethods {
14551455
return response.data;
14561456
};
14571457

1458-
getGroup = async (groupName: string): Promise<TypesGen.Group> => {
1458+
getGroup = async (orgId: string, groupName: string): Promise<TypesGen.Group> => {
14591459
const response = await this.axios.get(
1460-
`/api/v2/organizations/default/groups/${groupName}`,
1460+
`/api/v2/organizations/${orgId}/groups/${groupName}`,
14611461
);
14621462
return response.data;
14631463
};

site/src/api/queries/groups.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import type {
99
const GROUPS_QUERY_KEY = ["groups"];
1010
type GroupSortOrder = "asc" | "desc";
1111

12-
const getGroupQueryKey = (groupName: string) => ["group", groupName];
12+
const getGroupQueryKey = (organizationId: string, groupName: string) => [
13+
organizationId,
14+
"group",
15+
groupName,
16+
];
1317

1418
export const groups = (organizationId: string) => {
1519
return {
@@ -18,10 +22,10 @@ export const groups = (organizationId: string) => {
1822
} satisfies UseQueryOptions<Group[]>;
1923
};
2024

21-
export const group = (groupName: string) => {
25+
export const group = (organizationId: string, groupName: string) => {
2226
return {
23-
queryKey: getGroupQueryKey(groupName),
24-
queryFn: () => API.getGroup(groupName),
27+
queryKey: getGroupQueryKey(organizationId, groupName),
28+
queryFn: () => API.getGroup(organizationId, groupName),
2529
};
2630
};
2731

@@ -69,7 +73,7 @@ export function groupsForUser(organizationId: string, userId: string) {
6973

7074
export const groupPermissions = (groupId: string) => {
7175
return {
72-
queryKey: [...getGroupQueryKey(groupId), "permissions"],
76+
queryKey: ["group", groupId, "permissions"],
7377
queryFn: () =>
7478
API.checkAuthorization({
7579
checks: {
@@ -85,12 +89,12 @@ export const groupPermissions = (groupId: string) => {
8589
};
8690
};
8791

88-
export const createGroup = (queryClient: QueryClient) => {
92+
export const createGroup = (
93+
queryClient: QueryClient,
94+
organizationId: string,
95+
) => {
8996
return {
90-
mutationFn: ({
91-
organizationId,
92-
...request
93-
}: CreateGroupRequest & { organizationId: string }) =>
97+
mutationFn: (request: CreateGroupRequest) =>
9498
API.createGroup(organizationId, request),
9599
onSuccess: async () => {
96100
await queryClient.invalidateQueries(GROUPS_QUERY_KEY);
@@ -106,15 +110,15 @@ export const patchGroup = (queryClient: QueryClient) => {
106110
}: PatchGroupRequest & { groupId: string }) =>
107111
API.patchGroup(groupId, request),
108112
onSuccess: async (updatedGroup: Group) =>
109-
invalidateGroup(queryClient, updatedGroup.id),
113+
invalidateGroup(queryClient, "default", updatedGroup.id),
110114
};
111115
};
112116

113117
export const deleteGroup = (queryClient: QueryClient) => {
114118
return {
115119
mutationFn: API.deleteGroup,
116120
onSuccess: async (_: void, groupId: string) =>
117-
invalidateGroup(queryClient, groupId),
121+
invalidateGroup(queryClient, "default", groupId),
118122
};
119123
};
120124

@@ -123,7 +127,7 @@ export const addMember = (queryClient: QueryClient) => {
123127
mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
124128
API.addMember(groupId, userId),
125129
onSuccess: async (updatedGroup: Group) =>
126-
invalidateGroup(queryClient, updatedGroup.id),
130+
invalidateGroup(queryClient, "default", updatedGroup.id),
127131
};
128132
};
129133

@@ -132,14 +136,18 @@ export const removeMember = (queryClient: QueryClient) => {
132136
mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
133137
API.removeMember(groupId, userId),
134138
onSuccess: async (updatedGroup: Group) =>
135-
invalidateGroup(queryClient, updatedGroup.id),
139+
invalidateGroup(queryClient, "default", updatedGroup.id),
136140
};
137141
};
138142

139-
export const invalidateGroup = (queryClient: QueryClient, groupId: string) =>
143+
export const invalidateGroup = (
144+
queryClient: QueryClient,
145+
orgId: string,
146+
groupId: string,
147+
) =>
140148
Promise.all([
141149
queryClient.invalidateQueries(GROUPS_QUERY_KEY),
142-
queryClient.invalidateQueries(getGroupQueryKey(groupId)),
150+
queryClient.invalidateQueries(getGroupQueryKey(orgId, groupId)),
143151
]);
144152

145153
export function sortGroupsByName(

site/src/pages/GroupsPage/CreateGroupPage.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@ import { Helmet } from "react-helmet-async";
33
import { useMutation, useQueryClient } from "react-query";
44
import { useNavigate } from "react-router-dom";
55
import { createGroup } from "api/queries/groups";
6-
import { useDashboard } from "modules/dashboard/useDashboard";
76
import { pageTitle } from "utils/page";
87
import CreateGroupPageView from "./CreateGroupPageView";
98

109
export const CreateGroupPage: FC = () => {
1110
const queryClient = useQueryClient();
1211
const navigate = useNavigate();
13-
const { organizationId } = useDashboard();
14-
const createGroupMutation = useMutation(createGroup(queryClient));
12+
const createGroupMutation = useMutation(createGroup(queryClient, "default"));
1513

1614
return (
1715
<>
@@ -20,10 +18,7 @@ export const CreateGroupPage: FC = () => {
2018
</Helmet>
2119
<CreateGroupPageView
2220
onSubmit={async (data) => {
23-
const newGroup = await createGroupMutation.mutateAsync({
24-
organizationId,
25-
...data,
26-
});
21+
const newGroup = await createGroupMutation.mutateAsync(data);
2722
navigate(`/groups/${newGroup.name}`);
2823
}}
2924
error={createGroupMutation.error}

site/src/pages/GroupsPage/GroupPage.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,13 @@ import { isEveryoneGroup } from "utils/groups";
5454
import { pageTitle } from "utils/page";
5555

5656
export const GroupPage: FC = () => {
57-
const { groupName } = useParams() as { groupName: string };
57+
const { groupName, organization } = useParams() as {
58+
organization: string;
59+
groupName: string;
60+
};
5861
const queryClient = useQueryClient();
5962
const navigate = useNavigate();
60-
const groupQuery = useQuery(group(groupName));
63+
const groupQuery = useQuery(group(organization, groupName));
6164
const groupData = groupQuery.data;
6265
const { data: permissions } = useQuery(
6366
groupData !== undefined

site/src/pages/GroupsPage/SettingsGroupPage.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ import SettingsGroupPageView from "./SettingsGroupPageView";
1313
export const SettingsGroupPage: FC = () => {
1414
const { groupName } = useParams() as { groupName: string };
1515
const queryClient = useQueryClient();
16-
const groupQuery = useQuery(group(groupName));
17-
const { data: groupData, isLoading, error } = useQuery(group(groupName));
16+
const groupQuery = useQuery(group("default", groupName));
1817
const patchGroupMutation = useMutation(patchGroup(queryClient));
1918
const navigate = useNavigate();
2019

@@ -28,19 +27,20 @@ export const SettingsGroupPage: FC = () => {
2827
</Helmet>
2928
);
3029

31-
if (error) {
32-
return <ErrorAlert error={error} />;
30+
if (groupQuery.error) {
31+
return <ErrorAlert error={groupQuery.error} />;
3332
}
3433

35-
if (isLoading || !groupData) {
34+
if (groupQuery.isLoading || !groupQuery.data) {
3635
return (
3736
<>
3837
{helmet}
3938
<Loader />
4039
</>
4140
);
4241
}
43-
const groupId = groupData.id;
42+
43+
const groupId = groupQuery.data.id;
4444

4545
return (
4646
<>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { FC } from "react";
2+
import { Helmet } from "react-helmet-async";
3+
import { useMutation, useQueryClient } from "react-query";
4+
import { useNavigate, useParams } from "react-router-dom";
5+
import { createGroup } from "api/queries/groups";
6+
import { pageTitle } from "utils/page";
7+
import CreateGroupPageView from "./CreateGroupPageView";
8+
9+
export const CreateGroupPage: FC = () => {
10+
const queryClient = useQueryClient();
11+
const navigate = useNavigate();
12+
const { organization } = useParams() as { organization: string };
13+
const createGroupMutation = useMutation(
14+
createGroup(queryClient, organization),
15+
);
16+
17+
return (
18+
<>
19+
<Helmet>
20+
<title>{pageTitle("Create Group")}</title>
21+
</Helmet>
22+
<CreateGroupPageView
23+
onSubmit={async (data) => {
24+
const newGroup = await createGroupMutation.mutateAsync(data);
25+
navigate(`/groups/${newGroup.name}`);
26+
}}
27+
error={createGroupMutation.error}
28+
isLoading={createGroupMutation.isLoading}
29+
/>
30+
</>
31+
);
32+
};
33+
export default CreateGroupPage;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { mockApiError } from "testHelpers/entities";
3+
import { CreateGroupPageView } from "./CreateGroupPageView";
4+
5+
const meta: Meta<typeof CreateGroupPageView> = {
6+
title: "pages/GroupsPage/CreateGroupPageView",
7+
component: CreateGroupPageView,
8+
};
9+
10+
export default meta;
11+
type Story = StoryObj<typeof CreateGroupPageView>;
12+
13+
export const Example: Story = {};
14+
15+
export const WithError: Story = {
16+
args: {
17+
error: mockApiError({
18+
message: "A group named new-group already exists.",
19+
validations: [{ field: "name", detail: "Group names must be unique" }],
20+
}),
21+
initialTouched: { name: true },
22+
},
23+
};
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import TextField from "@mui/material/TextField";
2+
import { type FormikTouched, useFormik } from "formik";
3+
import type { FC } from "react";
4+
import { useNavigate } from "react-router-dom";
5+
import * as Yup from "yup";
6+
import { isApiValidationError } from "api/errors";
7+
import type { CreateGroupRequest } from "api/typesGenerated";
8+
import { ErrorAlert } from "components/Alert/ErrorAlert";
9+
import { FormFooter } from "components/FormFooter/FormFooter";
10+
import { FullPageForm } from "components/FullPageForm/FullPageForm";
11+
import { IconField } from "components/IconField/IconField";
12+
import { Margins } from "components/Margins/Margins";
13+
import { Stack } from "components/Stack/Stack";
14+
import { getFormHelpers, onChangeTrimmed } from "utils/formUtils";
15+
16+
const validationSchema = Yup.object({
17+
name: Yup.string().required().label("Name"),
18+
});
19+
20+
export type CreateGroupPageViewProps = {
21+
onSubmit: (data: CreateGroupRequest) => void;
22+
error?: unknown;
23+
isLoading: boolean;
24+
// Helpful to show field errors on Storybook
25+
initialTouched?: FormikTouched<CreateGroupRequest>;
26+
};
27+
28+
export const CreateGroupPageView: FC<CreateGroupPageViewProps> = ({
29+
onSubmit,
30+
error,
31+
isLoading,
32+
initialTouched,
33+
}) => {
34+
const navigate = useNavigate();
35+
const form = useFormik<CreateGroupRequest>({
36+
initialValues: {
37+
name: "",
38+
display_name: "",
39+
avatar_url: "",
40+
quota_allowance: 0,
41+
},
42+
validationSchema,
43+
onSubmit,
44+
initialTouched,
45+
});
46+
const getFieldHelpers = getFormHelpers<CreateGroupRequest>(form, error);
47+
const onCancel = () => navigate(-1);
48+
49+
return (
50+
<Margins>
51+
<FullPageForm title="Create group">
52+
<form onSubmit={form.handleSubmit}>
53+
<Stack spacing={2.5}>
54+
{Boolean(error) && !isApiValidationError(error) && (
55+
<ErrorAlert error={error} />
56+
)}
57+
58+
<TextField
59+
{...getFieldHelpers("name")}
60+
autoFocus
61+
fullWidth
62+
label="Name"
63+
/>
64+
<TextField
65+
{...getFieldHelpers("display_name", {
66+
helperText: "Optional: keep empty to default to the name.",
67+
})}
68+
fullWidth
69+
label="Display Name"
70+
/>
71+
<IconField
72+
{...getFieldHelpers("avatar_url")}
73+
onChange={onChangeTrimmed(form)}
74+
fullWidth
75+
label="Avatar URL"
76+
onPickEmoji={(value) => form.setFieldValue("avatar_url", value)}
77+
/>
78+
</Stack>
79+
<FormFooter onCancel={onCancel} isLoading={isLoading} />
80+
</form>
81+
</FullPageForm>
82+
</Margins>
83+
);
84+
};
85+
export default CreateGroupPageView;

0 commit comments

Comments
 (0)