From b95e4f5471a970a3133f8aa7905fd75173e1eb03 Mon Sep 17 00:00:00 2001 From: Russell Vinegar <38586679+rustyjux@users.noreply.github.com> Date: Thu, 12 Sep 2024 13:58:06 -0700 Subject: [PATCH] More Namespace to Gateway changes and fixes (#1170) * scroll to top, fix redirect for gateway detail * add return null * scroll to top of details on nav from /list * Cypress/ns to gw cli tests (#1158) Cypress tests for new features in `gwa` CLI from NS to GW. * replace Namespace with Gateway in org permission (#1159) * Feature/ns to gw redirect (#1162) * fix some capitlization * fix no gw redirect handling and add toast * more details on login * show error for invalid display name format * reset edit display name input on cancel * avoid white bg on button hover if disabled * match GatewayController activity in OrgController v3 (#1171) * match GatewayController activity in OrgController v3 * openapi update --- e2e/cypress/tests/20-gateways/01-list.ts | 6 +++ src/controllers/v3/OrganizationController.ts | 10 +++-- src/controllers/v3/openapi.yaml | 2 +- .../edit-display-name/edit-display-name.tsx | 40 ++++++++++++++---- .../gateway-get-started.tsx | 2 +- .../gateway-toast-handler.tsx | 34 +++++++++++++++ .../no-gateway-redirect.tsx | 41 +++++++++++++++---- src/nextapp/pages/_app.tsx | 2 + src/nextapp/pages/manager/gateways/list.tsx | 12 +++--- src/nextapp/shared/theme.ts | 5 ++- 10 files changed, 125 insertions(+), 29 deletions(-) create mode 100644 src/nextapp/components/no-gateway-redirect/gateway-toast-handler.tsx diff --git a/e2e/cypress/tests/20-gateways/01-list.ts b/e2e/cypress/tests/20-gateways/01-list.ts index c31ad87f1..6e7f5eb3b 100644 --- a/e2e/cypress/tests/20-gateways/01-list.ts +++ b/e2e/cypress/tests/20-gateways/01-list.ts @@ -47,6 +47,12 @@ describe('My Gateways list page', () => { }); }) + it('Verify redirect to My Gateways page if no gateway selected', () => { + cy.visit(ns.detailPath) + cy.wait(2000) + cy.verifyToastMessage('First select a Gateway to view that page') + }) + it('Check Gateway link goes to details page', () => { cy.visit(ns.listPath) cy.get(`[data-testid="ns-list-activate-link-${gateways["namespace1"].gatewayId + '-' + customId}"]`).click() diff --git a/src/controllers/v3/OrganizationController.ts b/src/controllers/v3/OrganizationController.ts index 9d717ffef..a4d904cfc 100644 --- a/src/controllers/v3/OrganizationController.ts +++ b/src/controllers/v3/OrganizationController.ts @@ -23,6 +23,7 @@ import { removeKeys, transformAllRefID, syncRecordsThrowErrors, + parseBlobString, } from '../../batch/feed-worker'; import { GroupAccessService, @@ -266,8 +267,8 @@ export class OrganizationController extends Controller { /** * > `Required Scope:` Namespace.Assign * - * @summary Get administration activity for gateways associated with this Organization Unit - * @param orgUnit + * @summary Get administration activity for gateways associated with this Organization + * @param org * @param first * @param skip * @returns Activity[] @@ -299,8 +300,9 @@ export class OrganizationController extends Controller { ); return transformActivity(records) + .map((o) => removeKeys(o, ['id'])) .map((o) => removeEmpty(o)) - .map((o) => transformAllRefID(o, ['blob'])) - .map((o) => parseJsonString(o, ['blob'])); + .map((o) => parseJsonString(o, ['context'])) + .map((o) => parseBlobString(o)); } } diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 2953c2a5f..ec2d79f47 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -1536,7 +1536,7 @@ paths: $ref: '#/components/schemas/ActivityDetail' type: array description: '> `Required Scope:` Namespace.Assign' - summary: 'Get administration activity for gateways associated with this Organization Unit' + summary: 'Get administration activity for gateways associated with this Organization' tags: - Organizations security: diff --git a/src/nextapp/components/edit-display-name/edit-display-name.tsx b/src/nextapp/components/edit-display-name/edit-display-name.tsx index 07dd2021c..061bb1c4d 100644 --- a/src/nextapp/components/edit-display-name/edit-display-name.tsx +++ b/src/nextapp/components/edit-display-name/edit-display-name.tsx @@ -33,7 +33,7 @@ const EditNamespaceDisplayName: React.FC = ({ queryKey, }) => { const toast = useToast(); - const { isOpen, onOpen, onClose } = useDisclosure(); + const { isOpen, onOpen, onClose: originalOnClose } = useDisclosure(); const queryClient = useQueryClient(); const mutate = useApiMutation(mutation); const [inputValue, setInputValue] = React.useState(data.displayName || ''); @@ -42,18 +42,24 @@ const EditNamespaceDisplayName: React.FC = ({ ); const minCharLimit = 3; const maxCharLimit = 30; + const [isValidFormat, setIsValidFormat] = React.useState(true); + const validNameRegex = /^[a-zA-Z0-9][\w\s().'/-]*$/; + const handleInputChange = (event) => { const { value } = event.target; setInputValue(value); setCharCount(value.length); + setIsValidFormat(validNameRegex.test(value)); }; + const form = React.useRef(); const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); - if (charCount >= minCharLimit && charCount <= maxCharLimit) { + if (charCount >= minCharLimit && charCount <= maxCharLimit && isValidFormat) { updateNamespaceDisplayName(); } }; + const updateNamespaceDisplayName = async () => { if (form.current) { try { @@ -62,7 +68,7 @@ const EditNamespaceDisplayName: React.FC = ({ const entries = Object.fromEntries(formData); await mutate.mutateAsync(entries); queryClient.invalidateQueries(queryKey); - onClose(); + originalOnClose(); toast({ title: 'Display name successfully edited', status: 'success', @@ -79,9 +85,21 @@ const EditNamespaceDisplayName: React.FC = ({ } } }; + const handleSaveClick = () => { form.current?.requestSubmit(); }; + + const isInputValid = charCount >= minCharLimit && charCount <= maxCharLimit && isValidFormat; + + // Add this new function to handle closing and resetting + const handleClose = () => { + setInputValue(data.displayName || ''); + setCharCount(data.displayName?.length || 0); + setIsValidFormat(true); + originalOnClose(); + }; + return ( <> - + Edit display name @@ -119,12 +137,12 @@ const EditNamespaceDisplayName: React.FC = ({ onChange={handleInputChange} name="displayName" variant="bc-input" - isInvalid={charCount > maxCharLimit || charCount < minCharLimit} + isInvalid={!isInputValid} data-testid="edit-display-name-input" /> {charCount > maxCharLimit && ( - You have reached the character limit + Display name must be less than 30 characters )} {charCount < minCharLimit && ( @@ -132,6 +150,11 @@ const EditNamespaceDisplayName: React.FC = ({ Display name must be at least 3 characters )} + {!isValidFormat && (charCount >= minCharLimit) && (charCount <= maxCharLimit) && ( + + Display name must start with an alphanumeric character and can only use special characters "-()_ .'/" + + )} @@ -139,7 +162,7 @@ const EditNamespaceDisplayName: React.FC = ({ diff --git a/src/nextapp/components/gateway-get-started/gateway-get-started.tsx b/src/nextapp/components/gateway-get-started/gateway-get-started.tsx index 6e50203df..973ff5a64 100644 --- a/src/nextapp/components/gateway-get-started/gateway-get-started.tsx +++ b/src/nextapp/components/gateway-get-started/gateway-get-started.tsx @@ -312,7 +312,7 @@ const GatewayGetStarted: React.FC = ({ )} { + const toast = useToast(); + const router = useRouter(); + + React.useEffect(() => { + const handleRouteChange = () => { + const showToast = localStorage.getItem('showNoGatewayToast'); + if (showToast === 'true') { + toast.closeAll(); + toast({ + title: `First select a Gateway to view that page`, + status: 'error', + isClosable: true, + duration: 5000, + }); + localStorage.removeItem('showNoGatewayToast'); + } + }; + + router.events.on('routeChangeComplete', handleRouteChange); + + return () => { + router.events.off('routeChangeComplete', handleRouteChange); + }; + }, [toast, router]); + + return null; +}; + +export default GatewayToastHandler; \ No newline at end of file diff --git a/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx b/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx index a9f45fcd6..0407a76c7 100644 --- a/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx +++ b/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx @@ -5,14 +5,41 @@ import { useAuth } from '@/shared/services/auth'; const NoGatewayRedirect = () => { const router = useRouter(); const { user } = useAuth(); - const hasNamespace = !!user?.namespace; + const [isChecking, setIsChecking] = React.useState(true); React.useEffect(() => { - if (!hasNamespace) { - router.push('/manager/gateways/list'); - } - }, [hasNamespace]); - return null + const checkNamespaceAndRedirect = async () => { + // Wait for a short period to ensure user data is loaded + await new Promise(resolve => setTimeout(resolve, 2000)); + + setIsChecking(false); + + if (!user?.namespace) { + try { + // Set localStorage item to show toast + await new Promise((resolve, reject) => { + try { + localStorage.setItem('showNoGatewayToast', 'true'); + resolve(); + } catch (error) { + reject(error); + } + }); + await router.push('/manager/gateways/list'); + } catch (error) { + console.error('Error during redirect process:', error); + } + } + }; + + checkNamespaceAndRedirect(); + }, [user, router]); + + if (isChecking) { + return null; // could return a loading indicator + } + + return null; }; -export default NoGatewayRedirect; +export default NoGatewayRedirect; \ No newline at end of file diff --git a/src/nextapp/pages/_app.tsx b/src/nextapp/pages/_app.tsx index 6b06ad3b1..7cf5709b3 100644 --- a/src/nextapp/pages/_app.tsx +++ b/src/nextapp/pages/_app.tsx @@ -31,6 +31,7 @@ import '@/shared/styles/global.css'; import { AppWrapper } from './context'; import '../../mocks'; import CompleteProfile from '@/components/complete-profile'; +import GatewayToastHandler from '@/components/no-gateway-redirect/gateway-toast-handler'; const footerItems = [ { href: 'http://www2.gov.bc.ca/gov/content/home', text: 'Home' }, @@ -122,6 +123,7 @@ const App: React.FC = ({ Component, pageProps }) => { }} > + diff --git a/src/nextapp/pages/manager/gateways/list.tsx b/src/nextapp/pages/manager/gateways/list.tsx index b0a6246a8..6e9389835 100644 --- a/src/nextapp/pages/manager/gateways/list.tsx +++ b/src/nextapp/pages/manager/gateways/list.tsx @@ -106,7 +106,7 @@ const MyGatewaysPage: React.FC = () => { const handleNamespaceChange = React.useCallback( (namespace: Namespace) => async () => { toast({ - title: `Switching to gateway: ${namespace.displayName}`, + title: `Switching to Gateway: ${namespace.displayName}`, status: 'info', isClosable: true, }); @@ -116,7 +116,7 @@ const MyGatewaysPage: React.FC = () => { toast.closeAll(); client.invalidateQueries(); toast({ - title: `Switched to gateway: ${namespace.displayName}`, + title: `Switched to Gateway: ${namespace.displayName}`, status: 'success', isClosable: true, }); @@ -124,7 +124,7 @@ const MyGatewaysPage: React.FC = () => { } catch (err) { toast.closeAll(); toast({ - title: 'Unable to switch gateways', + title: 'Unable to switch Gateways', status: 'error', isClosable: true, }); @@ -264,11 +264,11 @@ const MyGatewaysPage: React.FC = () => { {isSuccess && (namespaceSearchResults.length === 1 ? ( - {namespaceSearchResults.length} gateway + {namespaceSearchResults.length} Gateway ) : ( - {namespaceSearchResults.length} gateways + {namespaceSearchResults.length} Gateways ))} - {isLoading && Loading gateways...} + {isLoading && Loading Gateways...} {isError && Gateways failed to load} {isSuccess && ( <> diff --git a/src/nextapp/shared/theme.ts b/src/nextapp/shared/theme.ts index a73d8317b..b09e7e946 100644 --- a/src/nextapp/shared/theme.ts +++ b/src/nextapp/shared/theme.ts @@ -58,7 +58,7 @@ const _focus = { boxShadow: 'sm', }; const _disabled = { - opacity: 0.3, + opacity: 0.4, cursor: 'not-allowed', }; const _invalid = { @@ -122,7 +122,8 @@ const buttonVariants = { _disabled: { ..._disabled, _hover: { - background: 'bc-blue', + bg: '#003366 !important', // Use the hex value of bc-blue directly, necessary oddly. + opacity: 0.4, }, }, },