fix: sort orgs alphabetically in dropdown (#16583)

resolves https://github.com/coder/internal/issues/352
<img width="312" alt="Screenshot 2025-02-18 at 12 16 09 PM"
src="https://github.com/user-attachments/assets/c7863bfd-c7cc-4fa6-be88-68a03cd19c1f"
/>

---------

Co-authored-by: Jaayden Halko <jaayden.halko@gmail.com>
This commit is contained in:
Kira Pilot
2025-02-19 10:20:14 -05:00
committed by GitHub
co-authored by Jaayden Halko
parent 53f0007acb
commit 2c6df5a9ae
2 changed files with 139 additions and 15 deletions
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { expect, userEvent, waitFor, within } from "@storybook/test";
import type { AuthorizationResponse } from "api/typesGenerated";
import {
MockNoPermissions,
MockOrganization,
@@ -7,7 +8,10 @@ import {
MockPermissions,
} from "testHelpers/entities";
import { withDashboardProvider } from "testHelpers/storybook";
import { OrganizationSidebarView } from "./OrganizationSidebarView";
import {
OrganizationSidebarView,
type OrganizationWithPermissions,
} from "./OrganizationSidebarView";
const meta: Meta<typeof OrganizationSidebarView> = {
title: "modules/management/OrganizationSidebarView",
@@ -286,3 +290,114 @@ export const OrgsDisabled: Story = {
showOrganizations: false,
},
};
const commonPerms: AuthorizationResponse = {
editOrganization: true,
editMembers: true,
editGroups: true,
auditOrganization: true,
};
const activeOrganization: OrganizationWithPermissions = {
...MockOrganization,
display_name: "Omega org",
name: "omega",
id: "1",
permissions: {
...commonPerms,
},
};
export const OrgsSortedAlphabetically: Story = {
args: {
activeOrganization,
permissions: {
...MockPermissions,
createOrganization: true,
},
organizations: [
{
...MockOrganization,
display_name: "Zeta Org",
id: "2",
name: "zeta",
permissions: commonPerms,
},
{
...MockOrganization,
display_name: "alpha Org",
id: "3",
name: "alpha",
permissions: commonPerms,
},
activeOrganization,
],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole("button", { name: /Omega org/i }));
// dropdown is not in #storybook-root so must query full document
const globalScreen = within(document.body);
await waitFor(() => {
expect(globalScreen.queryByText("alpha Org")).toBeInTheDocument();
expect(globalScreen.queryByText("Zeta Org")).toBeInTheDocument();
});
const orgElements = globalScreen.getAllByRole("option");
// filter out Create btn
const filteredElems = orgElements.slice(0, 3);
const orgNames = filteredElems.map(
// handling fuzzy matching
(el) => el.textContent?.replace(/^[A-Z]/, "").trim() || "",
);
// active name first
expect(orgNames).toEqual(["Omega org", "alpha Org", "Zeta Org"]);
},
};
export const SearchForOrg: Story = {
args: {
activeOrganization,
permissions: MockPermissions,
organizations: [
{
...MockOrganization,
display_name: "Zeta Org",
id: "2",
name: "zeta",
permissions: commonPerms,
},
{
...MockOrganization,
display_name: "alpha Org",
id: "3",
name: "fish",
permissions: commonPerms,
},
activeOrganization,
],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole("button", { name: /Omega org/i }));
// dropdown is not in #storybook-root so must query full document
const globalScreen = within(document.body);
const searchInput =
await globalScreen.getByPlaceholderText("Find organization");
await userEvent.type(searchInput, "ALPHA");
const filteredResult = await globalScreen.findByText("alpha Org");
expect(filteredResult).toBeInTheDocument();
// Omega org remains visible as the default org
await waitFor(() => {
expect(globalScreen.queryByText("Zeta Org")).not.toBeInTheDocument();
});
},
};
@@ -3,9 +3,12 @@ import { Avatar } from "components/Avatar/Avatar";
import { Button } from "components/Button/Button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "components/Command/Command";
import { Loader } from "components/Loader/Loader";
import {
@@ -88,11 +91,15 @@ const OrganizationsSettingsNavigation: FC<
return <Loader />;
}
// Sort organizations to put active organization first
const sortedOrganizations = [
activeOrganization,
...organizations.filter((org) => org.id !== activeOrganization.id),
];
const sortedOrganizations = [...organizations].sort((a, b) => {
// active org first
if (a.id === activeOrganization.id) return -1;
if (b.id === activeOrganization.id) return 1;
return a.display_name
.toLowerCase()
.localeCompare(b.display_name.toLowerCase());
});
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const navigate = useNavigate();
@@ -123,14 +130,16 @@ const OrganizationsSettingsNavigation: FC<
</PopoverTrigger>
<PopoverContent align="start" className="w-60">
<Command loop>
<CommandInput placeholder="Find organization" />
<CommandList>
<CommandEmpty>No organization found.</CommandEmpty>
<CommandGroup className="pb-2">
{sortedOrganizations.length > 1 && (
<div className="flex flex-col max-h-[260px] overflow-y-auto">
{sortedOrganizations.map((organization) => (
<CommandItem
key={organization.id}
value={organization.name}
value={`${organization.display_name} ${organization.name}`}
onSelect={() => {
setIsPopoverOpen(false);
navigate(urlForSubpage(organization.name));
@@ -158,11 +167,11 @@ const OrganizationsSettingsNavigation: FC<
))}
</div>
)}
{permissions.createOrganization && (
<>
{organizations.length > 1 && (
<hr className="h-px my-2 border-none bg-border -mx-2" />
)}
</CommandGroup>
{permissions.createOrganization && (
<>
{organizations.length > 1 && <CommandSeparator />}
<CommandGroup>
<CommandItem
className="flex justify-center data-[selected=true]:bg-transparent"
onSelect={() => {
@@ -174,9 +183,9 @@ const OrganizationsSettingsNavigation: FC<
>
<Plus /> Create Organization
</CommandItem>
</>
)}
</CommandGroup>
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>