From 852b64d1c28159fd2377442dc8a5ae7ad32abbef Mon Sep 17 00:00:00 2001 From: Eleni Chappen Date: Wed, 25 Sep 2024 13:10:00 -0500 Subject: [PATCH 1/4] make org picker functional --- __tests__/helpers/text.test.js | 52 +++++++ next-env.d.ts | 2 +- src/app/orgs/[orgId]/layout.tsx | 19 +-- src/app/prototype/design-guide/page.tsx | 6 - src/components/OrgPicker/OrgPicker.tsx | 128 +++++++++++------- src/components/OrgPicker/OrgPickerFooter.tsx | 2 +- src/components/OrgPicker/OrgPickerList.tsx | 16 +-- .../OrgPicker/OrgPickerListItem.tsx | 13 +- src/helpers/text.tsx | 24 ++++ 9 files changed, 178 insertions(+), 84 deletions(-) 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..36dea5f1 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,17 @@ export default async function OrgLayout({ children: React.ReactNode; params: { orgId: string }; }) { - const { payload, meta } = await getOrg(params.orgId); + const orgsRes = await getOrgs(); + if (!orgsRes.ok) { + return <>orgs not found; + } + 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

-
-

Org Picker

- -
-

Headers in prose:

diff --git a/src/components/OrgPicker/OrgPicker.tsx b/src/components/OrgPicker/OrgPicker.tsx index e831df6d..5e49207f 100644 --- a/src/components/OrgPicker/OrgPicker.tsx +++ b/src/components/OrgPicker/OrgPicker.tsx @@ -1,71 +1,91 @@ +/* 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 = [], + single = false, +}: { + currentOrgId: string; + orgs?: Array; + single?: Boolean; +}) { 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 { @@ -90,12 +110,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 || 'loading'}
); 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 (