From 84cc03347e65f6192214f7aa168bfb180c33abff Mon Sep 17 00:00:00 2001 From: Matteo Guarnaccia Date: Thu, 4 Jan 2024 17:07:12 +0000 Subject: [PATCH 001/222] logic for menu actions #172 --- .../category/catalogueCard.component.tsx | 60 +++++++++++++++---- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/src/catalogue/category/catalogueCard.component.tsx b/src/catalogue/category/catalogueCard.component.tsx index 1881e54be..1ec0f6a9b 100644 --- a/src/catalogue/category/catalogueCard.component.tsx +++ b/src/catalogue/category/catalogueCard.component.tsx @@ -7,12 +7,15 @@ import { CardActions, IconButton, Checkbox, + MenuItem, + ListItemIcon, + Menu, } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; +import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; import { CatalogueCategory } from '../../app.types'; import { Link } from 'react-router-dom'; - export interface CatalogueCardProps extends CatalogueCategory { onChangeOpenDeleteDialog: (catalogueCategory: CatalogueCategory) => void; onChangeOpenEditDialog: (catalogueCategory: CatalogueCategory) => void; @@ -33,10 +36,13 @@ function CatalogueCard(props: CatalogueCardProps) { onToggleSelect(catalogueCategory); }; + const [menuOpen, setMenuOpen] = React.useState(false); + const [anchorEl, setAnchorEl] = React.useState(null); + return ( From 29c6360c6193b23b18988ea3e7765de6e2adc710 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Fri, 5 Jan 2024 10:42:58 +0000 Subject: [PATCH 002/222] Add menu button #137 --- src/systems/systemDialog.component.tsx | 6 +- src/systems/systems.component.tsx | 310 ++++++++++++++++--------- 2 files changed, 205 insertions(+), 111 deletions(-) diff --git a/src/systems/systemDialog.component.tsx b/src/systems/systemDialog.component.tsx index d7473973c..3536829e9 100644 --- a/src/systems/systemDialog.component.tsx +++ b/src/systems/systemDialog.component.tsx @@ -29,10 +29,12 @@ import { SystemImportanceType, } from '../app.types'; +export type SystemDialogType = 'add' | 'edit'; + export interface SystemDialogProps { open: boolean; onClose: () => void; - type: 'add' | 'edit'; + type: SystemDialogType; // Only required for add parentId?: string | null; // Only required for prepopulating fields for an edit dialog @@ -211,7 +213,7 @@ const SystemDialog = React.memo((props: SystemDialogProps) => { const systemText = parentId ? 'Subsystem' : 'System'; return ( - + {type === 'add' ? `Add ${systemText}` : `Edit ${systemText}`} diff --git a/src/systems/systems.component.tsx b/src/systems/systems.component.tsx index beec7b62d..380a81803 100644 --- a/src/systems/systems.component.tsx +++ b/src/systems/systems.component.tsx @@ -15,7 +15,10 @@ import { List, ListItem, ListItemButton, + ListItemIcon, ListItemText, + Menu, + MenuItem, Typography, } from '@mui/material'; import React from 'react'; @@ -24,8 +27,10 @@ import { useSystems, useSystemsBreadcrumbs } from '../api/systems'; import { System } from '../app.types'; import Breadcrumbs from '../view/breadcrumbs.component'; import SystemDetails from './systemDetails.component'; -import SystemDialog from './systemDialog.component'; +import SystemDialog, { SystemDialogType } from './systemDialog.component'; import { SystemDirectoryDialog } from './systemDirectoryDialog.component'; +import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; +import SaveAsIcon from '@mui/icons-material/SaveAs'; /* Returns function that navigates to a specific system id (or to the root of all systems if given null) */ @@ -122,6 +127,66 @@ const CopySystemsButton = (props: { ); }; +/* TODO: Remove this and use table menu items */ +const SubsystemMenu = (props: { + subsystem: System; + onOpen: () => void; + onItemClicked: (type: SystemDialogType) => void; +}) => { + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + + const handleOpen = ( + event: React.MouseEvent + ) => { + props.onOpen(); + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleClick = (type: SystemDialogType) => { + props.onItemClicked(type); + handleClose(); + }; + + return ( + <> + + + + + handleClick('edit')} + > + + + + Save as + + + + ); +}; + /* Returns the system id from the location pathname (null when not found) */ export const useSystemId = (): string | null => { // Navigation setup @@ -142,6 +207,16 @@ function Systems() { // States const [selectedSystems, setSelectedSystems] = React.useState([]); + // Specifically for the drop down menus/dialogues + const [selectedSystemForMenu, setSelectedSystemForMenu] = React.useState< + System | undefined + >(); + + // When all menu's closed will be undefined + const [menuDialogType, setMenuDialogType] = React.useState< + SystemDialogType | undefined + >(undefined); + // Data const { data: systemsBreadcrumbs, isLoading: systemsBreadcrumbsLoading } = useSystemsBreadcrumbs(systemId); @@ -166,122 +241,139 @@ function Systems() { }, [systemId]); return ( - - {systemsBreadcrumbsLoading && systemId !== null ? ( - - ) : ( - -
- { - navigateToSystem(null); - }} - navigateHomeAriaLabel={'navigate to systems home'} - /> - -
- {selectedSystems.length > 0 && ( - - + + {systemsBreadcrumbsLoading && systemId !== null ? ( + + ) : ( + +
+ { + navigateToSystem(null); + }} + navigateHomeAriaLabel={'navigate to systems home'} /> - -
+ {selectedSystems.length > 0 && ( + + + + + + )} +
+ )} + + + {subsystemsDataLoading ? ( + - {selectedSystems.length} selected - - - )} - - )} - - - {subsystemsDataLoading ? ( - - - - ) : ( - <> - - - {systemId === null ? 'Root systems' : 'Subsystems'} - - + - - - {subsystemsData?.map((system, index) => { - const selected = selectedSystems.some( - (selectedSystem) => selectedSystem.id === system.id - ); - return ( - - navigateToSystem(system.id)} - > - event.stopPropagation()} - onChange={(event) => - handleSystemCheckboxChange( - event.target.checked, - system - ) + ) : ( + <> + + + {systemId === null ? 'Root systems' : 'Subsystems'} + + + + + + {subsystemsData?.map((system, index) => { + const selected = selectedSystems.some( + (selectedSystem) => selectedSystem.id === system.id + ); + return ( + + navigateToSystem(system.id)} + > + event.stopPropagation()} + onChange={(event) => + handleSystemCheckboxChange( + event.target.checked, + system + ) + } + /> + {system.name} + + setSelectedSystemForMenu(system)} + onItemClicked={(type: SystemDialogType) => + setMenuDialogType(type) } /> - {system.name} - - - ); - })} - - - )} - - - + + ); + })} + + + )} + + + + -
+ { + setMenuDialogType(undefined); + }} + type={menuDialogType || 'edit'} + selectedSystem={selectedSystemForMenu} + /> + ); } From 85e855b3e265e446103637c1fab86d4fd7382e99 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Fri, 5 Jan 2024 10:53:14 +0000 Subject: [PATCH 003/222] Implement save as logic #137 --- src/systems/systemDialog.component.tsx | 53 +++++++++++++++----------- src/systems/systems.component.tsx | 2 +- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/systems/systemDialog.component.tsx b/src/systems/systemDialog.component.tsx index 3536829e9..424ba249e 100644 --- a/src/systems/systemDialog.component.tsx +++ b/src/systems/systemDialog.component.tsx @@ -29,7 +29,19 @@ import { SystemImportanceType, } from '../app.types'; -export type SystemDialogType = 'add' | 'edit'; +export type SystemDialogType = 'add' | 'edit' | 'save as'; + +const getEmptySystem = (): AddSystem => { + return { + // Here using null for optional values only, so that types for isUpdated parameters + // can match + name: '', + description: null, + location: null, + owner: null, + importance: SystemImportanceType.MEDIUM, + } as AddSystem; +}; export interface SystemDialogProps { open: boolean; @@ -45,20 +57,18 @@ const SystemDialog = React.memo((props: SystemDialogProps) => { const { open, onClose, parentId, type, selectedSystem } = props; // User entered properties - const [systemData, setSystemData] = React.useState({ - // Here using null for optional values only, so that types for isUpdated parameters - // can match - name: '', - description: null, - location: null, - owner: null, - importance: SystemImportanceType.MEDIUM, - }); + const [systemData, setSystemData] = React.useState( + getEmptySystem() + ); // Ensure system data is updated when the selected system changes useEffect(() => { - if (selectedSystem) setSystemData(selectedSystem as AddSystem); - }, [selectedSystem]); + if (open) { + if ((type === 'edit' || type === 'save as') && selectedSystem) + setSystemData(selectedSystem as AddSystem); + else setSystemData(getEmptySystem()); + } + }, [selectedSystem, open, type]); // Error messages for the above properties (undefined means no error) const [nameError, setNameError] = React.useState( @@ -74,14 +84,7 @@ const SystemDialog = React.memo((props: SystemDialogProps) => { const [otherError, setOtherError] = React.useState(false); const handleClose = React.useCallback(() => { - if (type === 'add') - setSystemData({ - name: '', - description: null, - location: null, - owner: null, - importance: SystemImportanceType.MEDIUM, - }); + if (type === 'add') setSystemData(getEmptySystem()); // Reset for edit else setSystemData(selectedSystem as AddSystem); @@ -106,7 +109,7 @@ const SystemDialog = React.memo((props: SystemDialogProps) => { return true; }, [systemData.name]); - const handleAddSystem = React.useCallback(() => { + const handleAddSaveSystem = React.useCallback(() => { // Validate the entered fields if (validateFields()) { // Should be valid so add the system @@ -215,7 +218,7 @@ const SystemDialog = React.memo((props: SystemDialogProps) => { return ( - {type === 'add' ? `Add ${systemText}` : `Edit ${systemText}`} + {type === 'edit' ? `Edit ${systemText}` : `Add ${systemText}`} @@ -319,7 +322,11 @@ const SystemDialog = React.memo((props: SystemDialogProps) => { diff --git a/src/systems/systems.component.tsx b/src/systems/systems.component.tsx index a5e9fed98..bd02e6b4d 100644 --- a/src/systems/systems.component.tsx +++ b/src/systems/systems.component.tsx @@ -1,8 +1,12 @@ import { NavigateNext } from '@mui/icons-material'; import AddIcon from '@mui/icons-material/Add'; import ClearIcon from '@mui/icons-material/Clear'; +import DeleteIcon from '@mui/icons-material/Delete'; import DriveFileMoveOutlinedIcon from '@mui/icons-material/DriveFileMoveOutlined'; +import EditIcon from '@mui/icons-material/Edit'; import FolderCopyOutlinedIcon from '@mui/icons-material/FolderCopyOutlined'; +import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; +import SaveAsIcon from '@mui/icons-material/SaveAs'; import { Box, Button, @@ -29,8 +33,7 @@ import Breadcrumbs from '../view/breadcrumbs.component'; import SystemDetails from './systemDetails.component'; import SystemDialog, { SystemDialogType } from './systemDialog.component'; import { SystemDirectoryDialog } from './systemDirectoryDialog.component'; -import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; -import SaveAsIcon from '@mui/icons-material/SaveAs'; +import { DeleteSystemDialog } from './deleteSystemDialog.component'; /* Returns function that navigates to a specific system id (or to the root of all systems if given null) */ @@ -127,11 +130,13 @@ const CopySystemsButton = (props: { ); }; +type MenuDialogType = SystemDialogType | 'delete'; + /* TODO: Remove this and use table menu items */ const SubsystemMenu = (props: { subsystem: System; onOpen: () => void; - onItemClicked: (type: SystemDialogType) => void; + onItemClicked: (type: MenuDialogType) => void; }) => { const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); @@ -147,7 +152,7 @@ const SubsystemMenu = (props: { setAnchorEl(null); }; - const handleClick = (type: SystemDialogType) => { + const handleClick = (type: MenuDialogType) => { props.onItemClicked(type); handleClose(); }; @@ -174,7 +179,16 @@ const SubsystemMenu = (props: { }} > handleClick('edit')} + > + + + + Edit + + handleClick('save as')} > @@ -182,6 +196,15 @@ const SubsystemMenu = (props: { Save as + handleClick('delete')} + > + + + + Delete + ); @@ -214,7 +237,7 @@ function Systems() { // When all menu's closed will be undefined const [menuDialogType, setMenuDialogType] = React.useState< - SystemDialogType | undefined + MenuDialogType | undefined >(undefined); // Data @@ -349,7 +372,7 @@ function Systems() { setSelectedSystemForMenu(system)} - onItemClicked={(type: SystemDialogType) => + onItemClicked={(type: SystemDialogType | 'delete') => setMenuDialogType(type) } /> @@ -366,12 +389,20 @@ function Systems() {
{ - setMenuDialogType(undefined); - }} - type={menuDialogType || 'edit'} + open={menuDialogType !== undefined && menuDialogType !== 'delete'} + onClose={() => setMenuDialogType(undefined)} + type={ + menuDialogType !== undefined && menuDialogType !== 'delete' + ? menuDialogType + : 'edit' + } selectedSystem={selectedSystemForMenu} + parentId={systemId} + /> + setMenuDialogType(undefined)} + system={selectedSystemForMenu} /> ); From 9cb072799995f346d227da349f619b2b484cfda8 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Fri, 5 Jan 2024 11:11:06 +0000 Subject: [PATCH 005/222] Remove delete button from landing page #137 --- src/systems/systemDetails.component.tsx | 32 +------------------------ 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/src/systems/systemDetails.component.tsx b/src/systems/systemDetails.component.tsx index 5a879cf74..4f006b51f 100644 --- a/src/systems/systemDetails.component.tsx +++ b/src/systems/systemDetails.component.tsx @@ -1,4 +1,3 @@ -import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; import { Box, @@ -13,7 +12,6 @@ import { import { useState } from 'react'; import { getSystemImportanceColour, useSystem } from '../api/systems'; import { System } from '../app.types'; -import { DeleteSystemDialog } from './deleteSystemDialog.component'; import SystemDialog from './systemDialog.component'; interface SystemButtonProps { @@ -44,28 +42,6 @@ const EditSystemButton = (props: SystemButtonProps) => { ); }; -const DeleteSystemButton = (props: SystemButtonProps) => { - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - - return ( - <> - - setDeleteDialogOpen(true)} - > - - - - setDeleteDialogOpen(false)} - system={props.system} - /> - - ); -}; - export interface SystemDetailsProps { id: string | null; } @@ -100,13 +76,7 @@ function SystemDetails(props: SystemDetailsProps) { ? 'No system selected' : system.name} - {system !== undefined && ( - <> - - - - - )} + {system !== undefined && } {systemLoading || system === undefined ? ( From d6bbe7160e657faa4f0bee109d5bcc1c2fbdce3c Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Fri, 5 Jan 2024 12:18:25 +0000 Subject: [PATCH 006/222] Add and fix unit tests #137 --- src/systems/systemDetails.component.test.tsx | 24 --- src/systems/systemDialog.component.test.tsx | 175 +++++++++++++++++++ src/systems/systemDialog.component.tsx | 2 +- src/systems/systems.component.test.tsx | 78 +++++++++ src/systems/systems.component.tsx | 1 + 5 files changed, 255 insertions(+), 25 deletions(-) diff --git a/src/systems/systemDetails.component.test.tsx b/src/systems/systemDetails.component.test.tsx index 25902bfaf..2d4436abc 100644 --- a/src/systems/systemDetails.component.test.tsx +++ b/src/systems/systemDetails.component.test.tsx @@ -102,28 +102,4 @@ describe('SystemDetails', () => { expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); }); - - it('can open the delete dialog and close it again', async () => { - createView(); - - await waitFor(() => { - expect(screen.getByText(mockSystemDetails.name)).toBeInTheDocument(); - }); - - expect(screen.queryByTestId('delete-system-name')).not.toBeInTheDocument(); - - await user.click(screen.getByRole('button', { name: 'Delete System' })); - - await waitFor(() => { - expect(screen.getByTestId('delete-system-name')).toBeInTheDocument(); - }); - - await user.click(screen.getByRole('button', { name: 'Cancel' })); - - await waitFor(() => { - expect( - screen.queryByTestId('delete-system-name') - ).not.toBeInTheDocument(); - }); - }); }); diff --git a/src/systems/systemDialog.component.test.tsx b/src/systems/systemDialog.component.test.tsx index 8f9af00a4..8478c6986 100644 --- a/src/systems/systemDialog.component.test.tsx +++ b/src/systems/systemDialog.component.test.tsx @@ -134,6 +134,7 @@ describe('Systems Dialog', () => { expect(axiosPostSpy).toHaveBeenCalledWith('/v1/systems', { ...values, importance: SystemImportanceType.MEDIUM, + parent_id: null, }); expect(mockOnClose).toHaveBeenCalled(); }); @@ -341,4 +342,178 @@ describe('Systems Dialog', () => { expect(mockOnClose).not.toHaveBeenCalled(); }); }); + + describe('Save as', () => { + const MOCK_SELECTED_SYSTEM: System = { + name: 'Mock laser', + location: 'Location', + owner: 'Owner', + importance: SystemImportanceType.HIGH, + description: 'Description', + parent_id: null, + id: '65328f34a40ff5301575a4e3', + code: 'mock-laser', + }; + + const MOCK_SELECTED_SYSTEM_POST_DATA = JSON.parse( + JSON.stringify(MOCK_SELECTED_SYSTEM) + ) as Partial; + delete MOCK_SELECTED_SYSTEM_POST_DATA.id; + delete MOCK_SELECTED_SYSTEM_POST_DATA.code; + + beforeEach(() => { + props.type = 'save as'; + props.parentId = null; + props.selectedSystem = MOCK_SELECTED_SYSTEM; + }); + + it('renders correctly when saving as', async () => { + createView(); + + expect(screen.getByText('Add System')).toBeInTheDocument(); + }); + + it('renders correctly when saving as a subsystem', async () => { + props.parentId = 'parent-id'; + + createView(); + + expect(screen.getByText('Add Subsystem')).toBeInTheDocument(); + }); + + it('calls onClose when cancel is clicked', async () => { + createView(); + + await user.click(screen.getByRole('button', { name: 'Cancel' })); + + expect(mockOnClose).toHaveBeenCalled(); + expect(axiosPatchSpy).not.toHaveBeenCalled(); + }); + + it('saves as a system', async () => { + props.parentId = 'parent-id'; + + createView(); + + await user.click(screen.getByRole('button', { name: 'Save' })); + + expect(axiosPostSpy).toHaveBeenCalledWith('/v1/systems', { + ...MOCK_SELECTED_SYSTEM_POST_DATA, + parent_id: 'parent-id', + }); + + expect(mockOnClose).toHaveBeenCalled(); + }); + + it('saves as a system editing all fields', async () => { + props.parentId = 'parent-id'; + + createView(); + + const values = { + name: 'System name', + description: 'System description', + location: 'System location', + owner: 'System owner', + importance: SystemImportanceType.LOW, + }; + modifyValues(values); + + await user.click(screen.getByRole('button', { name: 'Save' })); + + expect(axiosPostSpy).toHaveBeenCalledWith('/v1/systems', { + ...values, + parent_id: 'parent-id', + }); + + expect(mockOnClose).toHaveBeenCalled(); + }); + + it('saves as a system removing non-manditory fields', async () => { + createView(); + + const values = { + description: '', + location: '', + owner: '', + }; + + modifyValues(values); + + await user.click(screen.getByRole('button', { name: 'Save' })); + + expect(axiosPostSpy).toHaveBeenCalledWith(`/v1/systems`, { + ...MOCK_SELECTED_SYSTEM_POST_DATA, + description: undefined, + location: undefined, + owner: undefined, + }); + expect(mockOnClose).toHaveBeenCalled(); + }); + + it('save as editing only a systems name', async () => { + createView(); + + const values = { + name: 'System name', + }; + modifyValues(values); + + await user.click(screen.getByRole('button', { name: 'Save' })); + + expect(axiosPostSpy).toHaveBeenCalledWith(`/v1/systems`, { + ...MOCK_SELECTED_SYSTEM_POST_DATA, + ...values, + }); + + expect(mockOnClose).toHaveBeenCalled(); + }); + + it('displays error message when name field is not filled in', async () => { + createView(); + + modifyValues({ name: '' }); + + await user.click(screen.getByRole('button', { name: 'Save' })); + + expect(screen.getByText('Please enter a name')).toBeInTheDocument(); + expect(mockOnClose).not.toHaveBeenCalled(); + }); + + it('displays error message when attempting to save as a system with a duplicate name', async () => { + createView(); + + const values = { + name: 'Error 409', + }; + + modifyValues(values); + + await user.click(screen.getByRole('button', { name: 'Save' })); + + expect( + screen.getByText( + 'A System with the same name already exists within the same parent System' + ) + ).toBeInTheDocument(); + expect(mockOnClose).not.toHaveBeenCalled(); + }); + + it('displays error message when an unknown error occurs', async () => { + createView(); + + const values = { + name: 'Error 500', + }; + + modifyValues(values); + + await user.click(screen.getByRole('button', { name: 'Save' })); + + expect( + screen.getByText('Please refresh and try again') + ).toBeInTheDocument(); + expect(mockOnClose).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/systems/systemDialog.component.tsx b/src/systems/systemDialog.component.tsx index 424ba249e..737f2e71b 100644 --- a/src/systems/systemDialog.component.tsx +++ b/src/systems/systemDialog.component.tsx @@ -121,7 +121,7 @@ const SystemDialog = React.memo((props: SystemDialogProps) => { owner: systemData.owner || undefined, importance: systemData.importance, }; - if (parentId !== null) system.parent_id = parentId; + if (parentId !== undefined) system.parent_id = parentId; addSystem(system) .then((response) => handleClose()) .catch((error: AxiosError) => { diff --git a/src/systems/systems.component.test.tsx b/src/systems/systems.component.test.tsx index 6a644b7da..a0da35300 100644 --- a/src/systems/systems.component.test.tsx +++ b/src/systems/systems.component.test.tsx @@ -14,6 +14,14 @@ describe('Systems', () => { user = userEvent.setup(); }); + const clickRowAction = async (rowIndex: number, buttonText: string) => { + await user.click(screen.getAllByLabelText('Row Actions')[rowIndex]); + await waitFor(() => { + expect(screen.getByText(buttonText)).toBeInTheDocument(); + }); + await user.click(screen.getByText(buttonText)); + }; + it('renders correctly', async () => { createView('/systems'); @@ -272,4 +280,74 @@ describe('Systems', () => { expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); }); + + it('can open the edit dialog and close it again', async () => { + createView('/systems'); + + await waitFor(() => { + expect(screen.getByText('Giant laser')).toBeInTheDocument(); + }); + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + + await clickRowAction(0, 'Edit'); + + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + expect(screen.getByText('Edit System')).toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: 'Cancel' })); + + await waitFor(() => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + }); + + it('can open the save as dialog and close it again', async () => { + createView('/systems'); + + await waitFor(() => { + expect(screen.getByText('Giant laser')).toBeInTheDocument(); + }); + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + + await clickRowAction(0, 'Save as'); + + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + expect(screen.getByText('Add System')).toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: 'Cancel' })); + + await waitFor(() => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + }); + + it('can open the delete dialog and close it again', async () => { + createView('/systems'); + + await waitFor(() => { + expect(screen.getByText('Giant laser')).toBeInTheDocument(); + }); + + expect(screen.queryByTestId('delete-system-name')).not.toBeInTheDocument(); + + await clickRowAction(0, 'Delete'); + + await waitFor(() => { + expect(screen.getByTestId('delete-system-name')).toBeInTheDocument(); + }); + + await user.click(screen.getByRole('button', { name: 'Cancel' })); + + await waitFor(() => { + expect( + screen.queryByTestId('delete-system-name') + ).not.toBeInTheDocument(); + }); + }); }); diff --git a/src/systems/systems.component.tsx b/src/systems/systems.component.tsx index bd02e6b4d..017635a00 100644 --- a/src/systems/systems.component.tsx +++ b/src/systems/systems.component.tsx @@ -164,6 +164,7 @@ const SubsystemMenu = (props: { aria-controls={open ? `${props.subsystem.id}-menu` : undefined} aria-haspopup="true" aria-expanded={open ? 'true' : undefined} + aria-label="Row Actions" onClick={handleOpen} sx={{ marginRight: 1 }} > From e3d5b0304550a62a6f6bc07307403bc896a4647e Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Fri, 5 Jan 2024 12:50:31 +0000 Subject: [PATCH 007/222] Add and fix e2e tests #137 --- cypress/e2e/system.cy.ts | 227 ++++++++++++++++++++++++++++++++++----- 1 file changed, 199 insertions(+), 28 deletions(-) diff --git a/cypress/e2e/system.cy.ts b/cypress/e2e/system.cy.ts index f713ea05c..5b5dce2af 100644 --- a/cypress/e2e/system.cy.ts +++ b/cypress/e2e/system.cy.ts @@ -72,7 +72,11 @@ describe('System', () => { }).should(async (postRequests) => { expect(postRequests.length).equal(1); expect(JSON.stringify(await postRequests[0].json())).equal( - JSON.stringify({ name: 'System name', importance: 'medium' }) + JSON.stringify({ + name: 'System name', + importance: 'medium', + parent_id: null, + }) ); }); }); @@ -102,6 +106,7 @@ describe('System', () => { location: 'System location', owner: 'System owner', importance: 'high', + parent_id: null, }) ); }); @@ -176,9 +181,10 @@ describe('System', () => { describe('Edit', () => { it("edits all of a system's fields", () => { - cy.visit('/systems/65328f34a40ff5301575a4e3'); + cy.visit('/systems'); - cy.findByRole('button', { name: 'Edit System' }).click(); + cy.findAllByLabelText('Row Actions').eq(1).click(); + cy.findByText('Edit').click(); cy.findByLabelText('Name *').clear().type('System name'); cy.findByLabelText('Description').clear().type('System description'); @@ -192,7 +198,7 @@ describe('System', () => { cy.findBrowserMockedRequests({ method: 'PATCH', - url: '/v1/systems/65328f34a40ff5301575a4e3', + url: '/v1/systems/656da8ef9cba7a76c6f81a5d', }).should(async (patchRequests) => { expect(patchRequests.length).equal(1); expect(JSON.stringify(await patchRequests[0].json())).equal( @@ -208,9 +214,10 @@ describe('System', () => { }); it("edits only a system's name", () => { - cy.visit('/systems/65328f34a40ff5301575a4e3'); + cy.visit('/systems'); - cy.findByRole('button', { name: 'Edit System' }).click(); + cy.findAllByLabelText('Row Actions').eq(1).click(); + cy.findByText('Edit').click(); cy.findByLabelText('Name *').clear().type('System name'); @@ -219,7 +226,7 @@ describe('System', () => { cy.findBrowserMockedRequests({ method: 'PATCH', - url: '/v1/systems/65328f34a40ff5301575a4e3', + url: '/v1/systems/656da8ef9cba7a76c6f81a5d', }).should(async (patchRequests) => { expect(patchRequests.length).equal(1); expect(JSON.stringify(await patchRequests[0].json())).equal( @@ -231,9 +238,10 @@ describe('System', () => { }); it('displays error message when no field has been edited that disappears when description is edited', () => { - cy.visit('/systems/65328f34a40ff5301575a4e3'); + cy.visit('/systems'); - cy.findByRole('button', { name: 'Edit System' }).click(); + cy.findAllByLabelText('Row Actions').eq(1).click(); + cy.findByText('Edit').click(); cy.findByRole('button', { name: 'Save' }).click(); cy.findByText('Please edit a form entry before clicking save').should( @@ -247,23 +255,28 @@ describe('System', () => { }); it('displays error message when name is not given that disappears once closed', () => { - cy.visit('/systems/65328f34a40ff5301575a4e3'); + cy.visit('/systems'); - cy.findByRole('button', { name: 'Edit System' }).click(); + cy.findAllByLabelText('Row Actions').eq(1).click(); + cy.findByText('Edit').click(); cy.findByLabelText('Name *').clear(); cy.findByRole('button', { name: 'Save' }).click(); cy.findByText('Please enter a name').should('be.visible'); cy.findByRole('button', { name: 'Save' }).should('be.disabled'); cy.findByRole('button', { name: 'Cancel' }).click(); - cy.findByRole('button', { name: 'Edit System' }).click(); + + cy.findAllByLabelText('Row Actions').eq(1).click(); + cy.findByText('Edit').click(); + cy.findByText('Please enter a name').should('not.exist'); }); it('displays error message if the system has a duplicate name that disappears once closed', () => { - cy.visit('/systems/65328f34a40ff5301575a4e3'); + cy.visit('/systems'); - cy.findByRole('button', { name: 'Edit System' }).click(); + cy.findAllByLabelText('Row Actions').eq(1).click(); + cy.findByText('Edit').click(); cy.findByLabelText('Name *').clear().type('Error 409'); cy.findByRole('button', { name: 'Save' }).click(); @@ -272,49 +285,204 @@ describe('System', () => { ).should('be.visible'); cy.findByRole('button', { name: 'Save' }).should('be.disabled'); cy.findByRole('button', { name: 'Cancel' }).click(); - cy.findByRole('button', { name: 'Edit System' }).click(); + + cy.findAllByLabelText('Row Actions').eq(1).click(); + cy.findByText('Edit').click(); + cy.findByText( 'A System with the same name already exists within the same parent System' ).should('not.exist'); }); it('displays error message if any other error occurs that disappears once closed', () => { + cy.visit('/systems'); + + cy.findAllByLabelText('Row Actions').eq(1).click(); + cy.findByText('Edit').click(); + + cy.findByLabelText('Name *').clear().type('Error 500'); + cy.findByRole('button', { name: 'Save' }).click(); + cy.findByText('Please refresh and try again').should('be.visible'); + cy.findByRole('button', { name: 'Save' }).should('be.disabled'); + cy.findByRole('button', { name: 'Cancel' }).click(); + + cy.findAllByLabelText('Row Actions').eq(1).click(); + cy.findByText('Edit').click(); + + cy.findByText('Please refresh and try again').should('not.exist'); + }); + }); + + describe('Save as', () => { + it('save as a system editing all fields (in root)', () => { + cy.visit('/systems'); + + cy.findAllByLabelText('Row Actions').eq(1).click(); + cy.findByText('Save as').click(); + + cy.findByLabelText('Name *').clear().type('System name'); + cy.findByLabelText('Description').clear().type('System description'); + cy.findByLabelText('Location').clear().type('System location'); + cy.findByLabelText('Owner').clear().type('System owner'); + cy.findByLabelText('Importance').click(); + cy.findByRole('option', { name: 'medium' }).click(); + + cy.startSnoopingBrowserMockedRequest(); + cy.findByRole('button', { name: 'Save' }).click(); + + cy.findBrowserMockedRequests({ + method: 'POST', + url: '/v1/systems', + }).should(async (postRequests) => { + expect(postRequests.length).equal(1); + expect(JSON.stringify(await postRequests[0].json())).equal( + JSON.stringify({ + name: 'System name', + description: 'System description', + location: 'System location', + owner: 'System owner', + importance: 'medium', + parent_id: null, + }) + ); + }); + }); + + it("save as a system eidting only a system's name (in subsystem)", () => { cy.visit('/systems/65328f34a40ff5301575a4e3'); - cy.findByRole('button', { name: 'Edit System' }).click(); + cy.findAllByLabelText('Row Actions').eq(0).click(); + cy.findByText('Save as').click(); + + cy.findByLabelText('Name *').clear().type('System name'); + + cy.startSnoopingBrowserMockedRequest(); + cy.findByRole('button', { name: 'Save' }).click(); + + cy.findBrowserMockedRequests({ + method: 'POST', + url: '/v1/systems', + }).should(async (postRequests) => { + expect(postRequests.length).equal(1); + expect(JSON.stringify(await postRequests[0].json())).equal( + JSON.stringify({ + name: 'System name', + description: + 'Pretty speech spend mouth control skill. Fire together return message catch food wish.', + location: '848 James Lock Suite 863\nNew Robertbury, PW 17883', + owner: 'Daniel Morrison', + importance: 'low', + parent_id: '65328f34a40ff5301575a4e3', + }) + ); + }); + }); + + it('displays error message when name is not given that disappears once closed', () => { + cy.visit('/systems'); + + cy.findAllByLabelText('Row Actions').eq(1).click(); + cy.findByText('Save as').click(); + + cy.findByLabelText('Name *').clear(); + cy.findByRole('button', { name: 'Save' }).click(); + cy.findByText('Please enter a name').should('be.visible'); + cy.findByRole('button', { name: 'Save' }).should('be.disabled'); + cy.findByRole('button', { name: 'Cancel' }).click(); + + cy.findAllByLabelText('Row Actions').eq(1).click(); + cy.findByText('Save as').click(); + + cy.findByText('Please enter a name').should('not.exist'); + }); + + it('displays error message if the system has a duplicate name that disappears once closed', () => { + cy.visit('/systems'); + + cy.findAllByLabelText('Row Actions').eq(1).click(); + cy.findByText('Save as').click(); + + cy.findByLabelText('Name *').clear().type('Error 409'); + cy.findByRole('button', { name: 'Save' }).click(); + cy.findByText( + 'A System with the same name already exists within the same parent System' + ).should('be.visible'); + cy.findByRole('button', { name: 'Save' }).should('be.disabled'); + cy.findByRole('button', { name: 'Cancel' }).click(); + + cy.findAllByLabelText('Row Actions').eq(1).click(); + cy.findByText('Save as').click(); + + cy.findByText( + 'A System with the same name already exists within the same parent System' + ).should('not.exist'); + }); + + it('displays error message if any other error occurs that disappears once closed', () => { + cy.visit('/systems'); + + cy.findAllByLabelText('Row Actions').eq(1).click(); + cy.findByText('Save as').click(); cy.findByLabelText('Name *').clear().type('Error 500'); cy.findByRole('button', { name: 'Save' }).click(); cy.findByText('Please refresh and try again').should('be.visible'); cy.findByRole('button', { name: 'Save' }).should('be.disabled'); cy.findByRole('button', { name: 'Cancel' }).click(); - cy.findByRole('button', { name: 'Edit System' }).click(); + + cy.findAllByLabelText('Row Actions').eq(1).click(); + cy.findByText('Save as').click(); + cy.findByText('Please refresh and try again').should('not.exist'); }); }); + it('edits a system from a landing page', () => { + cy.visit('/systems/65328f34a40ff5301575a4e3'); + + cy.findByRole('button', { name: 'Edit System' }).click(); + + cy.findByLabelText('Name *').clear().type('System name'); + + cy.startSnoopingBrowserMockedRequest(); + cy.findByRole('button', { name: 'Save' }).click(); + + cy.findBrowserMockedRequests({ + method: 'PATCH', + url: '/v1/systems/65328f34a40ff5301575a4e3', + }).should(async (patchRequests) => { + expect(patchRequests.length).equal(1); + expect(JSON.stringify(await patchRequests[0].json())).equal( + JSON.stringify({ + name: 'System name', + }) + ); + }); + }); + it('deletes a system', () => { - cy.visit('/systems/65328f34a40ff5301575a4e9'); + cy.visit('/systems'); + + cy.findAllByLabelText('Row Actions').eq(1).click(); + cy.findByText('Delete').click(); - cy.findByRole('button', { name: 'Delete System' }).click(); cy.startSnoopingBrowserMockedRequest(); cy.findByRole('button', { name: 'Continue' }).click(); cy.findBrowserMockedRequests({ method: 'DELETE', - url: '/v1/systems/65328f34a40ff5301575a4e9', - }).should((patchRequests) => { - expect(patchRequests.length).equal(1); + url: '/v1/systems/656da8ef9cba7a76c6f81a5d', + }).should((deleteRequests) => { + expect(deleteRequests.length).equal(1); }); - - // ID of the parent - cy.url().should('include', '/systems/65328f34a40ff5301575a4e8'); }); it('displays an error when attempting to delete a system with children that hides once closed', () => { - cy.visit('/systems/65328f34a40ff5301575a4e3'); + cy.visit('/systems'); + + cy.findAllByLabelText('Row Actions').eq(0).click(); + cy.findByText('Delete').click(); - cy.findByRole('button', { name: 'Delete System' }).click(); cy.startSnoopingBrowserMockedRequest(); cy.findByRole('button', { name: 'Continue' }).click(); @@ -328,7 +496,10 @@ describe('System', () => { cy.findByRole('button', { name: 'Continue' }).should('be.disabled'); cy.findByRole('button', { name: 'Cancel' }).click(); - cy.findByRole('button', { name: 'Delete System' }).click(); + + cy.findAllByLabelText('Row Actions').eq(0).click(); + cy.findByText('Delete').click(); + cy.findByRole('dialog') .should('be.visible') .within(() => { From 3ebeff3d30c71ff843bef22c60aa6253d7ca21fb Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Fri, 5 Jan 2024 12:59:12 +0000 Subject: [PATCH 008/222] Remove redirect to parent on delete #137 --- .../deleteSystemDialog.component.test.tsx | 47 +++++++++---------- src/systems/deleteSystemDialog.component.tsx | 7 +-- 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/src/systems/deleteSystemDialog.component.test.tsx b/src/systems/deleteSystemDialog.component.test.tsx index ebaddaf64..daf8a50c5 100644 --- a/src/systems/deleteSystemDialog.component.test.tsx +++ b/src/systems/deleteSystemDialog.component.test.tsx @@ -11,6 +11,7 @@ import { } from './deleteSystemDialog.component'; describe('DeleteSystemDialog', () => { + let systemId = ''; let props: DeleteSystemDialogProps; let user; let axiosDeleteSpy; @@ -19,9 +20,20 @@ describe('DeleteSystemDialog', () => { // Load whatever system is requested (only assign if found to avoid errors // when rendering while testing a 404 error) const system = SystemsJSON.filter( - (system) => system.id === props.system.id + (system) => system.id === systemId )[0] as System; if (system) props.system = system; + else if (systemId === 'invalid_id') + props.system = { + id: systemId, + name: '', + description: null, + location: null, + owner: null, + importance: SystemImportanceType.LOW, + parent_id: null, + code: '', + }; return renderComponentWithBrowserRouter(); }; @@ -30,19 +42,9 @@ describe('DeleteSystemDialog', () => { props = { open: true, onClose: jest.fn(), - // This system data is just a placeholder until the actual data is loaded - // in createView - system: { - id: '65328f34a40ff5301575a4e9', - name: '', - description: null, - location: null, - owner: null, - importance: SystemImportanceType.LOW, - parent_id: null, - code: '', - }, + system: undefined, }; + systemId = '65328f34a40ff5301575a4e9'; user = userEvent.setup(); axiosDeleteSpy = jest.spyOn(axios, 'delete'); }); @@ -69,20 +71,17 @@ describe('DeleteSystemDialog', () => { expect(axiosDeleteSpy).not.toHaveBeenCalled(); }); - it('sends a delete request, closes the dialog and navigates to the parent system when continue button is clicked with a valid system', async () => { + it('sends a delete request and closes the dialog when continue button is clicked with a valid system', async () => { createView(); await user.click(screen.getByRole('button', { name: 'Continue' })); - expect(axiosDeleteSpy).toHaveBeenCalledWith( - `/v1/systems/${props.system.id}` - ); + expect(axiosDeleteSpy).toHaveBeenCalledWith(`/v1/systems/${systemId}`); expect(props.onClose).toHaveBeenCalled(); - expect(window.location.pathname).toBe(`/systems/${props.system.parent_id}`); }); it('displays error message when deleting a system with children', async () => { - props.system.id = '65328f34a40ff5301575a4e3'; + systemId = '65328f34a40ff5301575a4e3'; createView(); @@ -96,14 +95,12 @@ describe('DeleteSystemDialog', () => { ).toBeInTheDocument(); }); - expect(axiosDeleteSpy).toHaveBeenCalledWith( - `/v1/systems/${props.system.id}` - ); + expect(axiosDeleteSpy).toHaveBeenCalledWith(`/v1/systems/${systemId}`); expect(props.onClose).not.toHaveBeenCalled(); }); it('displays error message when an unknown error occurs', async () => { - props.system.id = 'invalid_id'; + systemId = 'invalid_id'; createView(); @@ -115,9 +112,7 @@ describe('DeleteSystemDialog', () => { ).toBeInTheDocument(); }); - expect(axiosDeleteSpy).toHaveBeenCalledWith( - `/v1/systems/${props.system.id}` - ); + expect(axiosDeleteSpy).toHaveBeenCalledWith(`/v1/systems/${systemId}`); expect(props.onClose).not.toHaveBeenCalled(); }); }); diff --git a/src/systems/deleteSystemDialog.component.tsx b/src/systems/deleteSystemDialog.component.tsx index ada246d07..3b18557dd 100644 --- a/src/systems/deleteSystemDialog.component.tsx +++ b/src/systems/deleteSystemDialog.component.tsx @@ -11,7 +11,6 @@ import { AxiosError } from 'axios'; import React from 'react'; import { useDeleteSystem } from '../api/systems'; import { ErrorParsing, System } from '../app.types'; -import { useNavigateToSystem } from './systems.component'; export interface DeleteSystemDialogProps { open: boolean; @@ -28,8 +27,6 @@ export const DeleteSystemDialog = (props: DeleteSystemDialogProps) => { const { mutateAsync: deleteSystem } = useDeleteSystem(); - const navigateToSystem = useNavigateToSystem(); - const handleClose = () => { onClose(); setErrorMessage(undefined); @@ -40,8 +37,6 @@ export const DeleteSystemDialog = (props: DeleteSystemDialogProps) => { deleteSystem(system.id) .then((response) => { onClose(); - // Navigate to parent as current system no longer exists - navigateToSystem(system.parent_id); }) .catch((error: AxiosError) => { const response = error.response?.data as ErrorParsing; @@ -54,7 +49,7 @@ export const DeleteSystemDialog = (props: DeleteSystemDialogProps) => { } setErrorMessage('Please refresh and try again'); }); - }, [deleteSystem, navigateToSystem, onClose, system]); + }, [deleteSystem, onClose, system]); return ( From cae2ecb843d5db8058f29f1bbd62b73d04e32044 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Fri, 5 Jan 2024 14:47:52 +0000 Subject: [PATCH 009/222] Cleanup #137 --- cypress/e2e/system.cy.ts | 60 +-------- .../items/catalogueItemsTable.component.tsx | 17 +-- src/manufacturer/manufacturer.component.tsx | 15 +-- src/systems/systemDialog.component.test.tsx | 119 +----------------- src/systems/systemDialog.component.tsx | 11 +- 5 files changed, 26 insertions(+), 196 deletions(-) diff --git a/cypress/e2e/system.cy.ts b/cypress/e2e/system.cy.ts index 5b5dce2af..7236405ac 100644 --- a/cypress/e2e/system.cy.ts +++ b/cypress/e2e/system.cy.ts @@ -314,6 +314,8 @@ describe('System', () => { }); describe('Save as', () => { + // Error checking is ommitted here as same logic as in add + it('save as a system editing all fields (in root)', () => { cy.visit('/systems'); @@ -377,64 +379,6 @@ describe('System', () => { ); }); }); - - it('displays error message when name is not given that disappears once closed', () => { - cy.visit('/systems'); - - cy.findAllByLabelText('Row Actions').eq(1).click(); - cy.findByText('Save as').click(); - - cy.findByLabelText('Name *').clear(); - cy.findByRole('button', { name: 'Save' }).click(); - cy.findByText('Please enter a name').should('be.visible'); - cy.findByRole('button', { name: 'Save' }).should('be.disabled'); - cy.findByRole('button', { name: 'Cancel' }).click(); - - cy.findAllByLabelText('Row Actions').eq(1).click(); - cy.findByText('Save as').click(); - - cy.findByText('Please enter a name').should('not.exist'); - }); - - it('displays error message if the system has a duplicate name that disappears once closed', () => { - cy.visit('/systems'); - - cy.findAllByLabelText('Row Actions').eq(1).click(); - cy.findByText('Save as').click(); - - cy.findByLabelText('Name *').clear().type('Error 409'); - cy.findByRole('button', { name: 'Save' }).click(); - cy.findByText( - 'A System with the same name already exists within the same parent System' - ).should('be.visible'); - cy.findByRole('button', { name: 'Save' }).should('be.disabled'); - cy.findByRole('button', { name: 'Cancel' }).click(); - - cy.findAllByLabelText('Row Actions').eq(1).click(); - cy.findByText('Save as').click(); - - cy.findByText( - 'A System with the same name already exists within the same parent System' - ).should('not.exist'); - }); - - it('displays error message if any other error occurs that disappears once closed', () => { - cy.visit('/systems'); - - cy.findAllByLabelText('Row Actions').eq(1).click(); - cy.findByText('Save as').click(); - - cy.findByLabelText('Name *').clear().type('Error 500'); - cy.findByRole('button', { name: 'Save' }).click(); - cy.findByText('Please refresh and try again').should('be.visible'); - cy.findByRole('button', { name: 'Save' }).should('be.disabled'); - cy.findByRole('button', { name: 'Cancel' }).click(); - - cy.findAllByLabelText('Row Actions').eq(1).click(); - cy.findByText('Save as').click(); - - cy.findByText('Please refresh and try again').should('not.exist'); - }); }); it('edits a system from a landing page', () => { diff --git a/src/catalogue/items/catalogueItemsTable.component.tsx b/src/catalogue/items/catalogueItemsTable.component.tsx index 7f957fc9d..9fe3289fd 100644 --- a/src/catalogue/items/catalogueItemsTable.component.tsx +++ b/src/catalogue/items/catalogueItemsTable.component.tsx @@ -11,6 +11,7 @@ import { Box, Button, ListItemIcon, + ListItemText, MenuItem, Link as MuiLink, TableRow, @@ -655,7 +656,7 @@ const CatalogueItemsTable = (props: CatalogueItemsTableProps) => { return [ { setItemsDialogType('edit'); table.setCreatingRow(row); @@ -666,11 +667,11 @@ const CatalogueItemsTable = (props: CatalogueItemsTableProps) => { - Edit + Edit , { setItemsDialogType('save as'); table.setCreatingRow(row); @@ -681,11 +682,11 @@ const CatalogueItemsTable = (props: CatalogueItemsTableProps) => { - Save as + Save as , { setDeleteItemDialogOpen(true); setSelectedCatalogueItem(row.original); @@ -696,11 +697,11 @@ const CatalogueItemsTable = (props: CatalogueItemsTableProps) => { - <>Delete + Delete , { setObsoleteItemDialogOpen(true); setSelectedCatalogueItem(row.original); @@ -712,7 +713,7 @@ const CatalogueItemsTable = (props: CatalogueItemsTableProps) => { - <>Obsolete + Obsolete , ]; }, diff --git a/src/manufacturer/manufacturer.component.tsx b/src/manufacturer/manufacturer.component.tsx index 41eb9d600..5eb60e9a4 100644 --- a/src/manufacturer/manufacturer.component.tsx +++ b/src/manufacturer/manufacturer.component.tsx @@ -1,11 +1,12 @@ +import AddIcon from '@mui/icons-material/Add'; +import ClearIcon from '@mui/icons-material/Clear'; import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; -import ClearIcon from '@mui/icons-material/Clear'; -import AddIcon from '@mui/icons-material/Add'; import { Box, Button, ListItemIcon, + ListItemText, MenuItem, Link as MuiLink, TableRow, @@ -17,13 +18,13 @@ import { type MRT_ColumnDef, type MRT_ColumnFiltersState, } from 'material-react-table'; +import { MRT_Localization_EN } from 'material-react-table/locales/en'; import React from 'react'; import { Link } from 'react-router-dom'; import { useManufacturers } from '../api/manufacturer'; import { Manufacturer } from '../app.types'; import DeleteManufacturerDialog from './deleteManufacturerDialog.component'; import ManufacturerDialog from './manufacturerDialog.component'; -import { MRT_Localization_EN } from 'material-react-table/locales/en'; function ManufacturerComponent() { const { data: ManufacturerData, isLoading: ManufacturerDataLoading } = @@ -195,7 +196,7 @@ function ManufacturerComponent() { return [ { setSelectedManufacturer(row.original); table.setCreatingRow(true); @@ -206,11 +207,11 @@ function ManufacturerComponent() { - Edit + Edit , { setDeleteManufacturerDialog(true); setSelectedManufacturer(row.original); @@ -220,7 +221,7 @@ function ManufacturerComponent() { - Delete + Delete , ]; }, diff --git a/src/systems/systemDialog.component.test.tsx b/src/systems/systemDialog.component.test.tsx index 8478c6986..2b7da9ee1 100644 --- a/src/systems/systemDialog.component.test.tsx +++ b/src/systems/systemDialog.component.test.tsx @@ -344,6 +344,9 @@ describe('Systems Dialog', () => { }); describe('Save as', () => { + // Mostly tested above anyway, so only a few checks here to ensure + // correct logic (out of add/edit) is applied when the dialogue type is 'save as' + const MOCK_SELECTED_SYSTEM: System = { name: 'Mock laser', location: 'Location', @@ -381,139 +384,25 @@ describe('Systems Dialog', () => { expect(screen.getByText('Add Subsystem')).toBeInTheDocument(); }); - it('calls onClose when cancel is clicked', async () => { - createView(); - - await user.click(screen.getByRole('button', { name: 'Cancel' })); - - expect(mockOnClose).toHaveBeenCalled(); - expect(axiosPatchSpy).not.toHaveBeenCalled(); - }); - it('saves as a system', async () => { props.parentId = 'parent-id'; createView(); - await user.click(screen.getByRole('button', { name: 'Save' })); - - expect(axiosPostSpy).toHaveBeenCalledWith('/v1/systems', { - ...MOCK_SELECTED_SYSTEM_POST_DATA, - parent_id: 'parent-id', - }); - - expect(mockOnClose).toHaveBeenCalled(); - }); - - it('saves as a system editing all fields', async () => { - props.parentId = 'parent-id'; - - createView(); - const values = { name: 'System name', - description: 'System description', - location: 'System location', - owner: 'System owner', - importance: SystemImportanceType.LOW, }; modifyValues(values); await user.click(screen.getByRole('button', { name: 'Save' })); expect(axiosPostSpy).toHaveBeenCalledWith('/v1/systems', { - ...values, - parent_id: 'parent-id', - }); - - expect(mockOnClose).toHaveBeenCalled(); - }); - - it('saves as a system removing non-manditory fields', async () => { - createView(); - - const values = { - description: '', - location: '', - owner: '', - }; - - modifyValues(values); - - await user.click(screen.getByRole('button', { name: 'Save' })); - - expect(axiosPostSpy).toHaveBeenCalledWith(`/v1/systems`, { - ...MOCK_SELECTED_SYSTEM_POST_DATA, - description: undefined, - location: undefined, - owner: undefined, - }); - expect(mockOnClose).toHaveBeenCalled(); - }); - - it('save as editing only a systems name', async () => { - createView(); - - const values = { - name: 'System name', - }; - modifyValues(values); - - await user.click(screen.getByRole('button', { name: 'Save' })); - - expect(axiosPostSpy).toHaveBeenCalledWith(`/v1/systems`, { ...MOCK_SELECTED_SYSTEM_POST_DATA, ...values, + parent_id: 'parent-id', }); expect(mockOnClose).toHaveBeenCalled(); }); - - it('displays error message when name field is not filled in', async () => { - createView(); - - modifyValues({ name: '' }); - - await user.click(screen.getByRole('button', { name: 'Save' })); - - expect(screen.getByText('Please enter a name')).toBeInTheDocument(); - expect(mockOnClose).not.toHaveBeenCalled(); - }); - - it('displays error message when attempting to save as a system with a duplicate name', async () => { - createView(); - - const values = { - name: 'Error 409', - }; - - modifyValues(values); - - await user.click(screen.getByRole('button', { name: 'Save' })); - - expect( - screen.getByText( - 'A System with the same name already exists within the same parent System' - ) - ).toBeInTheDocument(); - expect(mockOnClose).not.toHaveBeenCalled(); - }); - - it('displays error message when an unknown error occurs', async () => { - createView(); - - const values = { - name: 'Error 500', - }; - - modifyValues(values); - - await user.click(screen.getByRole('button', { name: 'Save' })); - - expect( - screen.getByText('Please refresh and try again') - ).toBeInTheDocument(); - expect(mockOnClose).not.toHaveBeenCalled(); - }); }); }); diff --git a/src/systems/systemDialog.component.tsx b/src/systems/systemDialog.component.tsx index 737f2e71b..6c790f2bf 100644 --- a/src/systems/systemDialog.component.tsx +++ b/src/systems/systemDialog.component.tsx @@ -64,9 +64,8 @@ const SystemDialog = React.memo((props: SystemDialogProps) => { // Ensure system data is updated when the selected system changes useEffect(() => { if (open) { - if ((type === 'edit' || type === 'save as') && selectedSystem) - setSystemData(selectedSystem as AddSystem); - else setSystemData(getEmptySystem()); + if (type === 'add') setSystemData(getEmptySystem()); + else if (selectedSystem) setSystemData(selectedSystem as AddSystem); } }, [selectedSystem, open, type]); @@ -322,11 +321,7 @@ const SystemDialog = React.memo((props: SystemDialogProps) => { + + + + +`; diff --git a/src/items/itemDialog.component.test.tsx b/src/items/itemDialog.component.test.tsx new file mode 100644 index 000000000..480884296 --- /dev/null +++ b/src/items/itemDialog.component.test.tsx @@ -0,0 +1,313 @@ +import React from 'react'; +import { + getCatalogueCategoryById, + getCatalogueItemById, + renderComponentWithBrowserRouter, +} from '../setupTests'; +import ItemDialog, { ItemDialogProps } from './itemDialog.component'; +import { fireEvent, screen, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import axios from 'axios'; + +describe('ItemDialog', () => { + let props: ItemDialogProps; + let user; + const onClose = jest.fn(); + + const createView = () => { + return renderComponentWithBrowserRouter(); + }; + beforeEach(() => { + props = { + open: true, + onClose: onClose, + type: 'add', + catalogueCategory: getCatalogueCategoryById('4'), + catalogueItem: getCatalogueItemById('1'), + }; + user = userEvent.setup(); + }); + + const modifyValues = async (values: { + serialNumber?: string; + assetNumber?: string; + purchaseOrderNumber?: string; + warrantyEndDate?: string; + deliveredDate?: string; + isDefective?: string; + notes?: string; + resolution?: string; + frameRate?: string; + sensorType?: string; + sensorBrand?: string; + broken?: string; + older?: string; + usageStatus?: string; + }) => { + values.serialNumber !== undefined && + fireEvent.change(screen.getByLabelText('Serial number'), { + target: { value: values.serialNumber }, + }); + + values.assetNumber !== undefined && + fireEvent.change(screen.getByLabelText('Asset number'), { + target: { value: values.assetNumber }, + }); + + values.purchaseOrderNumber !== undefined && + fireEvent.change(screen.getByLabelText('Purchase order number'), { + target: { value: values.purchaseOrderNumber }, + }); + + values.notes !== undefined && + fireEvent.change(screen.getByLabelText('Notes'), { + target: { value: values.notes }, + }); + + values.warrantyEndDate !== undefined && + fireEvent.change(screen.getByLabelText('Warranty end date'), { + target: { value: values.warrantyEndDate }, + }); + + values.deliveredDate !== undefined && + fireEvent.change(screen.getByLabelText('Delivered date'), { + target: { value: values.deliveredDate }, + }); + + values.resolution !== undefined && + fireEvent.change(screen.getByLabelText('Resolution (megapixels) *'), { + target: { value: values.resolution }, + }); + + values.frameRate !== undefined && + fireEvent.change(screen.getByLabelText('Frame Rate (fps)'), { + target: { value: values.frameRate }, + }); + + if (values.broken !== undefined) { + fireEvent.mouseDown(screen.getByLabelText('Broken *')); + fireEvent.click( + within(screen.getByRole('listbox')).getByText(values.broken) + ); + } + + if (values.older !== undefined) { + fireEvent.mouseDown(screen.getByLabelText('Older than five years')); + fireEvent.click( + within(screen.getByRole('listbox')).getByText(values.older) + ); + } + + values.sensorBrand !== undefined && + fireEvent.change(screen.getByLabelText('Sensor brand'), { + target: { value: values.sensorBrand }, + }); + + values.sensorType !== undefined && + fireEvent.change(screen.getByLabelText('Sensor Type *'), { + target: { value: values.sensorType }, + }); + if (values.isDefective !== undefined) { + fireEvent.mouseDown(screen.getByLabelText('Is defective *')); + fireEvent.click( + within(screen.getByRole('listbox')).getByText(values.isDefective) + ); + } + + if (values.usageStatus !== undefined) { + fireEvent.mouseDown(screen.getByLabelText('Usage status *')); + fireEvent.click( + within(screen.getByRole('listbox')).getByText(values.usageStatus) + ); + } + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('Add Item', () => { + let axiosPostSpy; + + beforeEach(() => { + axiosPostSpy = jest.spyOn(axios, 'post'); + }); + + it('adds a item with just the default values', async () => { + createView(); + const saveButton = screen.getByRole('button', { name: 'Save' }); + await user.click(saveButton); + expect(axiosPostSpy).toHaveBeenCalledWith('/v1/items/', { + asset_number: null, + catalogue_item_id: '1', + delivered_date: null, + is_defective: false, + notes: null, + properties: [ + { name: 'Resolution', value: 12 }, + { name: 'Frame Rate', value: 30 }, + { name: 'Sensor Type', value: 'CMOS' }, + { name: 'Broken', value: true }, + { name: 'Older than five years', value: false }, + ], + purchase_order_number: null, + serial_number: null, + system_id: null, + usage_status: 0, + warranty_end_date: null, + }); + }); + + it('adds a item (all input vales)', async () => { + createView(); + await modifyValues({ + serialNumber: 'test12', + assetNumber: 'test43', + purchaseOrderNumber: 'test21', + notes: 'test', + warrantyEndDate: '17/02/2035', + deliveredDate: '23/09/2045', + isDefective: 'Yes', + usageStatus: 'Used', + resolution: '12', + frameRate: '60', + sensorType: 'IO', + sensorBrand: 'pixel', + broken: 'True', + older: 'False', + }); + const saveButton = screen.getByRole('button', { name: 'Save' }); + await user.click(saveButton); + expect(axiosPostSpy).toHaveBeenCalledWith('/v1/items/', { + asset_number: 'test43', + catalogue_item_id: '1', + delivered_date: '2045-09-23T00:00:00.000Z', + is_defective: true, + notes: 'test', + properties: [ + { name: 'Resolution', value: 12 }, + { name: 'Frame Rate', value: 60 }, + { name: 'Sensor Type', value: 'IO' }, + { name: 'Sensor brand', value: 'pixel' }, + { name: 'Broken', value: true }, + { name: 'Older than five years', value: false }, + ], + purchase_order_number: 'test21', + serial_number: 'test12', + system_id: null, + usage_status: 2, + warranty_end_date: '2035-02-17T00:00:00.000Z', + }); + }); + + it('adds a item (case empty string with spaces returns null and chnage propetery boolean values)', async () => { + createView(); + await modifyValues({ + serialNumber: ' ', + assetNumber: 'test43', + purchaseOrderNumber: 'test21', + notes: 'test', + warrantyEndDate: '17/02/2035', + deliveredDate: '23/09/2045', + isDefective: 'Yes', + usageStatus: 'Used', + resolution: '12', + frameRate: '60', + sensorType: 'IO', + sensorBrand: 'pixel', + broken: 'False', + older: 'True', + }); + const saveButton = screen.getByRole('button', { name: 'Save' }); + await user.click(saveButton); + expect(axiosPostSpy).toHaveBeenCalledWith('/v1/items/', { + asset_number: 'test43', + catalogue_item_id: '1', + delivered_date: '2045-09-23T00:00:00.000Z', + is_defective: true, + notes: 'test', + properties: [ + { name: 'Resolution', value: 12 }, + { name: 'Frame Rate', value: 60 }, + { name: 'Sensor Type', value: 'IO' }, + { name: 'Sensor brand', value: 'pixel' }, + { name: 'Broken', value: false }, + { name: 'Older than five years', value: true }, + ], + purchase_order_number: 'test21', + serial_number: null, + system_id: null, + usage_status: 2, + warranty_end_date: '2035-02-17T00:00:00.000Z', + }); + }); + + it('displays error message when mandatory property values missing', async () => { + createView(); + await modifyValues({ + serialNumber: ' ', + assetNumber: 'test43', + purchaseOrderNumber: 'test21', + notes: 'test', + warrantyEndDate: '17/02/2035', + deliveredDate: '23/09/2045', + isDefective: 'Yes', + usageStatus: 'Used', + resolution: '', + sensorType: '', + broken: 'None', + }); + const saveButton = screen.getByRole('button', { name: 'Save' }); + await user.click(saveButton); + const mandatoryFieldHelperText = screen.getAllByText( + 'This field is mandatory' + ); + + const mandatoryFieldBooleanHelperText = screen.getByText( + 'Please select either True or False' + ); + + expect(mandatoryFieldBooleanHelperText).toBeInTheDocument(); + expect(mandatoryFieldHelperText.length).toBe(2); + }); + + it('displays error message when property values type is incorrect', async () => { + createView(); + await modifyValues({ + serialNumber: ' ', + assetNumber: 'test43', + purchaseOrderNumber: 'test21', + notes: 'test', + warrantyEndDate: '17/02/2035', + deliveredDate: '23/09/2045', + isDefective: 'Yes', + usageStatus: 'Used', + resolution: 'rwererw', + sensorType: '', + broken: 'None', + }); + const saveButton = screen.getByRole('button', { name: 'Save' }); + await user.click(saveButton); + + const validNumberHelperText = screen.getByText( + 'Please enter a valid number' + ); + expect(validNumberHelperText).toBeInTheDocument(); + }); + + it('displays warning message when an unknown error occurs', async () => { + createView(); + await modifyValues({ + serialNumber: 'error', + }); + const saveButton = screen.getByRole('button', { name: 'Save' }); + await user.click(saveButton); + await waitFor(() => { + expect( + screen.getByText('Please refresh and try again') + ).toBeInTheDocument(); + }); + expect(onClose).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/items/itemDialog.component.tsx b/src/items/itemDialog.component.tsx new file mode 100644 index 000000000..feb1c72d1 --- /dev/null +++ b/src/items/itemDialog.component.tsx @@ -0,0 +1,577 @@ +import React from 'react'; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControl, + FormHelperText, + Grid, + IconButton, + InputLabel, + MenuItem, + Select, + TextField, + Tooltip, + Typography, +} from '@mui/material'; +import { + AddItem, + CatalogueCategory, + CatalogueCategoryFormData, + CatalogueItem, + CatalogueItemProperty, + ItemDetailsPlaceholder, + UsageStatusType, +} from '../app.types'; +import { DatePicker } from '@mui/x-date-pickers'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import { matchCatalogueItemProperties } from '../catalogue/catalogue.component'; +import { useAddItem } from '../api/item'; +import { AxiosError } from 'axios'; +function isValidDateTime(date: string | null) { + // Attempt to create a Date object from the string + let dateObj = new Date(date ?? ''); + + // Check if the Date object is valid and the string was successfully parsed + // Also, check if the original string is not equal to 'Invalid Date' + return !isNaN(dateObj.getTime()) && dateObj.toString() !== 'Invalid Date'; +} + +export interface ItemDialogProps { + open: boolean; + onClose: () => void; + type: 'add' | 'edit'; + catalogueItem?: CatalogueItem; + catalogueCategory?: CatalogueCategory; +} + +function ItemDialog(props: ItemDialogProps) { + const { open, onClose, type, catalogueItem, catalogueCategory } = props; + const parentCatalogueItemPropertiesInfo = React.useMemo( + () => catalogueCategory?.catalogue_item_properties ?? [], + [catalogueCategory] + ); + + const [itemDetails, setItemDetails] = React.useState({ + catalogue_item_id: null, + system_id: null, + purchase_order_number: null, + is_defective: null, + usage_status: null, + warranty_end_date: null, + asset_number: null, + serial_number: null, + delivered_date: null, + notes: null, + }); + + const [catchAllError, setCatchAllError] = React.useState(false); + + const [propertyValues, setPropertyValues] = React.useState< + (string | number | boolean | null)[] + >([]); + + const [propertyErrors, setPropertyErrors] = React.useState( + new Array(parentCatalogueItemPropertiesInfo.length).fill(false) + ); + + const { mutateAsync: addItem } = useAddItem(); + + React.useEffect(() => { + if (type === 'add' && open) { + setPropertyValues( + matchCatalogueItemProperties( + parentCatalogueItemPropertiesInfo, + catalogueItem?.properties ?? [] + ) + ); + } + }, [parentCatalogueItemPropertiesInfo, catalogueItem, open, type]); + + const handlePropertyChange = ( + index: number, + name: string, + newValue: string | boolean | null + ) => { + const updatedPropertyValues = [...propertyValues]; + updatedPropertyValues[index] = newValue; + setPropertyValues(updatedPropertyValues); + + const updatedProperties: CatalogueItemProperty[] = []; + const propertyType = + parentCatalogueItemPropertiesInfo[index]?.type || 'string'; + + if (!updatedProperties[index]) { + // Initialize the property if it doesn't exist + updatedProperties[index] = { name: '', value: '' }; + } + + const updatedProperty = { + ...updatedProperties[index], + name: name, + }; + + if (propertyType === 'boolean') { + updatedProperty.value = + newValue === 'true' ? true : newValue === 'false' ? false : ''; + } else if (propertyType === 'number') { + if (newValue !== null) { + const parsedValue = Number(newValue); + updatedProperty.value = isNaN(parsedValue) ? null : parsedValue; + } + } else { + updatedProperty.value = newValue; + } + + updatedProperties[index] = updatedProperty; + + // Clear the error state for the changed property + const updatedPropertyErrors = [...propertyErrors]; + updatedPropertyErrors[index] = false; + setPropertyErrors(updatedPropertyErrors); + }; + const handleItemDetails = ( + field: keyof ItemDetailsPlaceholder, + value: string | null + ) => { + const updatedItemDetails = { ...itemDetails }; + + if (value?.trim() === '') { + updatedItemDetails[field] = null; + } else { + updatedItemDetails[field] = value as string; + } + + setItemDetails(updatedItemDetails); + }; + + const handleClose = React.useCallback(() => { + onClose(); + setPropertyValues([]); + setPropertyErrors( + new Array(parentCatalogueItemPropertiesInfo.length).fill(false) + ); + }, [onClose, parentCatalogueItemPropertiesInfo]); + + const handleFormErrorStates = React.useCallback(() => { + let hasErrors = false; + + // Check properties + const updatedPropertyErrors = [...propertyErrors]; + + const updatedProperties = parentCatalogueItemPropertiesInfo.map( + (property, index) => { + if (property.mandatory && !propertyValues[index]) { + updatedPropertyErrors[index] = true; + hasErrors = true; + } else { + updatedPropertyErrors[index] = false; + } + + if ( + propertyValues[index] !== undefined && + property.type === 'number' && + isNaN(Number(propertyValues[index])) + ) { + updatedPropertyErrors[index] = true; + hasErrors = true; + } + + if (!propertyValues[index]) { + if (property.type === 'boolean') { + if ( + propertyValues[index] === '' || + propertyValues[index] === undefined + ) { + return null; + } + } else { + return null; + } + } + + let typedValue: string | number | boolean | null = + propertyValues[index]; // Assume it's a string by default + + // Check if the type of the 'property' is boolean + if (property.type === 'boolean') { + // If the type is boolean, then check the type of 'propertyValues[index]' + typedValue = + typeof propertyValues[index] !== 'boolean' + ? // If 'propertyValues[index]' is not a boolean, convert it based on string values 'true' or 'false', + // otherwise, assign 'propertyValues[index]' directly to 'typedValue' + propertyValues[index] === 'true' + ? true + : false + : // If 'propertyValues[index]' is already a boolean, assign it directly to 'typedValue' + propertyValues[index]; + } else if (property.type === 'number') { + typedValue = Number(propertyValues[index]); + } + + return { + name: property.name, + value: typedValue, + }; + } + ); + + setPropertyErrors(updatedPropertyErrors); + + return { hasErrors, updatedProperties }; + }, [propertyErrors, parentCatalogueItemPropertiesInfo, propertyValues]); + + const details = React.useMemo(() => { + return { + catalogue_item_id: catalogueItem?.id ?? '', + system_id: null, + purchase_order_number: itemDetails.purchase_order_number, + is_defective: itemDetails.is_defective === 'true' ? true : false, + usage_status: itemDetails.usage_status + ? UsageStatusType[ + itemDetails.usage_status as keyof typeof UsageStatusType + ] + : UsageStatusType.new, + warranty_end_date: + itemDetails.warranty_end_date && + isValidDateTime(itemDetails.warranty_end_date) + ? new Date(itemDetails.warranty_end_date).toISOString() ?? null + : null, + asset_number: itemDetails.asset_number, + serial_number: itemDetails.serial_number, + delivered_date: + itemDetails.delivered_date && + isValidDateTime(itemDetails.delivered_date) + ? new Date(itemDetails.delivered_date).toISOString() ?? null + : null, + notes: itemDetails.notes, + }; + }, [itemDetails, catalogueItem]); + + const handleAddItem = React.useCallback(() => { + const { hasErrors, updatedProperties } = handleFormErrorStates(); + + if (hasErrors) { + return; // Do not proceed with saving if there are errors + } + + const filteredProperties = updatedProperties.filter( + (property) => property !== null + ) as CatalogueItemProperty[]; + + const item: AddItem = { + ...details, + properties: filteredProperties, + }; + + addItem(item) + .then((response) => handleClose()) + .catch((error: AxiosError) => { + setCatchAllError(true); + }); + }, [addItem, handleClose, details, handleFormErrorStates]); + return ( + + {`${type === 'edit' ? 'Edit' : 'Add'} Item`} + + + + + Details + + + { + handleItemDetails('serial_number', event.target.value); + }} + fullWidth + /> + + + { + handleItemDetails('asset_number', event.target.value); + }} + fullWidth + /> + + + { + handleItemDetails( + 'purchase_order_number', + event.target.value + ); + }} + fullWidth + /> + + + + + handleItemDetails( + 'warranty_end_date', + date ? date.toString() : null + ) + } + slotProps={{ + actionBar: { actions: ['clear'] }, + textField: { size: 'small', fullWidth: true }, + }} + /> + + + + handleItemDetails( + 'delivered_date', + date ? date.toString() : null + ) + } + slotProps={{ + actionBar: { actions: ['clear'] }, + textField: { size: 'small', fullWidth: true }, + }} + /> + + + + + Is defective + + + + + + + + + Usage status + + + + + + + { + handleItemDetails('notes', event.target.value); + }} + fullWidth + /> + + + + {parentCatalogueItemPropertiesInfo.length >= 1 && ( + + + Properties + + {parentCatalogueItemPropertiesInfo.map( + (property: CatalogueCategoryFormData, index: number) => ( + + + + {property.type === 'boolean' ? ( + + + {property.name} + + + {propertyErrors[index] && ( + + Please select either True or False + + )} + + ) : ( + + handlePropertyChange( + index, + property.name, + event.target.value ? event.target.value : null + ) + } + fullWidth + error={propertyErrors[index]} + helperText={ + // Check if 'propertyErrors[index]' exists and evaluate its value + propertyErrors[index] + ? // If 'propertyErrors[index]' is truthy, perform the following checks: + property.mandatory && !propertyValues[index] + ? // If 'property' is mandatory and 'propertyValues[index]' is empty, return a mandatory field error message + 'This field is mandatory' + : property.type === 'number' && + isNaN(Number(propertyValues[index])) && + 'Please enter a valid number' // If 'property' is of type 'number' and 'propertyValues[index]' is not a valid number, return an invalid number error message + : // If 'propertyErrors[index]' is falsy, return an empty string (no error) + '' + } + /> + )} + + + + Name: {property.name} + Unit: {property.unit} + + Type:{' '} + {property.type === 'string' + ? 'text' + : property.type} + + + } + placement="right" + enterTouchDelay={0} + > + + + + + + + + ) + )} + + )} + + + + + + + + + + {catchAllError && ( + + {'Please refresh and try again'} + + )} + + + ); +} + +export default ItemDialog; diff --git a/src/items/items.component.test.tsx b/src/items/items.component.test.tsx new file mode 100644 index 000000000..bb2ba7b22 --- /dev/null +++ b/src/items/items.component.test.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { renderComponentWithMemoryRouter } from '../setupTests'; +import Items from './items.component'; +import { waitFor, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +describe('Items', () => { + let user; + const createView = (path: string) => { + return renderComponentWithMemoryRouter(, path); + }; + beforeEach(() => { + user = userEvent.setup(); + }); + + it('renders correctly', async () => { + const view = createView('/catalogue/item/1/items'); + expect(view.asFragment()).toMatchSnapshot(); + }); + + it('navigates to catalogue category table view', async () => { + createView('/catalogue/item/1/items'); + await waitFor(() => { + expect( + screen.getByRole('link', { name: 'Back to Cameras table view' }) + ).toBeInTheDocument(); + }); + + const url = screen.getByRole('link', { + name: 'Back to Cameras table view', + }); + expect(url).toHaveAttribute('href', '/catalogue/4'); + }); + + it('navigates to catalogue item landing page', async () => { + createView('/catalogue/item/1/items'); + await waitFor(() => { + expect( + screen.getByRole('link', { name: 'Back to Cameras 1 landing page' }) + ).toBeInTheDocument(); + }); + + const url = screen.getByRole('link', { + name: 'Back to Cameras 1 landing page', + }); + expect(url).toHaveAttribute('href', '/catalogue/item/1'); + }); + + it('opens and closes the add item dialog', async () => { + createView('/catalogue/item/1/items'); + + await waitFor(() => { + expect( + screen.getByRole('button', { name: 'Add Item' }) + ).toBeInTheDocument(); + }); + + const addButton = screen.getByRole('button', { + name: 'Add Item', + }); + await user.click(addButton); + + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + + const cancelButton = screen.getByRole('button', { name: 'Cancel' }); + await user.click(cancelButton); + await waitFor(() => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/items/items.component.tsx b/src/items/items.component.tsx new file mode 100644 index 000000000..3d9d7cfab --- /dev/null +++ b/src/items/items.component.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { Box, Button } from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import ItemDialog from './itemDialog.component'; +import { useCatalogueItem } from '../api/catalogueItem'; +import { Link, useLocation } from 'react-router-dom'; +import { useCatalogueCategory } from '../api/catalogueCategory'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; + +export function Items() { + const [addItemDialogOpen, setAddItemDialogOpen] = + React.useState(false); + const location = useLocation(); + const catalogueItemId = location.pathname.split('/')[3]; + const { data: catalogueItem } = useCatalogueItem(catalogueItemId); + const { data: catalogueCategory } = useCatalogueCategory( + catalogueItem?.catalogue_category_id + ); + return ( +
+ + + + setAddItemDialogOpen(false)} + type="add" + catalogueCategory={catalogueCategory} + catalogueItem={catalogueItem} + /> +
+ ); +} + +export default Items; diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 5ccbeed49..42640ca47 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -1,5 +1,6 @@ import { rest } from 'msw'; import { + AddItem, AddSystem, CatalogueItem, EditCatalogueCategory, @@ -477,4 +478,20 @@ export const handlers = [ return res(ctx.status(404), ctx.json('')); } }), + // ------------------------------------ ITEMS ------------------------------------------------ + rest.post('/v1/items/', async (req, res, ctx) => { + const body = (await req.json()) as AddItem; + + if (body.serial_number === 'error') { + return res(ctx.status(500), ctx.json('')); + } + + return res( + ctx.status(200), + ctx.json({ + ...body, + id: '1', + }) + ); + }), ]; diff --git a/src/setupTests.tsx b/src/setupTests.tsx index d48903e57..5015c7aaf 100644 --- a/src/setupTests.tsx +++ b/src/setupTests.tsx @@ -16,6 +16,9 @@ import CatalogueCategoryJSON from './mocks/CatalogueCategory.json'; import CatalogueItemJSON from './mocks/CatalogueItems.json'; import ManufacturerJSON from './mocks/manufacturer.json'; import { server } from './mocks/server'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import enGB from 'date-fns/locale/en-GB'; // Establish API mocking before all tests. beforeAll(() => server.listen()); @@ -60,9 +63,11 @@ export function renderComponentWithBrowserRouter( }: React.PropsWithChildren): JSX.Element { return ( - - {children} - + + + {children} + + ); } @@ -87,9 +92,11 @@ export function renderComponentWithMemoryRouter( }: React.PropsWithChildren): JSX.Element { return ( - - {children} - + + + {children} + + ); } diff --git a/src/view/viewTabs.component.tsx b/src/view/viewTabs.component.tsx index c862bfcea..06df8d01a 100644 --- a/src/view/viewTabs.component.tsx +++ b/src/view/viewTabs.component.tsx @@ -11,6 +11,7 @@ import Systems from '../systems/systems.component'; import Manufacturer from '../manufacturer/manufacturer.component'; import CatalogueItemsLandingPage from '../catalogue/items/catalogueItemsLandingPage.component'; import ManufacturerLandingPage from '../manufacturer/manufacturerLandingPage.component'; +import Items from '../items/items.component'; export const paths = { home: '/', @@ -18,7 +19,8 @@ export const paths = { systems: '/systems/*', manufacturers: '/manufacturer', manufacturer: '/manufacturer/:id', - catalogueItems: '/catalogue/items/:id', + catalogueItem: '/catalogue/item/:id', + items: '/catalogue/item/:id/items', }; interface TabPanelProps { @@ -94,7 +96,7 @@ function ViewTabs() { }> } > }> @@ -103,6 +105,7 @@ function ViewTabs() { path={paths.manufacturer} element={} > + }> ); @@ -127,12 +130,7 @@ function ViewTabs() { {...a11yProps('Manufacturer')} /> - + {routing} diff --git a/yarn.lock b/yarn.lock index c344e54eb..c10f73bfa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1613,6 +1613,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.21.0": + version: 7.23.7 + resolution: "@babel/runtime@npm:7.23.7" + dependencies: + regenerator-runtime: ^0.14.0 + checksum: eba85bd24d250abb5ae19b16cffc15a54d3894d8228ace40fa4c0e2f1938f28b38ad3e3430ebff9a1ef511eeb8c527e36044ac19076d6deafa52cef35d8624b9 + languageName: node + linkType: hard + "@babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3": version: 7.22.5 resolution: "@babel/template@npm:7.22.5" @@ -1904,6 +1913,27 @@ __metadata: languageName: node linkType: hard +"@date-io/core@npm:^2.17.0": + version: 2.17.0 + resolution: "@date-io/core@npm:2.17.0" + checksum: 008dfc79eb54256805113d76feca82fe0b08a245ecbfb2d53809e6a129dc201f9dbd053c8ad63512203ab1a13ff7f76de0edc31829588ef507d53307974c29a8 + languageName: node + linkType: hard + +"@date-io/date-fns@npm:2.17.0": + version: 2.17.0 + resolution: "@date-io/date-fns@npm:2.17.0" + dependencies: + "@date-io/core": ^2.17.0 + peerDependencies: + date-fns: ^2.0.0 + peerDependenciesMeta: + date-fns: + optional: true + checksum: bfab634c985a98dd44fdfc5f982b0348da2886c9ad79a8d71fd7bfa05b740af7f5cf340508cd7ce51d67c141fb8766998c8efdf50653252c68ec078471cd76e6 + languageName: node + linkType: hard + "@emotion/babel-plugin@npm:^11.11.0": version: 11.11.0 resolution: "@emotion/babel-plugin@npm:11.11.0" @@ -6571,6 +6601,15 @@ __metadata: languageName: node linkType: hard +"date-fns@npm:2.30.0": + version: 2.30.0 + resolution: "date-fns@npm:2.30.0" + dependencies: + "@babel/runtime": ^7.21.0 + checksum: f7be01523282e9bb06c0cd2693d34f245247a29098527d4420628966a2d9aad154bd0e90a6b1cf66d37adcb769cd108cf8a7bd49d76db0fb119af5cdd13644f4 + languageName: node + linkType: hard + "dayjs@npm:^1.10.4": version: 1.11.8 resolution: "dayjs@npm:1.11.8" @@ -9237,6 +9276,7 @@ __metadata: dependencies: "@babel/eslint-parser": ^7.21.8 "@craco/craco": ^7.1.0 + "@date-io/date-fns": 2.17.0 "@emotion/react": ^11.11.1 "@emotion/styled": ^11.11.0 "@mui/icons-material": ^5.14.16 @@ -9262,6 +9302,7 @@ __metadata: cross-env: 7.0.3 cypress: ^13.0.0 cypress-failed-log: 2.10.0 + date-fns: 2.30.0 eslint: ^8.41.0 eslint-config-prettier: ^9.0.0 eslint-config-react-app: ^7.0.1 From 9efba3f1051d829003539b3e312f95f6f60a6bc3 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Fri, 5 Jan 2024 16:39:54 +0000 Subject: [PATCH 012/222] upadate yock lock file --- yarn.lock | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index 19e3e4f0c..00de35d82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1604,16 +1604,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.6, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": - version: 7.23.7 - resolution: "@babel/runtime@npm:7.23.7" - dependencies: - regenerator-runtime: ^0.14.0 - checksum: eba85bd24d250abb5ae19b16cffc15a54d3894d8228ace40fa4c0e2f1938f28b38ad3e3430ebff9a1ef511eeb8c527e36044ac19076d6deafa52cef35d8624b9 - languageName: node - linkType: hard - -"@babel/runtime@npm:^7.21.0": +"@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.6, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.23.7 resolution: "@babel/runtime@npm:7.23.7" dependencies: From 89c7a2b4fdee3531d0d448433f95ce713936e82e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 6 Jan 2024 01:32:31 +0000 Subject: [PATCH 013/222] Update dependency @reduxjs/toolkit to v2 --- package.json | 2 +- yarn.lock | 57 ++++++++++++++++++++++++++++------------------------ 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 96b10d8e1..2f646b5c8 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "@mui/icons-material": "^5.14.16", "@mui/material": "^5.14.17", "@mui/x-date-pickers": "^6.18.1", - "@reduxjs/toolkit": "^1.9.5", + "@reduxjs/toolkit": "^2.0.0", "@tanstack/react-query": "^4.29.7", "@tanstack/react-query-devtools": "^4.29.7", "@types/jest": "^29.0.0", diff --git a/yarn.lock b/yarn.lock index f131d9834..adcedb1e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2930,23 +2930,23 @@ __metadata: languageName: node linkType: hard -"@reduxjs/toolkit@npm:^1.9.5": - version: 1.9.5 - resolution: "@reduxjs/toolkit@npm:1.9.5" +"@reduxjs/toolkit@npm:^2.0.0": + version: 2.0.1 + resolution: "@reduxjs/toolkit@npm:2.0.1" dependencies: - immer: ^9.0.21 - redux: ^4.2.1 - redux-thunk: ^2.4.2 - reselect: ^4.1.8 + immer: ^10.0.3 + redux: ^5.0.0 + redux-thunk: ^3.1.0 + reselect: ^5.0.1 peerDependencies: react: ^16.9.0 || ^17.0.0 || ^18 - react-redux: ^7.2.1 || ^8.0.2 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 peerDependenciesMeta: react: optional: true react-redux: optional: true - checksum: 54672c5593d05208af577e948a338f23128d3aa01ef056ab0d40bcfa14400cf6566be99e11715388f12c1d7655cdf7c5c6b63cb92eb0fecf996c454a46a3914c + checksum: d7e4783263dc79cb85c8d50db41209f16f0994520193ac2b378e63dc12f336cc6f58323e37d66ccf09493c49ead2f4827aac3e8d9c6ca7def21f842efb4f5f3d languageName: node linkType: hard @@ -9032,7 +9032,14 @@ __metadata: languageName: node linkType: hard -"immer@npm:^9.0.21, immer@npm:^9.0.7": +"immer@npm:^10.0.3": + version: 10.0.3 + resolution: "immer@npm:10.0.3" + checksum: 76acabe6f40e752028313762ba477a5d901e57b669f3b8fb406b87b9bb9b14e663a6fbbf5a6d1ab323737dd38f4b2494a4e28002045b88948da8dbf482309f28 + languageName: node + linkType: hard + +"immer@npm:^9.0.7": version: 9.0.21 resolution: "immer@npm:9.0.21" checksum: 70e3c274165995352f6936695f0ef4723c52c92c92dd0e9afdfe008175af39fa28e76aafb3a2ca9d57d1fb8f796efc4dd1e1cc36f18d33fa5b74f3dfb0375432 @@ -9158,7 +9165,7 @@ __metadata: "@mui/icons-material": ^5.14.16 "@mui/material": ^5.14.17 "@mui/x-date-pickers": ^6.18.1 - "@reduxjs/toolkit": ^1.9.5 + "@reduxjs/toolkit": ^2.0.0 "@tanstack/react-query": ^4.29.7 "@tanstack/react-query-devtools": ^4.29.7 "@testing-library/cypress": ^10.0.0 @@ -13504,21 +13511,19 @@ __metadata: languageName: node linkType: hard -"redux-thunk@npm:^2.4.2": - version: 2.4.2 - resolution: "redux-thunk@npm:2.4.2" +"redux-thunk@npm:^3.1.0": + version: 3.1.0 + resolution: "redux-thunk@npm:3.1.0" peerDependencies: - redux: ^4 - checksum: c7f757f6c383b8ec26152c113e20087818d18ed3edf438aaad43539e9a6b77b427ade755c9595c4a163b6ad3063adf3497e5fe6a36c68884eb1f1cfb6f049a5c + redux: ^5.0.0 + checksum: bea96f8233975aad4c9f24ca1ffd08ac7ec91eaefc26e7ba9935544dc55d7f09ba2aa726676dab53dc79d0c91e8071f9729cddfea927f4c41839757d2ade0f50 languageName: node linkType: hard -"redux@npm:^4.2.1": - version: 4.2.1 - resolution: "redux@npm:4.2.1" - dependencies: - "@babel/runtime": ^7.9.2 - checksum: f63b9060c3a1d930ae775252bb6e579b42415aee7a23c4114e21a0b4ba7ec12f0ec76936c00f546893f06e139819f0e2855e0d55ebfce34ca9c026241a6950dd +"redux@npm:^5.0.0": + version: 5.0.1 + resolution: "redux@npm:5.0.1" + checksum: e74affa9009dd5d994878b9a1ce30d6569d986117175056edb003de2651c05b10fe7819d6fa94aea1a94de9a82f252f986547f007a2fbeb35c317a2e5f5ecf2c languageName: node linkType: hard @@ -13680,10 +13685,10 @@ __metadata: languageName: node linkType: hard -"reselect@npm:^4.1.8": - version: 4.1.8 - resolution: "reselect@npm:4.1.8" - checksum: a4ac87cedab198769a29be92bc221c32da76cfdad6911eda67b4d3e7136dca86208c3b210e31632eae31ebd2cded18596f0dd230d3ccc9e978df22f233b5583e +"reselect@npm:^5.0.1": + version: 5.1.0 + resolution: "reselect@npm:5.1.0" + checksum: 5bc9c5d03d7caea00d0c0e24330bf23d91801227346fec1cef6a60988ab8d3dd7cee76e6994ca0915bc1c20845bb2bd929b95753763e0a9db74c0f9dff5cb845 languageName: node linkType: hard From de2f37c66e46cb7b46f7a605bc0f1a9358c9dfd2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 6 Jan 2024 05:03:49 +0000 Subject: [PATCH 014/222] Update dependency prettier to v3 --- package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 96b10d8e1..7a591dbf4 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "eslint-plugin-cypress": "^2.13.3", "eslint-plugin-prettier": "^5.0.0", "express": "4.18.1", - "prettier": "^2.8.8", + "prettier": "^3.0.0", "serve": "^14.2.0", "serve-static": "1.15.0", "start-server-and-test": "^2.0.0" diff --git a/yarn.lock b/yarn.lock index f131d9834..fc7fc6d67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9188,7 +9188,7 @@ __metadata: loglevel: ^1.8.1 material-react-table: ^2.0.4 msw: 1.3.2 - prettier: ^2.8.8 + prettier: ^3.0.0 react: ^18.2.0 react-dom: ^18.2.0 react-redux: ^8.0.5 @@ -12912,12 +12912,12 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^2.8.8": - version: 2.8.8 - resolution: "prettier@npm:2.8.8" +"prettier@npm:^3.0.0": + version: 3.1.1 + resolution: "prettier@npm:3.1.1" bin: - prettier: bin-prettier.js - checksum: b49e409431bf129dd89238d64299ba80717b57ff5a6d1c1a8b1a28b590d998a34e083fa13573bc732bb8d2305becb4c9a4407f8486c81fa7d55100eb08263cf8 + prettier: bin/prettier.cjs + checksum: e386855e3a1af86a748e16953f168be555ce66d6233f4ba54eb6449b88eb0c6b2ca79441b11eae6d28a7f9a5c96440ce50864b9d5f6356d331d39d6bb66c648e languageName: node linkType: hard From 1e0bb47a2e53039c8963aa396dab3973cc2ee959 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 6 Jan 2024 11:24:51 +0000 Subject: [PATCH 015/222] Update typescript-eslint monorepo to v6 --- package.json | 4 +- yarn.lock | 192 +++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 164 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 96b10d8e1..d99c02ad9 100644 --- a/package.json +++ b/package.json @@ -78,8 +78,8 @@ "@testing-library/user-event": "^14.4.3", "@types/react-router-dom": "^5.3.3", "@types/testing-library__jest-dom": "^5.14.5", - "@typescript-eslint/eslint-plugin": "^5.59.6", - "@typescript-eslint/parser": "^5.59.6", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", "cross-env": "7.0.3", "cypress": "^13.0.0", "cypress-failed-log": "2.10.0", diff --git a/yarn.lock b/yarn.lock index f131d9834..383dd067b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2050,7 +2050,7 @@ __metadata: languageName: node linkType: hard -"@eslint-community/eslint-utils@npm:^4.2.0": +"@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" dependencies: @@ -2061,10 +2061,10 @@ __metadata: languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.4.0, @eslint-community/regexpp@npm:^4.6.1": - version: 4.6.2 - resolution: "@eslint-community/regexpp@npm:4.6.2" - checksum: a3c341377b46b54fa228f455771b901d1a2717f95d47dcdf40199df30abc000ba020f747f114f08560d119e979d882a94cf46cfc51744544d54b00319c0f2724 +"@eslint-community/regexpp@npm:^4.4.0, @eslint-community/regexpp@npm:^4.5.1, @eslint-community/regexpp@npm:^4.6.1": + version: 4.10.0 + resolution: "@eslint-community/regexpp@npm:4.10.0" + checksum: 2a6e345429ea8382aaaf3a61f865cae16ed44d31ca917910033c02dc00d505d939f10b81e079fa14d43b51499c640138e153b7e40743c4c094d9df97d4e56f7b languageName: node linkType: hard @@ -3682,10 +3682,10 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": - version: 7.0.12 - resolution: "@types/json-schema@npm:7.0.12" - checksum: 00239e97234eeb5ceefb0c1875d98ade6e922bfec39dd365ec6bd360b5c2f825e612ac4f6e5f1d13601b8b30f378f15e6faa805a3a732f4a1bbe61915163d293 +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": + version: 7.0.15 + resolution: "@types/json-schema@npm:7.0.15" + checksum: 97ed0cb44d4070aecea772b7b2e2ed971e10c81ec87dd4ecc160322ffa55ff330dace1793489540e3e318d90942064bb697cc0f8989391797792d919737b3b98 languageName: node linkType: hard @@ -3850,10 +3850,10 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7.3.12": - version: 7.5.0 - resolution: "@types/semver@npm:7.5.0" - checksum: 0a64b9b9c7424d9a467658b18dd70d1d781c2d6f033096a6e05762d20ebbad23c1b69b0083b0484722aabf35640b78ccc3de26368bcae1129c87e9df028a22e2 +"@types/semver@npm:^7.3.12, @types/semver@npm:^7.5.0": + version: 7.5.6 + resolution: "@types/semver@npm:7.5.6" + checksum: 563a0120ec0efcc326567db2ed920d5d98346f3638b6324ea6b50222b96f02a8add3c51a916b6897b51523aad8ac227d21d3dcf8913559f1bfc6c15b14d23037 languageName: node linkType: hard @@ -3991,7 +3991,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^5.5.0, @typescript-eslint/eslint-plugin@npm:^5.59.6": +"@typescript-eslint/eslint-plugin@npm:^5.5.0": version: 5.62.0 resolution: "@typescript-eslint/eslint-plugin@npm:5.62.0" dependencies: @@ -4015,6 +4015,31 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/eslint-plugin@npm:^6.0.0": + version: 6.17.0 + resolution: "@typescript-eslint/eslint-plugin@npm:6.17.0" + dependencies: + "@eslint-community/regexpp": ^4.5.1 + "@typescript-eslint/scope-manager": 6.17.0 + "@typescript-eslint/type-utils": 6.17.0 + "@typescript-eslint/utils": 6.17.0 + "@typescript-eslint/visitor-keys": 6.17.0 + debug: ^4.3.4 + graphemer: ^1.4.0 + ignore: ^5.2.4 + natural-compare: ^1.4.0 + semver: ^7.5.4 + ts-api-utils: ^1.0.1 + peerDependencies: + "@typescript-eslint/parser": ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 169646a705fdd1bc2a0d78678dbf3557ff3e534e9d4a11f7b5bba1d9f5cbec821f8c16b260413203efc8d6e0c0a3d7f9332bb1476e3dac80e60aa16eb9a0ad11 + languageName: node + linkType: hard + "@typescript-eslint/experimental-utils@npm:^5.0.0": version: 5.59.11 resolution: "@typescript-eslint/experimental-utils@npm:5.59.11" @@ -4026,7 +4051,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:^5.5.0, @typescript-eslint/parser@npm:^5.59.6": +"@typescript-eslint/parser@npm:^5.5.0": version: 5.62.0 resolution: "@typescript-eslint/parser@npm:5.62.0" dependencies: @@ -4043,6 +4068,24 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/parser@npm:^6.0.0": + version: 6.17.0 + resolution: "@typescript-eslint/parser@npm:6.17.0" + dependencies: + "@typescript-eslint/scope-manager": 6.17.0 + "@typescript-eslint/types": 6.17.0 + "@typescript-eslint/typescript-estree": 6.17.0 + "@typescript-eslint/visitor-keys": 6.17.0 + debug: ^4.3.4 + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: c48864aebf364332540f520d84630a6bb3e2ddc84492d75c14a453964b669a37f1fd43b60469e3683e618e8e8d3d7747baffe92e408599d5df6869cae86ac9e1 + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:5.59.11": version: 5.59.11 resolution: "@typescript-eslint/scope-manager@npm:5.59.11" @@ -4063,6 +4106,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:6.17.0": + version: 6.17.0 + resolution: "@typescript-eslint/scope-manager@npm:6.17.0" + dependencies: + "@typescript-eslint/types": 6.17.0 + "@typescript-eslint/visitor-keys": 6.17.0 + checksum: 6eabac1e52cd25714ab176c7bbf9919d065febf4580620eb067ab1b41607f5e592857bd831a2ab41daa873af4011217dbcae55ed248855e381127f1cabcd2d2c + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/type-utils@npm:5.62.0" @@ -4080,6 +4133,23 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/type-utils@npm:6.17.0": + version: 6.17.0 + resolution: "@typescript-eslint/type-utils@npm:6.17.0" + dependencies: + "@typescript-eslint/typescript-estree": 6.17.0 + "@typescript-eslint/utils": 6.17.0 + debug: ^4.3.4 + ts-api-utils: ^1.0.1 + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: bb6f824c1c7f8d25a21b7218a5bcb74e58c38121f85418eb1639f2931c6149285c2ede96dd677a3e7dc64886cc7640d74be6001d970c3ac9c9a4d889315c5d15 + languageName: node + linkType: hard + "@typescript-eslint/types@npm:5.59.11": version: 5.59.11 resolution: "@typescript-eslint/types@npm:5.59.11" @@ -4094,6 +4164,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:6.17.0": + version: 6.17.0 + resolution: "@typescript-eslint/types@npm:6.17.0" + checksum: a199516230b505f85de1b99cdf22c526cbae7604fa2dd0dd24e8bba5de45aeaee231263e7e59843af7b226cb91c4ba5447d06517a1a73b511a94c6b483af0d5b + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:5.59.11": version: 5.59.11 resolution: "@typescript-eslint/typescript-estree@npm:5.59.11" @@ -4130,6 +4207,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:6.17.0": + version: 6.17.0 + resolution: "@typescript-eslint/typescript-estree@npm:6.17.0" + dependencies: + "@typescript-eslint/types": 6.17.0 + "@typescript-eslint/visitor-keys": 6.17.0 + debug: ^4.3.4 + globby: ^11.1.0 + is-glob: ^4.0.3 + minimatch: 9.0.3 + semver: ^7.5.4 + ts-api-utils: ^1.0.1 + peerDependenciesMeta: + typescript: + optional: true + checksum: 4bf7811ddae66361cad55f7a6fcf9975eb77456ceb2eca5d7a6228387737845bdfe1b9eef4c76d5d6b7c7d6029a8f62bc67b509c0724cd37395ae16eb07dd7ab + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:5.59.11": version: 5.59.11 resolution: "@typescript-eslint/utils@npm:5.59.11" @@ -4166,6 +4262,23 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:6.17.0": + version: 6.17.0 + resolution: "@typescript-eslint/utils@npm:6.17.0" + dependencies: + "@eslint-community/eslint-utils": ^4.4.0 + "@types/json-schema": ^7.0.12 + "@types/semver": ^7.5.0 + "@typescript-eslint/scope-manager": 6.17.0 + "@typescript-eslint/types": 6.17.0 + "@typescript-eslint/typescript-estree": 6.17.0 + semver: ^7.5.4 + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + checksum: 2eea8fd3763b2ab9d86503c68b4d61df81071fd38851b8ba920d53b055c352d13e192a3d15ca853f11aee818c61e8af65946e963aa0e9b18d19e3254881bded0 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:5.59.11": version: 5.59.11 resolution: "@typescript-eslint/visitor-keys@npm:5.59.11" @@ -4186,6 +4299,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:6.17.0": + version: 6.17.0 + resolution: "@typescript-eslint/visitor-keys@npm:6.17.0" + dependencies: + "@typescript-eslint/types": 6.17.0 + eslint-visitor-keys: ^3.4.1 + checksum: e98755087bd067388d9a9182375e53f590183ca656d02b3d05d9718bab2ac571832fd16691060c7c979fd941e9d4b7923d8975632923697de0691f50fc97c8ac + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@ungap/structured-clone@npm:1.2.0" @@ -9025,10 +9148,10 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.2.0": - version: 5.2.4 - resolution: "ignore@npm:5.2.4" - checksum: 3d4c309c6006e2621659311783eaea7ebcd41fe4ca1d78c91c473157ad6666a57a2df790fe0d07a12300d9aac2888204d7be8d59f9aaf665b1c7fcdb432517ef +"ignore@npm:^5.2.0, ignore@npm:^5.2.4": + version: 5.3.0 + resolution: "ignore@npm:5.3.0" + checksum: 2736da6621f14ced652785cb05d86301a66d70248597537176612bd0c8630893564bd5f6421f8806b09e8472e75c591ef01672ab8059c07c6eb2c09cefe04bf9 languageName: node linkType: hard @@ -9172,8 +9295,8 @@ __metadata: "@types/react-dom": ^18.0.4 "@types/react-router-dom": ^5.3.3 "@types/testing-library__jest-dom": ^5.14.5 - "@typescript-eslint/eslint-plugin": ^5.59.6 - "@typescript-eslint/parser": ^5.59.6 + "@typescript-eslint/eslint-plugin": ^6.0.0 + "@typescript-eslint/parser": ^6.0.0 axios: ^1.4.0 cross-env: 7.0.3 cypress: ^13.0.0 @@ -11147,21 +11270,21 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^5.0.1": - version: 5.1.6 - resolution: "minimatch@npm:5.1.6" +"minimatch@npm:9.0.3, minimatch@npm:^9.0.1": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" dependencies: brace-expansion: ^2.0.1 - checksum: 7564208ef81d7065a370f788d337cd80a689e981042cb9a1d0e6580b6c6a8c9279eba80010516e258835a988363f99f54a6f711a315089b8b42694f5da9d0d77 + checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 languageName: node linkType: hard -"minimatch@npm:^9.0.1": - version: 9.0.1 - resolution: "minimatch@npm:9.0.1" +"minimatch@npm:^5.0.1": + version: 5.1.6 + resolution: "minimatch@npm:5.1.6" dependencies: brace-expansion: ^2.0.1 - checksum: 97f5f5284bb57dc65b9415dec7f17a0f6531a33572193991c60ff18450dcfad5c2dad24ffeaf60b5261dccd63aae58cc3306e2209d57e7f88c51295a532d8ec3 + checksum: 7564208ef81d7065a370f788d337cd80a689e981042cb9a1d0e6580b6c6a8c9279eba80010516e258835a988363f99f54a6f711a315089b8b42694f5da9d0d77 languageName: node linkType: hard @@ -14063,7 +14186,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3": +"semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4": version: 7.5.4 resolution: "semver@npm:7.5.4" dependencies: @@ -15242,6 +15365,15 @@ __metadata: languageName: node linkType: hard +"ts-api-utils@npm:^1.0.1": + version: 1.0.3 + resolution: "ts-api-utils@npm:1.0.3" + peerDependencies: + typescript: ">=4.2.0" + checksum: 441cc4489d65fd515ae6b0f4eb8690057add6f3b6a63a36073753547fb6ce0c9ea0e0530220a0b282b0eec535f52c4dfc315d35f8a4c9a91c0def0707a714ca6 + languageName: node + linkType: hard + "ts-interface-checker@npm:^0.1.9": version: 0.1.13 resolution: "ts-interface-checker@npm:0.1.13" From 2fcd35cc2b365c9f016cbb26d66bc86d0b8ede61 Mon Sep 17 00:00:00 2001 From: Matteo Guarnaccia Date: Mon, 8 Jan 2024 09:16:17 +0000 Subject: [PATCH 016/222] refactored unit tests #172 --- .../category/catalogueCard.component.test.tsx | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/catalogue/category/catalogueCard.component.test.tsx b/src/catalogue/category/catalogueCard.component.test.tsx index a26e00f83..d1eb4e2e2 100644 --- a/src/catalogue/category/catalogueCard.component.test.tsx +++ b/src/catalogue/category/catalogueCard.component.test.tsx @@ -35,9 +35,33 @@ describe('Catalogue Card', () => { expect(screen.getByText('Beam Characterization')).toBeInTheDocument(); }); + it('opens the actions menu', async () => { + createView(); + const actionsButton = screen.getByRole('button', { + name: 'actions Beam Characterization catalogue category button', + }); + await user.click(actionsButton); + + const editButton = screen.getByRole('menuitem', { + name: 'edit Beam Characterization catalogue category button', + }); + + const deleteButton = screen.getByRole('menuitem', { + name: 'delete Beam Characterization catalogue category button', + }); + + expect(editButton).toBeVisible(); + expect(deleteButton).toBeVisible(); + }); + it('opens the delete dialog', async () => { createView(); - const deleteButton = screen.getByRole('button', { + const actionsButton = screen.getByRole('button', { + name: 'actions Beam Characterization catalogue category button', + }); + await user.click(actionsButton); + + const deleteButton = screen.getByRole('menuitem', { name: 'delete Beam Characterization catalogue category button', }); await user.click(deleteButton); @@ -56,7 +80,12 @@ describe('Catalogue Card', () => { it('opens the edit dialog', async () => { createView(); - const editButton = screen.getByRole('button', { + const actionsButton = screen.getByRole('button', { + name: 'actions Beam Characterization catalogue category button', + }); + await user.click(actionsButton); + + const editButton = screen.getByRole('menuitem', { name: 'edit Beam Characterization catalogue category button', }); await user.click(editButton); From 9131b69c4ba192041fd21538ff7be90032e115ad Mon Sep 17 00:00:00 2001 From: Matteo Guarnaccia Date: Mon, 8 Jan 2024 09:42:59 +0000 Subject: [PATCH 017/222] refactored e2e tests #172 --- cypress/e2e/catalogue/catalogueCategory.cy.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/cypress/e2e/catalogue/catalogueCategory.cy.ts b/cypress/e2e/catalogue/catalogueCategory.cy.ts index 0f6f3f1f5..7c474133e 100644 --- a/cypress/e2e/catalogue/catalogueCategory.cy.ts +++ b/cypress/e2e/catalogue/catalogueCategory.cy.ts @@ -66,8 +66,25 @@ describe('Catalogue Category', () => { }); }); + it('opens actions menu', () => { + cy.findByRole('button', { + name: 'actions Motion catalogue category button', + }).click(); + + cy.findByRole('menuitem', { + name: 'delete Motion catalogue category button', + }).should('be.visible'); + cy.findByRole('menuitem', { + name: 'delete Motion catalogue category button', + }).should('be.visible'); + }); + it('displays error message when user tries to delete a catalogue category that has children elements', () => { cy.findByRole('button', { + name: 'actions Motion catalogue category button', + }).click(); + + cy.findByRole('menuitem', { name: 'delete Motion catalogue category button', }).click(); @@ -85,6 +102,10 @@ describe('Catalogue Category', () => { it('delete a catalogue category', () => { cy.findByRole('button', { + name: 'actions Beam Characterization catalogue category button', + }).click(); + + cy.findByRole('menuitem', { name: 'delete Beam Characterization catalogue category button', }).click(); @@ -178,6 +199,10 @@ describe('Catalogue Category', () => { it('edits a catalogue category (non leaf node)', () => { cy.visit('/catalogue/1'); cy.findByRole('button', { + name: 'actions Amp Meters catalogue category button', + }).click(); + + cy.findByRole('menuitem', { name: 'edit Amp Meters catalogue category button', }).click(); cy.findByLabelText('Name *').type('1'); @@ -199,6 +224,10 @@ describe('Catalogue Category', () => { it('displays error message if none of the fields have changed', () => { cy.findByRole('button', { + name: 'actions Beam Characterization catalogue category button', + }).click(); + + cy.findByRole('menuitem', { name: 'edit Beam Characterization catalogue category button', }).click(); @@ -215,6 +244,10 @@ describe('Catalogue Category', () => { it('displays error message if it received an unknown error from the api', () => { cy.visit('/catalogue/1'); cy.findByRole('button', { + name: 'actions Cameras catalogue category button', + }).click(); + + cy.findByRole('menuitem', { name: 'edit Cameras catalogue category button', }).click(); cy.findByLabelText('Name *').clear(); @@ -233,6 +266,10 @@ describe('Catalogue Category', () => { it('edits a catalogue category with catalogue properties', () => { cy.visit('/catalogue/1'); cy.findByRole('button', { + name: 'actions Voltage Meters catalogue category button', + }).click(); + + cy.findByRole('menuitem', { name: 'edit Voltage Meters catalogue category button', }).click(); @@ -259,6 +296,10 @@ describe('Catalogue Category', () => { it('displays error message when duplicate names for properties are entered', () => { cy.visit('/catalogue/1'); cy.findByRole('button', { + name: 'actions Voltage Meters catalogue category button', + }).click(); + + cy.findByRole('menuitem', { name: 'edit Voltage Meters catalogue category button', }).click(); @@ -280,6 +321,10 @@ describe('Catalogue Category', () => { it('edits a catalogue category from a leaf node to a non-leaf node ', () => { cy.visit('/catalogue/1'); cy.findByRole('button', { + name: 'actions Voltage Meters catalogue category button', + }).click(); + + cy.findByRole('menuitem', { name: 'edit Voltage Meters catalogue category button', }).click(); cy.findByLabelText('Catalogue Categories').click(); From a63bc0964b74d608b5fb746d32ed73c6b48ba6f0 Mon Sep 17 00:00:00 2001 From: Matteo Guarnaccia Date: Mon, 8 Jan 2024 09:58:49 +0000 Subject: [PATCH 018/222] fixed failing unit test --- src/catalogue/catalogue.component.test.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/catalogue/catalogue.component.test.tsx b/src/catalogue/catalogue.component.test.tsx index 495bdb150..5d7508394 100644 --- a/src/catalogue/catalogue.component.test.tsx +++ b/src/catalogue/catalogue.component.test.tsx @@ -236,7 +236,12 @@ describe('Catalogue', () => { expect(screen.getByText('Beam Characterization')).toBeInTheDocument(); }); - const deleteButton = screen.getByRole('button', { + const actionsButton = screen.getByRole('button', { + name: 'actions Beam Characterization catalogue category button', + }); + await user.click(actionsButton); + + const deleteButton = screen.getByRole('menuitem', { name: 'delete Beam Characterization catalogue category button', }); await user.click(deleteButton); @@ -259,7 +264,12 @@ describe('Catalogue', () => { expect(screen.getByText('Amp Meters')).toBeInTheDocument(); }); - const editButton = screen.getByRole('button', { + const actionsButton = screen.getByRole('button', { + name: 'actions Amp Meters catalogue category button', + }); + await user.click(actionsButton); + + const editButton = screen.getByRole('menuitem', { name: 'edit Amp Meters catalogue category button', }); await user.click(editButton); From f80e7934caf7ee621075fde3af768a5066cf1bef Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Mon, 8 Jan 2024 12:22:16 +0000 Subject: [PATCH 019/222] Intitial transition to using table #125 --- .../systemDirectoryDialog.component.tsx | 7 +- src/systems/systems.component.tsx | 196 ++++++++++++------ 2 files changed, 141 insertions(+), 62 deletions(-) diff --git a/src/systems/systemDirectoryDialog.component.tsx b/src/systems/systemDirectoryDialog.component.tsx index 1524e439d..8dc2304e5 100644 --- a/src/systems/systemDirectoryDialog.component.tsx +++ b/src/systems/systemDirectoryDialog.component.tsx @@ -21,12 +21,13 @@ import { System } from '../app.types'; import handleTransferState from '../handleTransferState'; import Breadcrumbs from '../view/breadcrumbs.component'; import { SystemsTableView } from './systemsTableView.component'; +import { MRT_RowSelectionState } from 'material-react-table'; export interface SystemDirectoryDialogProps { open: boolean; onClose: () => void; selectedSystems: System[]; - onChangeSelectedSystems: (selectedSystems: System[]) => void; + onChangeSelectedSystems: (selectedSystems: MRT_RowSelectionState) => void; parentSystemId: string | null; type: 'moveTo' | 'copyTo'; } @@ -72,7 +73,7 @@ export const SystemDirectoryDialog = (props: SystemDirectoryDialogProps) => { targetSystem: targetSystem || null, }).then((response) => { handleTransferState(response); - onChangeSelectedSystems([]); + onChangeSelectedSystems({}); handleClose(); }); } @@ -101,7 +102,7 @@ export const SystemDirectoryDialog = (props: SystemDirectoryDialogProps) => { existingSystemCodes: existingSystemCodes, }).then((response) => { handleTransferState(response); - onChangeSelectedSystems([]); + onChangeSelectedSystems({}); handleClose(); }); } diff --git a/src/systems/systems.component.tsx b/src/systems/systems.component.tsx index 017635a00..aa068d5a3 100644 --- a/src/systems/systems.component.tsx +++ b/src/systems/systems.component.tsx @@ -10,30 +10,39 @@ import SaveAsIcon from '@mui/icons-material/SaveAs'; import { Box, Button, - Checkbox, CircularProgress, Divider, Grid, IconButton, LinearProgress, - List, - ListItem, - ListItemButton, ListItemIcon, ListItemText, Menu, MenuItem, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, Typography, } from '@mui/material'; -import React from 'react'; +import { + MRT_ColumnDef, + MRT_GlobalFilterTextField, + MRT_RowSelectionState, + MRT_TableBodyCellValue, + useMaterialReactTable, +} from 'material-react-table'; +import React, { useEffect } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useSystems, useSystemsBreadcrumbs } from '../api/systems'; import { System } from '../app.types'; import Breadcrumbs from '../view/breadcrumbs.component'; +import { DeleteSystemDialog } from './deleteSystemDialog.component'; import SystemDetails from './systemDetails.component'; import SystemDialog, { SystemDialogType } from './systemDialog.component'; import { SystemDirectoryDialog } from './systemDirectoryDialog.component'; -import { DeleteSystemDialog } from './deleteSystemDialog.component'; /* Returns function that navigates to a specific system id (or to the root of all systems if given null) */ @@ -72,7 +81,7 @@ const AddSystemButton = (props: { systemId: string | null }) => { const MoveSystemsButton = (props: { selectedSystems: System[]; - onChangeSelectedSystems: (selectedSystems: System[]) => void; + onChangeSelectedSystems: (selectedSystems: MRT_RowSelectionState) => void; parentSystemId: string | null; }) => { const [moveSystemsDialogOpen, setMoveSystemsDialogOpen] = @@ -102,7 +111,7 @@ const MoveSystemsButton = (props: { const CopySystemsButton = (props: { selectedSystems: System[]; - onChangeSelectedSystems: (selectedSystems: System[]) => void; + onChangeSelectedSystems: (selectedSystems: MRT_RowSelectionState) => void; parentSystemId: string | null; }) => { const [copySystemsDialogOpen, setCopySystemsDialogOpen] = @@ -223,13 +232,19 @@ export const useSystemId = (): string | null => { }, [location.pathname]); }; +const columns: MRT_ColumnDef[] = [ + { accessorKey: 'name', header: 'Name' }, +]; + function Systems() { // Navigation const systemId = useSystemId(); const navigateToSystem = useNavigateToSystem(); // States - const [selectedSystems, setSelectedSystems] = React.useState([]); + const [rowSelection, setRowSelection] = React.useState( + {} + ); // Specifically for the drop down menus/dialogues const [selectedSystemForMenu, setSelectedSystemForMenu] = React.useState< @@ -249,21 +264,78 @@ function Systems() { systemId === null ? 'null' : systemId ); - const handleSystemCheckboxChange = (checked: boolean, system: System) => { - if (checked) setSelectedSystems([...selectedSystems, system]); - else - setSelectedSystems( - selectedSystems.filter( - (selectedSystem: System) => selectedSystem.id !== system.id - ) - ); - }; + // Obtain the selected system data, not just the selection state + const selectedRowIds = Object.keys(rowSelection); + const selectedSystems = + subsystemsData?.filter((subsystem) => + selectedRowIds.includes(subsystem.id) + ) ?? []; // Clear selected system when user navigates to a different page React.useEffect(() => { - setSelectedSystems([]); + setRowSelection({}); }, [systemId]); + const subsystemsTable = useMaterialReactTable({ + columns: columns, + data: subsystemsData !== undefined ? subsystemsData : [], + getRowId: (system) => system.id, + enableRowSelection: true, + enableRowActions: true, + positionActionsColumn: 'last', + initialState: { + showGlobalFilter: true, + }, + onRowSelectionChange: setRowSelection, + state: { rowSelection: rowSelection }, + renderRowActionMenuItems: ({ closeMenu, row }) => { + return [ + { + setMenuDialogType('edit'); + setSelectedSystemForMenu(row.original); + closeMenu(); + }} + > + + + + Edit + , + { + setMenuDialogType('save as'); + setSelectedSystemForMenu(row.original); + closeMenu(); + }} + > + + + + Save as + , + { + setMenuDialogType('delete'); + setSelectedSystemForMenu(row.original); + closeMenu(); + }} + > + + + + Delete + , + ]; + }, + }); + return ( <> @@ -300,19 +372,19 @@ function Systems() { @@ -344,43 +416,49 @@ function Systems() { - - {subsystemsData?.map((system, index) => { - const selected = selectedSystems.some( - (selectedSystem) => selectedSystem.id === system.id - ); - return ( - - navigateToSystem(system.id)} - > - event.stopPropagation()} - onChange={(event) => - handleSystemCheckboxChange( - event.target.checked, - system - ) - } - /> - {system.name} - - setSelectedSystemForMenu(system)} - onItemClicked={(type: SystemDialogType | 'delete') => - setMenuDialogType(type) - } - /> - - ); - })} - + + + + + + + + {subsystemsTable.getRowModel().rows.map((row) => ( + navigateToSystem(row.id)} + hover={true} + sx={{ cursor: 'pointer' }} + > + {row.getVisibleCells().map((cell) => ( + + + + ))} + + ))} + +
+
+
)}
From ffd1de350cf2e6585a6605255523ee54dd260315 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Mon, 8 Jan 2024 12:34:09 +0000 Subject: [PATCH 020/222] Fix typo and moving query invalidation #137 --- cypress/e2e/system.cy.ts | 2 +- src/api/systems.tsx | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/system.cy.ts b/cypress/e2e/system.cy.ts index 7236405ac..c0d8bfda2 100644 --- a/cypress/e2e/system.cy.ts +++ b/cypress/e2e/system.cy.ts @@ -350,7 +350,7 @@ describe('System', () => { }); }); - it("save as a system eidting only a system's name (in subsystem)", () => { + it("save as a system editing only a system's name (in subsystem)", () => { cy.visit('/systems/65328f34a40ff5301575a4e3'); cy.findAllByLabelText('Row Actions').eq(0).click(); diff --git a/src/api/systems.tsx b/src/api/systems.tsx index 8ae7f6aa8..0e6b889fc 100644 --- a/src/api/systems.tsx +++ b/src/api/systems.tsx @@ -244,7 +244,9 @@ export const useMoveToSystem = (): UseMutationResult< return useMutation(async (moveToSystem: MoveToSystem) => { const transferStates: TransferState[] = []; + // Ids for invalidation (parentIds must be a string value of 'null' for invalidation) const successfulIds: string[] = []; + const successfulParentIds: string[] = []; const promises = moveToSystem.selectedSystems.map( async (system: System) => { @@ -261,6 +263,7 @@ export const useMoveToSystem = (): UseMutationResult< }); successfulIds.push(system.id); + successfulParentIds.push(system.parent_id || 'null'); }) .catch((error) => { const response = error.response?.data as ErrorParsing; @@ -280,6 +283,13 @@ export const useMoveToSystem = (): UseMutationResult< queryClient.invalidateQueries({ queryKey: ['Systems', moveToSystem.targetSystem?.id || 'null'], }); + // Also need to invalidate each parent we are moving from (likely just the one) + const uniqueParentIds = new Set(successfulParentIds); + uniqueParentIds.forEach((parentId: string) => + queryClient.invalidateQueries({ + queryKey: ['Systems', parentId], + }) + ); queryClient.invalidateQueries({ queryKey: ['SystemBreadcrumbs'] }); successfulIds.forEach((id: string) => queryClient.invalidateQueries({ queryKey: ['System', id] }) From 82a175712fb0f805df4a0bc6a3ee37ada99a5a78 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Mon, 8 Jan 2024 12:55:47 +0000 Subject: [PATCH 021/222] add e2e tests --- cypress/e2e/catalogue/catalogueItems.cy.ts | 21 +++- cypress/e2e/items.cy.ts | 133 +++++++++++++++++++++ src/items/itemDialog.component.test.tsx | 34 ++++-- src/items/itemDialog.component.tsx | 57 ++++++++- 4 files changed, 227 insertions(+), 18 deletions(-) create mode 100644 cypress/e2e/items.cy.ts diff --git a/cypress/e2e/catalogue/catalogueItems.cy.ts b/cypress/e2e/catalogue/catalogueItems.cy.ts index 2b0178050..9475e152c 100644 --- a/cypress/e2e/catalogue/catalogueItems.cy.ts +++ b/cypress/e2e/catalogue/catalogueItems.cy.ts @@ -551,14 +551,31 @@ describe('Catalogue Items', () => { cy.findAllByText('Manufacturer Name').should('exist'); }); - it('can navigate to an items replacement', () => { + it('can navigate to an catalogue items replacement', () => { cy.visit('/catalogue/5'); - cy.findAllByRole('link', { name: 'Click here' }).eq(0).click(); + cy.findAllByRole('link', { name: 'Click here' }).eq(1).click(); cy.url().should('contain', 'catalogue/item/6'); }); + it('can navigate to an items page from the table view', () => { + cy.visit('/catalogue/5'); + + cy.findAllByRole('link', { name: 'Click here' }).eq(0).click(); + + cy.url().should('contain', 'catalogue/item/89/items'); + }); + + it('can navigate to an items page from the landing page', () => { + cy.visit('/catalogue/5'); + cy.findByText('Energy Meters 26').click(); + + cy.findAllByRole('link', { name: 'Items' }).eq(0).click(); + + cy.url().should('contain', 'catalogue/item/89/items'); + }); + it('can move multiple catalogue items', () => { cy.visit('/catalogue/5'); diff --git a/cypress/e2e/items.cy.ts b/cypress/e2e/items.cy.ts new file mode 100644 index 000000000..33c433ad3 --- /dev/null +++ b/cypress/e2e/items.cy.ts @@ -0,0 +1,133 @@ +describe('Items', () => { + beforeEach(() => { + cy.visit('/catalogue/item/1/items'); + }); + afterEach(() => { + cy.clearMocks(); + }); + it('should be able to navigate back to the catalogue catalogue item table view', () => { + cy.findByRole('link', { name: 'Back to Cameras table view' }).click(); + cy.findByText('Cameras 1').should('be.visible'); + cy.findByText('Cameras 2').should('be.visible'); + cy.findByText('Cameras 3').should('be.visible'); + }); + + it('should be able to navigate back to the catalogue catalogue item landing page', () => { + cy.findByRole('link', { name: 'Back to Cameras 1 landing page' }).click(); + cy.findByText('Cameras 1').should('be.visible'); + cy.findByText( + 'High-resolution cameras for beam characterization. 1' + ).should('be.visible'); + cy.findByText('Older than five years').should('be.visible'); + }); + + it('adds a item with only mandatory fields', () => { + cy.findByRole('button', { name: 'Add Item' }).click(); + + cy.startSnoopingBrowserMockedRequest(); + + cy.findByRole('button', { name: 'Save' }).click(); + + cy.findBrowserMockedRequests({ + method: 'POST', + url: '/v1/items/', + }).should(async (postRequests) => { + expect(postRequests.length).eq(1); + expect(JSON.stringify(await postRequests[0].json())).equal( + JSON.stringify({ + catalogue_item_id: '1', + system_id: null, + purchase_order_number: null, + is_defective: false, + usage_status: 0, + warranty_end_date: null, + asset_number: null, + serial_number: null, + delivered_date: null, + notes: null, + properties: [ + { name: 'Resolution', value: 12 }, + { name: 'Frame Rate', value: 30 }, + { name: 'Sensor Type', value: 'CMOS' }, + { name: 'Broken', value: true }, + { name: 'Older than five years', value: false }, + ], + }) + ); + }); + }); + + it('adds a item with all fields altered', () => { + cy.findByRole('button', { name: 'Add Item' }).click(); + + cy.findByLabelText('Serial number').type('test1234'); + cy.findByLabelText('Asset number').type('test13221'); + cy.findByLabelText('Purchase order number').type('test23'); + cy.findByLabelText('Warranty end date').type('12/02/2028'); + cy.findByLabelText('Delivered date').type('12/02/2028'); + cy.findByLabelText('Is defective *').click(); + cy.findByText('Yes').click(); + cy.findByLabelText('Usage status *').click(); + cy.findByText('Scrapped').click(); + cy.findByLabelText('Notes').type('test'); + + cy.findByLabelText('Resolution (megapixels) *').type('18'); + cy.findByLabelText('Frame Rate (fps)').type('60'); + cy.findByLabelText('Sensor Type *').type('IO'); + cy.findByLabelText('Sensor brand').type('pixel'); + cy.findByLabelText('Broken *').click(); + cy.findByRole('option', { name: 'False' }).click(); + cy.findByLabelText('Older than five years').click(); + cy.findByRole('option', { name: 'True' }).click(); + + cy.startSnoopingBrowserMockedRequest(); + + cy.findByRole('button', { name: 'Save' }).click(); + + cy.findBrowserMockedRequests({ + method: 'POST', + url: '/v1/items/', + }).should(async (postRequests) => { + expect(postRequests.length).eq(1); + expect(JSON.stringify(await postRequests[0].json())).equal( + JSON.stringify({ + catalogue_item_id: '1', + system_id: null, + purchase_order_number: 'test23', + is_defective: true, + usage_status: 3, + warranty_end_date: '2028-02-11T23:59:15.000Z', + asset_number: 'test13221', + serial_number: 'test1234', + delivered_date: '2028-02-11T23:59:15.000Z', + notes: 'test', + properties: [ + { name: 'Resolution', value: 1218 }, + { name: 'Frame Rate', value: 3060 }, + { name: 'Sensor Type', value: 'CMOSIO' }, + { name: 'Sensor brand', value: 'pixel' }, + { name: 'Broken', value: false }, + { name: 'Older than five years', value: true }, + ], + }) + ); + }); + }); + + it('displays messages for incorrect input types', () => { + cy.findByRole('button', { name: 'Add Item' }).click(); + + cy.findByLabelText('Warranty end date').type('12/02/'); + cy.findByLabelText('Delivered date').type('12/02/'); + + cy.findByLabelText('Resolution (megapixels) *').clear(); + cy.findByLabelText('Sensor Type *').clear(); + cy.findByLabelText('Broken *').click(); + cy.findByRole('option', { name: 'None' }).click(); + + cy.findByRole('button', { name: 'Save' }).click(); + + cy.findAllByText('This field is mandatory').should('have.length', 2); + cy.findAllByText('Date format: dd/MM/yyyy').should('have.length', 2); + }); +}); diff --git a/src/items/itemDialog.component.test.tsx b/src/items/itemDialog.component.test.tsx index 480884296..16659ea13 100644 --- a/src/items/itemDialog.component.test.tsx +++ b/src/items/itemDialog.component.test.tsx @@ -65,14 +65,16 @@ describe('ItemDialog', () => { }); values.warrantyEndDate !== undefined && - fireEvent.change(screen.getByLabelText('Warranty end date'), { - target: { value: values.warrantyEndDate }, - }); + (await user.type( + screen.getByLabelText('Warranty end date'), + values.warrantyEndDate + )); values.deliveredDate !== undefined && - fireEvent.change(screen.getByLabelText('Delivered date'), { - target: { value: values.deliveredDate }, - }); + (await user.type( + screen.getByLabelText('Delivered date'), + values.deliveredDate + )); values.resolution !== undefined && fireEvent.change(screen.getByLabelText('Resolution (megapixels) *'), { @@ -198,9 +200,9 @@ describe('ItemDialog', () => { usage_status: 2, warranty_end_date: '2035-02-17T00:00:00.000Z', }); - }); + }, 10000); - it('adds a item (case empty string with spaces returns null and chnage propetery boolean values)', async () => { + it('adds a item (case empty string with spaces returns null and change property boolean values)', async () => { createView(); await modifyValues({ serialNumber: ' ', @@ -240,7 +242,7 @@ describe('ItemDialog', () => { usage_status: 2, warranty_end_date: '2035-02-17T00:00:00.000Z', }); - }); + }, 10000); it('displays error message when mandatory property values missing', async () => { createView(); @@ -269,7 +271,7 @@ describe('ItemDialog', () => { expect(mandatoryFieldBooleanHelperText).toBeInTheDocument(); expect(mandatoryFieldHelperText.length).toBe(2); - }); + }, 10000); it('displays error message when property values type is incorrect', async () => { createView(); @@ -278,22 +280,28 @@ describe('ItemDialog', () => { assetNumber: 'test43', purchaseOrderNumber: 'test21', notes: 'test', - warrantyEndDate: '17/02/2035', - deliveredDate: '23/09/2045', + warrantyEndDate: '17', + deliveredDate: '23', isDefective: 'Yes', usageStatus: 'Used', resolution: 'rwererw', sensorType: '', broken: 'None', }); + const validDateHelperText = screen.getAllByText( + 'Date format: dd/MM/yyyy' + ); + expect(validDateHelperText.length).toEqual(2); + const saveButton = screen.getByRole('button', { name: 'Save' }); await user.click(saveButton); const validNumberHelperText = screen.getByText( 'Please enter a valid number' ); + expect(validNumberHelperText).toBeInTheDocument(); - }); + }, 10000); it('displays warning message when an unknown error occurs', async () => { createView(); diff --git a/src/items/itemDialog.component.tsx b/src/items/itemDialog.component.tsx index feb1c72d1..98bce5a09 100644 --- a/src/items/itemDialog.component.tsx +++ b/src/items/itemDialog.component.tsx @@ -14,6 +14,7 @@ import { MenuItem, Select, TextField, + TextFieldProps, Tooltip, Typography, } from '@mui/material'; @@ -40,6 +41,25 @@ function isValidDateTime(date: string | null) { return !isNaN(dateObj.getTime()) && dateObj.toString() !== 'Invalid Date'; } +const CustomTextField: React.FC = (renderProps) => { + const { id, ...inputProps } = renderProps.inputProps ?? {}; + let helperText = 'Date format: dd/MM/yyyy'; + + return ( + + ); +}; + export interface ItemDialogProps { open: boolean; onClose: () => void; @@ -150,6 +170,18 @@ function ItemDialog(props: ItemDialogProps) { const handleClose = React.useCallback(() => { onClose(); + setItemDetails({ + catalogue_item_id: null, + system_id: null, + purchase_order_number: null, + is_defective: null, + usage_status: null, + warranty_end_date: null, + asset_number: null, + serial_number: null, + delivered_date: null, + notes: null, + }); setPropertyValues([]); setPropertyErrors( new Array(parentCatalogueItemPropertiesInfo.length).fill(false) @@ -159,6 +191,20 @@ function ItemDialog(props: ItemDialogProps) { const handleFormErrorStates = React.useCallback(() => { let hasErrors = false; + if ( + itemDetails.warranty_end_date && + !isValidDateTime(itemDetails.warranty_end_date) + ) { + hasErrors = true; + } + + if ( + itemDetails.delivered_date && + !isValidDateTime(itemDetails.delivered_date) + ) { + hasErrors = true; + } + // Check properties const updatedPropertyErrors = [...propertyErrors]; @@ -222,7 +268,12 @@ function ItemDialog(props: ItemDialogProps) { setPropertyErrors(updatedPropertyErrors); return { hasErrors, updatedProperties }; - }, [propertyErrors, parentCatalogueItemPropertiesInfo, propertyValues]); + }, [ + propertyErrors, + parentCatalogueItemPropertiesInfo, + propertyValues, + itemDetails, + ]); const details = React.useMemo(() => { return { @@ -333,9 +384,9 @@ function ItemDialog(props: ItemDialogProps) { date ? date.toString() : null ) } + slots={{ textField: CustomTextField }} slotProps={{ actionBar: { actions: ['clear'] }, - textField: { size: 'small', fullWidth: true }, }} />
@@ -355,8 +406,8 @@ function ItemDialog(props: ItemDialogProps) { } slotProps={{ actionBar: { actions: ['clear'] }, - textField: { size: 'small', fullWidth: true }, }} + slots={{ textField: CustomTextField }} /> From c78dd37ab216643b854fcd283c6b98100a343783 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Mon, 8 Jan 2024 13:35:23 +0000 Subject: [PATCH 022/222] fix failing e2e test --- cypress/e2e/items.cy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/items.cy.ts b/cypress/e2e/items.cy.ts index 33c433ad3..fd429d5b1 100644 --- a/cypress/e2e/items.cy.ts +++ b/cypress/e2e/items.cy.ts @@ -96,10 +96,10 @@ describe('Items', () => { purchase_order_number: 'test23', is_defective: true, usage_status: 3, - warranty_end_date: '2028-02-11T23:59:15.000Z', + warranty_end_date: '2028-02-11T00:00:00.000Z', asset_number: 'test13221', serial_number: 'test1234', - delivered_date: '2028-02-11T23:59:15.000Z', + delivered_date: '2028-02-11T00:00:00.000Z', notes: 'test', properties: [ { name: 'Resolution', value: 1218 }, From 3e755ba5e518321ca3680d0de01e4db84cbf06f2 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Mon, 8 Jan 2024 13:43:01 +0000 Subject: [PATCH 023/222] fix e2e test --- cypress/e2e/items.cy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/items.cy.ts b/cypress/e2e/items.cy.ts index fd429d5b1..5cf40133a 100644 --- a/cypress/e2e/items.cy.ts +++ b/cypress/e2e/items.cy.ts @@ -96,10 +96,10 @@ describe('Items', () => { purchase_order_number: 'test23', is_defective: true, usage_status: 3, - warranty_end_date: '2028-02-11T00:00:00.000Z', + warranty_end_date: '2028-02-12T00:00:00.000Z', asset_number: 'test13221', serial_number: 'test1234', - delivered_date: '2028-02-11T00:00:00.000Z', + delivered_date: '2028-02-12T00:00:00.000Z', notes: 'test', properties: [ { name: 'Resolution', value: 1218 }, From 17b2b1f5f1a9a30c0bd752e8a152da6105b07d14 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Mon, 8 Jan 2024 14:32:55 +0000 Subject: [PATCH 024/222] Add pagination component and align actions button #125 --- src/systems/systems.component.tsx | 61 +++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/src/systems/systems.component.tsx b/src/systems/systems.component.tsx index aa068d5a3..250524883 100644 --- a/src/systems/systems.component.tsx +++ b/src/systems/systems.component.tsx @@ -32,9 +32,10 @@ import { MRT_GlobalFilterTextField, MRT_RowSelectionState, MRT_TableBodyCellValue, + MRT_TablePagination, useMaterialReactTable, } from 'material-react-table'; -import React, { useEffect } from 'react'; +import React from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useSystems, useSystemsBreadcrumbs } from '../api/systems'; import { System } from '../app.types'; @@ -283,6 +284,10 @@ function Systems() { enableRowSelection: true, enableRowActions: true, positionActionsColumn: 'last', + paginationDisplayMode: 'pages', + muiPaginationProps: { + showRowsPerPage: false, + }, initialState: { showGlobalFilter: true, }, @@ -427,8 +432,8 @@ function Systems() { - - +
+ {subsystemsTable.getRowModel().rows.map((row) => ( - {row.getVisibleCells().map((cell) => ( - - - - ))} + {row.getVisibleCells().map((cell) => { + console.log(cell.column.id); + return ( + + + + ); + })} ))}
+ )} From 80c20ed56d7bc6453504a78f114f5eead6fc05b1 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Mon, 8 Jan 2024 15:03:24 +0000 Subject: [PATCH 025/222] Fix scroll bar when viewing a system (Due to MUI GridV1) #125 --- src/systems/systemDetails.component.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/systemDetails.component.tsx b/src/systems/systemDetails.component.tsx index 4f006b51f..2f4e83c9e 100644 --- a/src/systems/systemDetails.component.tsx +++ b/src/systems/systemDetails.component.tsx @@ -90,7 +90,7 @@ function SystemDetails(props: SystemDetailsProps) { Please select a system ) : ( - + - + Description {system.description ?? 'None'} From 47145708b32a3dee77470fa777264eda07437d53 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Jan 2024 00:26:50 +0000 Subject: [PATCH 026/222] Update dependency @testing-library/jest-dom to v6.2.0 --- yarn.lock | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index 84a3df477..3af30597f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,10 +12,10 @@ __metadata: languageName: node linkType: hard -"@adobe/css-tools@npm:^4.3.0": - version: 4.3.1 - resolution: "@adobe/css-tools@npm:4.3.1" - checksum: ad43456379ff391132aff687ece190cb23ea69395e23c9b96690eeabe2468da89a4aaf266e4f8b6eaab53db3d1064107ce0f63c3a974e864f4a04affc768da3f +"@adobe/css-tools@npm:^4.3.2": + version: 4.3.2 + resolution: "@adobe/css-tools@npm:4.3.2" + checksum: 9667d61d55dc3b0a315c530ae84e016ce5267c4dd8ac00abb40108dc98e07b98e3090ce8b87acd51a41a68d9e84dcccb08cdf21c902572a9cf9dcaf830da4ae3 languageName: node linkType: hard @@ -3337,15 +3337,15 @@ __metadata: linkType: hard "@testing-library/jest-dom@npm:^6.0.0": - version: 6.1.3 - resolution: "@testing-library/jest-dom@npm:6.1.3" + version: 6.2.0 + resolution: "@testing-library/jest-dom@npm:6.2.0" dependencies: - "@adobe/css-tools": ^4.3.0 + "@adobe/css-tools": ^4.3.2 "@babel/runtime": ^7.9.2 aria-query: ^5.0.0 chalk: ^3.0.0 css.escape: ^1.5.1 - dom-accessibility-api: ^0.5.6 + dom-accessibility-api: ^0.6.3 lodash: ^4.17.15 redent: ^3.0.0 peerDependencies: @@ -3362,7 +3362,7 @@ __metadata: optional: true vitest: optional: true - checksum: 5bd14ba31fd3d64cff8ca55cccd335bedadf1db1119643954ca8cd30af835defe6f3a21e7d7617d20205b07abba1b2e668be1b9d6743504800f17fdc4344db75 + checksum: 3d46e36b1b7c2cb3c92f64d55d458aab44ae135ac77299df14d14dcf567a286590de58b2f140011b8f7a343f0703ff88f144f27c6ae4921fd612741771d8ee2c languageName: node linkType: hard @@ -6819,13 +6819,20 @@ __metadata: languageName: node linkType: hard -"dom-accessibility-api@npm:^0.5.6, dom-accessibility-api@npm:^0.5.9": +"dom-accessibility-api@npm:^0.5.9": version: 0.5.16 resolution: "dom-accessibility-api@npm:0.5.16" checksum: 005eb283caef57fc1adec4d5df4dd49189b628f2f575af45decb210e04d634459e3f1ee64f18b41e2dcf200c844bc1d9279d80807e686a30d69a4756151ad248 languageName: node linkType: hard +"dom-accessibility-api@npm:^0.6.3": + version: 0.6.3 + resolution: "dom-accessibility-api@npm:0.6.3" + checksum: c325b5144bb406df23f4affecffc117dbaec9af03daad9ee6b510c5be647b14d28ef0a4ea5ca06d696d8ab40bb777e5fed98b985976fdef9d8790178fa1d573f + languageName: node + linkType: hard + "dom-converter@npm:^0.2.0": version: 0.2.0 resolution: "dom-converter@npm:0.2.0" From e82bca874b6223d08177a43b6de8df878434e4b8 Mon Sep 17 00:00:00 2001 From: Matteo Guarnaccia Date: Tue, 9 Jan 2024 09:37:04 +0000 Subject: [PATCH 027/222] logic for save as #172 --- src/catalogue/catalogue.component.tsx | 19 +++++++++++++++++++ .../category/catalogueCard.component.tsx | 18 ++++++++++++++++++ .../catalogueCategoryDialog.component.tsx | 8 ++++---- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/catalogue/catalogue.component.tsx b/src/catalogue/catalogue.component.tsx index 14acf38cf..d22e02c5c 100644 --- a/src/catalogue/catalogue.component.tsx +++ b/src/catalogue/catalogue.component.tsx @@ -135,6 +135,9 @@ function Catalogue() { const [editCategoryDialogOpen, setEditCategoryDialogOpen] = React.useState(false); + const [saveAsCategoryDialogOpen, setSaveAsCategoryDialogOpen] = + React.useState(false); + const [selectedCatalogueCategory, setSelectedCatalogueCategory] = React.useState(undefined); @@ -152,6 +155,11 @@ function Catalogue() { setSelectedCatalogueCategory(catalogueCategory); }; + const onChangeOpenSaveAsDialog = (catalogueCategory: CatalogueCategory) => { + setSaveAsCategoryDialogOpen(true); + setSelectedCatalogueCategory(catalogueCategory); + }; + const [selectedCategories, setSelectedCategories] = React.useState< CatalogueCategory[] >([]); @@ -302,6 +310,7 @@ function Catalogue() { {...item} onChangeOpenDeleteDialog={onChangeOpenDeleteCategoryDialog} onChangeOpenEditDialog={onChangeOpenEditCategoryDialog} + onChangeOpenSaveAsDialog={onChangeOpenSaveAsDialog} onToggleSelect={handleToggleSelect} isSelected={selectedCategories.some( (selectedCategory: CatalogueCategory) => @@ -326,6 +335,16 @@ function Catalogue() { setSelectedCatalogueCategory(undefined) } /> + setSaveAsCategoryDialogOpen(false)} + parentId={parentId} + type="save as" + selectedCatalogueCategory={selectedCatalogueCategory} + resetSelectedCatalogueCategory={() => + setSelectedCatalogueCategory(undefined) + } + /> setDeleteCategoryDialogOpen(false)} diff --git a/src/catalogue/category/catalogueCard.component.tsx b/src/catalogue/category/catalogueCard.component.tsx index 1ec0f6a9b..c878eae44 100644 --- a/src/catalogue/category/catalogueCard.component.tsx +++ b/src/catalogue/category/catalogueCard.component.tsx @@ -14,11 +14,13 @@ import { import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; +import SaveAsIcon from '@mui/icons-material/SaveAs'; import { CatalogueCategory } from '../../app.types'; import { Link } from 'react-router-dom'; export interface CatalogueCardProps extends CatalogueCategory { onChangeOpenDeleteDialog: (catalogueCategory: CatalogueCategory) => void; onChangeOpenEditDialog: (catalogueCategory: CatalogueCategory) => void; + onChangeOpenSaveAsDialog: (catalogueCategory: CatalogueCategory) => void; onToggleSelect: (catalogueCategory: CatalogueCategory) => void; isSelected: boolean; } @@ -27,6 +29,7 @@ function CatalogueCard(props: CatalogueCardProps) { const { onChangeOpenDeleteDialog, onChangeOpenEditDialog, + onChangeOpenSaveAsDialog, onToggleSelect, isSelected, ...catalogueCategory @@ -120,6 +123,21 @@ function CatalogueCard(props: CatalogueCardProps) { { + event.preventDefault(); + onChangeOpenSaveAsDialog(catalogueCategory); + setMenuOpen(false); + }} + sx={{ m: 0 }} + > + + + + Save as + + { event.preventDefault(); onChangeOpenDeleteDialog(catalogueCategory); diff --git a/src/catalogue/category/catalogueCategoryDialog.component.tsx b/src/catalogue/category/catalogueCategoryDialog.component.tsx index ac7f66e35..eb31dbdc3 100644 --- a/src/catalogue/category/catalogueCategoryDialog.component.tsx +++ b/src/catalogue/category/catalogueCategoryDialog.component.tsx @@ -36,7 +36,7 @@ export interface CatalogueCategoryDialogProps { open: boolean; onClose: () => void; parentId: string | null; - type: 'add' | 'edit'; + type: 'add' | 'edit' | 'save as'; selectedCatalogueCategory?: CatalogueCategory; resetSelectedCatalogueCategory: () => void; } @@ -438,9 +438,9 @@ const CatalogueCategoryDialog = React.memo( variant="outlined" sx={{ width: '50%', mx: 1 }} onClick={ - type === 'add' - ? handleAddCatalogueCategory - : handleEditCatalogueCategory + type === 'edit' + ? handleEditCatalogueCategory + : handleAddCatalogueCategory } disabled={ formError !== undefined || From 7c3ae0327aa86d25562922e27b97b9bf65701fcc Mon Sep 17 00:00:00 2001 From: MatteoGuarnaccia5 Date: Tue, 9 Jan 2024 11:09:14 +0000 Subject: [PATCH 028/222] added _copy_(iterative) to name in save as #172 --- src/catalogue/catalogue.component.tsx | 30 ++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/catalogue/catalogue.component.tsx b/src/catalogue/catalogue.component.tsx index d22e02c5c..3ddc66b4e 100644 --- a/src/catalogue/catalogue.component.tsx +++ b/src/catalogue/catalogue.component.tsx @@ -63,6 +63,21 @@ const AddCategoryButton = (props: AddCatalogueButtonProps) => { ); }; +function generateUniqueName( + existingNames: (string | undefined)[], + originalName: string +) { + let newName = originalName; + let copyIndex = 1; + + while (existingNames.includes(newName)) { + newName = `${originalName}_copy_${copyIndex}`; + copyIndex++; + } + + return newName; +} + export function matchCatalogueItemProperties( form: CatalogueCategoryFormData[], items: CatalogueItemProperty[] @@ -129,6 +144,9 @@ function Catalogue() { !catalogueId ? 'null' : catalogueId.replace('/', '') ); + const catalogueCategoryNames: (string | undefined)[] = + catalogueCategoryData?.map((item) => item.name) || []; + const [deleteCategoryDialogOpen, setDeleteCategoryDialogOpen] = React.useState(false); @@ -340,7 +358,17 @@ function Catalogue() { onClose={() => setSaveAsCategoryDialogOpen(false)} parentId={parentId} type="save as" - selectedCatalogueCategory={selectedCatalogueCategory} + selectedCatalogueCategory={ + selectedCatalogueCategory + ? { + ...selectedCatalogueCategory, + name: generateUniqueName( + catalogueCategoryNames, + selectedCatalogueCategory.name + ), + } + : undefined + } resetSelectedCatalogueCategory={() => setSelectedCatalogueCategory(undefined) } From 03fb65d64b2658291b31126f1c444df6e5540254 Mon Sep 17 00:00:00 2001 From: MatteoGuarnaccia5 Date: Tue, 9 Jan 2024 11:49:37 +0000 Subject: [PATCH 029/222] unit tests for save as dialog #172 --- src/catalogue/catalogue.component.test.tsx | 30 +++++++++++ .../category/catalogueCard.component.tsx | 2 +- ...catalogueCategoryDialog.component.test.tsx | 50 +++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/catalogue/catalogue.component.test.tsx b/src/catalogue/catalogue.component.test.tsx index 5d7508394..9aefaa182 100644 --- a/src/catalogue/catalogue.component.test.tsx +++ b/src/catalogue/catalogue.component.test.tsx @@ -287,6 +287,36 @@ describe('Catalogue', () => { }); }); + it('opens the save as catalogue category dialog', async () => { + createView('/catalogue/1'); + + await waitFor(() => { + expect(screen.getByText('Amp Meters')).toBeInTheDocument(); + }); + + const actionsButton = screen.getByRole('button', { + name: 'actions Amp Meters catalogue category button', + }); + await user.click(actionsButton); + + const editButton = screen.getByRole('menuitem', { + name: 'save as Amp Meters catalogue category button', + }); + await user.click(editButton); + + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + + const saveButton = screen.getByRole('button', { name: 'Save' }); + + await user.type(screen.getByLabelText('Name *'), '1'); + await user.click(saveButton); + await waitFor(() => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + }); + it('renders the breadcrumbs and navigate to another directory', async () => { createView('/catalogue/8'); diff --git a/src/catalogue/category/catalogueCard.component.tsx b/src/catalogue/category/catalogueCard.component.tsx index c878eae44..5914f1422 100644 --- a/src/catalogue/category/catalogueCard.component.tsx +++ b/src/catalogue/category/catalogueCard.component.tsx @@ -123,7 +123,7 @@ function CatalogueCard(props: CatalogueCardProps) { { event.preventDefault(); onChangeOpenSaveAsDialog(catalogueCategory); diff --git a/src/catalogue/category/catalogueCategoryDialog.component.test.tsx b/src/catalogue/category/catalogueCategoryDialog.component.test.tsx index c312f83ae..4c3e77d90 100644 --- a/src/catalogue/category/catalogueCategoryDialog.component.test.tsx +++ b/src/catalogue/category/catalogueCategoryDialog.component.test.tsx @@ -611,4 +611,54 @@ describe('Catalogue Category Dialog', () => { expect(onClose).not.toHaveBeenCalled(); }); }); + + describe('Save as Catalogue Category Dialog', () => { + //All of actual logic is same as add so is tested above + //checks that the dialog renders/opens correctly for `save as` + + let axiosPostSpy; + let mockData: CatalogueCategory = { + name: 'test', + parent_id: null, + id: '1', + code: 'test', + is_leaf: false, + }; + + beforeEach(() => { + props = { + open: true, + onClose: onClose, + parentId: null, + type: 'save as', + selectedCatalogueCategory: mockData, + resetSelectedCatalogueCategory: resetSelectedCatalogueCategory, + }; + user = userEvent.setup(); + axiosPostSpy = jest.spyOn(axios, 'post'); + }); + + it('renders correctly when saving as', async () => { + createView(); + + expect(screen.getByText('Add Catalogue Category')).toBeInTheDocument(); + }); + + it('saves as a catalogue category', async () => { + createView(); + + const values = { + name: 'Catalogue Category name', + }; + await modifyValues(values); + + await user.click(screen.getByRole('button', { name: 'Save' })); + + expect(axiosPostSpy).toHaveBeenCalledWith('/v1/catalogue-categories', { + ...values, + is_leaf: false, + }); + expect(onClose).toHaveBeenCalled(); + }); + }); }); From 561ad373065409b5a84252011f5421fc83cbfaa1 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Tue, 9 Jan 2024 11:52:14 +0000 Subject: [PATCH 030/222] Minor refactor to move generateUniqueName to utils file --- src/api/catalogueCategory.test.tsx | 12 +++---- src/api/catalogueCategory.tsx | 26 ++++---------- src/api/systems.test.tsx | 12 +++---- src/api/systems.tsx | 20 ++++------- src/app.types.tsx | 8 ++--- ...logueCategoryDirectoryDialog.component.tsx | 6 ++-- .../items/catalogueItemsTable.component.tsx | 36 ++++++------------- .../systemDirectoryDialog.component.tsx | 4 +-- src/utils.test.tsx | 33 +++++++++++++++++ src/utils.tsx | 15 ++++++++ 10 files changed, 92 insertions(+), 80 deletions(-) create mode 100644 src/utils.test.tsx create mode 100644 src/utils.tsx diff --git a/src/api/catalogueCategory.test.tsx b/src/api/catalogueCategory.test.tsx index abb21c635..b42a08280 100644 --- a/src/api/catalogueCategory.test.tsx +++ b/src/api/catalogueCategory.test.tsx @@ -416,7 +416,7 @@ describe('catalogue category api functions', () => { copyToCatalogueCategory = { selectedCategories: mockCatalogueCategories, targetCategory: null, - existingCategoryCodes: [], + existingCategoryNames: [], }; axiosPostSpy = jest.spyOn(axios, 'post'); @@ -436,7 +436,7 @@ describe('catalogue category api functions', () => { result.current.mutate({ selectedCategories: mockCatalogueCategories, targetCategory: null, - existingCategoryCodes: [''], + existingCategoryNames: [''], }); await waitFor(() => { @@ -487,7 +487,7 @@ describe('catalogue category api functions', () => { result.current.mutate({ selectedCategories: mockCatalogueCategories, targetCategory: targetCategory, - existingCategoryCodes: [''], + existingCategoryNames: [''], }); await waitFor(() => { @@ -526,9 +526,9 @@ describe('catalogue category api functions', () => { expect(result.current.isIdle).toBe(true); - copyToCatalogueCategory.existingCategoryCodes = [ - ...mockCatalogueCategories.map((category) => category.code), - mockCatalogueCategories[1].code + '_copy_1', + copyToCatalogueCategory.existingCategoryNames = [ + ...mockCatalogueCategories.map((category) => category.name), + mockCatalogueCategories[1].name + '_copy_1', ]; result.current.mutate(copyToCatalogueCategory); diff --git a/src/api/catalogueCategory.tsx b/src/api/catalogueCategory.tsx index bd199c208..1c4c0e73c 100644 --- a/src/api/catalogueCategory.tsx +++ b/src/api/catalogueCategory.tsx @@ -17,6 +17,7 @@ import { TransferState, } from '../app.types'; import { settings } from '../settings'; +import { generateUniqueName } from '../utils'; const fetchCatalogueCategories = async ( parent_id: string @@ -287,26 +288,11 @@ export const useCopyToCatalogueCategory = (): UseMutationResult< categoryAdd.parent_id = copyToCatalogueCategory.targetCategory?.id || null; - // Avoid duplicates by appending _copy_n for nth copy - if ( - copyToCatalogueCategory.existingCategoryCodes.includes( - category.code - ) - ) { - let count = 1; - let newName = categoryAdd.name; - let newCode = category.code; - - while ( - copyToCatalogueCategory.existingCategoryCodes.includes(newCode) - ) { - newName = `${categoryAdd.name}_copy_${count}`; - newCode = `${category.code}_copy_${count}`; - count++; - } - - categoryAdd.name = newName; - } + // Avoid duplicates + categoryAdd.name = generateUniqueName( + categoryAdd.name, + copyToCatalogueCategory.existingCategoryNames + ); return addCatalogueCategory(categoryAdd) .then((result) => { diff --git a/src/api/systems.test.tsx b/src/api/systems.test.tsx index 42efbee90..2fdd589a5 100644 --- a/src/api/systems.test.tsx +++ b/src/api/systems.test.tsx @@ -374,7 +374,7 @@ describe('System api functions', () => { copyToSystem = { selectedSystems: mockSystems, targetSystem: null, - existingSystemCodes: [], + existingSystemNames: [], }; axiosPostSpy = jest.spyOn(axios, 'post'); @@ -452,11 +452,11 @@ describe('System api functions', () => { { ...(SystemsJSON[0] as System), name: 'System1', code: 'system1' }, { ...(SystemsJSON[1] as System), name: 'System2', code: 'system2' }, ]; - copyToSystem.existingSystemCodes = [ - 'system1', - 'system2', - 'system2_copy_1', - 'system2_copy_2', + copyToSystem.existingSystemNames = [ + 'System1', + 'System2', + 'System2_copy_1', + 'System2_copy_2', ]; const { result } = renderHook(() => useCopyToSystem(), { diff --git a/src/api/systems.tsx b/src/api/systems.tsx index 0e6b889fc..3bb669694 100644 --- a/src/api/systems.tsx +++ b/src/api/systems.tsx @@ -18,6 +18,7 @@ import { TransferState, } from '../app.types'; import { settings } from '../settings'; +import { generateUniqueName } from '../utils'; /** Utility for turning an importance into an MUI palette colour to display */ export const getSystemImportanceColour = ( @@ -322,20 +323,11 @@ export const useCopyToSystem = (): UseMutationResult< // Assign new parent systemAdd.parent_id = copyToSystem.targetSystem?.id || null; - // Avoid duplicates by appending _copy_n for nth copy - if (copyToSystem.existingSystemCodes.includes(system.code)) { - let count = 1; - let newName = systemAdd.name; - let newCode = system.code; - - while (copyToSystem.existingSystemCodes.includes(newCode)) { - newName = `${systemAdd.name}_copy_${count}`; - newCode = `${system.code}_copy_${count}`; - count++; - } - - systemAdd.name = newName; - } + // Avoid duplicates + systemAdd.name = generateUniqueName( + systemAdd.name, + copyToSystem.existingSystemNames + ); return addSystem(systemAdd) .then((result: System) => { diff --git a/src/app.types.tsx b/src/app.types.tsx index 8d4ffc5f7..7810b5536 100644 --- a/src/app.types.tsx +++ b/src/app.types.tsx @@ -28,9 +28,9 @@ export interface CopyToCatalogueCategory { selectedCategories: CatalogueCategory[]; // Null if root targetCategory: CatalogueCategory | null; - // Existing known catalogue category codes at the destination + // Existing known catalogue category names at the destination // (for appending to the names to avoid duplication) - existingCategoryCodes: string[]; + existingCategoryNames: string[]; } export interface CatalogueCategory { @@ -209,7 +209,7 @@ export interface CopyToSystem { selectedSystems: System[]; // Null if root targetSystem: System | null; - // Existing known system codes at the destination + // Existing known system names at the destination // (for appending to the names to avoid duplication) - existingSystemCodes: string[]; + existingSystemNames: string[]; } diff --git a/src/catalogue/category/catalogueCategoryDirectoryDialog.component.tsx b/src/catalogue/category/catalogueCategoryDirectoryDialog.component.tsx index 1eab7bf96..57cd94afd 100644 --- a/src/catalogue/category/catalogueCategoryDirectoryDialog.component.tsx +++ b/src/catalogue/category/catalogueCategoryDirectoryDialog.component.tsx @@ -93,8 +93,8 @@ const CatalogueCategoryDirectoryDialog = ( (!targetCategoryLoading || catalogueCurrDirId === null) && catalogueCategoryData !== undefined ) { - const existingCategoryCodes: string[] = catalogueCategoryData.map( - (category) => category.code + const existingCategoryNames: string[] = catalogueCategoryData.map( + (category) => category.name ); copyToCatalogueCategory({ @@ -102,7 +102,7 @@ const CatalogueCategoryDirectoryDialog = ( // Only reason for targetSystem to be undefined here is if not loading at all // which happens when at root targetCategory: targetCategory || null, - existingCategoryCodes: existingCategoryCodes, + existingCategoryNames: existingCategoryNames, }).then((response) => { handleTransferState(response); handleClose(); diff --git a/src/catalogue/items/catalogueItemsTable.component.tsx b/src/catalogue/items/catalogueItemsTable.component.tsx index 9fe3289fd..dfe60d113 100644 --- a/src/catalogue/items/catalogueItemsTable.component.tsx +++ b/src/catalogue/items/catalogueItemsTable.component.tsx @@ -1,12 +1,12 @@ +import AddIcon from '@mui/icons-material/Add'; import BlockIcon from '@mui/icons-material/Block'; +import ClearIcon from '@mui/icons-material/Clear'; import DeleteIcon from '@mui/icons-material/Delete'; +import DriveFileMoveOutlinedIcon from '@mui/icons-material/DriveFileMoveOutlined'; import EditIcon from '@mui/icons-material/Edit'; +import FolderCopyOutlinedIcon from '@mui/icons-material/FolderCopyOutlined'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import SaveAsIcon from '@mui/icons-material/SaveAs'; -import DriveFileMoveOutlinedIcon from '@mui/icons-material/DriveFileMoveOutlined'; -import FolderCopyOutlinedIcon from '@mui/icons-material/FolderCopyOutlined'; -import ClearIcon from '@mui/icons-material/Clear'; -import AddIcon from '@mui/icons-material/Add'; import { Box, Button, @@ -23,25 +23,26 @@ import { MaterialReactTable, useMaterialReactTable, type MRT_ColumnDef, - type MRT_RowSelectionState, type MRT_ColumnFiltersState, + type MRT_RowSelectionState, } from 'material-react-table'; import { MRT_Localization_EN } from 'material-react-table/locales/en'; import React from 'react'; import { Link } from 'react-router-dom'; import { useCatalogueItems } from '../../api/catalogueItem'; +import { useManufacturerIds } from '../../api/manufacturer'; import { CatalogueCategory, CatalogueItem, CatalogueItemPropertyResponse, Manufacturer, } from '../../app.types'; +import { generateUniqueName } from '../../utils'; import CatalogueItemsDetailsPanel from './CatalogueItemsDetailsPanel.component'; +import CatalogueItemDirectoryDialog from './catalogueItemDirectoryDialog.component'; import CatalogueItemsDialog from './catalogueItemsDialog.component'; import DeleteCatalogueItemsDialog from './deleteCatalogueItemDialog.component'; -import { useManufacturerIds } from '../../api/manufacturer'; import ObsoleteCatalogueItemDialog from './obsoleteCatalogueItemDialog.component'; -import CatalogueItemDirectoryDialog from './catalogueItemDirectoryDialog.component'; function findPropertyValue( properties: CatalogueItemPropertyResponse[], @@ -54,20 +55,6 @@ function findPropertyValue( return foundProperty ? foundProperty.value : ''; } -function generateUniqueName( - existingNames: (string | undefined)[], - originalName: string -) { - let newName = originalName; - let copyIndex = 1; - - while (existingNames.includes(newName)) { - newName = `${originalName}_copy_${copyIndex}`; - copyIndex++; - } - - return newName; -} export interface CatalogueItemsTableProps { parentInfo: CatalogueCategory; dense: boolean; @@ -130,8 +117,7 @@ const CatalogueItemsTable = (props: CatalogueItemsTableProps) => { string | null >(null); - const catalogueCategoryNames: (string | undefined)[] = - data?.map((item) => item.name) || []; + const catalogueCategoryNames: string[] = data?.map((item) => item.name) || []; const noResultsTxt = dense ? 'No catalogue items found' @@ -590,8 +576,8 @@ const CatalogueItemsTable = (props: CatalogueItemsTableProps) => { name: itemDialogType === 'save as' ? generateUniqueName( - catalogueCategoryNames, - row.original.name + row.original.name, + catalogueCategoryNames ) : row.original.name, } diff --git a/src/systems/systemDirectoryDialog.component.tsx b/src/systems/systemDirectoryDialog.component.tsx index 1524e439d..9ce3382f9 100644 --- a/src/systems/systemDirectoryDialog.component.tsx +++ b/src/systems/systemDirectoryDialog.component.tsx @@ -91,14 +91,14 @@ export const SystemDirectoryDialog = (props: SystemDirectoryDialogProps) => { (!targetSystemLoading || parentSystemId === null) && systemsData !== undefined ) { - const existingSystemCodes = systemsData.map((system) => system.code); + const existingSystemNames = systemsData.map((system) => system.name); copyToSystem({ selectedSystems: selectedSystems, // Only reason for targetSystem to be undefined here is if not loading at all // which happens when at root targetSystem: targetSystem || null, - existingSystemCodes: existingSystemCodes, + existingSystemNames: existingSystemNames, }).then((response) => { handleTransferState(response); onChangeSelectedSystems([]); diff --git a/src/utils.test.tsx b/src/utils.test.tsx new file mode 100644 index 000000000..7ae8efdf4 --- /dev/null +++ b/src/utils.test.tsx @@ -0,0 +1,33 @@ +import { generateUniqueName } from './utils'; + +describe('Utility functions', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('generateUniqueName', () => { + it('returns the given name if it is already unique', () => { + const mockName = 'test'; + const result = generateUniqueName(mockName, []); + + expect(result).toEqual(mockName); + }); + + it('returns the a name appended with _copy_1 when the name already exists', () => { + const mockName = 'test'; + const result = generateUniqueName(mockName, [mockName]); + + expect(result).toEqual(`${mockName}_copy_1`); + }); + + it('returns the a name appended with _copy_2 when the name and a copy already exist', () => { + const mockName = 'test'; + const result = generateUniqueName(mockName, [ + mockName, + `${mockName}_copy_1`, + ]); + + expect(result).toEqual(`${mockName}_copy_2`); + }); + }); +}); diff --git a/src/utils.tsx b/src/utils.tsx new file mode 100644 index 000000000..c59510878 --- /dev/null +++ b/src/utils.tsx @@ -0,0 +1,15 @@ +/* Returns a name avoiding duplicates by appending _copy_n for nth copy */ +export const generateUniqueName = ( + name: string, + existingNames: string[] +): string => { + let count = 1; + let newName = name; + + while (existingNames.includes(newName)) { + newName = `${name}_copy_${count}`; + count++; + } + + return newName; +}; From da4cc7e9e416644e26ed0fde8595f0d652858349 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge <83226114+joshuadkitenge@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:19:07 +0000 Subject: [PATCH 031/222] Update cypress/e2e/catalogue/catalogueItems.cy.ts Co-authored-by: Joel Davies <90245114+joelvdavies@users.noreply.github.com> --- cypress/e2e/catalogue/catalogueItems.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/catalogue/catalogueItems.cy.ts b/cypress/e2e/catalogue/catalogueItems.cy.ts index 9475e152c..5b3dd3100 100644 --- a/cypress/e2e/catalogue/catalogueItems.cy.ts +++ b/cypress/e2e/catalogue/catalogueItems.cy.ts @@ -551,7 +551,7 @@ describe('Catalogue Items', () => { cy.findAllByText('Manufacturer Name').should('exist'); }); - it('can navigate to an catalogue items replacement', () => { + it('can navigate to a catalogue items replacement', () => { cy.visit('/catalogue/5'); cy.findAllByRole('link', { name: 'Click here' }).eq(1).click(); From b39cabb4b63bc78534e02e2d2f58ace1a31d5013 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge <83226114+joshuadkitenge@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:19:14 +0000 Subject: [PATCH 032/222] Update cypress/e2e/items.cy.ts Co-authored-by: Joel Davies <90245114+joelvdavies@users.noreply.github.com> --- cypress/e2e/items.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/items.cy.ts b/cypress/e2e/items.cy.ts index 5cf40133a..b21de81ae 100644 --- a/cypress/e2e/items.cy.ts +++ b/cypress/e2e/items.cy.ts @@ -21,7 +21,7 @@ describe('Items', () => { cy.findByText('Older than five years').should('be.visible'); }); - it('adds a item with only mandatory fields', () => { + it('adds an item with only mandatory fields', () => { cy.findByRole('button', { name: 'Add Item' }).click(); cy.startSnoopingBrowserMockedRequest(); From 9a58888c31e42e827c41cf93b8ece66594db8297 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge <83226114+joshuadkitenge@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:19:21 +0000 Subject: [PATCH 033/222] Update cypress/e2e/items.cy.ts Co-authored-by: Joel Davies <90245114+joelvdavies@users.noreply.github.com> --- cypress/e2e/items.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/items.cy.ts b/cypress/e2e/items.cy.ts index b21de81ae..c87eacf33 100644 --- a/cypress/e2e/items.cy.ts +++ b/cypress/e2e/items.cy.ts @@ -57,7 +57,7 @@ describe('Items', () => { }); }); - it('adds a item with all fields altered', () => { + it('adds an item with all fields altered', () => { cy.findByRole('button', { name: 'Add Item' }).click(); cy.findByLabelText('Serial number').type('test1234'); From 1a75823caaf34082d8406c2541e983277b739a0e Mon Sep 17 00:00:00 2001 From: Joshua Kitenge <83226114+joshuadkitenge@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:19:29 +0000 Subject: [PATCH 034/222] Update src/api/Item.test.tsx Co-authored-by: Joel Davies <90245114+joelvdavies@users.noreply.github.com> --- src/api/Item.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/Item.test.tsx b/src/api/Item.test.tsx index ba75a889d..1deaff2f8 100644 --- a/src/api/Item.test.tsx +++ b/src/api/Item.test.tsx @@ -46,7 +46,7 @@ describe('catalogue items api functions', () => { ], }; }); - it('posts a request to add a user session and returns successful response', async () => { + it('posts a request to add an item and returns successful response', async () => { const { result } = renderHook(() => useAddItem(), { wrapper: hooksWrapperWithProviders(), }); From a74bc3652c6202547a42d1dc6ad7b9bbdd0ca55a Mon Sep 17 00:00:00 2001 From: Joshua Kitenge <83226114+joshuadkitenge@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:19:42 +0000 Subject: [PATCH 035/222] Update src/catalogue/items/catalogueItemsTable.component.tsx Co-authored-by: Joel Davies <90245114+joelvdavies@users.noreply.github.com> --- src/catalogue/items/catalogueItemsTable.component.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/catalogue/items/catalogueItemsTable.component.tsx b/src/catalogue/items/catalogueItemsTable.component.tsx index 659d906f3..3d0360cf9 100644 --- a/src/catalogue/items/catalogueItemsTable.component.tsx +++ b/src/catalogue/items/catalogueItemsTable.component.tsx @@ -177,7 +177,6 @@ const CatalogueItemsTable = (props: CatalogueItemsTableProps) => { }, { header: 'View Items', - // accessorFn: (row) => row.name, size: 200, Cell: ({ row }) => ( Date: Tue, 9 Jan 2024 12:19:52 +0000 Subject: [PATCH 036/222] Update src/items/itemDialog.component.test.tsx Co-authored-by: Joel Davies <90245114+joelvdavies@users.noreply.github.com> --- src/items/itemDialog.component.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/items/itemDialog.component.test.tsx b/src/items/itemDialog.component.test.tsx index 16659ea13..28bf6010a 100644 --- a/src/items/itemDialog.component.test.tsx +++ b/src/items/itemDialog.component.test.tsx @@ -135,7 +135,7 @@ describe('ItemDialog', () => { axiosPostSpy = jest.spyOn(axios, 'post'); }); - it('adds a item with just the default values', async () => { + it('adds an item with just the default values', async () => { createView(); const saveButton = screen.getByRole('button', { name: 'Save' }); await user.click(saveButton); From 1927bfd8e348fa8c3a44931e2b3c87d4fc0cd576 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge <83226114+joshuadkitenge@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:20:01 +0000 Subject: [PATCH 037/222] Update src/items/itemDialog.component.test.tsx Co-authored-by: Joel Davies <90245114+joelvdavies@users.noreply.github.com> --- src/items/itemDialog.component.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/items/itemDialog.component.test.tsx b/src/items/itemDialog.component.test.tsx index 28bf6010a..e5fac6572 100644 --- a/src/items/itemDialog.component.test.tsx +++ b/src/items/itemDialog.component.test.tsx @@ -160,7 +160,7 @@ describe('ItemDialog', () => { }); }); - it('adds a item (all input vales)', async () => { + it('adds an item (all input vales)', async () => { createView(); await modifyValues({ serialNumber: 'test12', From 233a8b217b128de89aa53c2bb516d593ee20c4f4 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge <83226114+joshuadkitenge@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:20:10 +0000 Subject: [PATCH 038/222] Update src/items/itemDialog.component.test.tsx Co-authored-by: Joel Davies <90245114+joelvdavies@users.noreply.github.com> --- src/items/itemDialog.component.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/items/itemDialog.component.test.tsx b/src/items/itemDialog.component.test.tsx index e5fac6572..3e76ea8a6 100644 --- a/src/items/itemDialog.component.test.tsx +++ b/src/items/itemDialog.component.test.tsx @@ -202,7 +202,7 @@ describe('ItemDialog', () => { }); }, 10000); - it('adds a item (case empty string with spaces returns null and change property boolean values)', async () => { + it('adds an item (case empty string with spaces returns null and change property boolean values)', async () => { createView(); await modifyValues({ serialNumber: ' ', From b731ac707961cd262c9e751ad1f8e056a460259b Mon Sep 17 00:00:00 2001 From: Matteo Guarnaccia Date: Tue, 9 Jan 2024 13:13:21 +0000 Subject: [PATCH 039/222] e2e tests for save as #172 --- cypress/e2e/catalogue/catalogueCategory.cy.ts | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/catalogue/catalogueCategory.cy.ts b/cypress/e2e/catalogue/catalogueCategory.cy.ts index 7c474133e..ba19b6881 100644 --- a/cypress/e2e/catalogue/catalogueCategory.cy.ts +++ b/cypress/e2e/catalogue/catalogueCategory.cy.ts @@ -66,19 +66,52 @@ describe('Catalogue Category', () => { }); }); - it('opens actions menu', () => { + it('opens actions menu and then closes', () => { cy.findByRole('button', { name: 'actions Motion catalogue category button', }).click(); cy.findByRole('menuitem', { - name: 'delete Motion catalogue category button', + name: 'edit Motion catalogue category button', + }).should('be.visible'); + cy.findByRole('menuitem', { + name: 'save as Motion catalogue category button', }).should('be.visible'); cy.findByRole('menuitem', { name: 'delete Motion catalogue category button', + }) + .should('be.visible') + .click(); + + cy.findByText('Cancel').click(); + + cy.findByRole('button', { + name: 'actions Motion catalogue category button', }).should('be.visible'); }); + it('"save as" a catalogue category', () => { + cy.findByRole('button', { + name: 'actions Motion catalogue category button', + }).click(); + cy.findByText('Save as').click(); + + cy.startSnoopingBrowserMockedRequest(); + + cy.findByRole('button', { name: 'Save' }).click(); + + cy.findBrowserMockedRequests({ + method: 'POST', + url: '/v1/catalogue-categories', + }).should((patchRequests) => { + expect(patchRequests.length).equal(1); + const request = patchRequests[0]; + expect(JSON.stringify(request.body)).equal( + '{"name":"Motion_copy_1","is_leaf":false}' + ); + }); + }); + it('displays error message when user tries to delete a catalogue category that has children elements', () => { cy.findByRole('button', { name: 'actions Motion catalogue category button', From 87f7a3365cdc1517afd67f54e46082d9bf51fa82 Mon Sep 17 00:00:00 2001 From: Matteo Guarnaccia Date: Tue, 9 Jan 2024 13:20:02 +0000 Subject: [PATCH 040/222] requested changes --- .../category/catalogueCard.component.test.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/catalogue/category/catalogueCard.component.test.tsx b/src/catalogue/category/catalogueCard.component.test.tsx index d1eb4e2e2..b469f2696 100644 --- a/src/catalogue/category/catalogueCard.component.test.tsx +++ b/src/catalogue/category/catalogueCard.component.test.tsx @@ -35,7 +35,7 @@ describe('Catalogue Card', () => { expect(screen.getByText('Beam Characterization')).toBeInTheDocument(); }); - it('opens the actions menu', async () => { + it.only('opens the actions menu and closes it', async () => { createView(); const actionsButton = screen.getByRole('button', { name: 'actions Beam Characterization catalogue category button', @@ -46,12 +46,26 @@ describe('Catalogue Card', () => { name: 'edit Beam Characterization catalogue category button', }); + const saveAsButton = screen.getByRole('menuitem', { + name: 'save as Beam Characterization catalogue category button', + }); + const deleteButton = screen.getByRole('menuitem', { name: 'delete Beam Characterization catalogue category button', }); expect(editButton).toBeVisible(); expect(deleteButton).toBeVisible(); + expect(saveAsButton).toBeVisible(); + + await user.click(editButton); + await user.click( + screen.getByRole('button', { + name: 'actions Beam Characterization catalogue category button', + }) + ); + + expect(editButton).not.toBeVisible(); }); it('opens the delete dialog', async () => { From 142acc0bdd44e2cbf1a281b871825262e645b475 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Tue, 9 Jan 2024 13:42:47 +0000 Subject: [PATCH 041/222] fix timezone issues --- src/app.types.tsx | 4 ++- src/items/itemDialog.component.test.tsx | 2 +- src/items/itemDialog.component.tsx | 40 ++++++++++++++----------- src/mocks/handlers.ts | 2 +- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/app.types.tsx b/src/app.types.tsx index b228552e8..417fc0217 100644 --- a/src/app.types.tsx +++ b/src/app.types.tsx @@ -225,7 +225,9 @@ export interface ItemDetails { notes: string | null; } export type ItemDetailsPlaceholder = { - [K in keyof ItemDetails]: string | null; + [K in keyof ItemDetails]: K extends 'delivered_date' | 'warranty_end_date' + ? Date | null + : string | null; }; export interface AddItem extends ItemDetails { diff --git a/src/items/itemDialog.component.test.tsx b/src/items/itemDialog.component.test.tsx index 3e76ea8a6..2f5623e9e 100644 --- a/src/items/itemDialog.component.test.tsx +++ b/src/items/itemDialog.component.test.tsx @@ -306,7 +306,7 @@ describe('ItemDialog', () => { it('displays warning message when an unknown error occurs', async () => { createView(); await modifyValues({ - serialNumber: 'error', + serialNumber: 'Error 500', }); const saveButton = screen.getByRole('button', { name: 'Save' }); await user.click(saveButton); diff --git a/src/items/itemDialog.component.tsx b/src/items/itemDialog.component.tsx index 98bce5a09..40d24f910 100644 --- a/src/items/itemDialog.component.tsx +++ b/src/items/itemDialog.component.tsx @@ -32,9 +32,9 @@ import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import { matchCatalogueItemProperties } from '../catalogue/catalogue.component'; import { useAddItem } from '../api/item'; import { AxiosError } from 'axios'; -function isValidDateTime(date: string | null) { +function isValidDateTime(date: Date | null) { // Attempt to create a Date object from the string - let dateObj = new Date(date ?? ''); + let dateObj = date ?? new Date(''); // Check if the Date object is valid and the string was successfully parsed // Also, check if the original string is not equal to 'Invalid Date' @@ -155,19 +155,29 @@ function ItemDialog(props: ItemDialogProps) { }; const handleItemDetails = ( field: keyof ItemDetailsPlaceholder, - value: string | null + value: string | Date | null ) => { const updatedItemDetails = { ...itemDetails }; - if (value?.trim() === '') { - updatedItemDetails[field] = null; - } else { - updatedItemDetails[field] = value as string; + switch (field) { + case 'delivered_date': + case 'warranty_end_date': + updatedItemDetails[field] = value as Date | null; + break; + default: + if ( + value === null || + (typeof value === 'string' && value.trim() === '') + ) { + updatedItemDetails[field] = null; + } else { + updatedItemDetails[field] = value as string; + } + break; } setItemDetails(updatedItemDetails); }; - const handleClose = React.useCallback(() => { onClose(); setItemDetails({ @@ -289,14 +299,14 @@ function ItemDialog(props: ItemDialogProps) { warranty_end_date: itemDetails.warranty_end_date && isValidDateTime(itemDetails.warranty_end_date) - ? new Date(itemDetails.warranty_end_date).toISOString() ?? null + ? itemDetails.warranty_end_date.toISOString() ?? null : null, asset_number: itemDetails.asset_number, serial_number: itemDetails.serial_number, delivered_date: itemDetails.delivered_date && isValidDateTime(itemDetails.delivered_date) - ? new Date(itemDetails.delivered_date).toISOString() ?? null + ? itemDetails.delivered_date.toISOString() ?? null : null, notes: itemDetails.notes, }; @@ -379,10 +389,7 @@ function ItemDialog(props: ItemDialogProps) { : null } onChange={(date) => - handleItemDetails( - 'warranty_end_date', - date ? date.toString() : null - ) + handleItemDetails('warranty_end_date', date ? date : null) } slots={{ textField: CustomTextField }} slotProps={{ @@ -399,10 +406,7 @@ function ItemDialog(props: ItemDialogProps) { : null } onChange={(date) => - handleItemDetails( - 'delivered_date', - date ? date.toString() : null - ) + handleItemDetails('delivered_date', date ? date : null) } slotProps={{ actionBar: { actions: ['clear'] }, diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 42640ca47..80e4fc605 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -482,7 +482,7 @@ export const handlers = [ rest.post('/v1/items/', async (req, res, ctx) => { const body = (await req.json()) as AddItem; - if (body.serial_number === 'error') { + if (body.serial_number === 'Error 500') { return res(ctx.status(500), ctx.json('')); } From dc28eb39f610e5516917c772f9b5f44f4950c2ea Mon Sep 17 00:00:00 2001 From: Matteo Guarnaccia Date: Tue, 9 Jan 2024 13:52:02 +0000 Subject: [PATCH 042/222] improved code coverage #172 --- src/catalogue/category/catalogueCard.component.test.tsx | 1 - src/catalogue/category/catalogueCard.component.tsx | 8 +++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/catalogue/category/catalogueCard.component.test.tsx b/src/catalogue/category/catalogueCard.component.test.tsx index b469f2696..5bea332e6 100644 --- a/src/catalogue/category/catalogueCard.component.test.tsx +++ b/src/catalogue/category/catalogueCard.component.test.tsx @@ -64,7 +64,6 @@ describe('Catalogue Card', () => { name: 'actions Beam Characterization catalogue category button', }) ); - expect(editButton).not.toBeVisible(); }); diff --git a/src/catalogue/category/catalogueCard.component.tsx b/src/catalogue/category/catalogueCard.component.tsx index 5914f1422..b34933105 100644 --- a/src/catalogue/category/catalogueCard.component.tsx +++ b/src/catalogue/category/catalogueCard.component.tsx @@ -39,6 +39,10 @@ function CatalogueCard(props: CatalogueCardProps) { onToggleSelect(catalogueCategory); }; + const handleActionsClose = () => { + setMenuOpen(false); + }; + const [menuOpen, setMenuOpen] = React.useState(false); const [anchorEl, setAnchorEl] = React.useState(null); @@ -102,9 +106,7 @@ function CatalogueCard(props: CatalogueCardProps) { { - setMenuOpen(false); - }} + onClose={handleActionsClose} > Date: Tue, 9 Jan 2024 17:09:51 +0000 Subject: [PATCH 043/222] address review comments --- cypress/e2e/items.cy.ts | 23 +++++++++++- global-setup.js | 3 ++ package.json | 7 ++-- src/items/itemDialog.component.test.tsx | 50 ++++++++++++++++++++++++- src/items/itemDialog.component.tsx | 40 +++++++++++++------- 5 files changed, 104 insertions(+), 19 deletions(-) create mode 100644 global-setup.js diff --git a/cypress/e2e/items.cy.ts b/cypress/e2e/items.cy.ts index c87eacf33..87174c9e7 100644 --- a/cypress/e2e/items.cy.ts +++ b/cypress/e2e/items.cy.ts @@ -125,9 +125,30 @@ describe('Items', () => { cy.findByLabelText('Broken *').click(); cy.findByRole('option', { name: 'None' }).click(); + cy.findAllByText('Date format: dd/MM/yyyy').should('have.length', 2); + cy.findByLabelText('Warranty end date').clear(); + cy.findByLabelText('Delivered date').clear(); + + cy.findByLabelText('Warranty end date').type('12/02/4000'); + cy.findByLabelText('Delivered date').type('12/02/4000'); + cy.findAllByText('Exceeded maximum date').should('have.length', 2); + + cy.findByLabelText('Warranty end date').type('12/02/2000'); + cy.findByLabelText('Delivered date').type('12/02/2000'); + cy.findByText('Exceeded maximum date').should('not.exist'); + cy.findByText('Date format: dd/MM/yyyy').should('not.exist'); + cy.findByRole('button', { name: 'Save' }).click(); cy.findAllByText('This field is mandatory').should('have.length', 2); - cy.findAllByText('Date format: dd/MM/yyyy').should('have.length', 2); + cy.findByText('Please select either True or False').should('exist'); + + cy.findByLabelText('Resolution (megapixels) *').type('test'); + cy.findByLabelText('Sensor Type *').type('test'); + cy.findByLabelText('Broken *').click(); + cy.findByRole('option', { name: 'True' }).click(); + + cy.findByText('Please select either True or False').should('not.exist'); + cy.findAllByText('This field is mandatory').should('not.exist'); }); }); diff --git a/global-setup.js b/global-setup.js new file mode 100644 index 000000000..6e1fbf41d --- /dev/null +++ b/global-setup.js @@ -0,0 +1,3 @@ +module.exports = async () => { + process.env.TZ = 'UTC'; +}; diff --git a/package.json b/package.json index 5382d2274..76ba85b0b 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,8 @@ "e2e:serve": "yarn build:e2e && node ./server/e2e-test-server.js", "e2e:interactive": "start-server-and-test e2e:serve http://localhost:3000 cy:open", "e2e": "start-server-and-test e2e:serve http://localhost:3000 cy:run", - "cy:open": "cypress open", - "cy:run": "cypress run" + "cy:open": "TZ=UTC cypress open", + "cy:run": "TZ=UTC cypress run" }, "eslintConfig": { "extends": [ @@ -68,7 +68,8 @@ "resetMocks": false, "transformIgnorePatterns": [ "node_modules/(?!axios)" - ] + ], + "globalSetup": "./global-setup.js" }, "packageManager": "yarn@3.7.0", "devDependencies": { diff --git a/src/items/itemDialog.component.test.tsx b/src/items/itemDialog.component.test.tsx index 2f5623e9e..400bfb3d6 100644 --- a/src/items/itemDialog.component.test.tsx +++ b/src/items/itemDialog.component.test.tsx @@ -160,7 +160,7 @@ describe('ItemDialog', () => { }); }); - it('adds an item (all input vales)', async () => { + it('adds an item (all input values)', async () => { createView(); await modifyValues({ serialNumber: 'test12', @@ -271,9 +271,26 @@ describe('ItemDialog', () => { expect(mandatoryFieldBooleanHelperText).toBeInTheDocument(); expect(mandatoryFieldHelperText.length).toBe(2); + + await modifyValues({ + broken: 'False', + resolution: '12', + frameRate: '60', + sensorType: 'IO', + sensorBrand: 'pixel', + }); + + await user.type(screen.getByLabelText('Resolution (megapixels) *'), '12'); + await user.type(screen.getByLabelText('Sensor Type *'), 'test'); + + expect(mandatoryFieldBooleanHelperText).not.toBeInTheDocument(); + + expect( + screen.queryByText('This field is mandatory') + ).not.toBeInTheDocument(); }, 10000); - it('displays error message when property values type is incorrect', async () => { + it.only('displays error message when property values type is incorrect', async () => { createView(); await modifyValues({ serialNumber: ' ', @@ -293,6 +310,28 @@ describe('ItemDialog', () => { ); expect(validDateHelperText.length).toEqual(2); + await modifyValues({ + warrantyEndDate: '17/02/4000', + deliveredDate: '23/09/4000', + }); + + const validDateMaxHelperText = screen.getAllByText( + 'Exceeded maximum date' + ); + expect(validDateMaxHelperText.length).toEqual(2); + + await modifyValues({ + warrantyEndDate: '17/02/2000', + deliveredDate: '23/09/2000', + }); + + expect( + screen.queryByText('Exceeded maximum date') + ).not.toBeInTheDocument(); + expect( + screen.queryByText('This field is mandatory') + ).not.toBeInTheDocument(); + const saveButton = screen.getByRole('button', { name: 'Save' }); await user.click(saveButton); @@ -301,6 +340,13 @@ describe('ItemDialog', () => { ); expect(validNumberHelperText).toBeInTheDocument(); + + await modifyValues({ + resolution: '12', + }); + expect( + screen.queryByText('Please enter a valid number') + ).not.toBeInTheDocument(); }, 10000); it('displays warning message when an unknown error occurs', async () => { diff --git a/src/items/itemDialog.component.tsx b/src/items/itemDialog.component.tsx index 40d24f910..8ccc4140e 100644 --- a/src/items/itemDialog.component.tsx +++ b/src/items/itemDialog.component.tsx @@ -32,19 +32,31 @@ import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import { matchCatalogueItemProperties } from '../catalogue/catalogue.component'; import { useAddItem } from '../api/item'; import { AxiosError } from 'axios'; +const maxYear = 2100; function isValidDateTime(date: Date | null) { // Attempt to create a Date object from the string let dateObj = date ?? new Date(''); // Check if the Date object is valid and the string was successfully parsed // Also, check if the original string is not equal to 'Invalid Date' - return !isNaN(dateObj.getTime()) && dateObj.toString() !== 'Invalid Date'; + // Check if the date is larger the year 2100. The maximum date of the date picker + return ( + !isNaN(dateObj.getTime()) && + dateObj.toString() !== 'Invalid Date' && + !(Number(dateObj.toLocaleDateString().split('/')[2]) >= maxYear) + ); } const CustomTextField: React.FC = (renderProps) => { const { id, ...inputProps } = renderProps.inputProps ?? {}; let helperText = 'Date format: dd/MM/yyyy'; + if ( + renderProps.value && + Number((renderProps.value as string).split('/')[2]) >= maxYear + ) { + helperText = 'Exceeded maximum date'; + } return ( handleItemDetails('warranty_end_date', date ? date : null) } @@ -400,11 +408,7 @@ function ItemDialog(props: ItemDialogProps) { handleItemDetails('delivered_date', date ? date : null) } @@ -615,6 +619,16 @@ function ItemDialog(props: ItemDialogProps) { variant="outlined" sx={{ width: '50%', mx: 1 }} onClick={handleAddItem} + disabled={ + catchAllError || + propertyErrors.some((value) => { + return value === true; + }) || + (!!itemDetails.warranty_end_date && + !isValidDateTime(itemDetails.warranty_end_date)) || + (!!itemDetails.delivered_date && + !isValidDateTime(itemDetails.delivered_date)) + } > Save From 6a870054e3bd1ef4d9c765ff307d5f903ddb1888 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Tue, 9 Jan 2024 17:15:58 +0000 Subject: [PATCH 044/222] remove it.only --- src/items/itemDialog.component.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/items/itemDialog.component.test.tsx b/src/items/itemDialog.component.test.tsx index 400bfb3d6..f734e7f9e 100644 --- a/src/items/itemDialog.component.test.tsx +++ b/src/items/itemDialog.component.test.tsx @@ -290,7 +290,7 @@ describe('ItemDialog', () => { ).not.toBeInTheDocument(); }, 10000); - it.only('displays error message when property values type is incorrect', async () => { + it('displays error message when property values type is incorrect', async () => { createView(); await modifyValues({ serialNumber: ' ', From a345d40b1cc4b23b208524ac365be41a0d9e97e6 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Wed, 10 Jan 2024 09:23:09 +0000 Subject: [PATCH 045/222] Add scroll bar behaviour and layout changes #125 --- src/systems/systems.component.tsx | 186 ++++++++++-------------------- src/utils.tsx | 30 +++++ src/view/viewTabs.component.tsx | 32 ++--- 3 files changed, 107 insertions(+), 141 deletions(-) diff --git a/src/systems/systems.component.tsx b/src/systems/systems.component.tsx index 250524883..a69eccd3f 100644 --- a/src/systems/systems.component.tsx +++ b/src/systems/systems.component.tsx @@ -5,7 +5,6 @@ import DeleteIcon from '@mui/icons-material/Delete'; import DriveFileMoveOutlinedIcon from '@mui/icons-material/DriveFileMoveOutlined'; import EditIcon from '@mui/icons-material/Edit'; import FolderCopyOutlinedIcon from '@mui/icons-material/FolderCopyOutlined'; -import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; import SaveAsIcon from '@mui/icons-material/SaveAs'; import { Box, @@ -17,7 +16,6 @@ import { LinearProgress, ListItemIcon, ListItemText, - Menu, MenuItem, Stack, Table, @@ -29,16 +27,17 @@ import { } from '@mui/material'; import { MRT_ColumnDef, - MRT_GlobalFilterTextField, + // To resolve react/jsx-pascal-case + MRT_GlobalFilterTextField as MRTGlobalFilterTextField, MRT_RowSelectionState, - MRT_TableBodyCellValue, - MRT_TablePagination, + MRT_TableBodyCellValue as MRTTableBodyCellValue, useMaterialReactTable, } from 'material-react-table'; import React from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useSystems, useSystemsBreadcrumbs } from '../api/systems'; import { System } from '../app.types'; +import { getPageHeightCalc } from '../utils'; import Breadcrumbs from '../view/breadcrumbs.component'; import { DeleteSystemDialog } from './deleteSystemDialog.component'; import SystemDetails from './systemDetails.component'; @@ -142,85 +141,6 @@ const CopySystemsButton = (props: { type MenuDialogType = SystemDialogType | 'delete'; -/* TODO: Remove this and use table menu items */ -const SubsystemMenu = (props: { - subsystem: System; - onOpen: () => void; - onItemClicked: (type: MenuDialogType) => void; -}) => { - const [anchorEl, setAnchorEl] = React.useState(null); - const open = Boolean(anchorEl); - - const handleOpen = ( - event: React.MouseEvent - ) => { - props.onOpen(); - setAnchorEl(event.currentTarget); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - const handleClick = (type: MenuDialogType) => { - props.onItemClicked(type); - handleClose(); - }; - - return ( - <> - - - - - handleClick('edit')} - > - - - - Edit - - handleClick('save as')} - > - - - - Save as - - handleClick('delete')} - > - - - - Delete - - - - ); -}; - /* Returns the system id from the location pathname (null when not found) */ export const useSystemId = (): string | null => { // Navigation setup @@ -234,7 +154,10 @@ export const useSystemId = (): string | null => { }; const columns: MRT_ColumnDef[] = [ - { accessorKey: 'name', header: 'Name' }, + { + accessorKey: 'name', + header: 'Name', + }, ]; function Systems() { @@ -343,19 +266,16 @@ function Systems() { return ( <> - + {systemsBreadcrumbsLoading && systemId !== null ? ( ) : ( @@ -398,7 +318,15 @@ function Systems() { )} - + {subsystemsDataLoading ? ( - + - + @@ -442,54 +378,50 @@ function Systems() { hover={true} sx={{ cursor: 'pointer' }} > - {row.getVisibleCells().map((cell) => { - console.log(cell.column.id); - return ( - ( + - - - ); - })} + ? 1.5 + : 0, + width: + // Make name take up as much space as possible to make other cells + // as small as possible + cell.column.id === 'name' + ? '100%' + : undefined, + }} + > + + + ))} ))}
-
)}
- + -
+
setMenuDialogType(undefined)} diff --git a/src/utils.tsx b/src/utils.tsx index c59510878..f9ccf7792 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -13,3 +13,33 @@ export const generateUniqueName = ( return newName; }; + +/* Returns whether running in development mode */ +export const isRunningInDevelopment = (): boolean => { + return process.env.NODE_ENV !== 'production'; +}; + +/* Returns a calc function giving the page height excluding SciGateway related components + (header and footer) to use for CSS e.g. giving 48px it will return the calc(page height + - all SciGateway related heights - 48px)*/ +export const getSciGatewayPageHeightCalc = ( + additionalSubtraction?: string +): string => { + // Page height - unknown - app bar height - footer height - additional + return `calc(100vh - 8px - 64px - 24px - ${additionalSubtraction || ''})`; +}; + +/* Returns a calc function giving the page height excluding the optional view tabs component + that only appears in development */ +export const getPageHeightCalc = (additionalSubtraction?: string): string => { + // SciGateway heights - view tabs (if in development) - additional + let newAdditional = undefined; + + if (isRunningInDevelopment()) newAdditional = '48px'; + if (additionalSubtraction !== undefined) { + if (newAdditional === undefined) newAdditional = additionalSubtraction; + else newAdditional += ' - ' + additionalSubtraction; + } + + return getSciGatewayPageHeightCalc(newAdditional); +}; diff --git a/src/view/viewTabs.component.tsx b/src/view/viewTabs.component.tsx index c862bfcea..4d8b3abc5 100644 --- a/src/view/viewTabs.component.tsx +++ b/src/view/viewTabs.component.tsx @@ -1,16 +1,16 @@ -import React from 'react'; -import { styled } from '@mui/material/styles'; -import Tabs from '@mui/material/Tabs'; -import Tab from '@mui/material/Tab'; import Box from '@mui/material/Box'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import { styled } from '@mui/material/styles'; +import React from 'react'; +import { Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import { TabValue } from '../app.types'; -import { useNavigate, useLocation } from 'react-router-dom'; -import { Routes, Route } from 'react-router-dom'; import Catalogue from '../catalogue/catalogue.component'; -import Systems from '../systems/systems.component'; -import Manufacturer from '../manufacturer/manufacturer.component'; import CatalogueItemsLandingPage from '../catalogue/items/catalogueItemsLandingPage.component'; +import Manufacturer from '../manufacturer/manufacturer.component'; import ManufacturerLandingPage from '../manufacturer/manufacturerLandingPage.component'; +import Systems from '../systems/systems.component'; +import { getSciGatewayPageHeightCalc, isRunningInDevelopment } from '../utils'; export const paths = { home: '/', @@ -107,9 +107,14 @@ function ViewTabs() { ); return ( - - {process.env.NODE_ENV !== 'production' ? ( - + + {isRunningInDevelopment() ? ( + <> {routing} - + ) : ( routing )} From e30f4873bddde9670b8b657b1a2445ee602bd016 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Wed, 10 Jan 2024 09:59:00 +0000 Subject: [PATCH 046/222] Add back pagination component #125 --- src/systems/systems.component.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/systems/systems.component.tsx b/src/systems/systems.component.tsx index a69eccd3f..fcb4ad4d7 100644 --- a/src/systems/systems.component.tsx +++ b/src/systems/systems.component.tsx @@ -26,11 +26,12 @@ import { Typography, } from '@mui/material'; import { - MRT_ColumnDef, // To resolve react/jsx-pascal-case MRT_GlobalFilterTextField as MRTGlobalFilterTextField, - MRT_RowSelectionState, MRT_TableBodyCellValue as MRTTableBodyCellValue, + MRT_TablePagination as MRTTablePagination, + MRT_ColumnDef, + MRT_RowSelectionState, useMaterialReactTable, } from 'material-react-table'; import React from 'react'; @@ -209,10 +210,15 @@ function Systems() { positionActionsColumn: 'last', paginationDisplayMode: 'pages', muiPaginationProps: { - showRowsPerPage: false, + showRowsPerPage: true, + rowsPerPageOptions: [10, 25, 50], + showFirstButton: false, + showLastButton: false, + size: 'small', }, initialState: { showGlobalFilter: true, + pagination: { pageSize: 10, pageIndex: 0 }, }, onRowSelectionChange: setRowSelection, state: { rowSelection: rowSelection }, @@ -322,7 +328,7 @@ function Systems() { item xs={12} md={2} - minWidth="300px" + minWidth="320px" textAlign="left" padding={1} paddingBottom={0} @@ -354,7 +360,7 @@ function Systems() { marginTop: 1, marginBottom: 'auto', flexWrap: 'no-wrap', - // Breadcrumbs and header + // Breadcrumbs and header - pagination component maxHeight: getPageHeightCalc('130px'), }} > @@ -413,6 +419,7 @@ function Systems() { + )} From 1dc8b7fd7458a67d894df9f08f874284a3da83aa Mon Sep 17 00:00:00 2001 From: Matteo Guarnaccia Date: Wed, 10 Jan 2024 10:08:17 +0000 Subject: [PATCH 047/222] changes to address in landing page and details panel #218 --- .../CatalogueItemsDetailsPanel.component.tsx | 34 ++++++------------- .../catalogueItemsLandingPage.component.tsx | 22 +----------- 2 files changed, 11 insertions(+), 45 deletions(-) diff --git a/src/catalogue/items/CatalogueItemsDetailsPanel.component.tsx b/src/catalogue/items/CatalogueItemsDetailsPanel.component.tsx index a55e3f5a6..29922be10 100644 --- a/src/catalogue/items/CatalogueItemsDetailsPanel.component.tsx +++ b/src/catalogue/items/CatalogueItemsDetailsPanel.component.tsx @@ -237,37 +237,23 @@ function CatalogueItemsDetailsPanel(props: CatalogueItemsDetailsPanelProps) { )}
- - - Manufacturer Address Line + + + Address - + {manufacturerData?.address.address_line} - - - Manufacturer Town - - {manufacturerData?.address.town ?? 'None'} + + {manufacturerData?.address.town} - - - Manufacturer County - - {manufacturerData?.address.county ?? 'None'} + + {manufacturerData?.address.county} - - - Manufacturer Country - + {manufacturerData?.address.country} - - - - Manufacturer Post/Zip code - - + {manufacturerData?.address.postcode} diff --git a/src/catalogue/items/catalogueItemsLandingPage.component.tsx b/src/catalogue/items/catalogueItemsLandingPage.component.tsx index ce86efe0f..8ad45fe3c 100644 --- a/src/catalogue/items/catalogueItemsLandingPage.component.tsx +++ b/src/catalogue/items/catalogueItemsLandingPage.component.tsx @@ -374,40 +374,20 @@ function CatalogueItemsLandingPage() { - Address Line + Address {manufacturer?.address.address_line} - - - - Town - {manufacturer?.address.town} - - - - County - {manufacturer?.address.county} - - - - Country - {manufacturer?.address.country} - - - - Post/Zip code - {manufacturer?.address.postcode} From f06cecde5b899edd9b00e7391d26b671e19b76c3 Mon Sep 17 00:00:00 2001 From: Matteo Guarnaccia Date: Wed, 10 Jan 2024 10:45:25 +0000 Subject: [PATCH 048/222] fixed manufacturer landing page styling #218 --- .../manufacturerLandingPage.component.tsx | 119 +++++++++--------- 1 file changed, 60 insertions(+), 59 deletions(-) diff --git a/src/manufacturer/manufacturerLandingPage.component.tsx b/src/manufacturer/manufacturerLandingPage.component.tsx index 22d059a11..9a565f9fd 100644 --- a/src/manufacturer/manufacturerLandingPage.component.tsx +++ b/src/manufacturer/manufacturerLandingPage.component.tsx @@ -63,66 +63,67 @@ function ManufacturerLandingPage() { {manufacturerData && ( - - - - - {manufacturerData.name} + + + + {manufacturerData.name} + + + + + URL: + + + + {manufacturerData.url && ( + + + {manufacturerData.url} + - - - - - URL: - - - - {manufacturerData.url && ( - - - {manufacturerData.url} - - - )} - - - Telephone number: - - - - {manufacturerData.telephone} - - - - Address: - - - - {manufacturerData.address.address_line} - - - {manufacturerData.address.town} - - - {manufacturerData.address.county} - - - {manufacturerData.address.postcode} - - - {manufacturerData.address.country} - - - + )} + + + + Telephone number: + + + + + {manufacturerData.telephone} + + + + + Address: + + + + + {manufacturerData.address.address_line} + + + {manufacturerData.address.town} + + + {manufacturerData.address.county} + + + {manufacturerData.address.postcode} + + + {manufacturerData.address.country} + )} From b4354ce369ca084cff8cde061755c799bcd475b4 Mon Sep 17 00:00:00 2001 From: MatteoGuarnaccia5 Date: Wed, 10 Jan 2024 10:52:17 +0000 Subject: [PATCH 049/222] fixed snapshots #218 --- ...eItemsDetailsPanel.component.test.tsx.snap | 240 +++--------------- 1 file changed, 40 insertions(+), 200 deletions(-) diff --git a/src/catalogue/items/__snapshots__/CatalogueItemsDetailsPanel.component.test.tsx.snap b/src/catalogue/items/__snapshots__/CatalogueItemsDetailsPanel.component.test.tsx.snap index 8c3ddd185..9fd4a9509 100644 --- a/src/catalogue/items/__snapshots__/CatalogueItemsDetailsPanel.component.test.tsx.snap +++ b/src/catalogue/items/__snapshots__/CatalogueItemsDetailsPanel.component.test.tsx.snap @@ -407,67 +407,27 @@ exports[`Catalogue Items details panel renders details panel correctly (with obs

- Manufacturer Address Line + Address

-

-
-

- Manufacturer Town -

-

- None -

-
-
-

- Manufacturer County -

- None -

-
-
-

- Manufacturer Country -

+ class="MuiTypography-root MuiTypography-body1 MuiTypography-alignLeft css-1adeo1o-MuiTypography-root" + />

-

-

- Manufacturer Post/Zip code -

+ class="MuiTypography-root MuiTypography-body1 MuiTypography-alignLeft css-1adeo1o-MuiTypography-root" + />

@@ -888,67 +848,27 @@ exports[`Catalogue Items details panel renders details panel correctly 1`] = `

- Manufacturer Address Line + Address

-

-
-

- Manufacturer Town -

-

- None -

-
-
-

- Manufacturer County -

- None -

-
-
-

- Manufacturer Country -

+ class="MuiTypography-root MuiTypography-body1 MuiTypography-alignLeft css-1adeo1o-MuiTypography-root" + />

-

-

- Manufacturer Post/Zip code -

+ class="MuiTypography-root MuiTypography-body1 MuiTypography-alignLeft css-1adeo1o-MuiTypography-root" + />

@@ -1378,67 +1298,27 @@ exports[`Catalogue Items details panel renders manufacturer panel correctly 1`]

- Manufacturer Address Line + Address

-

-
-

- Manufacturer Town -

-

- None -

-
-
-

- Manufacturer County -

- None -

-
-
-

- Manufacturer Country -

+ class="MuiTypography-root MuiTypography-body1 MuiTypography-alignLeft css-1adeo1o-MuiTypography-root" + />

-

-

- Manufacturer Post/Zip code -

+ class="MuiTypography-root MuiTypography-body1 MuiTypography-alignLeft css-1adeo1o-MuiTypography-root" + />

@@ -1868,67 +1748,27 @@ exports[`Catalogue Items details panel renders properties panel correctly 1`] =

- Manufacturer Address Line + Address

-

-
-

- Manufacturer Town -

-

- None -

-
-
-

- Manufacturer County -

- None -

-
-
-

- Manufacturer Country -

+ class="MuiTypography-root MuiTypography-body1 MuiTypography-alignLeft css-1adeo1o-MuiTypography-root" + />

-

-

- Manufacturer Post/Zip code -

+ class="MuiTypography-root MuiTypography-body1 MuiTypography-alignLeft css-1adeo1o-MuiTypography-root" + />

From 21210691e68891f5f22fb5c6d98eabfa786ee136 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Wed, 10 Jan 2024 11:09:11 +0000 Subject: [PATCH 050/222] Fix other table views #125 --- src/catalogue/items/catalogueItemsTable.component.tsx | 6 +++--- src/manufacturer/manufacturer.component.tsx | 7 ++++--- src/systems/systems.component.tsx | 4 ++-- src/utils.tsx | 6 ++++-- src/view/viewTabs.component.tsx | 3 ++- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/catalogue/items/catalogueItemsTable.component.tsx b/src/catalogue/items/catalogueItemsTable.component.tsx index dfe60d113..578f3ba53 100644 --- a/src/catalogue/items/catalogueItemsTable.component.tsx +++ b/src/catalogue/items/catalogueItemsTable.component.tsx @@ -37,7 +37,7 @@ import { CatalogueItemPropertyResponse, Manufacturer, } from '../../app.types'; -import { generateUniqueName } from '../../utils'; +import { generateUniqueName, getPageHeightCalc } from '../../utils'; import CatalogueItemsDetailsPanel from './CatalogueItemsDetailsPanel.component'; import CatalogueItemDirectoryDialog from './catalogueItemDirectoryDialog.component'; import CatalogueItemsDialog from './catalogueItemsDialog.component'; @@ -75,8 +75,8 @@ const CatalogueItemsTable = (props: CatalogueItemsTableProps) => { selectedRowState, isItemSelectable, } = props; - // SG header + SG footer + tabs #add breadcrumbs + Mui table V2 - const tableHeight = `calc(100vh - (64px + 36px + 50px + 125px))`; + // Breadcrumbs + Mui table V2 + extra + const tableHeight = getPageHeightCalc('50px + 110px + 32px'); const { data, isLoading } = useCatalogueItems(parentInfo.id); diff --git a/src/manufacturer/manufacturer.component.tsx b/src/manufacturer/manufacturer.component.tsx index 5eb60e9a4..46d42a580 100644 --- a/src/manufacturer/manufacturer.component.tsx +++ b/src/manufacturer/manufacturer.component.tsx @@ -25,6 +25,7 @@ import { useManufacturers } from '../api/manufacturer'; import { Manufacturer } from '../app.types'; import DeleteManufacturerDialog from './deleteManufacturerDialog.component'; import ManufacturerDialog from './manufacturerDialog.component'; +import { getPageHeightCalc } from '../utils'; function ManufacturerComponent() { const { data: ManufacturerData, isLoading: ManufacturerDataLoading } = @@ -37,7 +38,7 @@ function ManufacturerComponent() { Manufacturer | undefined >(undefined); - const tableHeight = `calc(100vh - (64px + 36px + 111px))`; + const tableHeight = getPageHeightCalc('110px'); const columns = React.useMemo[]>(() => { return [ @@ -135,6 +136,7 @@ function ManufacturerComponent() { muiTableBodyRowProps: ({ row }) => { return { component: TableRow, 'aria-label': `${row.original.name} row` }; }, + muiTablePaperProps: { sx: { maxHeight: '100%' } }, muiTableContainerProps: { sx: { height: tableHeight } }, paginationDisplayMode: 'pages', positionToolbarAlertBanner: 'bottom', @@ -228,9 +230,8 @@ function ManufacturerComponent() { }); return ( -
+
- setDeleteManufacturerDialog(false)} diff --git a/src/systems/systems.component.tsx b/src/systems/systems.component.tsx index fcb4ad4d7..9400b4a1c 100644 --- a/src/systems/systems.component.tsx +++ b/src/systems/systems.component.tsx @@ -360,8 +360,8 @@ function Systems() { marginTop: 1, marginBottom: 'auto', flexWrap: 'no-wrap', - // Breadcrumbs and header - pagination component - maxHeight: getPageHeightCalc('130px'), + // Breadcrumbs and rest + maxHeight: getPageHeightCalc('56px + 74px'), }} > { // Page height - unknown - app bar height - footer height - additional - return `calc(100vh - 8px - 64px - 24px - ${additionalSubtraction || ''})`; + return `calc(100vh - 8px - 64px - 24px${ + additionalSubtraction !== undefined ? ` - (${additionalSubtraction})` : '' + })`; }; /* Returns a calc function giving the page height excluding the optional view tabs component @@ -38,7 +40,7 @@ export const getPageHeightCalc = (additionalSubtraction?: string): string => { if (isRunningInDevelopment()) newAdditional = '48px'; if (additionalSubtraction !== undefined) { if (newAdditional === undefined) newAdditional = additionalSubtraction; - else newAdditional += ' - ' + additionalSubtraction; + else newAdditional += ' + ' + additionalSubtraction; } return getSciGatewayPageHeightCalc(newAdditional); diff --git a/src/view/viewTabs.component.tsx b/src/view/viewTabs.component.tsx index 4d8b3abc5..0c5064a0e 100644 --- a/src/view/viewTabs.component.tsx +++ b/src/view/viewTabs.component.tsx @@ -36,9 +36,10 @@ function TabPanel(props: TabPanelProps) { hidden={value !== label} id={`${label}-tabpanel`} aria-labelledby={`${label}-tab`} + style={{ height: '100%' }} {...other} > - {value === label && {children}} + {value === label && {children}}
); } From 2df8c3b3f14bf833398f90222b72796057329599 Mon Sep 17 00:00:00 2001 From: Joel Davies Date: Wed, 10 Jan 2024 12:04:37 +0000 Subject: [PATCH 051/222] Fix existing unit tests #125 --- ...atalogueItemsTable.component.test.tsx.snap | 4 +-- .../manufacturer.component.test.tsx.snap | 6 ++--- .../systemDirectoryDialog.component.test.tsx | 8 +++--- src/systems/systems.component.test.tsx | 27 +++++++++---------- 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/catalogue/items/__snapshots__/catalogueItemsTable.component.test.tsx.snap b/src/catalogue/items/__snapshots__/catalogueItemsTable.component.test.tsx.snap index 1bd50d8ca..cb47e40a8 100644 --- a/src/catalogue/items/__snapshots__/catalogueItemsTable.component.test.tsx.snap +++ b/src/catalogue/items/__snapshots__/catalogueItemsTable.component.test.tsx.snap @@ -192,7 +192,7 @@ exports[`Catalogue Items Table renders the dense table correctly 1`] = ` aria-label="Filter by Name" autocomplete="new-password" class="MuiInputBase-input MuiInput-input MuiInputBase-inputAdornedEnd css-929hxt-MuiInputBase-input-MuiInput-input" - id=":r1i7:" + id=":r1h1:" placeholder="Filter by Name" title="Filter by Name" type="text" @@ -739,7 +739,7 @@ exports[`Catalogue Items Table renders the dense table correctly 1`] = ` class="MuiInputBase-root MuiInput-root MuiInputBase-colorPrimary css-1mmm5cp-MuiInputBase-root-MuiInput-root-MuiSelect-root" >