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.
This commit is contained in:
Asher
2024-08-09 13:31:03 -08:00
committed by GitHub
parent 0b9ed57c10
commit ba4186dacc
7 changed files with 150 additions and 57 deletions
@@ -0,0 +1,63 @@
import type { Meta, StoryObj } from "@storybook/react";
import { reactRouterParameters } from "storybook-addon-remix-react-router";
import { MockDefaultOrganization, MockUser } from "testHelpers/entities";
import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook";
import OrganizationSettingsPage from "./OrganizationSettingsPage";
const meta: Meta<typeof OrganizationSettingsPage> = {
title: "pages/OrganizationSettingsPage",
component: OrganizationSettingsPage,
decorators: [withAuthProvider, withDashboardProvider],
parameters: {
user: MockUser,
permissions: { viewDeploymentValues: true },
queries: [
{
key: ["organizations", [MockDefaultOrganization.id], "permissions"],
data: {},
},
],
},
};
export default meta;
type Story = StoryObj<typeof OrganizationSettingsPage>;
export const NoRedirectableOrganizations: Story = {};
export const OrganizationDoesNotExist: Story = {
parameters: {
reactRouter: reactRouterParameters({
location: { pathParams: { organization: "does-not-exist" } },
routing: { path: "/organizations/:organization" },
}),
},
};
export const CannotEditOrganization: Story = {
parameters: {
reactRouter: reactRouterParameters({
location: { pathParams: { organization: MockDefaultOrganization.name } },
routing: { path: "/organizations/:organization" },
}),
},
};
export const CanEditOrganization: Story = {
parameters: {
reactRouter: reactRouterParameters({
location: { pathParams: { organization: MockDefaultOrganization.name } },
routing: { path: "/organizations/:organization" },
}),
queries: [
{
key: ["organizations", [MockDefaultOrganization.id], "permissions"],
data: {
[MockDefaultOrganization.id]: {
editOrganization: true,
},
},
},
],
},
};
@@ -13,7 +13,7 @@ import OrganizationSettingsPage from "./OrganizationSettingsPage";
jest.spyOn(console, "error").mockImplementation(() => {});
const renderRootPage = async () => {
const renderPage = async () => {
renderWithManagementSettingsLayout(<OrganizationSettingsPage />, {
route: "/organizations",
path: "/organizations/:organization?",
@@ -21,31 +21,7 @@ const renderRootPage = async () => {
await waitForLoaderToBeRemoved();
};
const renderPage = async (orgName: string) => {
renderWithManagementSettingsLayout(<OrganizationSettingsPage />, {
route: `/organizations/${orgName}`,
path: "/organizations/:organization",
});
await waitForLoaderToBeRemoved();
};
describe("OrganizationSettingsPage", () => {
it("has no organizations", async () => {
server.use(
http.get("/api/v2/organizations", () => {
return HttpResponse.json([]);
}),
http.post("/api/v2/authcheck", async () => {
return HttpResponse.json({
[`${MockDefaultOrganization.id}.editOrganization`]: true,
viewDeploymentValues: true,
});
}),
);
await renderRootPage();
await screen.findByText("No organizations found");
});
it("has no editable organizations", async () => {
server.use(
http.get("/api/v2/organizations", () => {
@@ -57,7 +33,7 @@ describe("OrganizationSettingsPage", () => {
});
}),
);
await renderRootPage();
await renderPage();
await screen.findByText("No organizations found");
});
@@ -75,7 +51,7 @@ describe("OrganizationSettingsPage", () => {
});
}),
);
await renderRootPage();
await renderPage();
const form = screen.getByTestId("org-settings-form");
expect(within(form).getByRole("textbox", { name: "Name" })).toHaveValue(
MockDefaultOrganization.name,
@@ -94,26 +70,10 @@ describe("OrganizationSettingsPage", () => {
});
}),
);
await renderRootPage();
await renderPage();
const form = screen.getByTestId("org-settings-form");
expect(within(form).getByRole("textbox", { name: "Name" })).toHaveValue(
MockOrganization2.name,
);
});
it("cannot find organization", async () => {
server.use(
http.get("/api/v2/organizations", () => {
return HttpResponse.json([MockDefaultOrganization, MockOrganization2]);
}),
http.post("/api/v2/authcheck", async () => {
return HttpResponse.json({
[`${MockOrganization2.id}.editOrganization`]: true,
viewDeploymentValues: true,
});
}),
);
await renderPage("the-endless-void");
await screen.findByText("Organization not found");
});
});
@@ -15,6 +15,7 @@ import {
useOrganizationSettings,
} from "./ManagementSettingsLayout";
import { OrganizationSettingsPageView } from "./OrganizationSettingsPageView";
import { OrganizationSummaryPageView } from "./OrganizationSummaryPageView";
const OrganizationSettingsPage: FC = () => {
const { organization: organizationName } = useParams() as {
@@ -65,12 +66,18 @@ const OrganizationSettingsPage: FC = () => {
return <EmptyState message="Organization not found" />;
}
// The user may not be able to edit this org but they can still see it because
// they can edit members, etc. In this case they will be shown a read-only
// summary page instead of the settings form.
if (!permissions[organization.id]?.editOrganization) {
return <OrganizationSummaryPageView organization={organization} />;
}
const error =
updateOrganizationMutation.error ?? deleteOrganizationMutation.error;
return (
<OrganizationSettingsPageView
canEdit={permissions[organization.id]?.editOrganization ?? false}
organization={organization}
error={error}
onSubmit={async (values) => {
@@ -10,7 +10,6 @@ const meta: Meta<typeof OrganizationSettingsPageView> = {
component: OrganizationSettingsPageView,
args: {
organization: MockOrganization,
canEdit: true,
},
};
@@ -24,9 +23,3 @@ export const DefaultOrg: Story = {
organization: MockDefaultOrganization,
},
};
export const CannotEdit: Story = {
args: {
canEdit: false,
},
};
@@ -44,12 +44,11 @@ interface OrganizationSettingsPageViewProps {
error: unknown;
onSubmit: (values: UpdateOrganizationRequest) => Promise<void>;
onDeleteOrganization: () => void;
canEdit: boolean;
}
export const OrganizationSettingsPageView: FC<
OrganizationSettingsPageViewProps
> = ({ organization, error, onSubmit, onDeleteOrganization, canEdit }) => {
> = ({ organization, error, onSubmit, onDeleteOrganization }) => {
const form = useFormik<UpdateOrganizationRequest>({
initialValues: {
name: organization.name,
@@ -85,7 +84,7 @@ export const OrganizationSettingsPageView: FC<
description="The name and description of the organization."
>
<fieldset
disabled={form.isSubmitting || !canEdit}
disabled={form.isSubmitting}
css={{ border: "unset", padding: 0, margin: 0, width: "100%" }}
>
<FormFields>
@@ -117,10 +116,10 @@ export const OrganizationSettingsPageView: FC<
</FormFields>
</fieldset>
</FormSection>
{canEdit && <FormFooter isLoading={form.isSubmitting} />}
<FormFooter isLoading={form.isSubmitting} />
</HorizontalForm>
{canEdit && !organization.is_default && (
{!organization.is_default && (
<HorizontalContainer css={{ marginTop: 48 }}>
<HorizontalSection
title="Settings"
@@ -0,0 +1,23 @@
import type { Meta, StoryObj } from "@storybook/react";
import {
MockDefaultOrganization,
MockOrganization,
} from "testHelpers/entities";
import { OrganizationSummaryPageView } from "./OrganizationSummaryPageView";
const meta: Meta<typeof OrganizationSummaryPageView> = {
title: "pages/OrganizationSummaryPageView",
component: OrganizationSummaryPageView,
args: {
organization: MockOrganization,
},
};
export default meta;
type Story = StoryObj<typeof OrganizationSummaryPageView>;
export const DefaultOrg: Story = {
args: {
organization: MockDefaultOrganization,
},
};
@@ -0,0 +1,48 @@
import type { FC } from "react";
import type { Organization } from "api/typesGenerated";
import {
PageHeader,
PageHeaderTitle,
PageHeaderSubtitle,
} from "components/PageHeader/PageHeader";
import { Stack } from "components/Stack/Stack";
import { UserAvatar } from "components/UserAvatar/UserAvatar";
interface OrganizationSummaryPageViewProps {
organization: Organization;
}
export const OrganizationSummaryPageView: FC<
OrganizationSummaryPageViewProps
> = ({ organization }) => {
return (
<div>
<PageHeader
css={{
// The deployment settings layout already has padding.
paddingTop: 0,
}}
>
<Stack direction="row" spacing={3} alignItems="center">
<UserAvatar
key={organization.id}
size="xl"
username={organization.display_name || organization.name}
avatarURL={organization.icon}
/>
<div>
<PageHeaderTitle>
{organization.display_name || organization.name}
</PageHeaderTitle>
{organization.description && (
<PageHeaderSubtitle>
{organization.description}
</PageHeaderSubtitle>
)}
</div>
</Stack>
</PageHeader>
You are a member of this organization.
</div>
);
};