From 14959147c9323ceded55df10a368cbebe02f4925 Mon Sep 17 00:00:00 2001 From: Taylor Grafft Date: Wed, 25 Oct 2023 15:14:22 -0500 Subject: [PATCH] task/WP-273: Category icon (#874) * task/WP-273-CategoryIcon * task/WP-273-CategoryIcon-v2 * task/WP-273-CategoryIcon-v3 * task/WP-273-CategoryIcon-v4 * task/WP-273-CategoryIcon-v5 * task/WP-273-CategoryIcon-v6 * Update client/src/components/Applications/AppForm/AppForm.jsx Co-authored-by: Chandra Y * formatting fix --------- Co-authored-by: Taylor Grafft Co-authored-by: Taylor Grafft Co-authored-by: Taylor Grafft Co-authored-by: Taylor Grafft Co-authored-by: Chandra Y Co-authored-by: Taylor Grafft --- .../Applications/AppBrowser/AppBrowser.jsx | 2 +- .../Applications/AppForm/AppForm.jsx | 14 ++++- .../components/_common/AppIcon/AppIcon.jsx | 18 +++++- .../_common/AppIcon/AppIcon.test.js | 58 +++++++++++++------ client/src/utils/doesClassExist.js | 16 +++++ 5 files changed, 86 insertions(+), 22 deletions(-) create mode 100644 client/src/utils/doesClassExist.js diff --git a/client/src/components/Applications/AppBrowser/AppBrowser.jsx b/client/src/components/Applications/AppBrowser/AppBrowser.jsx index f70890fc8..75535098e 100644 --- a/client/src/components/Applications/AppBrowser/AppBrowser.jsx +++ b/client/src/components/Applications/AppBrowser/AppBrowser.jsx @@ -90,7 +90,7 @@ const AppBrowser = () => { } > - + {app.label || app.appId} diff --git a/client/src/components/Applications/AppForm/AppForm.jsx b/client/src/components/Applications/AppForm/AppForm.jsx index f197be6cd..e90801b4a 100644 --- a/client/src/components/Applications/AppForm/AppForm.jsx +++ b/client/src/components/Applications/AppForm/AppForm.jsx @@ -150,6 +150,18 @@ const AdjustValuesWhenQueueChanges = ({ app }) => { }; const AppInfo = ({ app }) => { + const categoryDict = useSelector((state) => state.apps.categoryDict); + const getAppCategory = (appId) => { + for (const [cat, apps] of Object.entries(categoryDict)) { + if (apps.some((app) => app.appId === appId)) { + return cat; + } + } + return null; + }; + + const appCategory = getAppCategory(app.definition.id); + return (
{app.definition.label}
@@ -163,7 +175,7 @@ const AppInfo = ({ app }) => { target="_blank" rel="noreferrer noopener" > - {' '} + {' '} {app.definition.notes.label} Documentation ) : null} diff --git a/client/src/components/_common/AppIcon/AppIcon.jsx b/client/src/components/_common/AppIcon/AppIcon.jsx index 18bcf3592..ac8f8a792 100644 --- a/client/src/components/_common/AppIcon/AppIcon.jsx +++ b/client/src/components/_common/AppIcon/AppIcon.jsx @@ -3,11 +3,16 @@ import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import Icon from '_common/Icon'; import './AppIcon.scss'; +import iconStyles from '../../../styles/trumps/icon.css'; +import iconFontsStyles from '../../../styles/trumps/icon.fonts.css'; +import doesClassExist from 'utils/doesClassExist'; -const AppIcon = ({ appId }) => { +const AppIcon = ({ appId, category }) => { const appIcons = useSelector((state) => state.apps.appIcons); const findAppIcon = (id) => { - let appIcon = 'applications'; + let appIcon = category + ? category.replace(' ', '-').toLowerCase() + : 'applications'; Object.keys(appIcons).forEach((appName) => { if (id.includes(appName)) { appIcon = appIcons[appName].toLowerCase(); @@ -19,6 +24,10 @@ const AppIcon = ({ appId }) => { } else if (id.includes('extract')) { appIcon = 'extract'; } + // Check if the CSS class exists, if not default to 'icon-applications' + if (!doesClassExist(`icon-${appIcon}`, [iconFontsStyles, iconStyles])) { + appIcon = 'applications'; + } return appIcon; }; const iconName = findAppIcon(appId); @@ -27,6 +36,11 @@ const AppIcon = ({ appId }) => { }; AppIcon.propTypes = { appId: PropTypes.string.isRequired, + category: PropTypes.string, +}; + +AppIcon.defaultProps = { + category: 'applications', }; export default AppIcon; diff --git a/client/src/components/_common/AppIcon/AppIcon.test.js b/client/src/components/_common/AppIcon/AppIcon.test.js index c5c64c228..b183b079b 100644 --- a/client/src/components/_common/AppIcon/AppIcon.test.js +++ b/client/src/components/_common/AppIcon/AppIcon.test.js @@ -1,9 +1,6 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { - toHaveAttribute, - toHaveTextContent, -} from '@testing-library/jest-dom/dist/matchers'; +import '@testing-library/jest-dom/extend-expect'; import { Provider } from 'react-redux'; import configureStore from 'redux-mock-store'; import AppIcon from './AppIcon'; @@ -15,43 +12,68 @@ const store = mockStore({ jupyter: 'jupyter', }, }, + categories: { + visualization: ['vasp'], + 'data-processing': ['jupyter'], + }, }); -expect.extend({ toHaveAttribute }); +// Mock document.styleSheets to simulate the existence of the CSS classes we're testing for +Object.defineProperty(document, 'styleSheets', { + value: [ + { + cssRules: [ + { selectorText: '.icon-jupyter::before' }, + { selectorText: '.icon-visualization::before' }, + { selectorText: '.icon-compress::before' }, + { selectorText: '.icon-extract::before' }, + ], + }, + ], + writable: true, +}); -function renderAppIcon(appId) { +function renderAppIcon(appId, category = 'default') { return render( - + ); } describe('AppIcon', () => { it('should render icons for known app IDs', () => { - const { getByRole } = renderAppIcon('jupyter'); - expect(getByRole('img')).toHaveAttribute('class', 'icon icon-jupyter'); + const { container } = renderAppIcon('jupyter', 'data-processing'); + expect(container.firstChild).toHaveClass('icon-jupyter'); }); - it('should show generic icons for apps with no appIcon', () => { - const { getByRole } = renderAppIcon('vasp'); - expect(getByRole('img')).toHaveAttribute('class', 'icon icon-applications'); + + it('should show category icons for apps with no appIcon', () => { + const { container } = renderAppIcon('vasp', 'visualization'); + expect(container.firstChild).toHaveClass('icon-visualization'); }); + it('should render icons for prtl.clone apps', () => { - const { getByRole } = renderAppIcon( + const { container } = renderAppIcon( 'prtl.clone.username.allocation.jupyter' ); - expect(getByRole('img')).toHaveAttribute('class', 'icon icon-jupyter'); + expect(container.firstChild).toHaveClass('icon-jupyter'); }); + it('should render icon for zippy toolbar app', () => { - const { getByRole } = renderAppIcon( + const { container } = renderAppIcon( 'prtl.clone.username.FORK.zippy-0.2u2-2.0' ); - expect(getByRole('img')).toHaveAttribute('class', 'icon icon-compress'); + expect(container.firstChild).toHaveClass('icon-compress'); }); + it('should render icon for extract toolbar app', () => { - const { getByRole } = renderAppIcon( + const { container } = renderAppIcon( 'prtl.clone.username.FORK.extract-0.1u7-7.0' ); - expect(getByRole('img')).toHaveAttribute('class', 'icon icon-extract'); + expect(container.firstChild).toHaveClass('icon-extract'); }); }); diff --git a/client/src/utils/doesClassExist.js b/client/src/utils/doesClassExist.js new file mode 100644 index 000000000..b3ddab017 --- /dev/null +++ b/client/src/utils/doesClassExist.js @@ -0,0 +1,16 @@ +function doesClassExist(className, stylesheets) { + for (let stylesheet of stylesheets) { + //Required to make this work with Jest/identity-obj-proxy + if (typeof stylesheet === 'object') { + if (stylesheet[className]) { + return true; + } + } else if (typeof stylesheet === 'string') { + if (stylesheet.includes(`.${className}::before`)) { + return true; + } + } + } + return false; +} +export default doesClassExist;