Skip to content

Commit

Permalink
Merge pull request #1118 from ral-facilities/bugfix/jump-to-anchor-#1115
Browse files Browse the repository at this point in the history
  • Loading branch information
kennethnym authored Aug 2, 2022
2 parents 30d01c6 + 1e3261b commit 7ad6be5
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 15 deletions.
2 changes: 1 addition & 1 deletion public/res/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"title": "Help",
"table-of-contents": "Table of contents",
"top-of-page-icon": "<a href=#scigateway>&#11014;</a>",
"contents": "<h2 id='logging-in'> Logging in</h2>When you first access DataGateway, you will be presented with a login page.<br><img src='/res/images/login.png'><br>You can login by entering your username and password provided by the <a href='http://www.diamond.ac.uk/Users/'>Diamond User Office</a>. Alternatively, to view only public data, select <strong>Anonymous</strong> from the <strong>Authentication Type</strong> drop down menu and click login.<br><br>Once you've successfully logged in, you will see a page with three tabs.<br><img src='/res/images/tabs.png'><h2 id='my-data'> My Data</h2>The <a href='#'>My Data</a> tab gives you quick access to all your visits and is presented in a grid.<br><img src='/res/images/my-data-grid.png'><br><strong>Meta Data</strong><br>If you click on a row (make sure it is not a hyperlink), tabs will appear on the bottom half of the page. Each tab gives you more information about the visit.<h2 id='browse'> Browse</h2>The <a href='#'>Browse</a> tab allows you to browse your data and any public data.<br><br>The grid works the same as in the My Data tab. You can sort and filter as well as drill drown the data hierarchy using the hyperlinks on each row. You can use the breadcrumb to go up the hierarchy. The breadcrumb is a useful reference to tell you where you are in the data hierarchy.<br><img src='/res/images/breadcrumb.png'><br>If the first column of the grid is a tick box, you can click the box to add the item to your cart. Please note if you select a dataset, all datafiles belonging to that dataset will be selected. It is not possible for example to unselect one datafile if the parent visit is already selected. Similar to My Data, clicking on a row (not hyperlink) will display its meta data information in the panel at the bottom of the page.<h2 id='search'> Search</h2>The search tabs contains an interface the allows you search across all the visits, datasets and datafiles.<h2 id='cart'> The Cart</h2>On the very first column of each row on a grid (at the dataset or datafile level), you will see tick boxes.<br><img src='/res/images/tickbox.png'><br>Clicking this box will add or remove the selected visit to your cart. Multiple items can be added using the 'Shift' key.<br><br>You can use the Cart items counter on the top right corner to check if an item was added or removed.<br><img src='/res/images/cart-button.png'><br>Clicking on the above will open up the cart window:<br><img src='/res/images/cart.png'><br>Here you can remove already added items or request download of the items in the cart by clicking the <strong>Download Cart</strong> button. When the button is clicked, you will be presented with a window with several options.<br><img src='/res/images/cart-download.png'><br>The <strong>Download Name</strong> is a name that identifies your particular download. It is used as the filename when your download becomes ready.<br><br>The <strong>Access Method</strong> selection option determines whether you would like to download your files or whether you want them restored to another location.<br><br>You can optionally enter your email address. When your download is available, an email will be sent notifying you that your download is available.<br>Clicking the <strong>OK</strong> button on the window will submit your download request to be processed. You should see a <strong>Cart successfully submitted</strong> notification popup on the right corner. The request will be added inside the downloads window.<h2 id='download'> Download</h2>Clicking on the above will open up the downloads window:<br><img src='/res/images/downloads-button.png'><br>The Downloads window is a grid that list all your download requests.<br><img src='/res/images/downloads.png'><br>The <strong>Status</strong> column gives you the current status of your download. Possible values are <strong>Restoring from Tape</strong> and <strong>Available</strong>. The <strong>Download</strong> button is disabled when the status is restoring. Once available, you can click the download button to download the file via the browser if <strong>Https</strong> transport type was used for that request.<br><br>Please note that download pausing and resuming is not supported. Multi-part download managers are also not supported.<br><br>For downloads using Globus as the transport type, when the status is complete, you can login to <a href='https://www.globus.org/SignIn'>Globus Online</a> and initiate file transfer to your endpoint."
"contents": "<h2 id='logging-in'> Logging in</h2>When you first access DataGateway, you will be presented with a login page.<br><img width=\"270\" height=\"210\" src='/res/images/login.png'><br>You can login by entering your username and password provided by the <a href='http://www.diamond.ac.uk/Users/'>Diamond User Office</a>. Alternatively, to view only public data, select <strong>Anonymous</strong> from the <strong>Authentication Type</strong> drop down menu and click login.<br><br>Once you've successfully logged in, you will see a page with three tabs.<br><img width=\"299\" height=\"70\" src='/res/images/tabs.png'><h2 id='my-data'> My Data</h2>The <a href='#'>My Data</a> tab gives you quick access to all your visits and is presented in a grid.<br><img width=\"1001\" height=\"533\" src='/res/images/my-data-grid.png'><br><strong>Meta Data</strong><br>If you click on a row (make sure it is not a hyperlink), tabs will appear on the bottom half of the page. Each tab gives you more information about the visit.<h2 id='browse'> Browse</h2>The <a href='#'>Browse</a> tab allows you to browse your data and any public data.<br><br>The grid works the same as in the My Data tab. You can sort and filter as well as drill drown the data hierarchy using the hyperlinks on each row. You can use the breadcrumb to go up the hierarchy. The breadcrumb is a useful reference to tell you where you are in the data hierarchy.<br><img width=\"701\" height=\"63\" src='/res/images/breadcrumb.png'><br>If the first column of the grid is a tick box, you can click the box to add the item to your cart. Please note if you select a dataset, all datafiles belonging to that dataset will be selected. It is not possible for example to unselect one datafile if the parent visit is already selected. Similar to My Data, clicking on a row (not hyperlink) will display its meta data information in the panel at the bottom of the page.<h2 id='search'> Search</h2>The search tabs contains an interface the allows you search across all the visits, datasets and datafiles.<h2 id='cart'> The Cart</h2>On the very first column of each row on a grid (at the dataset or datafile level), you will see tick boxes.<br><img width=\"509\" height=\"95\" src='/res/images/tickbox.png'><br>Clicking this box will add or remove the selected visit to your cart. Multiple items can be added using the 'Shift' key.<br><br>You can use the Cart items counter on the top right corner to check if an item was added or removed.<br><img width=\"86\" height=\"40\" src='/res/images/cart-button.png'><br>Clicking on the above will open up the cart window:<br><img width=\"959\" height=\"464\" src='/res/images/cart.png'><br>Here you can remove already added items or request download of the items in the cart by clicking the <strong>Download Cart</strong> button. When the button is clicked, you will be presented with a window with several options.<br><img width=\"957\" height=\"451\" src='/res/images/cart-download.png'><br>The <strong>Download Name</strong> is a name that identifies your particular download. It is used as the filename when your download becomes ready.<br><br>The <strong>Access Method</strong> selection option determines whether you would like to download your files or whether you want them restored to another location.<br><br>You can optionally enter your email address. When your download is available, an email will be sent notifying you that your download is available.<br>Clicking the <strong>OK</strong> button on the window will submit your download request to be processed. You should see a <strong>Cart successfully submitted</strong> notification popup on the right corner. The request will be added inside the downloads window.<h2 id='download'> Download</h2>Clicking on the above will open up the downloads window:<br><img width=\"267\" height=\"69\" src='/res/images/downloads-button.png'><br>The Downloads window is a grid that list all your download requests.<br><img width=\"954\" height=\"448\" src='/res/images/downloads.png'><br>The <strong>Status</strong> column gives you the current status of your download. Possible values are <strong>Restoring from Tape</strong> and <strong>Available</strong>. The <strong>Download</strong> button is disabled when the status is restoring. Once available, you can click the download button to download the file via the browser if <strong>Https</strong> transport type was used for that request.<br><br>Please note that download pausing and resuming is not supported. Multi-part download managers are also not supported.<br><br>For downloads using Globus as the transport type, when the status is complete, you can login to <a href='https://www.globus.org/SignIn'>Globus Online</a> and initiate file transfer to your endpoint."
},
"admin": {
"title": "Admin",
Expand Down
17 changes: 13 additions & 4 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { mount } from 'enzyme';
import { mount, shallow } from 'enzyme';
import axios from 'axios';
import React from 'react';
import ReactDOM from 'react-dom';
import { act } from 'react-dom/test-utils';
import { Provider } from 'react-redux';
import * as singleSpa from 'single-spa';
import App from './App';
import App, { AppSansHoc } from './App';
import { flushPromises } from './setupTests';
import { loadAuthProvider } from './state/actions/scigateway.actions';
import { Preloader } from './preloader/preloader.component';

describe('App', () => {
beforeEach(() => {
Expand All @@ -17,12 +18,20 @@ describe('App', () => {
afterEach(() => {
jest.useRealTimers();
});

it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.render(<App useSuspense={false} />, div);
ReactDOM.unmountComponentAtNode(div);
});

it('should show preloader when react-i18next is not ready', () => {
const wrapper = shallow(
<AppSansHoc t={jest.fn()} i18n={{}} tReady={false} />
);
expect(wrapper.find(Preloader).exists()).toBe(true);
});

it('loadMaintenanceState dispatched when maintenance changes', async () => {
// this test only works with old jest fake timers
// when they remove legacy timers refactor this test to use real timers
Expand All @@ -36,7 +45,7 @@ describe('App', () => {
})
);

const wrapper = mount(<App />);
const wrapper = mount(<App useSuspense={false} />);
const realStore = wrapper.find(Provider).prop('store');
// Set provider to icat as that supports maintenance states
realStore.dispatch(
Expand Down
24 changes: 15 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
loadMaintenanceState,
} from './state/actions/scigateway.actions';
import ScigatewayMiddleware, {
listenToPlugins,
autoLoginMiddleware,
listenToPlugins,
} from './state/middleware/scigateway.middleware';
import AppReducer from './state/reducers/App.reducer';
import { StateType } from './state/state.types';
Expand All @@ -21,6 +21,7 @@ import { ConnectedThemeProvider } from './theming';
import ReduxToastr from 'react-redux-toastr';
import PageContainer from './pageContainer.component';
import { Preloader } from './preloader/preloader.component';
import { WithTranslation, withTranslation } from 'react-i18next';

const history = createBrowserHistory();

Expand Down Expand Up @@ -68,7 +69,7 @@ const toastrConfig = (): React.ReactElement => (
/>
);

class App extends React.Component {
class App extends React.Component<WithTranslation> {
public componentDidMount(): void {
// Check for changes in maintenance state. Ensures that state changes are
// loaded when a user does not reload the site for longer than an hour.
Expand All @@ -94,12 +95,14 @@ class App extends React.Component {
<Provider store={store}>
<ConnectedRouter history={history}>
<ConnectedThemeProvider>
<React.Suspense
fallback={<Preloader fullScreen={true} loading={true} />}
>
{toastrConfig()}
<PageContainer />
</React.Suspense>
{this.props.tReady ? (
<>
{toastrConfig()}
<PageContainer />
</>
) : (
<Preloader fullScreen loading />
)}
</ConnectedThemeProvider>
</ConnectedRouter>
</Provider>
Expand All @@ -108,4 +111,7 @@ class App extends React.Component {
}
}

export default App;
// export app with no hoc for testing
export { App as AppSansHoc };

export default withTranslation()(App);
5 changes: 5 additions & 0 deletions src/helpPage/helpPage.component.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import {
} from './helpPage.component';
import { shallow } from 'enzyme';

jest.mock('../hooks/useAnchor', () => ({
__esModule: true,
default: jest.fn(),
}));

describe('Help page component', () => {
let props: CombinedHelpPageProps;

Expand Down
3 changes: 3 additions & 0 deletions src/helpPage/helpPage.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getAppStrings, getString } from '../state/strings';
import { connect } from 'react-redux';
import { AppStrings } from '../state/scigateway.types';
import { StateType } from '../state/state.types';
import useAnchor from '../hooks/useAnchor';

const RootDiv = styled('div')(({ theme }) => ({
padding: theme.spacing(2),
Expand Down Expand Up @@ -110,6 +111,8 @@ const HelpPage = (props: CombinedHelpPageProps): React.ReactElement => {
el.insertAdjacentHTML('afterbegin', topOfPageIcon);
});

useAnchor();

return (
<RootDiv>
<Typography
Expand Down
140 changes: 140 additions & 0 deletions src/hooks/useAnchor.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import type { MockStoreCreator } from 'redux-mock-store';
import configureStore from 'redux-mock-store';
import type { DeepPartial } from 'redux';
import * as React from 'react';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import { useLocation } from 'react-router';
import { createLocation } from 'history';
import useAnchor from './useAnchor';
import { StateType } from '../state/state.types';

/**
* A simple React component that uses useAnchor for testing purposes.
*/
function TestComponent(): JSX.Element {
useAnchor();
return <></>;
}

/**
* A mock value for what useLocation from react-router would return
*/
const MOCK_REACT_ROUTER_LOCATION: Partial<Location> = {
hash: '#fragment',
};

// mock implementation of useLocation to return the mock URL
jest.mock('react-router', () => ({
__esModule: true,
...jest.requireActual('react-router'),
useLocation: jest.fn(),
}));

describe('useAnchor', () => {
let createMockStore: MockStoreCreator<DeepPartial<StateType>>;

beforeEach(() => {
// use fake timers bc useAnchor uses setTimeout under the hood
jest.useFakeTimers();
(useLocation as jest.Mock).mockReturnValue(MOCK_REACT_ROUTER_LOCATION);
createMockStore = configureStore();
});

afterEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
});

it('should scroll the element into view if the fragment in URL matches an element', () => {
const mockStore = createMockStore({
scigateway: {
siteLoading: false,
},
router: { location: createLocation('/') },
});

const mockScrollIntoView = jest.fn();
// pretend an element is found that matches the fragment
// the weird type cast is to get around TypeScript error saying
// the object is missing a bunch of other properties
// we obviously don't care about them so there's no point in stubbing them.
jest.spyOn(document, 'getElementById').mockReturnValueOnce({
scrollIntoView: mockScrollIntoView,
} as unknown as HTMLDivElement);

mount(
<Provider store={mockStore}>
<TestComponent />
</Provider>
);

jest.runAllTimers();

// fragment matches an element, should be scrolled into view
expect(mockScrollIntoView).toBeCalledTimes(1);
});

it('should do nothing if the fragment in URL does not match any element', () => {
const mockStore = createMockStore({
scigateway: {
siteLoading: false,
},
router: { location: createLocation('/') },
});

const mockScrollIntoView = jest.fn();
// pretend no element with #fragment is found
// and pretend there is other elements with IDs != fragment
jest.spyOn(document, 'getElementById').mockImplementation((id) =>
id === 'fragment'
? null
: ({
scrollIntoView: mockScrollIntoView,
} as unknown as HTMLDivElement)
);
// another element with ID "other", which is obv != fragment
const otherElem = document.getElementById('other');

mount(
<Provider store={mockStore}>
<TestComponent />
</Provider>
);

jest.runAllTimers();

// fragment doesn't match any element, useAnchor should not randomly
// jump to other elements
expect(otherElem.scrollIntoView).not.toBeCalled();
});

it('should do nothing even when fragment matches an element when website is loading', function () {
const mockStore = createMockStore({
scigateway: {
siteLoading: true,
},
router: { location: createLocation('/') },
});

const mockScrollIntoView = jest.fn();
// pretend an element is found that matches the fragment
// the weird type cast is to get around TypeScript error saying
// the object is missing a bunch of other properties
// we obviously don't care about them so there's no point in stubbing them.
jest.spyOn(document, 'getElementById').mockReturnValueOnce({
scrollIntoView: mockScrollIntoView,
} as unknown as HTMLDivElement);

mount(
<Provider store={mockStore}>
<TestComponent />
</Provider>
);

jest.runAllTimers();

// fragment matches an element but website still loading
expect(mockScrollIntoView).not.toBeCalled();
});
});
35 changes: 35 additions & 0 deletions src/hooks/useAnchor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useLocation } from 'react-router';
import { useEffect } from 'react';
import { useSelector } from 'react-redux';
import type { StateType } from '../state/state.types';

/**
* A React hook that detects fragments in the current URL, and makes the page jump to the element with the corresponding ID if required.
*
* For example,
* https://example.com#section causes useAnchor to jump to the element with ID 'section'
*/
function useAnchor(): void {
// get the current fragment of the URL from react-router
const { hash } = useLocation();
const isSiteLoading = useSelector<StateType>(
(state) => state.scigateway.siteLoading
);

useEffect(() => {
// need to make sure the website is not loading,
// if the website is not done loading and the hook runs prematurely,
// document.getElementById will return null because the page is not rendered fully.
// once the website is done loading, the page should be fully rendered, and the target element should be available.
if (hash && !isSiteLoading) {
const elemId = hash.replace('#', '');
// find the element with the ID specified by the fragment
// scroll to the element if found
setTimeout(() => {
document.getElementById(elemId)?.scrollIntoView(true);
}, 0);
}
}, [hash, isSiteLoading]);
}

export default useAnchor;
5 changes: 4 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ import ReactDOM from 'react-dom';
import App from './App';
import 'typeface-roboto';

ReactDOM.render(<App />, document.getElementById('scigateway'));
ReactDOM.render(
<App useSuspense={false} />,
document.getElementById('scigateway')
);

0 comments on commit 7ad6be5

Please sign in to comment.