diff --git a/public/res/default.json b/public/res/default.json index 6645df69..af3738f7 100644 --- a/public/res/default.json +++ b/public/res/default.json @@ -75,7 +75,7 @@ "title": "Help", "table-of-contents": "Table of contents", "top-of-page-icon": "", - "contents": "

Logging in

When you first access DataGateway, you will be presented with a login page.

You can login by entering your username and password provided by the Diamond User Office. Alternatively, to view only public data, select Anonymous from the Authentication Type drop down menu and click login.

Once you've successfully logged in, you will see a page with three tabs.

My Data

The My Data tab gives you quick access to all your visits and is presented in a grid.

Meta Data
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.

Browse

The Browse tab allows you to browse your data and any public data.

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.

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.The search tabs contains an interface the allows you search across all the visits, datasets and datafiles.

The Cart

On the very first column of each row on a grid (at the dataset or datafile level), you will see tick boxes.

Clicking this box will add or remove the selected visit to your cart. Multiple items can be added using the 'Shift' key.

You can use the Cart items counter on the top right corner to check if an item was added or removed.

Clicking on the above will open up the cart window:

Here you can remove already added items or request download of the items in the cart by clicking the Download Cart button. When the button is clicked, you will be presented with a window with several options.

The Download Name is a name that identifies your particular download. It is used as the filename when your download becomes ready.

The Access Method selection option determines whether you would like to download your files or whether you want them restored to another location.

You can optionally enter your email address. When your download is available, an email will be sent notifying you that your download is available.
Clicking the OK button on the window will submit your download request to be processed. You should see a Cart successfully submitted notification popup on the right corner. The request will be added inside the downloads window.

Download

Clicking on the above will open up the downloads window:

The Downloads window is a grid that list all your download requests.

The Status column gives you the current status of your download. Possible values are Restoring from Tape and Available. The Download button is disabled when the status is restoring. Once available, you can click the download button to download the file via the browser if Https transport type was used for that request.

Please note that download pausing and resuming is not supported. Multi-part download managers are also not supported.

For downloads using Globus as the transport type, when the status is complete, you can login to Globus Online and initiate file transfer to your endpoint." + "contents": "

Logging in

When you first access DataGateway, you will be presented with a login page.

You can login by entering your username and password provided by the Diamond User Office. Alternatively, to view only public data, select Anonymous from the Authentication Type drop down menu and click login.

Once you've successfully logged in, you will see a page with three tabs.

My Data

The My Data tab gives you quick access to all your visits and is presented in a grid.

Meta Data
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.

Browse

The Browse tab allows you to browse your data and any public data.

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.

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.The search tabs contains an interface the allows you search across all the visits, datasets and datafiles.

The Cart

On the very first column of each row on a grid (at the dataset or datafile level), you will see tick boxes.

Clicking this box will add or remove the selected visit to your cart. Multiple items can be added using the 'Shift' key.

You can use the Cart items counter on the top right corner to check if an item was added or removed.

Clicking on the above will open up the cart window:

Here you can remove already added items or request download of the items in the cart by clicking the Download Cart button. When the button is clicked, you will be presented with a window with several options.

The Download Name is a name that identifies your particular download. It is used as the filename when your download becomes ready.

The Access Method selection option determines whether you would like to download your files or whether you want them restored to another location.

You can optionally enter your email address. When your download is available, an email will be sent notifying you that your download is available.
Clicking the OK button on the window will submit your download request to be processed. You should see a Cart successfully submitted notification popup on the right corner. The request will be added inside the downloads window.

Download

Clicking on the above will open up the downloads window:

The Downloads window is a grid that list all your download requests.

The Status column gives you the current status of your download. Possible values are Restoring from Tape and Available. The Download button is disabled when the status is restoring. Once available, you can click the download button to download the file via the browser if Https transport type was used for that request.

Please note that download pausing and resuming is not supported. Multi-part download managers are also not supported.

For downloads using Globus as the transport type, when the status is complete, you can login to Globus Online and initiate file transfer to your endpoint." }, "admin": { "title": "Admin", diff --git a/src/App.test.tsx b/src/App.test.tsx index ff23aeaa..8fb3e0ee 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -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(() => { @@ -17,12 +18,20 @@ describe('App', () => { afterEach(() => { jest.useRealTimers(); }); + it('renders without crashing', () => { const div = document.createElement('div'); - ReactDOM.render(, div); + ReactDOM.render(, div); ReactDOM.unmountComponentAtNode(div); }); + it('should show preloader when react-i18next is not ready', () => { + const wrapper = shallow( + + ); + 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 @@ -36,7 +45,7 @@ describe('App', () => { }) ); - const wrapper = mount(); + const wrapper = mount(); const realStore = wrapper.find(Provider).prop('store'); // Set provider to icat as that supports maintenance states realStore.dispatch( diff --git a/src/App.tsx b/src/App.tsx index 62e0d1d0..2c459478 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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'; @@ -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(); @@ -68,7 +69,7 @@ const toastrConfig = (): React.ReactElement => ( /> ); -class App extends React.Component { +class App extends React.Component { 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. @@ -94,12 +95,14 @@ class App extends React.Component { - } - > - {toastrConfig()} - - + {this.props.tReady ? ( + <> + {toastrConfig()} + + + ) : ( + + )} @@ -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); diff --git a/src/helpPage/helpPage.component.test.tsx b/src/helpPage/helpPage.component.test.tsx index 63c4e815..0c1f1237 100644 --- a/src/helpPage/helpPage.component.test.tsx +++ b/src/helpPage/helpPage.component.test.tsx @@ -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; diff --git a/src/helpPage/helpPage.component.tsx b/src/helpPage/helpPage.component.tsx index 0daffc8f..22f3b63f 100644 --- a/src/helpPage/helpPage.component.tsx +++ b/src/helpPage/helpPage.component.tsx @@ -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), @@ -110,6 +111,8 @@ const HelpPage = (props: CombinedHelpPageProps): React.ReactElement => { el.insertAdjacentHTML('afterbegin', topOfPageIcon); }); + useAnchor(); + return ( ; +} + +/** + * A mock value for what useLocation from react-router would return + */ +const MOCK_REACT_ROUTER_LOCATION: Partial = { + 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>; + + 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( + + + + ); + + 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( + + + + ); + + 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( + + + + ); + + jest.runAllTimers(); + + // fragment matches an element but website still loading + expect(mockScrollIntoView).not.toBeCalled(); + }); +}); diff --git a/src/hooks/useAnchor.ts b/src/hooks/useAnchor.ts new file mode 100644 index 00000000..2f99fa05 --- /dev/null +++ b/src/hooks/useAnchor.ts @@ -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( + (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; diff --git a/src/index.tsx b/src/index.tsx index b0fa5cad..a165bb45 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,4 +7,7 @@ import ReactDOM from 'react-dom'; import App from './App'; import 'typeface-roboto'; -ReactDOM.render(, document.getElementById('scigateway')); +ReactDOM.render( + , + document.getElementById('scigateway') +);