Skip to content

Commit

Permalink
task/WP-273: Category icon (#874)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* formatting fix

---------

Co-authored-by: Taylor Grafft <[email protected]>
Co-authored-by: Taylor Grafft <[email protected]>
Co-authored-by: Taylor Grafft <[email protected]>
Co-authored-by: Taylor Grafft <[email protected]>
Co-authored-by: Chandra Y <[email protected]>
Co-authored-by: Taylor Grafft <[email protected]>
  • Loading branch information
7 people authored Oct 25, 2023
1 parent f3e7f18 commit 1495914
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const AppBrowser = () => {
}
>
<span className="nav-content">
<AppIcon appId={app.appId} />
<AppIcon appId={app.appId} category={category} />
<span className="nav-text">{app.label || app.appId}</span>
</span>
</NavLink>
Expand Down
14 changes: 13 additions & 1 deletion client/src/components/Applications/AppForm/AppForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="appInfo-wrapper">
<h5 className="appInfo-title">{app.definition.label}</h5>
Expand All @@ -163,7 +175,7 @@ const AppInfo = ({ app }) => {
target="_blank"
rel="noreferrer noopener"
>
<AppIcon appId={app.definition.id} />{' '}
<AppIcon appId={app.definition.id} category={appCategory} />{' '}
<span>{app.definition.notes.label} Documentation</span>
</a>
) : null}
Expand Down
18 changes: 16 additions & 2 deletions client/src/components/_common/AppIcon/AppIcon.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -27,6 +36,11 @@ const AppIcon = ({ appId }) => {
};
AppIcon.propTypes = {
appId: PropTypes.string.isRequired,
category: PropTypes.string,
};

AppIcon.defaultProps = {
category: 'applications',
};

export default AppIcon;
58 changes: 40 additions & 18 deletions client/src/components/_common/AppIcon/AppIcon.test.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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(
<Provider store={store}>
<AppIcon appId={appId} />
<AppIcon
appId={appId}
category={category}
appIcons={store.getState().apps.appIcons}
/>
</Provider>
);
}

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');
});
});
16 changes: 16 additions & 0 deletions client/src/utils/doesClassExist.js
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 1495914

Please sign in to comment.