Skip to content

Commit ba4186d

Browse files
authored
feat: show summary if unable to edit org (#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
Lines changed: 63 additions & 0 deletions
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

Lines changed: 4 additions & 44 deletions
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

Lines changed: 8 additions & 1 deletion
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

Lines changed: 0 additions & 7 deletions
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

Lines changed: 4 additions & 5 deletions
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"
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 {
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+
};
Lines changed: 48 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy