diff --git a/package-lock.json b/package-lock.json index 11a01886c..c80b7540b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@mui/material": "^5.12.2", "@mui/styled-engine-sc": "^5.12.0", "@mui/system": "^5.12.1", + "@mui/x-data-grid": "^6.16.2", "@mui/x-date-pickers": "^6.5.0", "@tanstack/react-query": "^4.32.0", "@zxing/browser": "^0.1.3", @@ -360,10 +361,11 @@ } }, "node_modules/@babel/runtime": { - "version": "7.21.0", - "license": "MIT", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -3348,12 +3350,12 @@ } }, "node_modules/@mui/utils": { - "version": "5.13.1", - "license": "MIT", + "version": "5.14.13", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.13.tgz", + "integrity": "sha512-2AFpyXWw7uDCIqRu7eU2i/EplZtks5LAMzQvIhC79sPV9IhOZU2qwOWVnPtdctRXiQJOAaXulg+A37pfhEueQw==", "dependencies": { - "@babel/runtime": "^7.21.0", - "@types/prop-types": "^15.7.5", - "@types/react-is": "^18.2.0", + "@babel/runtime": "^7.23.1", + "@types/prop-types": "^15.7.7", "prop-types": "^15.8.1", "react-is": "^18.2.0" }, @@ -3365,13 +3367,52 @@ "url": "https://opencollective.com/mui" }, "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/@mui/utils/node_modules/react-is": { "version": "18.2.0", "license": "MIT" }, + "node_modules/@mui/x-data-grid": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-6.16.2.tgz", + "integrity": "sha512-nKZxlzkcqXUbgxvz01J0okziBH4uvnEVVneOSzp5TeCOC7Wke1YFBJRRjR6H6tVpbaqlOw/LFULrTfcJtiaMBQ==", + "dependencies": { + "@babel/runtime": "^7.23.1", + "@mui/utils": "^5.14.11", + "clsx": "^2.0.0", + "prop-types": "^15.8.1", + "reselect": "^4.1.8" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@mui/material": "^5.4.1", + "@mui/system": "^5.4.1", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + } + }, + "node_modules/@mui/x-data-grid/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/@mui/x-date-pickers": { "version": "6.7.0", "license": "MIT", @@ -4427,8 +4468,9 @@ "license": "MIT" }, "node_modules/@types/prop-types": { - "version": "15.7.5", - "license": "MIT" + "version": "15.7.8", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.8.tgz", + "integrity": "sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ==" }, "node_modules/@types/proper-lockfile": { "version": "4.1.2", @@ -4495,13 +4537,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-is": { - "version": "18.2.0", - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-transition-group": { "version": "4.4.6", "license": "MIT", @@ -12688,8 +12723,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "license": "MIT" + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, "node_modules/regexp.prototype.flags": { "version": "1.4.3", @@ -12744,6 +12780,11 @@ "lodash": "^4.17.21" } }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "node_modules/resolve": { "version": "1.22.1", "license": "MIT", @@ -14934,9 +14975,11 @@ } }, "@babel/runtime": { - "version": "7.21.0", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", "requires": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" } }, "@babel/template": { @@ -17373,11 +17416,12 @@ "requires": {} }, "@mui/utils": { - "version": "5.13.1", + "version": "5.14.13", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.13.tgz", + "integrity": "sha512-2AFpyXWw7uDCIqRu7eU2i/EplZtks5LAMzQvIhC79sPV9IhOZU2qwOWVnPtdctRXiQJOAaXulg+A37pfhEueQw==", "requires": { - "@babel/runtime": "^7.21.0", - "@types/prop-types": "^15.7.5", - "@types/react-is": "^18.2.0", + "@babel/runtime": "^7.23.1", + "@types/prop-types": "^15.7.7", "prop-types": "^15.8.1", "react-is": "^18.2.0" }, @@ -17387,6 +17431,25 @@ } } }, + "@mui/x-data-grid": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-6.16.2.tgz", + "integrity": "sha512-nKZxlzkcqXUbgxvz01J0okziBH4uvnEVVneOSzp5TeCOC7Wke1YFBJRRjR6H6tVpbaqlOw/LFULrTfcJtiaMBQ==", + "requires": { + "@babel/runtime": "^7.23.1", + "@mui/utils": "^5.14.11", + "clsx": "^2.0.0", + "prop-types": "^15.8.1", + "reselect": "^4.1.8" + }, + "dependencies": { + "clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==" + } + } + }, "@mui/x-date-pickers": { "version": "6.7.0", "requires": { @@ -18134,7 +18197,9 @@ "version": "4.0.0" }, "@types/prop-types": { - "version": "15.7.5" + "version": "15.7.8", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.8.tgz", + "integrity": "sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ==" }, "@types/proper-lockfile": { "version": "4.1.2", @@ -18193,12 +18258,6 @@ "@types/react": "*" } }, - "@types/react-is": { - "version": "18.2.0", - "requires": { - "@types/react": "*" - } - }, "@types/react-transition-group": { "version": "4.4.6", "requires": { @@ -23424,7 +23483,9 @@ } }, "regenerator-runtime": { - "version": "0.13.11" + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, "regexp.prototype.flags": { "version": "1.4.3", @@ -23458,6 +23519,11 @@ "lodash": "^4.17.21" } }, + "reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "resolve": { "version": "1.22.1", "requires": { diff --git a/package.json b/package.json index eb2584259..923277cc8 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@mui/material": "^5.12.2", "@mui/styled-engine-sc": "^5.12.0", "@mui/system": "^5.12.1", + "@mui/x-data-grid": "^6.16.2", "@mui/x-date-pickers": "^6.5.0", "@tanstack/react-query": "^4.32.0", "@zxing/browser": "^0.1.3", diff --git a/src/components/Contacts/ContactListTable.jsx b/src/components/Contacts/ContactListTable.jsx index fdf988692..d167dd2d5 100644 --- a/src/components/Contacts/ContactListTable.jsx +++ b/src/components/Contacts/ContactListTable.jsx @@ -1,28 +1,34 @@ // React Imports import React from 'react'; // Material UI Imports -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableHead from '@mui/material/TableHead'; -import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; +import Box from '@mui/material/Box'; +import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined'; +import { + DataGrid, + GridToolbarContainer, + GridActionsCellItem, + GridToolbarFilterButton, + GridToolbarDensitySelector +} from '@mui/x-data-grid'; // MUI Theme -import { ThemeProvider } from '@mui/material/styles'; import theme from '../../theme'; // Component Imports -import ContactListTableRow from './ContactListTableRow'; +import ContactProfileIcon from './ContactProfileIcon'; -// ===== MAKE CHANGES HERE FOR TABLE HEADER / COLUMN TITLES ===== -const columnTitlesArray = ['Contact', 'Pin', 'Delete']; +const CustomToolbar = () => ( + + + + +); /** * @typedef {import("../../typedefs.js").userListObject} userListObject */ /** - * ContactListTable Component - Component that generates table of contacts from data within ContactList + * ContactListTable Component - Component that generates the list of contacts + * from data within ContactList * * @memberof Contacts * @name ContactListTable @@ -32,43 +38,81 @@ const columnTitlesArray = ['Contact', 'Pin', 'Delete']; * @returns {React.JSX.Element} The ContactListTable Component */ const ContactListTable = ({ contacts, deleteContact }) => { - const comparePerson = (a, b) => { - if (a.familyName[0].toLowerCase() < b.familyName[0].toLowerCase()) { - return -1; + const columnTitlesArray = [ + { + field: 'First Name', + minWidth: 120, + flex: 1, + headerAlign: 'center', + align: 'center' + }, + { + field: 'Last Name', + minWidth: 120, + flex: 1, + headerAlign: 'center', + align: 'center' + }, + { + field: 'Profile', + renderCell: (contactData) => , + sortable: false, + filterable: false, + width: 70, + headerAlign: 'center', + align: 'center' + }, + { + field: 'actions', + type: 'actions', + headerName: 'Delete', + width: 70, + getActions: (contactData) => [ + } + onClick={() => deleteContact(contactData.row.Delete)} + label="Delete" + /> + ] } - if (a.familyName[0].toLowerCase() > b.familyName[0].toLowerCase()) { - return 1; - } - return 0; - }; - const contactsCopy = [...contacts]; - const sortedContacts = contactsCopy.sort(comparePerson); + ]; return ( - - - - - - {columnTitlesArray.map((columnTitle) => ( - - {columnTitle} - - ))} - - - - {sortedContacts?.map((contact) => ( - - ))} - -
-
-
+ + ({ + id: contact.webId, + 'First Name': contact.givenName, + 'Last Name': contact.familyName, + Profile: contact, + Delete: contact + }))} + slots={{ + toolbar: CustomToolbar + }} + sx={{ + '.MuiDataGrid-columnHeader': { + background: theme.palette.primary.light, + color: 'white' + }, + '.MuiDataGrid-columnSeparator': { + display: 'none' + } + }} + pageSizeOptions={[10]} + initialState={{ + pagination: { + paginationModel: { pageSize: 10, page: 0 } + }, + sorting: { + sortModel: [{ field: 'Last Name', sort: 'asc' }] + } + }} + disableColumnMenu + disableRowSelectionOnClick + /> + ); }; diff --git a/src/components/Contacts/ContactListTableRow.jsx b/src/components/Contacts/ContactListTableRow.jsx deleted file mode 100644 index 8c4cea6ce..000000000 --- a/src/components/Contacts/ContactListTableRow.jsx +++ /dev/null @@ -1,100 +0,0 @@ -// React Imports -import React, { useContext, useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -// Inrupt Imports -import { getWebIdDataset } from '@inrupt/solid-client'; -// Material UI Imports -import Button from '@mui/material/Button'; -import IconButton from '@mui/material/IconButton'; -import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined'; -import PushPinIcon from '@mui/icons-material/PushPin'; -import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined'; -import TableCell from '@mui/material/TableCell'; -import TableRow from '@mui/material/TableRow'; -// Context Imports -import { DocumentListContext } from '@contexts'; -// Custom Hook Imports -import useNotification from '@hooks/useNotification'; -// MUI Theme -import { ThemeProvider } from '@mui/material/styles'; -import theme from '../../theme'; - -/** - * @typedef {import("../../typedefs.js").userListObject} userListObject - */ - -/** - * ContactListTableRow Component - Component that generates the individual table - * rows of contacts from data within ContactList - * - * @memberof Contacts - * @name ContactListTableRow - * @param {object} Props - Props for ContactListTableRow - * @param {userListObject} Props.contact - contact object that store's contact - * information - * @param {Function} Props.deleteContact - method to delete contact - * @returns {React.JSX.Element} The ContactListTableRow Component - */ -const ContactListTableRow = ({ contact, deleteContact }) => { - const [pinned, setPinned] = useState(false); - const { setContact } = useContext(DocumentListContext); - const navigate = useNavigate(); - const { addNotification } = useNotification(); - - // determine what icon gets rendered in the pinned column - const pinnedIcon = pinned ? : ; - - // Event handler for pinning contact to top of table - // ***** TODO: Add in moving pinned row to top of table - const handlePinClick = () => { - setPinned(!pinned); - }; - - // Event handler for profile page routing - const handleSelectProfile = async (contactInfo) => { - try { - await getWebIdDataset(contactInfo.webId); - setContact(contact); - } catch { - setContact(null); - navigate('/contacts'); - addNotification('error', 'WebId does not exist'); - } - }; - - return ( - - - - - - - - - - {pinnedIcon} - - - - deleteContact(contact)}> - - - - - - ); -}; - -export default ContactListTableRow; diff --git a/src/components/Contacts/ContactProfileIcon.jsx b/src/components/Contacts/ContactProfileIcon.jsx new file mode 100644 index 000000000..30e8a8f07 --- /dev/null +++ b/src/components/Contacts/ContactProfileIcon.jsx @@ -0,0 +1,60 @@ +// React Imports +import React, { useContext } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +// Inrupt Imports +import { getWebIdDataset } from '@inrupt/solid-client'; +// Material UI Imports +import ContactPageIcon from '@mui/icons-material/ContactPage'; +// Custom Hook Imports +import { useNotification } from '@hooks'; +// Context Imports +import { DocumentListContext } from '@contexts'; + +/** + * contactProfileIconProps is an object that stores the props for the + * ContactProfileIcon component + * + * @typedef {object} contactProfileIconProps + * @property {object} contact - Contain the object that stores contact metadata + * @memberof typedefs + */ + +/** + * ContactProfileIcon Component - Component that generates the contact profile + * icon for the Contacts List which is a special link to their profile page + * + * @memberof Contacts + * @name ContactProfileIcon + * @param {contactProfileIconProps} Props - Props for ContactProfileIcon + * @returns {React.JSX.Element} The ContactProfileIcon Component + */ +const ContactProfileIcon = ({ contact }) => { + const { setContact } = useContext(DocumentListContext); + const navigate = useNavigate(); + const { addNotification } = useNotification(); + + // Event handler for profile page routing + const handleSelectProfile = async (contactInfo) => { + try { + await getWebIdDataset(contactInfo.webId); + setContact(contactInfo); + } catch { + setContact(null); + navigate('/contacts'); + addNotification('error', 'WebId does not exist'); + } + }; + + return ( + handleSelectProfile(contact.value)} + style={{ display: 'flex', alignItems: 'center' }} + > + + + ); +}; + +export default ContactProfileIcon; diff --git a/src/components/Contacts/index.js b/src/components/Contacts/index.js index 41ea89b2a..50fa85800 100644 --- a/src/components/Contacts/index.js +++ b/src/components/Contacts/index.js @@ -1,5 +1,5 @@ import ContactListTable from './ContactListTable'; -import ContactListTableRow from './ContactListTableRow'; +import ContactProfileIcon from './ContactProfileIcon'; /** * Components and functions related to Contacts functionality within project PASS @@ -7,4 +7,4 @@ import ContactListTableRow from './ContactListTableRow'; * @namespace Contacts */ -export { ContactListTable, ContactListTableRow }; +export { ContactListTable, ContactProfileIcon }; diff --git a/src/components/Profile/ProfileImageField.jsx b/src/components/Profile/ProfileImageField.jsx index 0e5068189..619202d2a 100644 --- a/src/components/Profile/ProfileImageField.jsx +++ b/src/components/Profile/ProfileImageField.jsx @@ -71,7 +71,7 @@ const ProfileImageField = ({ loadProfileData, contactProfile }) => { > Profile Image: diff --git a/src/pages/Contacts.jsx b/src/pages/Contacts.jsx index 9ec7f3736..17132bd47 100644 --- a/src/pages/Contacts.jsx +++ b/src/pages/Contacts.jsx @@ -54,7 +54,8 @@ const Contacts = () => { sx={{ display: 'flex', flexDirection: 'column', - alignItems: 'center' + alignItems: 'center', + width: '100%' }} > diff --git a/test/components/Contacts/ContactsListTable.test.jsx b/test/components/Contacts/ContactsListTable.test.jsx index 1eeb43181..80db573b2 100644 --- a/test/components/Contacts/ContactsListTable.test.jsx +++ b/test/components/Contacts/ContactsListTable.test.jsx @@ -14,34 +14,59 @@ const MockTableComponent = ({ contacts }) => ( it('renders all clients from client context', () => { const contacts = [ - { familyName: 'Abby', person: 'Aaron Abby', webId: 'https://example.com/Abby' }, - { familyName: 'Builder', person: 'Bob Builder', webId: 'https://example.com/Builder' } + { + familyName: 'Abby', + givenName: 'Aaron', + person: 'Aaron Abby', + webId: 'https://example.com/Abby' + }, + { + familyName: 'Builder', + givenName: 'Bob', + person: 'Bob Builder', + webId: 'https://example.com/Builder' + } ]; - const { getAllByRole } = render(); + const { getAllByRole, queryByRole } = render(); const allRows = getAllByRole('row'); // Expect 3 rows: the header, Abby's row, Builder's Row expect(allRows.length).toBe(3); - const row1 = allRows[1]; - const row2 = allRows[2]; + const row1GivenName = queryByRole('cell', { name: 'Aaron' }); + const row1FamilyName = queryByRole('cell', { name: 'Abby' }); - expect(row1.cells.item(0).innerHTML).toContain('Aaron Abby'); - expect(row2.cells.item(0).innerHTML).toContain('Bob Builder'); + const row2GivenName = queryByRole('cell', { name: 'Bob' }); + const row2FamilyName = queryByRole('cell', { name: 'Builder' }); + + expect(row1GivenName).not.toBeNull(); + expect(row1FamilyName).not.toBeNull(); + expect(row2GivenName).not.toBeNull(); + expect(row2FamilyName).not.toBeNull(); }); it('sorts clients by familyName', () => { const originalArray = [ - { familyName: 'Zeigler', person: 'Aaron Zeigler', webId: 'https://example.com/Zeigler' }, - { familyName: 'Builder', person: 'Bob Builder', webId: 'https://example.com/Builder' } + { + familyName: 'Zeigler', + givenName: 'Aaron', + person: 'Aaron Zeigler', + webId: 'https://example.com/Zeigler' + }, + { + familyName: 'Builder', + givenName: 'Bob', + person: 'Bob Builder', + webId: 'https://example.com/Builder' + } ]; - const { getByText } = render(); + const { getByRole } = render(); - const client1 = getByText('Aaron Zeigler'); - const client2 = getByText('Bob Builder'); + const client1 = getByRole('cell', { name: 'Zeigler' }); + const client2 = getByRole('cell', { name: 'Builder' }); expect(client1.compareDocumentPosition(client2)).toBe(2); }); diff --git a/test/components/Profile/ProfileImageField.test.jsx b/test/components/Profile/ProfileImageField.test.jsx index e04658808..8a7d9e64e 100644 --- a/test/components/Profile/ProfileImageField.test.jsx +++ b/test/components/Profile/ProfileImageField.test.jsx @@ -47,7 +47,7 @@ describe('ProfileImageField', () => { }); it('renders no button with image if contactProfile is not null and has profile image', () => { - const mockContactProfile = { profileImg: 'https://example.com/client.png' }; + const mockContactProfile = { profileImage: 'https://example.com/client.png' }; const { queryByRole, queryByTestId } = render( ); diff --git a/test/pages/Contacts.test.jsx b/test/pages/Contacts.test.jsx index 844593588..8170eb6a8 100644 --- a/test/pages/Contacts.test.jsx +++ b/test/pages/Contacts.test.jsx @@ -52,7 +52,7 @@ describe('Contacts Page', () => { ); - const contacts = getByRole('table'); + const contacts = getByRole('grid'); expect(contacts).not.toBeNull(); }); it('displays empty list message when there are no contacts', () => {