diff --git a/__tests__/components/OrgPicker/OrgPicker.test.js b/__tests__/components/OrgPicker/OrgPicker.test.js
index ec546fc4..7acf41db 100644
--- a/__tests__/components/OrgPicker/OrgPicker.test.js
+++ b/__tests__/components/OrgPicker/OrgPicker.test.js
@@ -3,14 +3,27 @@
*/
import { describe, expect, it } from '@jest/globals';
import { render, screen } from '@testing-library/react';
+// eslint-disable-next-line no-unused-vars
+import { usePathname } from 'next/navigation';
import userEvent from '@testing-library/user-event';
import { OrgPicker } from '@/components/OrgPicker/OrgPicker';
+/* global jest */
+/* eslint no-undef: "off" */
+jest.mock('next/navigation', () => ({
+ usePathname: jest.fn(() => '/orgs/470bd8ff-ed0e-4d11-95c4-cf765202cebd'),
+}));
+/* eslint no-undef: "error" */
+
describe('', () => {
+ const mockOrgs = [
+ { name: 'foobar org 1', guid: 'baz' },
+ { name: 'foobar org 2', guid: 'baz2' },
+ ];
describe('on initial load', () => {
it('content is collapsed', () => {
// act
- render();
+ render();
// assert
const content = screen.queryByText('View all organizations');
expect(content).not.toBeInTheDocument();
@@ -21,7 +34,7 @@ describe('', () => {
it('content expands', async () => {
// setup
const user = userEvent.setup();
- render();
+ render();
// act
const button = screen.getByRole('button', { expanded: false });
await user.click(button);
@@ -30,4 +43,43 @@ describe('', () => {
expect(content).toBeInTheDocument();
});
});
+
+ describe('when only one org', () => {
+ it('only shows org name instead of the dropdown', () => {
+ // setup
+ render();
+ // act
+ const orgName = screen.getByText(/foobar org 1/);
+ const button = screen.queryByRole('button');
+ // assert
+ expect(orgName).toBeInTheDocument();
+ expect(button).not.toBeInTheDocument();
+ });
+ });
+
+ describe('when no orgs at all', () => {
+ it('shows nothing', () => {
+ // setup
+ render();
+ // act
+ const orgName = screen.queryByText(/foobar org 1/);
+ const button = screen.queryByRole('button');
+ // assert
+ expect(orgName).not.toBeInTheDocument();
+ expect(button).not.toBeInTheDocument();
+ });
+ });
+
+ describe('when no current org', () => {
+ it('shows nothing', () => {
+ // setup
+ render();
+ // act
+ const orgName = screen.queryByText(/foobar org 1/);
+ const button = screen.queryByRole('button');
+ // assert
+ expect(orgName).not.toBeInTheDocument();
+ expect(button).not.toBeInTheDocument();
+ });
+ });
});
diff --git a/__tests__/helpers/text.test.js b/__tests__/helpers/text.test.js
index b28418a5..da665de9 100644
--- a/__tests__/helpers/text.test.js
+++ b/__tests__/helpers/text.test.js
@@ -4,6 +4,7 @@ import {
emailIsValid,
underscoreToText,
pluralize,
+ newOrgPathname,
} from '@/helpers/text';
describe('text helpers', () => {
@@ -47,4 +48,55 @@ describe('text helpers', () => {
expect(pluralize('role', -1)).toBe('role');
});
});
+
+ describe('newOrgPathname', () => {
+ const newGUID = 'foobar';
+
+ describe('when last GUID starts with an a-z character', () => {
+ it('removes last GUID from pathname', () => {
+ const path =
+ '/orgs/470bd8ff-ed0e-4d11-95c4-cf765202cebd/users/add/b70bd8ff-ed0e-4d11-95c4-cf765202cebd'; // last GUID starts with an a-z character
+ const result = newOrgPathname(path, newGUID);
+ expect(result).toEqual('/orgs/foobar/users/add/');
+ });
+ });
+
+ describe('last GUID starts with an a-z character, but then keeps going with more path sections', () => {
+ it('removes last GUID and beyond from pathname', () => {
+ const path =
+ '/orgs/470bd8ff-ed0e-4d11-95c4-cf765202cebd/users/add/b70bd8ff-ed0e-4d11-95c4-cf765202cebd/keeps-going'; // last GUID starts with an a-z character, but then keeps going with more path sections
+ const result = newOrgPathname(path, newGUID);
+ expect(result).toEqual('/orgs/foobar/users/add/');
+ });
+ });
+
+ describe('when last GUID starts with a digit', () => {
+ it('removes last GUID from pathname', () => {
+ const path =
+ '/orgs/470bd8ff-ed0e-4d11-95c4-cf765202cebd/users/add/470bd8ff-ed0e-4d11-95c4-cf765202cebd/'; // next GUID starts with a digit
+ const result = newOrgPathname(path, newGUID);
+ expect(result).toEqual('/orgs/foobar/users/add/');
+ });
+ });
+
+ describe('when no other GUID but the org GUID', () => {
+ it('just replaces the org guid with new guid', () => {
+ const path = '/orgs/470bd8ff-ed0e-4d11-95c4-cf765202cebd'; // without trailing slash
+ const result = newOrgPathname(path, newGUID);
+ expect(result).toEqual('/orgs/foobar');
+ });
+
+ it('just replaces the org guid with new guid', () => {
+ const path = '/orgs/470bd8ff-ed0e-4d11-95c4-cf765202cebd/'; // with trailing slash
+ const result = newOrgPathname(path, newGUID);
+ expect(result).toEqual('/orgs/foobar/');
+ });
+
+ it('just replaces the org guid with new guid', () => {
+ const path = '/orgs/470bd8ff-ed0e-4d11-95c4-cf765202cebd/users/add'; // with trailing slash
+ const result = newOrgPathname(path, newGUID);
+ expect(result).toEqual('/orgs/foobar/users/add');
+ });
+ });
+ });
});
diff --git a/next-env.d.ts b/next-env.d.ts
index 4f11a03d..40c3d680 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -2,4 +2,4 @@
///
// NOTE: This file should not be edited
-// see https://nextjs.org/docs/basic-features/typescript for more information.
+// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
diff --git a/src/app/orgs/[orgId]/layout.tsx b/src/app/orgs/[orgId]/layout.tsx
index 43bd8313..3350d42c 100644
--- a/src/app/orgs/[orgId]/layout.tsx
+++ b/src/app/orgs/[orgId]/layout.tsx
@@ -1,7 +1,6 @@
import React from 'react';
-import Link from 'next/link';
-import { LayoutHeader } from '@/components/LayoutHeader';
-import { getOrg } from '@/controllers/controllers';
+import { OrgPicker } from '@/components/OrgPicker/OrgPicker';
+import { getOrgs } from '@/api/cf/cloudfoundry';
export default async function OrgLayout({
children,
@@ -10,15 +9,14 @@ export default async function OrgLayout({
children: React.ReactNode;
params: { orgId: string };
}) {
- const { payload, meta } = await getOrg(params.orgId);
+ const orgsRes = await getOrgs();
+ const orgResJson = await orgsRes.json();
+ const orgs = orgResJson.resources;
return (
<>
-
- {meta.status === 'success' ? payload.name : 'Org name not found'}
-
-
-
Back to all organizations
+
+
{children}
>
diff --git a/src/app/prototype/design-guide/page.tsx b/src/app/prototype/design-guide/page.tsx
index b50dc736..656785ce 100644
--- a/src/app/prototype/design-guide/page.tsx
+++ b/src/app/prototype/design-guide/page.tsx
@@ -5,7 +5,6 @@ import { Button } from '@/components/uswds/Button';
import { Banner } from '@/components/uswds/Banner';
import Checkbox from '@/components/uswds/Checkbox';
import { useState } from 'react';
-import { OrgPicker } from '@/components/OrgPicker/OrgPicker';
export default function DesignGuidePage() {
const initialCheckboxes = {
@@ -37,11 +36,6 @@ export default function DesignGuidePage() {
USA banner
-
-
Headers in prose:
diff --git a/src/assets/stylesheets/styles.scss b/src/assets/stylesheets/styles.scss
index c944a049..0332d90e 100644
--- a/src/assets/stylesheets/styles.scss
+++ b/src/assets/stylesheets/styles.scss
@@ -139,6 +139,13 @@
// 3. Load any custom SASS
/* Custom utilities classes */
+
+// Flex
+
+.flex-shrink-0 {
+ flex-shrink: 0;
+}
+
// text styling
.text-capitalize {
text-transform: capitalize;
diff --git a/src/components/OrgPicker/OrgPicker.tsx b/src/components/OrgPicker/OrgPicker.tsx
index e831df6d..46393cc8 100644
--- a/src/components/OrgPicker/OrgPicker.tsx
+++ b/src/components/OrgPicker/OrgPicker.tsx
@@ -1,71 +1,89 @@
+/* eslint-disable jsx-a11y/role-supports-aria-props */
/*
* While visually similar to the USWDS Combo box component, this expandable element presents a scrollable list of links. The link at the bottom of the menu directs the user to a page listing all the organizations they can access.
*/
+'use client';
import React from 'react';
import { useState, useRef, useEffect } from 'react';
import Image from 'next/image';
+import { usePathname } from 'next/navigation';
import collapseIcon from '@/../public/img/uswds/usa-icons/expand_more.svg';
import { OrgPickerList } from './OrgPickerList';
+import { OrgPickerListItem } from './OrgPickerListItem';
import { OrgPickerFooter } from './OrgPickerFooter';
+import { OrgObj } from '@/api/cf/cloudfoundry-types';
+import { sortObjectsByParam } from '@/helpers/arrays';
+import { newOrgPathname } from '@/helpers/text';
-export function OrgPicker({ single }: { single: Boolean }) {
+export function OrgPicker({
+ currentOrgId,
+ orgs = [],
+}: {
+ currentOrgId: string;
+ orgs?: Array
;
+}) {
const [isOpen, setIsOpen] = useState(false);
const orgsSelectorRef = useRef(null);
const toggleButtonRef = useRef(null);
+ const currentOrg = orgs.find((org) => org.guid === currentOrgId);
+ const curPathName = usePathname();
- // Toggle the org picker
- const togglePicker = () => setIsOpen(!isOpen);
-
- // Return focus to toggle button
- const returnFocus = () => toggleButtonRef.current?.focus();
-
- // Close the picker when clicking outside
- const handleOutsideClick = (e: MouseEvent) => {
- if (
- orgsSelectorRef.current &&
- !orgsSelectorRef.current.contains(e.target as Node)
- ) {
- setIsOpen(false);
- }
+ // Get new pathname for another org in the list
+ const orgPathname = (guid: string): string => {
+ return newOrgPathname(curPathName, guid);
};
- // Close the picker when pressing the ESC key
- const handleEscapeKeyPress = (e: KeyboardEvent) => {
- if (e.key === 'Escape') {
- setIsOpen(false);
- returnFocus();
- }
- };
+ // Toggle the org picker
+ const togglePicker = () => setIsOpen(!isOpen);
- // Focus back on the toggle button after tabbing through list
- const handleTabKeyPress = (e: KeyboardEvent) => {
- if (e.key === 'Tab' && isOpen && orgsSelectorRef.current) {
- const lastElement = orgsSelectorRef.current.querySelector('footer a');
+ useEffect(() => {
+ // Return focus to toggle button
+ const returnFocus = () => toggleButtonRef.current?.focus();
+ // Close the picker when clicking outside
+ const handleOutsideClick = (e: MouseEvent) => {
+ if (
+ orgsSelectorRef.current &&
+ !orgsSelectorRef.current.contains(e.target as Node)
+ ) {
+ setIsOpen(false);
+ }
+ };
- if (!e.shiftKey && document.activeElement === lastElement) {
- e.preventDefault();
+ // Close the picker when pressing the ESC key
+ const handleEscapeKeyPress = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
setIsOpen(false);
returnFocus();
}
- }
- };
+ };
- // Add event listners to the org picker
- const addListeners = () => {
- document.addEventListener('mousedown', handleOutsideClick);
- document.addEventListener('keydown', handleEscapeKeyPress);
- document.addEventListener('keydown', handleTabKeyPress);
- };
+ // Focus back on the toggle button after tabbing through list
+ const handleTabKeyPress = (e: KeyboardEvent) => {
+ if (e.key === 'Tab' && isOpen && orgsSelectorRef.current) {
+ const lastElement = orgsSelectorRef.current.querySelector('footer a');
- // Remove event listners
- const removeListeners = () => {
- // Remove listeners if org picker is not open
- document.removeEventListener('mousedown', handleOutsideClick);
- document.removeEventListener('keydown', handleEscapeKeyPress);
- document.removeEventListener('keydown', handleTabKeyPress);
- };
+ if (!e.shiftKey && document.activeElement === lastElement) {
+ e.preventDefault();
+ setIsOpen(false);
+ returnFocus();
+ }
+ }
+ };
+ // Add event listners to the org picker
+ const addListeners = () => {
+ document.addEventListener('mousedown', handleOutsideClick);
+ document.addEventListener('keydown', handleEscapeKeyPress);
+ document.addEventListener('keydown', handleTabKeyPress);
+ };
+
+ // Remove event listners
+ const removeListeners = () => {
+ // Remove listeners if org picker is not open
+ document.removeEventListener('mousedown', handleOutsideClick);
+ document.removeEventListener('keydown', handleEscapeKeyPress);
+ document.removeEventListener('keydown', handleTabKeyPress);
+ };
- useEffect(() => {
if (isOpen) {
addListeners();
} else {
@@ -79,7 +97,24 @@ export function OrgPicker({ single }: { single: Boolean }) {
};
}, [isOpen]);
- return !single ? (
+ if (orgs.length <= 0 || !currentOrg) {
+ return null;
+ }
+
+ if (orgs.length === 1) {
+ return (
+
+
+ Current organization:
+
+
+ {currentOrg.name}
+
+
+ );
+ }
+
+ return (
Current organization:
@@ -90,12 +125,12 @@ export function OrgPicker({ single }: { single: Boolean }) {
aria-expanded={isOpen}
ref={orgsSelectorRef}
>
-
-
- sandbox-gsa-much-longer-name-goes-here-and-is-very-very-long
+
+
+ {currentOrg.name}
- ) : (
-
-
- Current organization:
-
-
- sandbox-gsa-much-longer-name-goes-here-and-is-very-very-long
-
-
);
}
diff --git a/src/components/OrgPicker/OrgPickerFooter.tsx b/src/components/OrgPicker/OrgPickerFooter.tsx
index 290d1215..b4c06fe8 100644
--- a/src/components/OrgPicker/OrgPickerFooter.tsx
+++ b/src/components/OrgPicker/OrgPickerFooter.tsx
@@ -4,7 +4,7 @@ import Link from 'next/link';
export function OrgPickerFooter() {
return (