Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ba4186d

Browse files
authored
feat: show summary if unable to edit org (coder#14214)
This can happen if you can edit the members, for example, but not the organization settings. In this case you will see a new summary page instead of the edit form.
1 parent 0b9ed57 commit ba4186d

7 files changed

+150
-57
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { reactRouterParameters } from "storybook-addon-remix-react-router";
3+
import { MockDefaultOrganization, MockUser } from "testHelpers/entities";
4+
import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook";
5+
import OrganizationSettingsPage from "./OrganizationSettingsPage";
6+
7+
const meta: Meta<typeof OrganizationSettingsPage> = {
8+
title: "pages/OrganizationSettingsPage",
9+
component: OrganizationSettingsPage,
10+
decorators: [withAuthProvider, withDashboardProvider],
11+
parameters: {
12+
user: MockUser,
13+
permissions: { viewDeploymentValues: true },
14+
queries: [
15+
{
16+
key: ["organizations", [MockDefaultOrganization.id], "permissions"],
17+
data: {},
18+
},
19+
],
20+
},
21+
};
22+
23+
export default meta;
24+
type Story = StoryObj<typeof OrganizationSettingsPage>;
25+
26+
export const NoRedirectableOrganizations: Story = {};
27+
28+
export const OrganizationDoesNotExist: Story = {
29+
parameters: {
30+
reactRouter: reactRouterParameters({
31+
location: { pathParams: { organization: "does-not-exist" } },
32+
routing: { path: "/organizations/:organization" },
33+
}),
34+
},
35+
};
36+
37+
export const CannotEditOrganization: Story = {
38+
parameters: {
39+
reactRouter: reactRouterParameters({
40+
location: { pathParams: { organization: MockDefaultOrganization.name } },
41+
routing: { path: "/organizations/:organization" },
42+
}),
43+
},
44+
};
45+
46+
export const CanEditOrganization: Story = {
47+
parameters: {
48+
reactRouter: reactRouterParameters({
49+
location: { pathParams: { organization: MockDefaultOrganization.name } },
50+
routing: { path: "/organizations/:organization" },
51+
}),
52+
queries: [
53+
{
54+
key: ["organizations", [MockDefaultOrganization.id], "permissions"],
55+
data: {
56+
[MockDefaultOrganization.id]: {
57+
editOrganization: true,
58+
},
59+
},
60+
},
61+
],
62+
},
63+
};

site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.test.tsx

+4-44
Original file line numberDiff line numberDiff line change
@@ -13,39 +13,15 @@ import OrganizationSettingsPage from "./OrganizationSettingsPage";
1313

1414
jest.spyOn(console, "error").mockImplementation(() => {});
1515

16-
const renderRootPage = async () => {
16+
const renderPage = async () => {
1717
renderWithManagementSettingsLayout(<OrganizationSettingsPage />, {
1818
route: "/organizations",
1919
path: "/organizations/:organization?",
2020
});
2121
await waitForLoaderToBeRemoved();
2222
};
2323

24-
const renderPage = async (orgName: string) => {
25-
renderWithManagementSettingsLayout(<OrganizationSettingsPage />, {
26-
route: `/organizations/${orgName}`,
27-
path: "/organizations/:organization",
28-
});
29-
await waitForLoaderToBeRemoved();
30-
};
31-
3224
describe("OrganizationSettingsPage", () => {
33-
it("has no organizations", async () => {
34-
server.use(
35-
http.get("/api/v2/organizations", () => {
36-
return HttpResponse.json([]);
37-
}),
38-
http.post("/api/v2/authcheck", async () => {
39-
return HttpResponse.json({
40-
[`${MockDefaultOrganization.id}.editOrganization`]: true,
41-
viewDeploymentValues: true,
42-
});
43-
}),
44-
);
45-
await renderRootPage();
46-
await screen.findByText("No organizations found");
47-
});
48-
4925
it("has no editable organizations", async () => {
5026
server.use(
5127
http.get("/api/v2/organizations", () => {
@@ -57,7 +33,7 @@ describe("OrganizationSettingsPage", () => {
5733
});
5834
}),
5935
);
60-
await renderRootPage();
36+
await renderPage();
6137
await screen.findByText("No organizations found");
6238
});
6339

@@ -75,7 +51,7 @@ describe("OrganizationSettingsPage", () => {
7551
});
7652
}),
7753
);
78-
await renderRootPage();
54+
await renderPage();
7955
const form = screen.getByTestId("org-settings-form");
8056
expect(within(form).getByRole("textbox", { name: "Name" })).toHaveValue(
8157
MockDefaultOrganization.name,
@@ -94,26 +70,10 @@ describe("OrganizationSettingsPage", () => {
9470
});
9571
}),
9672
);
97-
await renderRootPage();
73+
await renderPage();
9874
const form = screen.getByTestId("org-settings-form");
9975
expect(within(form).getByRole("textbox", { name: "Name" })).toHaveValue(
10076
MockOrganization2.name,
10177
);
10278
});
103-
104-
it("cannot find organization", async () => {
105-
server.use(
106-
http.get("/api/v2/organizations", () => {
107-
return HttpResponse.json([MockDefaultOrganization, MockOrganization2]);
108-
}),
109-
http.post("/api/v2/authcheck", async () => {
110-
return HttpResponse.json({
111-
[`${MockOrganization2.id}.editOrganization`]: true,
112-
viewDeploymentValues: true,
113-
});
114-
}),
115-
);
116-
await renderPage("the-endless-void");
117-
await screen.findByText("Organization not found");
118-
});
11979
});

site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
useOrganizationSettings,
1616
} from "./ManagementSettingsLayout";
1717
import { OrganizationSettingsPageView } from "./OrganizationSettingsPageView";
18+
import { OrganizationSummaryPageView } from "./OrganizationSummaryPageView";
1819

1920
const OrganizationSettingsPage: FC = () => {
2021
const { organization: organizationName } = useParams() as {
@@ -65,12 +66,18 @@ const OrganizationSettingsPage: FC = () => {
6566
return <EmptyState message="Organization not found" />;
6667
}
6768

69+
// The user may not be able to edit this org but they can still see it because
70+
// they can edit members, etc. In this case they will be shown a read-only
71+
// summary page instead of the settings form.
72+
if (!permissions[organization.id]?.editOrganization) {
73+
return <OrganizationSummaryPageView organization={organization} />;
74+
}
75+
6876
const error =
6977
updateOrganizationMutation.error ?? deleteOrganizationMutation.error;
7078

7179
return (
7280
<OrganizationSettingsPageView
73-
canEdit={permissions[organization.id]?.editOrganization ?? false}
7481
organization={organization}
7582
error={error}
7683
onSubmit={async (values) => {

site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.stories.tsx

-7
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ const meta: Meta<typeof OrganizationSettingsPageView> = {
1010
component: OrganizationSettingsPageView,
1111
args: {
1212
organization: MockOrganization,
13-
canEdit: true,
1413
},
1514
};
1615

@@ -24,9 +23,3 @@ export const DefaultOrg: Story = {
2423
organization: MockDefaultOrganization,
2524
},
2625
};
27-
28-
export const CannotEdit: Story = {
29-
args: {
30-
canEdit: false,
31-
},
32-
};

site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.tsx

+4-5
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,11 @@ interface OrganizationSettingsPageViewProps {
4444
error: unknown;
4545
onSubmit: (values: UpdateOrganizationRequest) => Promise<void>;
4646
onDeleteOrganization: () => void;
47-
canEdit: boolean;
4847
}
4948

5049
export const OrganizationSettingsPageView: FC<
5150
OrganizationSettingsPageViewProps
52-
> = ({ organization, error, onSubmit, onDeleteOrganization, canEdit }) => {
51+
> = ({ organization, error, onSubmit, onDeleteOrganization }) => {
5352
const form = useFormik<UpdateOrganizationRequest>({
5453
initialValues: {
5554
name: organization.name,
@@ -85,7 +84,7 @@ export const OrganizationSettingsPageView: FC<
8584
description="The name and description of the organization."
8685
>
8786
<fieldset
88-
disabled={form.isSubmitting || !canEdit}
87+
disabled={form.isSubmitting}
8988
css={{ border: "unset", padding: 0, margin: 0, width: "100%" }}
9089
>
9190
<FormFields>
@@ -117,10 +116,10 @@ export const OrganizationSettingsPageView: FC<
117116
</FormFields>
118117
</fieldset>
119118
</FormSection>
120-
{canEdit && <FormFooter isLoading={form.isSubmitting} />}
119+
<FormFooter isLoading={form.isSubmitting} />
121120
</HorizontalForm>
122121

123-
{canEdit && !organization.is_default && (
122+
{!organization.is_default && (
124123
<HorizontalContainer css={{ marginTop: 48 }}>
125124
<HorizontalSection
126125
title="Settings"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import {
3+
MockDefaultOrganization,
4+
MockOrganization,
5+
} from "testHelpers/entities";
6+
import { OrganizationSummaryPageView } from "./OrganizationSummaryPageView";
7+
8+
const meta: Meta<typeof OrganizationSummaryPageView> = {
9+
title: "pages/OrganizationSummaryPageView",
10+
component: OrganizationSummaryPageView,
11+
args: {
12+
organization: MockOrganization,
13+
},
14+
};
15+
16+
export default meta;
17+
type Story = StoryObj<typeof OrganizationSummaryPageView>;
18+
19+
export const DefaultOrg: Story = {
20+
args: {
21+
organization: MockDefaultOrganization,
22+
},
23+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { FC } from "react";
2+
import type { Organization } from "api/typesGenerated";
3+
import {
4+
PageHeader,
5+
PageHeaderTitle,
6+
PageHeaderSubtitle,
7+
} from "components/PageHeader/PageHeader";
8+
import { Stack } from "components/Stack/Stack";
9+
import { UserAvatar } from "components/UserAvatar/UserAvatar";
10+
11+
interface OrganizationSummaryPageViewProps {
12+
organization: Organization;
13+
}
14+
15+
export const OrganizationSummaryPageView: FC<
16+
OrganizationSummaryPageViewProps
17+
> = ({ organization }) => {
18+
return (
19+
<div>
20+
<PageHeader
21+
css={{
22+
// The deployment settings layout already has padding.
23+
paddingTop: 0,
24+
}}
25+
>
26+
<Stack direction="row" spacing={3} alignItems="center">
27+
<UserAvatar
28+
key={organization.id}
29+
size="xl"
30+
username={organization.display_name || organization.name}
31+
avatarURL={organization.icon}
32+
/>
33+
<div>
34+
<PageHeaderTitle>
35+
{organization.display_name || organization.name}
36+
</PageHeaderTitle>
37+
{organization.description && (
38+
<PageHeaderSubtitle>
39+
{organization.description}
40+
</PageHeaderSubtitle>
41+
)}
42+
</div>
43+
</Stack>
44+
</PageHeader>
45+
You are a member of this organization.
46+
</div>
47+
);
48+
};

0 commit comments

Comments
 (0)