diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh
index c36f66645..20df0fc5c 100755
--- a/docker/docker-entrypoint.sh
+++ b/docker/docker-entrypoint.sh
@@ -8,6 +8,7 @@ sed -e "s|\"facilityName\": \".*\"|\"facilityName\": \"$FACILITY_NAME\"|" \
-e "s|\"idsUrl\": \".*\"|\"idsUrl\": \"$IDS_URL\"|" \
-e "s|\"apiUrl\": \".*\"|\"apiUrl\": \"$API_URL\"|" \
-e "s|\"downloadApiUrl\": \".*\"|\"downloadApiUrl\": \"$DOWNLOAD_API_URL\"|" \
+ -e "s|\"uploadUrl\": \".*\"|\"uploadUrl\": \"$UPLOAD_URL\"|" \
-e "s|\"pluginHost\": \".*\"|\"pluginHost\": \"/datagateway-dataview\"|" \
/usr/local/apache2/htdocs/datagateway-dataview/datagateway-dataview-settings.json > "$TEMPFILE"
diff --git a/packages/datagateway-common/package.json b/packages/datagateway-common/package.json
index bf8f05482..68b79f959 100644
--- a/packages/datagateway-common/package.json
+++ b/packages/datagateway-common/package.json
@@ -12,6 +12,15 @@
"@emotion/styled": "11.11.0",
"@mui/x-date-pickers": "6.11.2",
"@types/lodash.debounce": "4.0.6",
+ "@uppy/core": "^3.7.1",
+ "@uppy/dashboard": "^3.7.1",
+ "@uppy/drag-drop": "^3.0.3",
+ "@uppy/file-input": "^3.0.4",
+ "@uppy/form": "^3.0.3",
+ "@uppy/golden-retriever": "^3.1.1",
+ "@uppy/progress-bar": "^3.0.4",
+ "@uppy/react": "^3.2.1",
+ "@uppy/tus": "^3.4.0",
"axios": "1.6.1",
"connected-react-router": "6.9.1",
"date-fns": "2.30.0",
@@ -103,7 +112,7 @@
},
"jest": {
"transformIgnorePatterns": [
- "node_modules/(?!axios)"
+ "node_modules/(?!(axios|@uppy|nanoid|exifr|p-queue|p-timeout)/)"
],
"collectCoverageFrom": [
"src/**/*.{tsx,ts,js,jsx}",
diff --git a/packages/datagateway-common/src/__mocks__/axios.ts b/packages/datagateway-common/src/__mocks__/axios.ts
index 9705993d1..903813628 100644
--- a/packages/datagateway-common/src/__mocks__/axios.ts
+++ b/packages/datagateway-common/src/__mocks__/axios.ts
@@ -6,7 +6,9 @@ const requests = {
get: jest.fn(() => Promise.resolve({ data: {} })),
post: jest.fn(() => Promise.resolve({ data: {} })),
delete: jest.fn(() => Promise.resolve({ data: {} })),
+ put: jest.fn(() => Promise.resolve({ data: {} })),
CancelToken: axios.CancelToken,
+ isAxiosError: axios.isAxiosError,
};
export default requests;
diff --git a/packages/datagateway-common/src/api/index.test.tsx b/packages/datagateway-common/src/api/index.test.tsx
index 6b1b699d5..a7ac9e191 100644
--- a/packages/datagateway-common/src/api/index.test.tsx
+++ b/packages/datagateway-common/src/api/index.test.tsx
@@ -12,6 +12,7 @@ import {
useSort,
usePushCurrentTab,
useUpdateView,
+ refreshSession,
} from './index';
import {
FiltersType,
@@ -1074,4 +1075,20 @@ describe('generic api functions', () => {
);
});
});
+
+ it('sends a PUT request to refresh the session', () => {
+ const apiUrl = 'https://example.com/api';
+
+ refreshSession(apiUrl);
+
+ expect(axios.put).toHaveBeenCalledWith(
+ `${apiUrl}/sessions`,
+ {},
+ {
+ headers: {
+ Authorization: `Bearer null`,
+ },
+ }
+ );
+ });
});
diff --git a/packages/datagateway-common/src/api/index.tsx b/packages/datagateway-common/src/api/index.tsx
index d0b2638e2..51de5d047 100644
--- a/packages/datagateway-common/src/api/index.tsx
+++ b/packages/datagateway-common/src/api/index.tsx
@@ -819,3 +819,15 @@ export const useCustomFilterCount = (
// @ts-ignore
return useQueries(queryConfigs);
};
+
+export const refreshSession = (apiUrl: string): void => {
+ axios.put(
+ `${apiUrl}/sessions`,
+ {},
+ {
+ headers: {
+ Authorization: `Bearer ${readSciGatewayToken().sessionId}`,
+ },
+ }
+ );
+};
diff --git a/packages/datagateway-common/src/index.css b/packages/datagateway-common/src/index.css
deleted file mode 100644
index d07361807..000000000
--- a/packages/datagateway-common/src/index.css
+++ /dev/null
@@ -1,19 +0,0 @@
-body {
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
- sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
-}
-
-@keyframes rotate {
- to {
- transform: rotateZ(360deg);
- }
-}
diff --git a/packages/datagateway-common/src/index.tsx b/packages/datagateway-common/src/index.tsx
index 965e33ba7..14d9c5431 100644
--- a/packages/datagateway-common/src/index.tsx
+++ b/packages/datagateway-common/src/index.tsx
@@ -2,7 +2,6 @@
// import React from 'react';
// import ReactDOM from 'react-dom';
// import App from './App';
-// import './index.css';
import { StateType } from './state/app.types';
@@ -66,6 +65,7 @@ export type { CartProps } from './views/viewCartButton.component';
export { default as ViewButton } from './views/viewButton.component';
export { default as ClearFiltersButton } from './views/clearFiltersButton.component';
export { default as DownloadButton } from './views/downloadButton.component';
+export { default as UploadButton } from './views/uploadButton.component';
export { default as SelectionAlert } from './views/selectionAlert.component';
export { default as ISISDatafileDetailsPanel } from './detailsPanels/isis/datafileDetailsPanel.component';
diff --git a/packages/datagateway-common/src/preloader/__snapshots__/preloader.component.test.tsx.snap b/packages/datagateway-common/src/preloader/__snapshots__/preloader.component.test.tsx.snap
index 91c6aa27e..74bf9e490 100644
--- a/packages/datagateway-common/src/preloader/__snapshots__/preloader.component.test.tsx.snap
+++ b/packages/datagateway-common/src/preloader/__snapshots__/preloader.component.test.tsx.snap
@@ -18,22 +18,22 @@ exports[`Preloader component renders when the site is loading 1`] = `
style="box-sizing: border-box; padding: 10px 0px;"
>
diff --git a/packages/datagateway-common/src/preloader/preloader.component.tsx b/packages/datagateway-common/src/preloader/preloader.component.tsx
index 7b6bd5729..f0080a840 100644
--- a/packages/datagateway-common/src/preloader/preloader.component.tsx
+++ b/packages/datagateway-common/src/preloader/preloader.component.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Box } from '@mui/material';
+import { Box, keyframes, styled } from '@mui/material';
const colors = ['#8C4799', '#1D4F91', '#C34613', '#008275', '#63666A'];
const innerRadius = 140;
@@ -11,11 +11,13 @@ interface PreloaderProps {
children: React.ReactNode;
}
-interface SpinnerStyle {
- [id: string]: string | number;
-}
+const rotate = keyframes`
+ to {
+ transform: rotateZ(360deg);
+ }
+`;
-const spinnerStyle = (index: number): SpinnerStyle => {
+const StyledI = styled('i')(({ index }: { index: number }) => {
const size = innerRadius + index * 2 * (border + spacing);
return {
@@ -32,7 +34,10 @@ const spinnerStyle = (index: number): SpinnerStyle => {
width: size,
marginTop: -size / 2,
marginLeft: -size / 2,
- animationName: 'rotate',
+ animation: `${rotate} 3s infinite cubic-bezier(.09, ${0.3 * index}, ${
+ 0.12 * index
+ }, .03)`,
+ animationName: `${rotate}`,
animationIterationCount: 'infinite',
animationDuration: '3s',
animationTimingFunction: `cubic-bezier(.09, ${0.3 * index}, ${
@@ -41,7 +46,7 @@ const spinnerStyle = (index: number): SpinnerStyle => {
transformOrigin: '50% 100% 0',
boxSizing: 'border-box',
};
-};
+});
const Preloader: React.FC = (props: PreloaderProps) => (
@@ -59,21 +64,21 @@ const Preloader: React.FC
= (props: PreloaderProps) => (
}}
>
-
-
-
-
-
-
-
+
+
+
+
+
+
Loading...
diff --git a/packages/datagateway-common/src/setupTests.tsx b/packages/datagateway-common/src/setupTests.tsx
index dd4f896aa..c638d406f 100644
--- a/packages/datagateway-common/src/setupTests.tsx
+++ b/packages/datagateway-common/src/setupTests.tsx
@@ -90,6 +90,7 @@ export const createReactQueryWrapper = (
icatUrl: 'https://example.com/icat',
idsUrl: 'https://example.com/ids',
downloadApiUrl: 'https://example.com/topcat',
+ uploadUrl: 'https://example.com/upload',
},
},
};
diff --git a/packages/datagateway-common/src/state/actions/actions.types.tsx b/packages/datagateway-common/src/state/actions/actions.types.tsx
index 4f3d4e6d6..a744e134f 100644
--- a/packages/datagateway-common/src/state/actions/actions.types.tsx
+++ b/packages/datagateway-common/src/state/actions/actions.types.tsx
@@ -277,6 +277,7 @@ export interface URLs {
apiUrl: string;
downloadApiUrl: string;
icatUrl: string;
+ uploadUrl?: string;
}
export interface PluginRoute {
diff --git a/packages/datagateway-common/src/views/__snapshots__/uploadDialog.component.test.tsx.snap b/packages/datagateway-common/src/views/__snapshots__/uploadDialog.component.test.tsx.snap
new file mode 100644
index 000000000..f0a2b5a13
--- /dev/null
+++ b/packages/datagateway-common/src/views/__snapshots__/uploadDialog.component.test.tsx.snap
@@ -0,0 +1,5 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Upload dialog component Datafile renders correctly 1`] = ``;
+
+exports[`Upload dialog component Dataset renders correctly 1`] = ``;
diff --git a/packages/datagateway-common/src/views/uploadButton.component.test.tsx b/packages/datagateway-common/src/views/uploadButton.component.test.tsx
new file mode 100644
index 000000000..add37130a
--- /dev/null
+++ b/packages/datagateway-common/src/views/uploadButton.component.test.tsx
@@ -0,0 +1,221 @@
+import * as React from 'react';
+import UploadButton, { UploadButtonProps } from './uploadButton.component';
+import configureStore from 'redux-mock-store';
+import { initialState as dGCommonInitialState } from '../state/reducers/dgcommon.reducer';
+import { StateType } from '../state/app.types';
+import { Provider } from 'react-redux';
+import thunk from 'redux-thunk';
+import { MemoryRouter } from 'react-router-dom';
+import { QueryClient, QueryClientProvider } from 'react-query';
+import { render, type RenderResult, screen } from '@testing-library/react';
+import { UserEvent } from '@testing-library/user-event/setup/setup';
+import userEvent from '@testing-library/user-event';
+
+jest.mock('../api/datafiles');
+jest.mock('../api/datasets');
+jest.mock('../api/investigations');
+
+describe('Generic upload button', () => {
+ const mockStore = configureStore([thunk]);
+ let state: StateType;
+ let user: UserEvent;
+
+ function renderComponent(props: UploadButtonProps): RenderResult {
+ const store = mockStore(state);
+ return render(
+
+
+
+
+
+
+
+ );
+ }
+
+ beforeEach(() => {
+ user = userEvent.setup();
+ state = JSON.parse(
+ JSON.stringify({
+ dgdataview: {},
+ dgcommon: {
+ ...dGCommonInitialState,
+ urls: {
+ ...dGCommonInitialState.urls,
+ idsUrl: 'https://www.example.com/ids',
+ },
+ },
+ })
+ );
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('text variant', () => {
+ it('renders correctly dataset', async () => {
+ renderComponent({
+ entityType: 'dataset',
+ entityId: 1,
+ variant: 'text',
+ });
+ expect(
+ await screen.findByRole('button', { name: 'buttons.upload_datafile' })
+ ).toBeInTheDocument();
+ expect(await screen.findByText('Upload Datafile')).toBeInTheDocument();
+ });
+
+ it('renders correctly investigation', async () => {
+ renderComponent({
+ entityType: 'investigation',
+ entityId: 1,
+ variant: 'text',
+ });
+ expect(
+ await screen.findByRole('button', { name: 'buttons.upload_dataset' })
+ ).toBeInTheDocument();
+ expect(await screen.findByText('Upload Dataset')).toBeInTheDocument();
+ });
+
+ it('opens upload dialog when clicked', async () => {
+ renderComponent({
+ entityType: 'dataset',
+ entityId: 1,
+ variant: 'text',
+ });
+
+ const uploadButton = await screen.findByRole('button', {
+ name: 'buttons.upload_datafile',
+ });
+
+ // Trigger the onClick handler directly
+ uploadButton.click();
+
+ expect(await screen.findByRole('dialog')).toBeInTheDocument();
+ });
+
+ it('renders a tooltip when hovered', async () => {
+ renderComponent({
+ entityType: 'dataset',
+ entityId: 1,
+ variant: 'text',
+ });
+
+ await user.hover(
+ await screen.findByRole('button', {
+ name: 'buttons.upload_datafile',
+ })
+ );
+
+ expect(
+ await screen.findByRole('tooltip', { name: 'buttons.upload_datafile' })
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe('full text variant', () => {
+ it('renders correctly', async () => {
+ renderComponent({
+ entityType: 'datafile',
+ entityId: 1,
+ variant: 'text',
+ });
+ expect(
+ await screen.findByRole('button', { name: 'buttons.upload_datafile' })
+ ).toBeInTheDocument();
+ expect(
+ await screen.findByText('buttons.upload_datafile')
+ ).toBeInTheDocument();
+ });
+
+ it('opens upload dialog when clicked', async () => {
+ renderComponent({
+ entityType: 'datafile',
+ entityId: 1,
+ variant: 'text',
+ });
+
+ const uploadButton = await screen.findByRole('button', {
+ name: 'buttons.upload_datafile',
+ });
+
+ // Trigger the onClick handler directly
+ uploadButton.click();
+
+ expect(await screen.findByRole('dialog')).toBeInTheDocument();
+ });
+
+ it('renders a tooltip when hovered', async () => {
+ renderComponent({
+ entityType: 'datafile',
+ entityId: 1,
+ variant: 'text',
+ });
+
+ await user.hover(
+ await screen.findByRole('button', {
+ name: 'buttons.upload_datafile',
+ })
+ );
+
+ expect(
+ await screen.findByRole('tooltip', { name: 'buttons.upload_datafile' })
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe('icon variant', () => {
+ it('renders correctly', async () => {
+ renderComponent({
+ entityType: 'dataset',
+ entityId: 1,
+ variant: 'icon',
+ });
+
+ expect(
+ await screen.findByRole('button', { name: 'buttons.upload_datafile' })
+ ).toBeInTheDocument();
+
+ // The text should not be rendered
+ expect(
+ screen.queryByText('buttons.upload_datafile')
+ ).not.toBeInTheDocument();
+ });
+
+ it('opens upload dialog when clicked', async () => {
+ renderComponent({
+ entityType: 'dataset',
+ entityId: 1,
+ variant: 'icon',
+ });
+
+ const uploadButton = await screen.findByRole('button', {
+ name: 'buttons.upload_datafile',
+ });
+
+ // Trigger the onClick handler directly
+ uploadButton.click();
+
+ expect(await screen.findByRole('dialog')).toBeInTheDocument();
+ });
+
+ it('renders a tooltip when hovered', async () => {
+ renderComponent({
+ entityType: 'investigation',
+ entityId: 1,
+ variant: 'icon',
+ });
+
+ await user.hover(
+ await screen.findByRole('button', {
+ name: 'buttons.upload_dataset',
+ })
+ );
+
+ expect(
+ await screen.findByRole('tooltip', { name: 'buttons.upload_dataset' })
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/datagateway-common/src/views/uploadButton.component.tsx b/packages/datagateway-common/src/views/uploadButton.component.tsx
new file mode 100644
index 000000000..d693ab516
--- /dev/null
+++ b/packages/datagateway-common/src/views/uploadButton.component.tsx
@@ -0,0 +1,106 @@
+import {
+ Button,
+ ButtonProps,
+ IconButton,
+ IconButtonProps,
+} from '@mui/material';
+import { Publish } from '@mui/icons-material';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { StyledTooltip } from '../arrowtooltip.component';
+import UploadDialog from './uploadDialog.component';
+
+export interface UploadButtonProps {
+ entityType: 'investigation' | 'dataset' | 'datafile';
+ entityId: number;
+ variant?: 'text' | 'outlined' | 'contained' | 'icon';
+}
+
+const UploadButton: React.FC = (
+ props: UploadButtonProps
+) => {
+ const { entityType, entityId, variant } = props;
+
+ const [t] = useTranslation();
+
+ const [showUploadDialog, setShowUploadDialog] = React.useState(false);
+ const [showTooltip, setShowTooltip] = React.useState(false);
+
+ const BaseUploadButton = React.useCallback(
+ (props: ButtonProps & IconButtonProps): React.ReactElement => {
+ const OurButton = (props: ButtonProps): React.ReactElement => (
+ }
+ disableElevation
+ {...props}
+ >
+ {t(
+ entityType === 'datafile'
+ ? 'buttons.upload_datafile'
+ : entityType === 'dataset'
+ ? 'Upload Datafile'
+ : 'Upload Dataset'
+ )}
+
+ );
+ const OurIconButton = (props: IconButtonProps): React.ReactElement => (
+
+
+
+ );
+ const ButtonToUse = variant === 'icon' ? OurIconButton : OurButton;
+ return (
+
+ );
+ },
+ [variant, t, entityId, entityType]
+ );
+
+ return (
+ <>
+ setShowTooltip(true)}
+ onMouseLeave={() => setShowTooltip(false)}
+ title={t(
+ entityType === 'investigation'
+ ? 'buttons.upload_dataset'
+ : 'buttons.upload_datafile'
+ )}
+ id={`tooltip-${entityId}`}
+ placement={variant === 'icon' ? 'left' : 'bottom'}
+ arrow
+ >
+
+ {
+ setShowTooltip(false);
+ setShowUploadDialog(true);
+ }}
+ />
+
+
+ setShowUploadDialog(false)}
+ />
+ >
+ );
+};
+
+export default UploadButton;
diff --git a/packages/datagateway-common/src/views/uploadDialog.component.test.tsx b/packages/datagateway-common/src/views/uploadDialog.component.test.tsx
new file mode 100644
index 000000000..7aa0b7c99
--- /dev/null
+++ b/packages/datagateway-common/src/views/uploadDialog.component.test.tsx
@@ -0,0 +1,577 @@
+import type { RenderResult } from '@testing-library/react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import { QueryClient, QueryClientProvider, setLogger } from 'react-query';
+import UploadDialog, {
+ checkNameExists,
+ beforeFileAdded,
+ postProcessor,
+} from './uploadDialog.component';
+import { Provider } from 'react-redux';
+import { combineReducers, createStore } from 'redux';
+import dGCommonReducer from '../state/reducers/dgcommon.reducer';
+import { StateType } from '../state/app.types';
+import axios from 'axios';
+import { readSciGatewayToken } from '../parseTokens';
+import Uppy, { UppyFile } from '@uppy/core';
+
+// TODO: see if we can remove this
+// eslint-disable-next-line import/no-extraneous-dependencies
+// import { fireEvent } from '@testing-library/dom';
+
+jest.mock('../api');
+
+const createUppyInstance = (): Uppy => {
+ // jest mock the uppy instance
+ const uppy = new Uppy();
+ uppy.info = jest.fn();
+ uppy.getFiles = jest.fn().mockReturnValue([]);
+ uppy.setFileState = jest.fn();
+ return uppy;
+};
+
+const createUppyFile = (name: string): UppyFile => {
+ // jest mock the uppy file
+ const file = {
+ id: 'test-id',
+ name,
+ type: 'text/plain',
+ size: 100,
+ meta: { name: name },
+ data: { lastModified: 123456 },
+ } as UppyFile;
+
+ return file;
+};
+
+describe('Upload dialog component', () => {
+ describe('Dataset', () => {
+ let queryClient: QueryClient;
+
+ const createWrapper = (): RenderResult =>
+ render(
+ >({ dgcommon: dGCommonReducer })
+ )}
+ >
+
+
+
+
+ );
+
+ beforeEach(() => {
+ queryClient = new QueryClient();
+ setLogger({
+ log: console.log,
+ warn: console.warn,
+ error: () => undefined,
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders correctly', () => {
+ const { asFragment } = createWrapper();
+ expect(asFragment()).toMatchSnapshot();
+ });
+
+ it('renders a cancel and upload buttons', () => {
+ createWrapper();
+ expect(
+ screen.getByRole('button', { name: 'cancel' })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'upload' })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a name and description text field', () => {
+ createWrapper();
+ expect(
+ screen.getByRole('textbox', { name: 'upload.name' })
+ ).toBeInTheDocument();
+
+ expect(
+ screen.getByRole('textbox', { name: 'upload.description' })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a file upload dashboard', () => {
+ createWrapper();
+ expect(screen.getByLabelText('Uppy Dashboard')).toBeInTheDocument();
+ });
+
+ it('Closes dialog when cancel button is clicked', async () => {
+ const closeFunction = jest.fn();
+
+ render(
+ >({ dgcommon: dGCommonReducer })
+ )}
+ >
+
+
+
+
+ );
+
+ await userEvent.click(screen.getByRole('button', { name: 'cancel' }));
+
+ expect(closeFunction).toHaveBeenCalled();
+ });
+ });
+
+ describe('Datafile', () => {
+ let queryClient: QueryClient;
+
+ const createWrapper = (): RenderResult =>
+ render(
+ >({ dgcommon: dGCommonReducer })
+ )}
+ >
+
+
+
+
+ );
+
+ beforeEach(() => {
+ queryClient = new QueryClient();
+ setLogger({
+ log: console.log,
+ warn: console.warn,
+ error: () => undefined,
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders correctly', () => {
+ const { asFragment } = createWrapper();
+ expect(asFragment()).toMatchSnapshot();
+ });
+
+ it('renders a cancel and upload buttons', () => {
+ createWrapper();
+ expect(
+ screen.getByRole('button', { name: 'cancel' })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'upload' })
+ ).toBeInTheDocument();
+ });
+
+ it("doesn't render a name and description text field", () => {
+ createWrapper();
+ expect(
+ screen.queryByRole('textbox', { name: 'upload.name' })
+ ).not.toBeInTheDocument();
+
+ expect(
+ screen.queryByRole('textbox', { name: 'upload.description' })
+ ).not.toBeInTheDocument();
+ });
+
+ it('renders a file upload dashboard', () => {
+ createWrapper();
+ expect(screen.getByLabelText('Uppy Dashboard')).toBeInTheDocument();
+ });
+
+ it('Closes dialog when cancel button is clicked', async () => {
+ const closeFunction = jest.fn();
+
+ render(
+ >({ dgcommon: dGCommonReducer })
+ )}
+ >
+
+
+
+
+ );
+
+ await userEvent.click(screen.getByRole('button', { name: 'cancel' }));
+
+ expect(closeFunction).toHaveBeenCalled();
+ });
+ });
+
+ it('checks if datafile name exists in the dataset', async () => {
+ const apiUrl = 'https://example.com/api';
+ const name = 'test.txt';
+ const datasetId = 1;
+
+ const params = new URLSearchParams();
+ params.append(
+ 'where',
+ JSON.stringify({
+ name: { eq: name },
+ })
+ );
+ params.append(
+ 'where',
+ JSON.stringify({
+ 'dataset.id': { eq: datasetId },
+ })
+ );
+
+ const axiosGetSpy = jest.spyOn(axios, 'get');
+ axiosGetSpy.mockResolvedValueOnce({});
+
+ const result = await checkNameExists(apiUrl, name, 'datafile', datasetId);
+
+ expect(axios.get).toHaveBeenCalledWith(`${apiUrl}/datafiles/findone`, {
+ params,
+ headers: {
+ Authorization: `Bearer ${readSciGatewayToken().sessionId}`,
+ },
+ });
+ expect(result).toBe(true);
+ });
+
+ it('returns false if datafile name does not exist in the dataset', async () => {
+ const apiUrl = 'https://example.com/api';
+ const name = 'test.txt';
+ const datasetId = 1;
+
+ const params = new URLSearchParams();
+ params.append(
+ 'where',
+ JSON.stringify({
+ name: { eq: name },
+ })
+ );
+ params.append(
+ 'where',
+ JSON.stringify({
+ 'dataset.id': { eq: datasetId },
+ })
+ );
+
+ const axiosGetSpy = jest.spyOn(axios, 'get');
+ const mockError = {
+ isAxiosError: true,
+ response: { status: 404 },
+ };
+ axiosGetSpy.mockRejectedValueOnce(mockError);
+
+ const result = await checkNameExists(apiUrl, name, 'datafile', datasetId);
+
+ expect(axios.get).toHaveBeenCalledWith(`${apiUrl}/datafiles/findone`, {
+ params,
+ headers: {
+ Authorization: `Bearer ${readSciGatewayToken().sessionId}`,
+ },
+ });
+ expect(result).toBe(false);
+ });
+
+ it('throws an error if an unexpected error occurs', async () => {
+ const apiUrl = 'https://example.com/api';
+ const name = 'test.txt';
+ const datasetId = 1;
+
+ const params = new URLSearchParams();
+ params.append(
+ 'where',
+ JSON.stringify({
+ name: { eq: name },
+ })
+ );
+ params.append(
+ 'where',
+ JSON.stringify({
+ 'dataset.id': { eq: datasetId },
+ })
+ );
+
+ const axiosGetSpy = jest.spyOn(axios, 'get');
+ axiosGetSpy.mockRejectedValueOnce(new Error('Unexpected error'));
+
+ await expect(
+ checkNameExists(apiUrl, name, 'datafile', datasetId)
+ ).rejects.toThrowError('Unexpected error');
+
+ expect(axios.get).toHaveBeenCalledWith(`${apiUrl}/datafiles/findone`, {
+ params,
+ headers: {
+ Authorization: `Bearer ${readSciGatewayToken().sessionId}`,
+ },
+ });
+ });
+
+ it('checks if dataset name exists in the investigation', async () => {
+ const apiUrl = 'https://example.com/api';
+ const name = 'test';
+ const investigationId = 1;
+
+ const params = new URLSearchParams();
+ params.append(
+ 'where',
+ JSON.stringify({
+ name: { eq: name },
+ })
+ );
+ params.append(
+ 'where',
+ JSON.stringify({
+ 'investigation.id': { eq: investigationId },
+ })
+ );
+
+ const axiosGetSpy = jest.spyOn(axios, 'get');
+ axiosGetSpy.mockResolvedValueOnce({});
+
+ const result = await checkNameExists(
+ apiUrl,
+ name,
+ 'dataset',
+ investigationId
+ );
+
+ expect(axios.get).toHaveBeenCalledWith(`${apiUrl}/datasets/findone`, {
+ params,
+ headers: {
+ Authorization: `Bearer ${readSciGatewayToken().sessionId}`,
+ },
+ });
+ expect(result).toBe(true);
+ });
+
+ it('checks if the file has a correct extension', () => {
+ const uppy = createUppyInstance();
+
+ // correct extension
+ const file1 = createUppyFile('test.txt');
+
+ expect(beforeFileAdded(uppy, file1)).toBe(true);
+ expect(uppy.info).not.toHaveBeenCalled();
+
+ // incorrect extension
+ const file2 = createUppyFile('test.xml');
+
+ expect(beforeFileAdded(uppy, file2)).toBe(false);
+ expect(uppy.info).toHaveBeenCalledWith(
+ '.xml files are not allowed',
+ 'error',
+ 5000
+ );
+ });
+
+ it('checks if the file is a duplicate', () => {
+ const uppy = createUppyInstance();
+ const file1 = createUppyFile('test.txt');
+ const file2 = createUppyFile('test.txt');
+
+ uppy.getFiles = jest.fn().mockReturnValue([file1]);
+
+ const result = beforeFileAdded(uppy, file2);
+
+ expect(result).toBe(false);
+ expect(uppy.info).toHaveBeenCalledWith(
+ 'File named "test.txt" is already in the upload queue',
+ 'error',
+ 5000
+ );
+ });
+
+ it('checks if the file in the queue is a ghost', () => {
+ const uppy = createUppyInstance();
+ const file1 = createUppyFile('test.txt');
+ const file2 = createUppyFile('test.txt');
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (file2 as any).isGhost = true;
+
+ uppy.getFiles = jest.fn().mockReturnValue([file2]);
+
+ const result = beforeFileAdded(uppy, file1);
+
+ expect(result).toBe(true);
+ expect(uppy.info).not.toHaveBeenCalled();
+ expect(file2.size).toBe(0);
+ });
+
+ it('should post processes dataset upload', async () => {
+ const file = createUppyFile('test.txt');
+ file.meta = { name: 'test.txt' };
+ file.response = {
+ body: { id: 1 },
+ status: 200,
+ uploadURL: 'example.com/upload/123',
+ };
+
+ // simulate the failed file
+ const file2 = createUppyFile('test2.txt');
+ file2.meta = { name: 'test2.txt' };
+ file2.response = undefined;
+
+ const uppy = createUppyInstance();
+ uppy.getFiles = jest.fn().mockReturnValue([file, file2]);
+
+ const refObject = { current: null };
+
+ axios.post = jest
+ .fn()
+ .mockResolvedValueOnce({ status: 200, data: { datasetId: 123 } });
+
+ const queryClient = new QueryClient();
+ queryClient.invalidateQueries = jest.fn();
+
+ await postProcessor(
+ uppy,
+ refObject,
+ 'investigation',
+ 'testName',
+ 'testDescription',
+ 1,
+ 'example.com',
+ queryClient
+ );
+
+ // should only commit the successful file
+ expect(axios.post).toHaveBeenCalledWith(
+ 'example.com/commit',
+ {
+ datafiles: [
+ {
+ name: 'test.txt',
+ size: 100,
+ lastModified: 123456,
+ url: 'example.com/upload/123',
+ },
+ ],
+ dataset: {
+ datasetDescription: 'testDescription',
+ datasetName: 'testName',
+ investigationId: 1,
+ },
+ },
+ { headers: { authorization: 'Bearer null' } }
+ );
+ expect(queryClient.invalidateQueries).toHaveBeenCalledWith(['dataset']);
+ expect(refObject.current).toBe(123);
+ });
+
+ it('should post processes datafile upload', async () => {
+ const file = createUppyFile('test.txt');
+ file.meta = { name: 'test.txt' };
+ file.response = {
+ body: { id: 1 },
+ status: 200,
+ uploadURL: 'example.com/upload/123',
+ };
+
+ // simulate the failed file
+ const file2 = createUppyFile('test2.txt');
+ file2.meta = { name: 'test2.txt' };
+ file2.response = undefined;
+
+ const uppy = createUppyInstance();
+ uppy.getFiles = jest.fn().mockReturnValue([file, file2]);
+
+ const refObject = { current: 123 };
+
+ axios.post = jest
+ .fn()
+ .mockResolvedValueOnce({ status: 200, data: { datasetId: 123 } });
+
+ const queryClient = new QueryClient();
+ queryClient.invalidateQueries = jest.fn();
+
+ await postProcessor(
+ uppy,
+ refObject,
+ 'datafile',
+ 'testName',
+ 'testDescription',
+ 123,
+ 'example.com',
+ queryClient
+ );
+
+ // should only commit the successful file
+ expect(axios.post).toHaveBeenCalledWith(
+ 'example.com/commit',
+ {
+ datafiles: [
+ {
+ datasetId: 123,
+ name: 'test.txt',
+ size: 100,
+ lastModified: 123456,
+ url: 'example.com/upload/123',
+ },
+ ],
+ },
+ { headers: { authorization: 'Bearer null' } }
+ );
+ expect(queryClient.invalidateQueries).toHaveBeenCalledWith(['datafile']);
+ expect(refObject.current).toBe(123);
+ });
+
+ it('should not commit if no files are successful', async () => {
+ const file = createUppyFile('test.txt');
+ file.meta = { name: 'test.txt' };
+ file.response = undefined;
+
+ const uppy = createUppyInstance();
+ uppy.getFiles = jest.fn().mockReturnValue([file]);
+
+ const refObject = { current: null };
+
+ axios.post = jest
+ .fn()
+ .mockResolvedValueOnce({ status: 200, data: { datasetId: 123 } });
+
+ const queryClient = new QueryClient();
+ queryClient.invalidateQueries = jest.fn();
+
+ await postProcessor(
+ uppy,
+ refObject,
+ 'investigation',
+ 'testName',
+ 'testDescription',
+ 1,
+ 'example.com',
+ queryClient
+ );
+
+ expect(axios.post).not.toHaveBeenCalled();
+ expect(queryClient.invalidateQueries).not.toHaveBeenCalled();
+ expect(refObject.current).toBe(null);
+ });
+});
diff --git a/packages/datagateway-common/src/views/uploadDialog.component.tsx b/packages/datagateway-common/src/views/uploadDialog.component.tsx
new file mode 100644
index 000000000..96244ab26
--- /dev/null
+++ b/packages/datagateway-common/src/views/uploadDialog.component.tsx
@@ -0,0 +1,536 @@
+import React from 'react';
+import {
+ Button,
+ Dialog,
+ DialogTitle,
+ DialogActions,
+ DialogContent as MuiDialogContent,
+ Grid,
+ styled,
+ TextField,
+ Typography,
+ Box,
+ useTheme,
+} from '@mui/material';
+
+import Uppy, { UppyFile } from '@uppy/core';
+import { Dashboard } from '@uppy/react';
+import Tus from '@uppy/tus';
+import GoldenRetriever from '@uppy/golden-retriever';
+import '@uppy/core/dist/style.min.css';
+import '@uppy/drag-drop/dist/style.min.css';
+import '@uppy/dashboard/dist/style.min.css';
+
+import { readSciGatewayToken } from '../parseTokens';
+import { refreshSession } from '../api';
+import { useTranslation } from 'react-i18next';
+import { useQueryClient } from 'react-query';
+import { useSelector } from 'react-redux';
+import { StateType } from '../state/app.types';
+import axios from 'axios';
+
+const DialogContent = styled(MuiDialogContent)(({ theme }) => ({
+ padding: theme.spacing(2),
+}));
+
+export const checkNameExists = async (
+ apiUrl: string,
+ name: string,
+ entityType: 'datafile' | 'dataset',
+ entityId: number
+): Promise => {
+ const params = new URLSearchParams();
+ params.append(
+ 'where',
+ JSON.stringify({
+ name: { eq: name },
+ })
+ );
+ params.append(
+ 'where',
+ JSON.stringify({
+ [`${entityType === 'datafile' ? 'dataset' : 'investigation'}.id`]: {
+ eq: entityId,
+ },
+ })
+ );
+
+ try {
+ await axios.get(`${apiUrl}/${entityType}s/findone`, {
+ params,
+ headers: {
+ Authorization: `Bearer ${readSciGatewayToken().sessionId}`,
+ },
+ });
+ return true;
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response?.status === 404) {
+ return false;
+ }
+ throw error;
+ }
+};
+
+export const beforeFileAdded = (uppy: Uppy, currentFile: UppyFile): boolean => {
+ const isCorrectExtension = [
+ '.xml',
+ '.exe',
+ '.dll',
+ '.bat',
+ '.sh',
+ '.sqlite',
+ '.js',
+ '.vbs',
+ '.PHP',
+ '.wmv',
+ '.mp3',
+ '.flv',
+ ].some((ext) => currentFile.name.endsWith(ext));
+
+ const isDuplicate = uppy.getFiles().some((file) => {
+ // have to use any here as isGhost is not in the Uppy file type (?)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return file.name === currentFile.name && !(file as any).isGhost;
+ });
+
+ if (isDuplicate) {
+ uppy.info(
+ `File named "${currentFile.name}" is already in the upload queue`,
+ 'error',
+ 5000
+ );
+ return false;
+ }
+
+ if (isCorrectExtension) {
+ uppy.info(
+ `.${currentFile.name
+ .split('.')
+ .pop()
+ ?.toLowerCase()} files are not allowed`,
+ 'error',
+ 5000
+ );
+ return false;
+ } else {
+ // TODO: is there another way to do this?
+ // Workaround for Uppy bug where it doubles the size of restored files
+ uppy.getFiles().forEach((file) => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if (file.id === currentFile.id && (file as any).isGhost) {
+ file.size = 0;
+ }
+ });
+ return true;
+ }
+};
+
+export const postProcessor = async (
+ uppy: Uppy,
+ datasetId: React.MutableRefObject,
+ entityType: 'investigation' | 'dataset' | 'datafile',
+ uploadName: string,
+ uploadDescription: string,
+ entityId: number,
+ uploadUrl: string | undefined,
+ queryClient: ReturnType
+): Promise => {
+ // check if the files have been uploaded
+ const files = uppy.getFiles();
+ const uploadedFiles = files.filter(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (file) => file?.response !== undefined && !(file as any).isCommited
+ );
+ if (uploadedFiles.length > 0) {
+ let params = {};
+ if (entityType === 'investigation' && datasetId.current === null) {
+ params = {
+ dataset: {
+ datasetName: uploadName,
+ datasetDescription: uploadDescription,
+ investigationId: entityId,
+ },
+ datafiles: uploadedFiles.map((file) => {
+ return {
+ name: file.name,
+ url: file.response?.uploadURL,
+ size: file.size,
+ lastModified: (file.data as File).lastModified,
+ };
+ }),
+ };
+ } else {
+ params = {
+ datafiles: uploadedFiles.map((file) => {
+ return {
+ name: file.name,
+ url: file.response?.uploadURL,
+ size: file.size,
+ datasetId: datasetId.current ? datasetId.current : entityId,
+ lastModified: (file.data as File).lastModified,
+ };
+ }),
+ };
+ }
+ return axios
+ .post(`${uploadUrl}/commit`, params, {
+ headers: {
+ authorization: `Bearer ${readSciGatewayToken().sessionId}`,
+ },
+ })
+ .then((response) => {
+ uploadedFiles.forEach((file) => {
+ uppy.setFileState(file.id, { isCommited: true });
+ });
+
+ datasetId.current = response.data.datasetId ?? null;
+ if (entityType === 'datafile') {
+ queryClient.invalidateQueries(['datafile']);
+ } else {
+ queryClient.invalidateQueries(['dataset']);
+ }
+ });
+ }
+
+ return Promise.resolve();
+};
+
+interface UploadDialogProps {
+ entityType: 'investigation' | 'dataset' | 'datafile';
+ entityId: number;
+ open: boolean;
+
+ setClose: () => void;
+}
+
+const UploadDialog: React.FC = (
+ props: UploadDialogProps
+) => {
+ const { entityType, entityId, open, setClose } = props;
+ const queryClient = useQueryClient();
+ const uploadUrl = useSelector(
+ (state: StateType) => state.dgcommon.urls.uploadUrl
+ );
+ const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl);
+ const [uploadDisabled, setUploadDisabled] = React.useState(true);
+ const [textInputDisabled, setTextInputDisabled] =
+ React.useState(false);
+
+ const datasetId = React.useRef(null);
+
+ const [uploadName, setUploadName] = React.useState('');
+ const [uploadDescription, setUploadDescription] = React.useState('');
+
+ const [uppy] = React.useState(() =>
+ new Uppy({
+ // debug: true,
+ id:
+ `${
+ entityType === 'investigation' ? 'investigation' : 'dataset'
+ }-${props.entityId?.toString()}` ?? 'null',
+ autoProceed: false,
+ restrictions: {
+ maxTotalFileSize: 5368709120,
+ },
+ // TODO: ask Alex/users about ux of this
+ allowMultipleUploadBatches: false,
+ onBeforeFileAdded: (currentFile) => {
+ // TODO: why can't we return the result of beforeFileAdded directly?
+ const addFile = beforeFileAdded(uppy, currentFile);
+ return addFile;
+ },
+ onBeforeUpload: (files) => {
+ // Refresh the session before uploading so that the session doesn't expire
+ // while the user is idly uploading large files
+ refreshSession(apiUrl);
+ return true;
+ },
+ })
+ .on('file-added', async (file) => {
+ let prevUploadDisabled = false;
+ setUploadDisabled((oldUploadDisabled) => {
+ prevUploadDisabled = oldUploadDisabled;
+ return true;
+ });
+ if (entityType !== 'investigation') {
+ const fileExists = await checkNameExists(
+ apiUrl,
+ file.name,
+ 'datafile',
+ entityId
+ ).catch((error) => {
+ uppy.info(error.message, 'error', 5000);
+ return true;
+ });
+
+ if (fileExists) {
+ uppy.info(
+ `File "${file.name}" already exists in this dataset`,
+ 'error',
+ 5000
+ );
+ uppy.removeFile(file.id);
+ setUploadDisabled(prevUploadDisabled);
+ return;
+ }
+ }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if (!uppy.getFiles().some((file) => (file as any).isGhost)) {
+ setUploadDisabled(false);
+ } else {
+ setUploadDisabled(true);
+ }
+ })
+ .on('file-removed', () => {
+ if (uppy.getFiles().length === 0) {
+ setUploadDisabled(true);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } else if (!uppy.getFiles().some((file) => (file as any).isGhost)) {
+ setUploadDisabled(false);
+ }
+ })
+ .on('error', (error) => {
+ uppy.info(error.message, 'error', 5000);
+ })
+ // // TODO: is this needed here?
+ // .on('complete', (result) => {
+ // if (result.failed.length === 0) {
+ // datasetId.current = null;
+ // }
+ // })
+ .use(Tus, {
+ endpoint: `${uploadUrl}/upload/`,
+ uploadDataDuringCreation: true,
+ headers: {
+ authorization: `Bearer ${readSciGatewayToken().sessionId}`,
+ },
+ })
+ );
+
+ React.useEffect(() => {
+ const postProcessorWrapper = (): Promise =>
+ postProcessor(
+ uppy,
+ datasetId,
+ entityType,
+ uploadName,
+ uploadDescription,
+ entityId,
+ uploadUrl,
+ queryClient
+ );
+ uppy.addPostProcessor(postProcessorWrapper);
+
+ return () => {
+ uppy.removePostProcessor(postProcessorWrapper);
+ };
+ }, [
+ entityId,
+ entityType,
+ queryClient,
+ uploadDescription,
+ uploadName,
+ uploadUrl,
+ uppy,
+ ]);
+
+ // TODO: check it doesn't break anything
+ React.useEffect(() => {
+ if (
+ open &&
+ uppy.getFiles().length > 0 &&
+ uppy.getState().recoveredState &&
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ !uppy.getFiles().some((file) => (file as any).isGhost)
+ ) {
+ setUploadDisabled(false);
+ }
+ }, [open, uppy]);
+
+ // TODO: investigate why this causes tests to fail
+ // Temporary fix (?): only use GoldenRetriever if indexedDB is available
+ React.useEffect(() => {
+ /* istanbul ignore if */
+ if (
+ process.env.NODE_ENV === 'development' ||
+ process.env.REACT_APP_E2E_TESTING
+ ) {
+ const preProcessor = async (): Promise => {
+ const files = uppy.getFiles();
+ files.forEach((file) => {
+ if (file.name === 'fail') {
+ uppy.setFileState(file.id, {
+ data: null,
+ name: 'notfail',
+ });
+ file.meta.name = `not${file.meta.name}`;
+ } else if (file.name === 'notfail') {
+ uppy.setFileState(file.id, {
+ data: new File(['test'], 'notfail'),
+ });
+ }
+ });
+ };
+ uppy.addPreProcessor(preProcessor);
+ }
+
+ if (window.indexedDB) {
+ uppy.use(GoldenRetriever);
+ }
+ }, [uppy]);
+
+ const [t] = useTranslation();
+ const theme = useTheme();
+
+ const dialogClose = (_event?: unknown, reason?: string): void => {
+ if (reason !== 'backdropClick') {
+ uppy.cancelAll({ reason: 'user' });
+ setUploadName('');
+ setUploadDescription('');
+ setClose();
+ datasetId.current = null;
+ setTextInputDisabled(false);
+ setUploadDisabled(true);
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default UploadDialog;
diff --git a/packages/datagateway-dataview/craco.config.js b/packages/datagateway-dataview/craco.config.js
index 2489e78d3..a21c78cb2 100644
--- a/packages/datagateway-dataview/craco.config.js
+++ b/packages/datagateway-dataview/craco.config.js
@@ -17,6 +17,12 @@ module.exports = {
webpackConfig.optimization.runtimeChunk = false;
}
+ // TODO: should we do this, or should we import uppy styles from CDN instead?
+ const cssRule = webpackConfig.module.rules
+ .find((r) => r.oneOf)
+ .oneOf.find((r) => r.test.toString() === '/\\.css$/');
+ cssRule.use = ['style-loader', 'css-loader'];
+
return webpackConfig;
},
},
diff --git a/packages/datagateway-dataview/cypress/e2e/dlsUpload.cy.ts b/packages/datagateway-dataview/cypress/e2e/dlsUpload.cy.ts
new file mode 100644
index 000000000..b2d9abefe
--- /dev/null
+++ b/packages/datagateway-dataview/cypress/e2e/dlsUpload.cy.ts
@@ -0,0 +1,795 @@
+const datasets: number[] = [];
+const datafiles: number[] = [];
+
+describe.skip('DLS Upload functionality', () => {
+ beforeEach(() => {
+ cy.intercept('**/upload/').as('upload');
+ cy.intercept('**/commit').as('commit');
+ cy.intercept('**/datasets/?*').as('deleteDataset');
+ cy.intercept('**/datafiles/?*').as('deleteDatafile');
+ cy.login({
+ username: 'root',
+ password: 'pw',
+ mechanism: 'simple',
+ });
+ });
+
+ it('should correctly render all upload buttons', () => {
+ cy.visit('/browse/proposal/INVESTIGATION%201/investigation/');
+
+ cy.get('[aria-label="Upload Dataset to Visit"]').should('be.visible');
+
+ cy.get('[aria-label="page view Display as cards"]').click();
+ cy.get('[aria-label="Upload Dataset to Visit"]')
+ .contains('Upload Dataset')
+ .should('be.visible');
+
+ cy.get('[aria-label="card-title"]').first().click();
+ cy.get('[aria-label="Upload Datafiles to Dataset"]')
+ .contains('Upload Datafile')
+ .should('be.visible');
+
+ cy.get('[aria-label="page view Display as table"]').click();
+ cy.get('[aria-label="Upload Datafiles to Dataset"]').should('be.visible');
+
+ cy.get('[data-testid="dls-datasets-table-title"]').first().click();
+ cy.get('[aria-label="Upload Datafiles to Dataset"]')
+ .contains('Upload Datafiles to Dataset')
+ .should('be.visible');
+ });
+
+ describe('Datasets', () => {
+ beforeEach(() => {
+ cy.visit('/browse/proposal/INVESTIGATION%201/investigation/');
+ });
+
+ afterEach(() => {
+ cy.removeUploads(datasets, datafiles).then(() => {
+ datasets.length = 0;
+ datafiles.length = 0;
+ });
+ });
+
+ it('should render dataset upload dialog', () => {
+ cy.get('[aria-label="Upload Dataset to Visit"]').first().click();
+
+ cy.get('[role="dialog"]').should('be.visible');
+
+ cy.contains('Upload Dataset to Visit').should('be.visible');
+ cy.get('[aria-label="upload"]').should('be.disabled');
+ cy.get('[aria-label="close"]').should('be.visible');
+ cy.get('[aria-label="cancel"]').should('be.visible');
+ cy.get('[aria-label="Uppy Dashboard"]').should('be.visible');
+ cy.get('input[id="upload-name"]').should('be.visible');
+ cy.get('textarea[id="upload-description"]').should('be.visible');
+
+ cy.get('[aria-label="close"]').click();
+ cy.get('[role="dialog"]').should('not.exist');
+ });
+
+ it('should be able to upload a dataset with a single file', () => {
+ cy.get('[aria-label="Upload Dataset to Visit"]').first().click();
+
+ cy.get('[id="upload-name"]').type('Test dataset');
+ cy.get('[id="upload-description"]').type('Test description');
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[aria-label="upload"]').should('be.enabled');
+
+ cy.get('[aria-label="upload"]').click();
+ cy.wait('@upload');
+ cy.wait('@commit').then((commit) => {
+ expect(commit.response?.statusCode).to.equal(201);
+ datasets.push(commit.response?.body.datasetId);
+ });
+
+ cy.get('[aria-label="upload"]').should('be.disabled');
+
+ cy.get('button').contains('Done').click();
+ cy.get('[data-testid="dls-visits-table-visitId"]').first().click();
+
+ cy.get('[aria-label="Test dataset"]').should('be.visible').click();
+ cy.get('[aria-label="datafile.txt"]').should('be.visible');
+ });
+
+ it('should be able to upload a dataset with multiple files', () => {
+ cy.get('[aria-label="Upload Dataset to Visit"]').first().click();
+
+ cy.get('[id="upload-name"]').type('Test dataset');
+ cy.get('[id="upload-description"]').type('Test description');
+
+ // add a file via drag-drop
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+
+ // TODO: add a file via file explorer
+ cy.get('[aria-label="Add more files"]').click();
+ cy.get('button').contains('browse files').should('be.visible');
+ cy.get('button').contains('browse folders').should('be.visible');
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.json',
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[aria-label="upload"]').should('be.enabled');
+
+ cy.get('[aria-label="upload"]').click();
+ cy.wait(['@upload', '@upload']);
+ cy.wait('@commit').then((commit) => {
+ expect(commit.response?.statusCode).to.equal(201);
+ datasets.push(commit.response?.body.datasetId);
+ });
+
+ cy.get('[aria-label="upload"]').should('be.disabled');
+ cy.get('input[id="upload-name"]').should('be.disabled');
+ cy.get('textarea[id="upload-description"]').should('be.disabled');
+
+ // should not allow any more files to be added
+ cy.get('[aria-label="Add more files"]').should('not.exist');
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.json',
+ { action: 'drag-drop' }
+ );
+ cy.contains('Cannot add more files');
+
+ cy.get('button').contains('Done').click();
+ cy.get('[data-testid="dls-visits-table-visitId"]').first().click();
+
+ cy.get('[aria-label="Test dataset"]').should('be.visible').click();
+ cy.get('[aria-label="datafile.txt"]').should('be.visible');
+ cy.get('[aria-label="datafile.json"]').should('be.visible');
+ });
+
+ it('should not allow to upload .xml files', () => {
+ cy.get('[aria-label="Upload Dataset to Visit"]').first().click();
+
+ cy.get('[id="upload-name"]').type('Test dataset');
+ cy.get('[id="upload-description"]').type('Test description');
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ {
+ contents: Cypress.Buffer.from('file contents'),
+ fileName: 'datafile.xml',
+ mimeType: 'application/xml',
+ },
+ { action: 'drag-drop' }
+ );
+
+ cy.contains('.xml files are not allowed');
+ });
+
+ it('should not be able to upload a dataset without a name', () => {
+ cy.get('[aria-label="Upload Dataset to Visit"]').first().click();
+ cy.get('[id="upload-description"]').type('Test description');
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[aria-label="upload"]').click();
+ cy.contains('Dataset name cannot be empty');
+ });
+
+ it('should not be able to upload a dataset with a name that already exists', () => {
+ cy.get('[aria-label="Upload Dataset to Visit"]').first().click();
+
+ cy.get('[id="upload-name"]').type('Dataset 1');
+ cy.get('[id="upload-description"]').type('Test description');
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[aria-label="upload"]').click();
+ cy.contains('Dataset "Dataset 1" already exists in this investigation');
+ });
+
+ it('should not allow to add a file with the same name', () => {
+ cy.get('[aria-label="Upload Dataset to Visit"]').first().click();
+
+ cy.get('[id="upload-name"]').type('Test dataset');
+ cy.get('[id="upload-description"]').type('Test description');
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+
+ cy.contains('File named "datafile.txt" is already in the upload queue');
+ });
+
+ it('should reset the form when the cancel button is clicked', () => {
+ cy.get('[aria-label="Upload Dataset to Visit"]').first().click();
+
+ cy.get('[id="upload-name"]').type('Test dataset');
+ cy.get('[id="upload-description"]').type('Test description');
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[aria-label="cancel"]').click();
+ cy.get('[role="dialog"]').should('not.exist');
+
+ cy.get('[aria-label="Upload Dataset to Visit"]').first().click();
+ cy.get('[id="upload-name"]').should('have.value', '');
+ cy.get('[id="upload-description"]').should('have.value', '');
+ });
+
+ it('should not reset the form when the close button is clicked', () => {
+ cy.get('[aria-label="Upload Dataset to Visit"]').first().click();
+
+ cy.get('[id="upload-name"]').type('Test dataset');
+ cy.get('[id="upload-description"]').type('Test description');
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[aria-label="close"]').click();
+ cy.get('[role="dialog"]').should('not.exist');
+
+ cy.get('[aria-label="Upload Dataset to Visit"]').first().click();
+ cy.get('[id="upload-name"]').should('have.value', 'Test dataset');
+ cy.get('[id="upload-description"]').should(
+ 'have.value',
+ 'Test description'
+ );
+ cy.contains('datafile.txt');
+ });
+
+ it('should allow to retry a failed upload', () => {
+ cy.get('[aria-label="Upload Dataset to Visit"]').first().click();
+
+ cy.get('[id="upload-name"]').type('Test dataset');
+ cy.get('[id="upload-description"]').type('Test description');
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ {
+ contents: Cypress.Buffer.from('file contents'),
+ fileName: 'fail',
+ mimeType: '',
+ },
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[aria-label="upload"]').click();
+
+ cy.wait('@upload');
+ cy.wait('@commit').then((commit) => {
+ expect(commit.response?.statusCode).to.equal(201);
+ datasets.push(commit.response?.body.datasetId);
+ });
+
+ cy.contains('Failed to upload notfail');
+
+ cy.contains('Retry').click();
+
+ cy.contains('datafile.txt').should('be.visible');
+ cy.contains('notfail').should('be.visible');
+
+ cy.wait('@upload');
+ cy.wait('@commit').then((commit) => {
+ expect(commit.response?.statusCode).to.equal(201);
+ });
+ });
+
+ it('should recover files after a browser crash', () => {
+ cy.get('[aria-label="Upload Dataset to Visit"]').first().click();
+
+ cy.get('[id="upload-name"]').type('Test dataset');
+ cy.get('[id="upload-description"]').type('Test description');
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ {
+ contents: Cypress.Buffer.alloc(20 * 1024 * 1024),
+ fileName: 'bigfile.txt',
+ mimeType: '',
+ lastModified: 0,
+ },
+ { action: 'drag-drop' }
+ );
+
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(500);
+ cy.reload(true);
+
+ cy.get('[aria-label="Upload Dataset to Visit"]').first().click();
+ cy.contains('datafile.txt').should('be.visible');
+ cy.get('[id="uppy_uppy-bigfile/txt-1e-text/plain-20971520-0"]').should(
+ 'be.visible'
+ );
+ cy.get('[id="uppy_uppy-bigfile/txt-1e-text/plain-20971520-0"]').should(
+ 'have.class',
+ 'is-ghost'
+ );
+ cy.contains('Session restored').should('be.visible');
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ {
+ contents: Cypress.Buffer.alloc(20 * 1024 * 1024),
+ fileName: 'bigfile.txt',
+ mimeType: '',
+ lastModified: 0,
+ },
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[id="uppy_uppy-bigfile/txt-1e-text/plain-20971520-0"]').should(
+ 'not.have.class',
+ 'is-ghost'
+ );
+ cy.get('input[id="upload-name"]').type('Test dataset');
+ cy.get('[aria-label="upload"]').click();
+ cy.wait('@upload');
+ cy.wait('@commit').then((commit) => {
+ expect(commit.response?.statusCode).to.equal(201);
+ datasets.push(commit.response?.body.datasetId);
+ });
+ });
+
+ it('disables the upload button correctly', () => {
+ cy.get('[aria-label="Upload Dataset to Visit"]').first().click();
+
+ // Upload button should be disabled when no files are selected
+ cy.get('[aria-label="upload"]').should('be.disabled');
+
+ // should be disabled if invalid file is selected
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ {
+ contents: Cypress.Buffer.from('file contents'),
+ fileName: 'datafile.xml',
+ mimeType: 'application/xml',
+ },
+ { action: 'drag-drop' }
+ );
+ cy.get('[aria-label="upload"]').should('be.disabled');
+
+ // should be enabled if valid file is selected
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+ cy.get('[aria-label="upload"]').should('be.enabled');
+
+ // should be disabled if all files are removed
+ cy.get('[aria-label="Remove file"]').click();
+ cy.get('[aria-label="upload"]').should('be.disabled');
+
+ // should be enabled when files are restored (without ghost files)
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.json',
+ { action: 'drag-drop' }
+ );
+ cy.get('[aria-label="upload"]').should('be.enabled');
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(500);
+ cy.reload(true);
+ cy.get('[aria-label="Upload Dataset to Visit"]').first().click();
+ cy.get('[aria-label="upload"]').should('be.enabled');
+
+ // should be disabled when files are restored (with ghost files)
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ {
+ contents: Cypress.Buffer.alloc(20 * 1024 * 1024),
+ fileName: 'bigfile.txt',
+ mimeType: '',
+ lastModified: 0,
+ },
+ { action: 'drag-drop' }
+ );
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(500);
+ cy.reload(true);
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(500);
+ cy.get('[aria-label="Upload Dataset to Visit"]').first().click();
+ cy.get('[aria-label="upload"]').should('be.disabled');
+
+ // should be enabled when ghost files are removed
+ cy.get('[aria-label="Remove file"]').first().click({ force: true });
+ cy.get('[aria-label="upload"]').should('be.enabled');
+
+ // should be enabled when ghost file is reselcted
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ {
+ contents: Cypress.Buffer.alloc(20 * 1024 * 1024),
+ fileName: 'bigfile.txt',
+ mimeType: '',
+ lastModified: 0,
+ },
+ { action: 'drag-drop' }
+ );
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(500);
+ cy.reload(true);
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(500);
+ cy.get('[aria-label="Upload Dataset to Visit"]').first().click();
+ cy.get('[aria-label="upload"]').should('be.disabled');
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ {
+ contents: Cypress.Buffer.alloc(20 * 1024 * 1024),
+ fileName: 'bigfile.txt',
+ mimeType: '',
+ lastModified: 0,
+ },
+ { action: 'drag-drop' }
+ );
+ cy.get('[aria-label="upload"]').should('be.enabled');
+ });
+ });
+
+ describe('Datafiles', () => {
+ beforeEach(() => {
+ cy.visit(
+ '/browse/proposal/INVESTIGATION%201/investigation/1/dataset/13/datafile'
+ );
+ });
+
+ afterEach(() => {
+ cy.removeUploads(datasets, datafiles).then(() => {
+ datasets.length = 0;
+ datafiles.length = 0;
+ });
+ });
+
+ it('should render datafile upload dialog', () => {
+ cy.get('[aria-label="Upload Datafiles to Dataset"]').first().click();
+
+ cy.get('[role="dialog"]').should('be.visible');
+
+ cy.contains('Upload Datafiles to Dataset').should('be.visible');
+ cy.get('[aria-label="upload"]').should('be.disabled');
+ cy.get('[aria-label="close"]').should('be.visible');
+ cy.get('[aria-label="cancel"]').should('be.visible');
+ cy.get('[aria-label="Uppy Dashboard"]').should('be.visible');
+ cy.get('input[id="upload-name"]').should('not.exist');
+ cy.get('textarea[id="upload-description"]').should('not.exist');
+
+ cy.get('[aria-label="close"]').click();
+ cy.get('[role="dialog"]').should('not.exist');
+ });
+
+ it('should be able to upload a single datafile', () => {
+ cy.get('[aria-label="Upload Datafiles to Dataset"]').first().click();
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[aria-label="upload"]').should('be.enabled');
+
+ cy.get('[aria-label="upload"]').click();
+ cy.wait('@upload');
+ cy.wait('@commit').then((commit) => {
+ expect(commit.response?.statusCode).to.equal(201);
+ commit.response?.body.datafiles.forEach((datafile: number) => {
+ datafiles.push(datafile);
+ });
+ });
+
+ cy.get('[aria-label="upload"]').should('be.disabled');
+
+ cy.get('button').contains('Done').click();
+ cy.get('[aria-label="datafile.txt"]').should('be.visible');
+ });
+
+ it('should be able to upload multiple datafiles', () => {
+ cy.get('[aria-label="Upload Datafiles to Dataset"]').first().click();
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.json',
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[aria-label="upload"]').should('be.enabled');
+
+ cy.get('[aria-label="upload"]').click();
+ cy.wait(['@upload', '@upload']);
+ cy.wait('@commit').then((commit) => {
+ expect(commit.response?.statusCode).to.equal(201);
+ commit.response?.body.datafiles.forEach((datafile: number) => {
+ datafiles.push(datafile);
+ });
+ });
+
+ cy.get('[aria-label="upload"]').should('be.disabled');
+
+ cy.get('button').contains('Done').click();
+ cy.get('[aria-label="datafile.txt"]').should('be.visible');
+ cy.get('[aria-label="datafile.json"]').should('be.visible');
+ });
+
+ it('should not allow to upload .xml files', () => {
+ cy.get('[aria-label="Upload Datafiles to Dataset"]').first().click();
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ {
+ contents: Cypress.Buffer.from('file contents'),
+ fileName: 'datafile.xml',
+ mimeType: 'application/xml',
+ },
+ { action: 'drag-drop' }
+ );
+
+ cy.contains('.xml files are not allowed');
+ });
+
+ it('should not allow to add a file with the same name', () => {
+ cy.get('[aria-label="Upload Datafiles to Dataset"]').first().click();
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+
+ cy.contains('File named "datafile.txt" is already in the upload queue');
+ });
+
+ it('should reset the form when the cancel button is clicked', () => {
+ cy.get('[aria-label="Upload Datafiles to Dataset"]').first().click();
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[aria-label="cancel"]').click();
+ cy.get('[role="dialog"]').should('not.exist');
+
+ cy.get('[aria-label="Upload Datafiles to Dataset"]').first().click();
+ cy.contains('datafile.txt').should('not.exist');
+ });
+
+ it('should not reset the form when the close button is clicked', () => {
+ cy.get('[aria-label="Upload Datafiles to Dataset"]').first().click();
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[aria-label="close"]').click();
+ cy.get('[role="dialog"]').should('not.exist');
+
+ cy.get('[aria-label="Upload Datafiles to Dataset"]').first().click();
+ cy.contains('datafile.txt').should('be.visible');
+ });
+
+ it('should allow to retry a failed upload', () => {
+ cy.get('[aria-label="Upload Datafiles to Dataset"]').first().click();
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ {
+ contents: Cypress.Buffer.from('file contents'),
+ fileName: 'fail',
+ mimeType: '',
+ },
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[aria-label="upload"]').click();
+
+ cy.wait('@upload');
+ cy.wait('@commit').then((commit) => {
+ expect(commit.response?.statusCode).to.equal(201);
+ commit.response?.body.datafiles.forEach((datafile: number) => {
+ datafiles.push(datafile);
+ });
+ });
+
+ cy.contains('Failed to upload notfail');
+
+ // upload button should be disabled
+ cy.get('[aria-label="upload"]').should('be.disabled');
+
+ cy.contains('Retry').click();
+
+ cy.contains('datafile.txt').should('be.visible');
+ cy.contains('notfail').should('be.visible');
+
+ cy.wait('@upload');
+ cy.wait('@commit').then((commit) => {
+ expect(commit.response?.statusCode).to.equal(201);
+ commit.response?.body.datafiles.forEach((datafile: number) => {
+ datafiles.push(datafile);
+ });
+ });
+ });
+
+ it('should recover files after a browser crash', () => {
+ cy.get('[aria-label="Upload Datafiles to Dataset"]').first().click();
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ {
+ contents: Cypress.Buffer.alloc(20 * 1024 * 1024),
+ fileName: 'bigfile.txt',
+ mimeType: '',
+ lastModified: 0,
+ },
+ { action: 'drag-drop' }
+ );
+
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(500);
+ cy.reload(true);
+
+ cy.get('[aria-label="Upload Datafiles to Dataset"]').first().click();
+ cy.contains('datafile.txt').should('be.visible');
+ cy.get('[id="uppy_uppy-bigfile/txt-1e-text/plain-20971520-0"]').should(
+ 'be.visible'
+ );
+ cy.get('[id="uppy_uppy-bigfile/txt-1e-text/plain-20971520-0"]').should(
+ 'have.class',
+ 'is-ghost'
+ );
+ cy.contains('Session restored').should('be.visible');
+
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ {
+ contents: Cypress.Buffer.alloc(20 * 1024 * 1024),
+ fileName: 'bigfile.txt',
+ mimeType: '',
+ lastModified: 0,
+ },
+ { action: 'drag-drop' }
+ );
+
+ cy.get('[id="uppy_uppy-bigfile/txt-1e-text/plain-20971520-0"]').should(
+ 'not.have.class',
+ 'is-ghost'
+ );
+
+ cy.get('[aria-label="upload"]').click();
+ cy.wait('@upload');
+ cy.wait('@commit').then((commit) => {
+ expect(commit.response?.statusCode).to.equal(201);
+ commit.response?.body.datafiles.forEach((datafile: number) => {
+ datafiles.push(datafile);
+ });
+ });
+
+ cy.get('[aria-label="close"]').click();
+ cy.get('[aria-label="datafile.txt"]').should('be.visible');
+ cy.get('[aria-label="bigfile.txt"]').should('be.visible');
+ });
+
+ it('disables the upload button correctly', () => {
+ cy.get('[aria-label="Upload Datafiles to Dataset"]').first().click();
+
+ // Upload button should be disabled when no files are selected
+ cy.get('[aria-label="upload"]').should('be.disabled');
+
+ // should be disabled if invalid file is selected
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ {
+ contents: Cypress.Buffer.from('file contents'),
+ fileName: 'datafile.xml',
+ mimeType: 'application/xml',
+ },
+ { action: 'drag-drop' }
+ );
+ cy.get('[aria-label="upload"]').should('be.disabled');
+
+ // should be enabled if valid file is selected
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.txt',
+ { action: 'drag-drop' }
+ );
+ cy.get('[aria-label="upload"]').should('be.enabled');
+
+ // should be disabled if all files are removed
+ cy.get('[aria-label="Remove file"]').click();
+ cy.get('[aria-label="upload"]').should('be.disabled');
+
+ // should be enabled when files are restored (without ghost files)
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ 'cypress/fixtures/datafile.json',
+ { action: 'drag-drop' }
+ );
+ cy.get('[aria-label="upload"]').should('be.enabled');
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(500);
+ cy.reload(true);
+ cy.get('[aria-label="Upload Datafiles to Dataset"]').first().click();
+ cy.get('[aria-label="upload"]').should('be.enabled');
+
+ // should be disabled when files are restored (with ghost files)
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ {
+ contents: Cypress.Buffer.alloc(20 * 1024 * 1024),
+ fileName: 'bigfile.txt',
+ mimeType: '',
+ lastModified: 0,
+ },
+ { action: 'drag-drop' }
+ );
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(500);
+ cy.reload(true);
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(500);
+ cy.get('[aria-label="Upload Datafiles to Dataset"]').first().click();
+ cy.get('[aria-label="upload"]').should('be.disabled');
+
+ // should be enabled when ghost files are removed
+ cy.get('[aria-label="Remove file"]').first().click({ force: true });
+ cy.get('[aria-label="upload"]').should('be.enabled');
+
+ // should be enabled when ghost file is reselcted
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ {
+ contents: Cypress.Buffer.alloc(20 * 1024 * 1024),
+ fileName: 'bigfile.txt',
+ mimeType: '',
+ lastModified: 0,
+ },
+ { action: 'drag-drop' }
+ );
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(500);
+ cy.reload(true);
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(500);
+ cy.get('[aria-label="Upload Datafiles to Dataset"]').first().click();
+ cy.get('[aria-label="upload"]').should('be.disabled');
+ cy.get('[aria-label="Uppy Dashboard"]').selectFile(
+ {
+ contents: Cypress.Buffer.alloc(20 * 1024 * 1024),
+ fileName: 'bigfile.txt',
+ mimeType: '',
+ lastModified: 0,
+ },
+ { action: 'drag-drop' }
+ );
+ cy.get('[aria-label="upload"]').should('be.enabled');
+ });
+ });
+});
diff --git a/packages/datagateway-dataview/cypress/support/commands.js b/packages/datagateway-dataview/cypress/support/commands.js
index c38ae30cb..0cf003bad 100644
--- a/packages/datagateway-dataview/cypress/support/commands.js
+++ b/packages/datagateway-dataview/cypress/support/commands.js
@@ -121,3 +121,28 @@ Cypress.Commands.add('isScrolledTo', { prevSubject: true }, (element) => {
);
});
});
+
+// Clean up the uploaded files and datasets
+Cypress.Commands.add('removeUploads', (datasets, datafiles) => {
+ cy.request('datagateway-dataview-settings.json').then((response) => {
+ const settings = response.body;
+ datasets.forEach((datasetId) => {
+ cy.request({
+ method: 'DELETE',
+ url: `${settings.apiUrl}/datasets/${datasetId}`,
+ headers: {
+ Authorization: `Bearer ${readSciGatewayToken().sessionId}`,
+ },
+ });
+ });
+ datafiles.forEach((datafileId) => {
+ cy.request({
+ method: 'DELETE',
+ url: `${settings.apiUrl}/datafiles/${datafileId}`,
+ headers: {
+ Authorization: `Bearer ${readSciGatewayToken().sessionId}`,
+ },
+ });
+ });
+ });
+});
diff --git a/packages/datagateway-dataview/cypress/support/index.d.ts b/packages/datagateway-dataview/cypress/support/index.d.ts
index 004d508dc..7229ec8a1 100644
--- a/packages/datagateway-dataview/cypress/support/index.d.ts
+++ b/packages/datagateway-dataview/cypress/support/index.d.ts
@@ -9,5 +9,9 @@ declare namespace Cypress {
user?: string
): Cypress.Chainable;
clearDownloadCart(): Cypress.Chainable;
+ removeUploads(
+ datasets: number[],
+ datafiles: number[]
+ ): Cypress.Chainable;
}
}
diff --git a/packages/datagateway-dataview/package.json b/packages/datagateway-dataview/package.json
index 64ecec321..b0a2c2c34 100644
--- a/packages/datagateway-dataview/package.json
+++ b/packages/datagateway-dataview/package.json
@@ -84,7 +84,7 @@
},
"jest": {
"transformIgnorePatterns": [
- "node_modules/(?!axios)"
+ "node_modules/(?!(axios|@uppy|nanoid|exifr|p-queue|p-timeout)/)"
],
"collectCoverageFrom": [
"src/**/*.{tsx,ts,js,jsx}",
diff --git a/packages/datagateway-dataview/public/datagateway-dataview-settings.example.json b/packages/datagateway-dataview/public/datagateway-dataview-settings.example.json
index 8b48c10eb..fec129244 100644
--- a/packages/datagateway-dataview/public/datagateway-dataview-settings.example.json
+++ b/packages/datagateway-dataview/public/datagateway-dataview-settings.example.json
@@ -4,6 +4,7 @@
"features": {},
"idsUrl": "example.ids.url",
"apiUrl": "example.api.url",
+ "uploadUrl": "example.upload.url",
"downloadApiUrl": "example.download.api.url",
"selectAllSetting": true,
"helpSteps": [
@@ -97,6 +98,10 @@
"target": ".tour-dataview-download",
"content": "Click on this button to download this specific item"
},
+ {
+ "target": ".tour-dataview-upload",
+ "content": "Click on this button to upload to this specific item"
+ },
{
"target": ".tour-dataview-citation-formatter",
"content": "Here you can format the citation for this data. Use the dropdown to select the citation format and click the button to copy the citation"
diff --git a/packages/datagateway-dataview/public/res/default.json b/packages/datagateway-dataview/public/res/default.json
index 6f40ef68a..92730af37 100644
--- a/packages/datagateway-dataview/public/res/default.json
+++ b/packages/datagateway-dataview/public/res/default.json
@@ -247,10 +247,16 @@
"parent_selected_tooltip": "Selection disabled, parent entity has already been added to cart",
"remove_from_cart": "Remove from selection",
"download": "Download",
+ "upload_datafile": "Upload Datafiles to Dataset",
+ "upload_dataset": "Upload Dataset to Visit",
"unable_to_download_tooltip": "Unable to download - this item is empty",
"preview": "Preview datafile",
"preview_tooltip": "Preview this datafile"
},
+ "upload": {
+ "name": "Name",
+ "description": "Description"
+ },
"advanced_filters": {
"show": "Show Advanced Filters",
"hide": "Hide Advanced Filters",
diff --git a/packages/datagateway-dataview/src/App.css b/packages/datagateway-dataview/src/App.css
deleted file mode 100644
index c6cb7523f..000000000
--- a/packages/datagateway-dataview/src/App.css
+++ /dev/null
@@ -1,29 +0,0 @@
-.App-logo {
- animation: App-logo-spin infinite 20s linear;
- height: 40vmin;
- pointer-events: none;
-}
-
-.App-header {
- background-color: #282c34;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: calc(10px + 2vmin);
- color: white;
-}
-
-.App-link {
- color: #61dafb;
-}
-
-@keyframes App-logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
diff --git a/packages/datagateway-dataview/src/App.tsx b/packages/datagateway-dataview/src/App.tsx
index ccd84b7f1..3c3cf5514 100644
--- a/packages/datagateway-dataview/src/App.tsx
+++ b/packages/datagateway-dataview/src/App.tsx
@@ -21,7 +21,6 @@ import { batch, connect, Provider } from 'react-redux';
import { AnyAction, applyMiddleware, compose, createStore, Store } from 'redux';
import { createLogger } from 'redux-logger';
import thunk, { ThunkDispatch } from 'redux-thunk';
-import './App.css';
import { saveApiUrlMiddleware } from './page/idCheckFunctions';
import PageContainer from './page/pageContainer.component';
import { configureApp } from './state/actions';
diff --git a/packages/datagateway-dataview/src/index.css b/packages/datagateway-dataview/src/index.css
index d07361807..896cb5bbe 100644
--- a/packages/datagateway-dataview/src/index.css
+++ b/packages/datagateway-dataview/src/index.css
@@ -1,19 +1,8 @@
body {
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
- sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
+ margin: 0; /* can remove this when we're using CssBaseline (React 18 branch) */
}
-code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
-}
-
-@keyframes rotate {
- to {
- transform: rotateZ(360deg);
- }
+/* TODO: Temporary/remove */
+.uppy-StatusBar-actionBtn--upload {
+ display: none !important;
}
diff --git a/packages/datagateway-dataview/src/page/pageContainer.component.test.tsx b/packages/datagateway-dataview/src/page/pageContainer.component.test.tsx
index ab09c7cd9..c47a40419 100644
--- a/packages/datagateway-dataview/src/page/pageContainer.component.test.tsx
+++ b/packages/datagateway-dataview/src/page/pageContainer.component.test.tsx
@@ -100,6 +100,11 @@ describe('PageContainer - Tests', () => {
});
user = userEvent.setup();
+ dGCommonInitialState.urls = {
+ ...dGCommonInitialState.urls,
+ uploadUrl: 'https://example.com/upload',
+ };
+
delete window.location;
window.location = new URL(`http://localhost/`);
@@ -282,6 +287,18 @@ describe('PageContainer - Tests', () => {
).toBeDisabled();
});
+ it('display upload datafile button on dls datafile table', async () => {
+ history.replace('/browse/proposal/1/investigation/1/dataset/25/datafile');
+ renderComponent();
+
+ // wait for the page to load
+ await waitFor(() => {
+ expect(
+ screen.getByRole('button', { name: 'buttons.upload_datafile' })
+ ).toBeInTheDocument();
+ });
+ });
+
it('display filter warning on datafile table', async () => {
history.replace('/browse/investigation/1/dataset/25/datafile');
(checkInvestigationId as jest.Mock).mockResolvedValueOnce(true);
diff --git a/packages/datagateway-dataview/src/page/pageContainer.component.tsx b/packages/datagateway-dataview/src/page/pageContainer.component.tsx
index 163e606c8..15fb133c9 100644
--- a/packages/datagateway-dataview/src/page/pageContainer.component.tsx
+++ b/packages/datagateway-dataview/src/page/pageContainer.component.tsx
@@ -23,6 +23,7 @@ import {
useUpdateQueryParam,
ViewButton,
ClearFiltersButton,
+ UploadButton,
} from 'datagateway-common';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -41,6 +42,8 @@ import TranslatedHomePage from './translatedHomePage.component';
import DoiRedirect from './doiRedirect.component';
import RoleSelector from '../views/roleSelector.component';
import { useIsFetching, useQueryClient } from 'react-query';
+import { StateType } from 'datagateway-common';
+import { useSelector } from 'react-redux';
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const getTablePaperStyle = (
@@ -653,6 +656,10 @@ const DataviewPageContainer: React.FC = () => {
}
};
+ const uploadUrl = useSelector(
+ (state: StateType) => state.dgcommon.urls.uploadUrl
+ );
+
return (
{
/>
)}
/>
+ {
+ const datasetId = match.params.datasetId as string;
+ return (
+ uploadUrl && (
+
+ )
+ );
+ }}
+ />
> => {
idsUrl: settingsResult['idsUrl'],
apiUrl: settingsResult['apiUrl'],
downloadApiUrl: settingsResult['downloadApiUrl'],
+ uploadUrl: settingsResult['uploadUrl'],
icatUrl: '', // we currently don't need icatUrl in dataview so just pass empty string for now
})
);
diff --git a/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.test.tsx
index dd65fcc15..2c2df015c 100644
--- a/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.test.tsx
+++ b/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.test.tsx
@@ -65,6 +65,11 @@ describe('DLS Datasets - Card View', () => {
history = createMemoryHistory();
user = userEvent.setup();
+ dGCommonInitialState.urls = {
+ ...dGCommonInitialState.urls,
+ uploadUrl: 'https://example.com/upload',
+ };
+
mockStore = configureStore([thunk]);
state = JSON.parse(
JSON.stringify({
@@ -187,6 +192,9 @@ describe('DLS Datasets - Card View', () => {
expect(
await screen.findByRole('button', { name: 'buttons.add_to_cart' })
).toBeInTheDocument();
+ expect(
+ await screen.findByRole('button', { name: 'buttons.upload_datafile' })
+ ).toBeInTheDocument();
});
it('renders fine with incomplete data', () => {
diff --git a/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.tsx
index d13b455ea..fc4689c3d 100644
--- a/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.tsx
+++ b/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.tsx
@@ -15,12 +15,25 @@ import {
useTextFilter,
AddToCartButton,
DLSDatasetDetailsPanel,
+ UploadButton,
formatBytes,
} from 'datagateway-common';
+import { styled } from '@mui/material/styles';
import { CalendarToday, Save } from '@mui/icons-material';
import ConfirmationNumberIcon from '@mui/icons-material/ConfirmationNumber';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
+import { useSelector } from 'react-redux';
+import { StateType } from 'datagateway-common';
+
+const ActionButtonsContainer = styled('div')(({ theme }) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ '& button': {
+ margin: 'auto',
+ marginTop: theme.spacing(1),
+ },
+}));
interface DLSDatasetsCVProps {
proposalName: string;
@@ -142,18 +155,27 @@ const DLSDatasetsCardView = (props: DLSDatasetsCVProps): React.ReactElement => {
[dateFilter, t]
);
+ const uploadUrl = useSelector(
+ (state: StateType) => state.dgcommon.urls.uploadUrl
+ );
+
const buttons = React.useMemo(
() => [
(dataset: Dataset) => (
- dataset.id) ?? []}
- entityId={dataset.id}
- parentId={investigationId}
- />
+
+ dataset.id) ?? []}
+ entityId={dataset.id}
+ parentId={investigationId}
+ />
+ {uploadUrl && (
+
+ )}
+
),
],
- [data, investigationId]
+ [data, investigationId, uploadUrl]
);
return (
diff --git a/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.test.tsx
index 7b60bc467..a0971856f 100644
--- a/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.test.tsx
+++ b/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.test.tsx
@@ -65,6 +65,11 @@ describe('DLS Visits - Card View', () => {
history = createMemoryHistory();
user = userEvent.setup();
+ dGCommonInitialState.urls = {
+ ...dGCommonInitialState.urls,
+ uploadUrl: 'https://example.com/upload',
+ };
+
mockStore = configureStore([thunk]);
state = JSON.parse(
JSON.stringify({
@@ -189,6 +194,13 @@ describe('DLS Visits - Card View', () => {
).toBeInTheDocument();
});
+ it('renders buttons correctly', async () => {
+ renderComponent();
+ expect(
+ await screen.findByRole('button', { name: 'buttons.upload_dataset' })
+ ).toBeInTheDocument();
+ });
+
it('renders fine with incomplete data', () => {
(useInvestigationCount as jest.Mock).mockReturnValueOnce({});
(useInvestigationsPaginated as jest.Mock).mockReturnValueOnce({});
diff --git a/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.tsx
index b11fc2d07..86acc9af3 100644
--- a/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.tsx
+++ b/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.tsx
@@ -3,6 +3,7 @@ import React from 'react';
import {
CardView,
CardViewDetails,
+ Dataset,
Investigation,
tableLink,
parseSearchToQuery,
@@ -18,11 +19,14 @@ import {
ArrowTooltip,
DLSVisitDetailsPanel,
formatBytes,
+ UploadButton,
} from 'datagateway-common';
import { Assessment, CalendarToday, Save } from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { Typography } from '@mui/material';
+import { useSelector } from 'react-redux';
+import { StateType } from 'datagateway-common';
interface DLSVisitsCVProps {
proposalName: string;
@@ -102,6 +106,20 @@ const DLSVisitsCardView = (props: DLSVisitsCVProps): React.ReactElement => {
[t, textFilter]
);
+ const uploadUrl = useSelector(
+ (state: StateType) => state.dgcommon.urls.uploadUrl
+ );
+
+ const buttons = React.useMemo(
+ () => [
+ (dataset: Dataset) =>
+ uploadUrl && (
+
+ ),
+ ],
+ [uploadUrl]
+ );
+
const information: CardViewDetails[] = React.useMemo(
() => [
{
@@ -167,6 +185,7 @@ const DLSVisitsCardView = (props: DLSVisitsCVProps): React.ReactElement => {
moreInformation={(investigation: Investigation) => (
)}
+ buttons={buttons}
/>
);
};
diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.test.tsx
index e0007c557..e427b9263 100644
--- a/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.test.tsx
+++ b/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.test.tsx
@@ -91,6 +91,11 @@ describe('DLS Dataset table component', () => {
history = createMemoryHistory();
user = userEvent.setup();
+ dGCommonInitialState.urls = {
+ ...dGCommonInitialState.urls,
+ uploadUrl: 'https://example.com/upload',
+ };
+
mockStore = configureStore([thunk]);
state = JSON.parse(
JSON.stringify({
@@ -363,4 +368,11 @@ describe('DLS Dataset table component', () => {
expect(await screen.findByTestId('dls-dataset-details-panel')).toBeTruthy();
});
+
+ it('renders actions correctly', async () => {
+ renderComponent();
+ expect(
+ await screen.findByRole('button', { name: 'buttons.upload_datafile' })
+ ).toBeInTheDocument();
+ });
});
diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.tsx
index 445d059ee..7c7b5aa6d 100644
--- a/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.tsx
+++ b/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.tsx
@@ -22,6 +22,8 @@ import {
useRemoveFromCart,
DLSDatasetDetailsPanel,
formatBytes,
+ UploadButton,
+ TableActionProps,
} from 'datagateway-common';
import { IndexRange, TableCellProps } from 'react-virtualized';
import { useTranslation } from 'react-i18next';
@@ -29,6 +31,12 @@ import { useLocation } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { StateType } from '../../../state/app.types';
+const actions = [
+ ({ rowData }: TableActionProps) => (
+
+ ),
+];
+
interface DLSDatasetsTableProps {
proposalName: string;
investigationId: string;
@@ -186,6 +194,10 @@ const DLSDatasetsTable = (props: DLSDatasetsTableProps): React.ReactElement => {
);
}, [cartItems, investigationId]);
+ const uploadUrl = useSelector(
+ (state: StateType) => state.dgcommon.urls.uploadUrl
+ );
+
return (
{
disableSelectAll={!selectAllSetting}
detailsPanel={DLSDatasetDetailsPanel}
columns={columns}
+ actions={uploadUrl ? actions : undefined}
/>
);
};
diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.test.tsx
index e4b49fdd1..86ed8e3c4 100644
--- a/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.test.tsx
+++ b/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.test.tsx
@@ -91,6 +91,11 @@ describe('DLS Visits table component', () => {
history = createMemoryHistory();
user = userEvent.setup();
+ dGCommonInitialState.urls = {
+ ...dGCommonInitialState.urls,
+ uploadUrl: 'https://example.com/upload',
+ };
+
mockStore = configureStore([thunk]);
state = JSON.parse(
JSON.stringify({
@@ -344,4 +349,11 @@ describe('DLS Visits table component', () => {
expect(instrumentNameCell).toHaveTextContent('');
});
+
+ it('renders actions correctly', async () => {
+ renderComponent();
+ expect(
+ await screen.findByRole('button', { name: 'buttons.upload_dataset' })
+ ).toBeInTheDocument();
+ });
});
diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.tsx
index 42b1cb4bc..d07c5778a 100644
--- a/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.tsx
+++ b/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.tsx
@@ -11,6 +11,8 @@ import {
useTextFilter,
DLSVisitDetailsPanel,
formatBytes,
+ UploadButton,
+ TableActionProps,
} from 'datagateway-common';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -22,6 +24,18 @@ import {
Save,
} from '@mui/icons-material';
import { useLocation } from 'react-router-dom';
+import { useSelector } from 'react-redux';
+import { StateType } from 'datagateway-common';
+
+const actions = [
+ ({ rowData }: TableActionProps) => (
+
+ ),
+];
interface DLSVisitsTableProps {
proposalName: string;
@@ -148,6 +162,10 @@ const DLSVisitsTable = (props: DLSVisitsTableProps): React.ReactElement => {
[t, dateFilter, textFilter, view, proposalName]
);
+ const uploadUrl = useSelector(
+ (state: StateType) => state.dgcommon.urls.uploadUrl
+ );
+
return (
{
onSort={handleSort}
detailsPanel={DLSVisitDetailsPanel}
columns={columns}
+ actions={uploadUrl ? actions : undefined}
/>
);
};
diff --git a/packages/datagateway-download/package.json b/packages/datagateway-download/package.json
index 58b082748..4147512d9 100644
--- a/packages/datagateway-download/package.json
+++ b/packages/datagateway-download/package.json
@@ -105,7 +105,7 @@
},
"jest": {
"transformIgnorePatterns": [
- "node_modules/(?!axios)"
+ "node_modules/(?!(axios|@uppy|nanoid|exifr|p-queue|p-timeout)/)"
],
"snapshotSerializers": [
"enzyme-to-json/serializer"
diff --git a/packages/datagateway-download/public/logo192.png b/packages/datagateway-download/public/logo192.png
deleted file mode 100644
index fa313abf5..000000000
Binary files a/packages/datagateway-download/public/logo192.png and /dev/null differ
diff --git a/packages/datagateway-download/public/logo512.png b/packages/datagateway-download/public/logo512.png
deleted file mode 100644
index bd5d4b5e2..000000000
Binary files a/packages/datagateway-download/public/logo512.png and /dev/null differ
diff --git a/packages/datagateway-download/public/manifest.json b/packages/datagateway-download/public/manifest.json
deleted file mode 100644
index 080d6c77a..000000000
--- a/packages/datagateway-download/public/manifest.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "short_name": "React App",
- "name": "Create React App Sample",
- "icons": [
- {
- "src": "favicon.ico",
- "sizes": "64x64 32x32 24x24 16x16",
- "type": "image/x-icon"
- },
- {
- "src": "logo192.png",
- "type": "image/png",
- "sizes": "192x192"
- },
- {
- "src": "logo512.png",
- "type": "image/png",
- "sizes": "512x512"
- }
- ],
- "start_url": ".",
- "display": "standalone",
- "theme_color": "#000000",
- "background_color": "#ffffff"
-}
diff --git a/packages/datagateway-download/public/robots.txt b/packages/datagateway-download/public/robots.txt
deleted file mode 100644
index 01b0f9a10..000000000
--- a/packages/datagateway-download/public/robots.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-# https://www.robotstxt.org/robotstxt.html
-User-agent: *
diff --git a/packages/datagateway-download/src/index.css b/packages/datagateway-download/src/index.css
index d07361807..325c05afd 100644
--- a/packages/datagateway-download/src/index.css
+++ b/packages/datagateway-download/src/index.css
@@ -1,19 +1,3 @@
body {
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
- sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
-}
-
-@keyframes rotate {
- to {
- transform: rotateZ(360deg);
- }
-}
+ margin: 0; /* can remove this when we're using CssBaseline (React 18 branch) */
+}
\ No newline at end of file
diff --git a/packages/datagateway-search/package.json b/packages/datagateway-search/package.json
index 24f61d91b..66b07cac9 100644
--- a/packages/datagateway-search/package.json
+++ b/packages/datagateway-search/package.json
@@ -84,7 +84,7 @@
},
"jest": {
"transformIgnorePatterns": [
- "node_modules/(?!axios)"
+ "node_modules/(?!(axios|@uppy|nanoid|exifr|p-queue|p-timeout)/)"
],
"snapshotSerializers": [
"enzyme-to-json/serializer"
diff --git a/packages/datagateway-search/src/App.css b/packages/datagateway-search/src/App.css
deleted file mode 100644
index c6cb7523f..000000000
--- a/packages/datagateway-search/src/App.css
+++ /dev/null
@@ -1,29 +0,0 @@
-.App-logo {
- animation: App-logo-spin infinite 20s linear;
- height: 40vmin;
- pointer-events: none;
-}
-
-.App-header {
- background-color: #282c34;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: calc(10px + 2vmin);
- color: white;
-}
-
-.App-link {
- color: #61dafb;
-}
-
-@keyframes App-logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
diff --git a/packages/datagateway-search/src/App.tsx b/packages/datagateway-search/src/App.tsx
index 89b44063b..7331dedc0 100644
--- a/packages/datagateway-search/src/App.tsx
+++ b/packages/datagateway-search/src/App.tsx
@@ -20,7 +20,6 @@ import { batch, connect, Provider } from 'react-redux';
import { AnyAction, applyMiddleware, compose, createStore, Store } from 'redux';
import { createLogger } from 'redux-logger';
import thunk, { ThunkDispatch } from 'redux-thunk';
-import './App.css';
import SearchPageContainer from './searchPageContainer.component';
import { configureApp } from './state/actions';
import { StateType } from './state/app.types';
diff --git a/packages/datagateway-search/src/index.css b/packages/datagateway-search/src/index.css
index c0238d7cd..c12934096 100644
--- a/packages/datagateway-search/src/index.css
+++ b/packages/datagateway-search/src/index.css
@@ -1,26 +1,3 @@
-@media screen and (min-width: 960px) {
- html {
- margin-left: calc(100vw - 100%);
- margin-right: 0;
- }
-}
-
body {
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
- sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
-}
-
-@keyframes rotate {
- to {
- transform: rotateZ(360deg);
- }
+ margin: 0; /* can remove this when we're using CssBaseline (React 18 branch) */
}
diff --git a/yarn.lock b/yarn.lock
index 38eded315..f54913bc0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3092,6 +3092,20 @@ __metadata:
languageName: node
linkType: hard
+"@transloadit/prettier-bytes@npm:0.0.7":
+ version: 0.0.7
+ resolution: "@transloadit/prettier-bytes@npm:0.0.7"
+ checksum: af075e1b2b045cac55d5ab854b5c94273ef7f4bd825f05fe578559465f206d2a1535343b50170252a9efb31ffdc2d6420e64eb32d2faeabeeb8b7d81d5d46ef0
+ languageName: node
+ linkType: hard
+
+"@transloadit/prettier-bytes@npm:0.0.9":
+ version: 0.0.9
+ resolution: "@transloadit/prettier-bytes@npm:0.0.9"
+ checksum: efa5a723c41e7bce7ad17d1affe6a43209df857e17dc2b12a7c7bd6d3c921df8298086dbfb62ed740ca3e617d8c7f47485bb311adb637b20f2f75a28b08bac4f
+ languageName: node
+ linkType: hard
+
"@trysound/sax@npm:0.2.0":
version: 0.2.0
resolution: "@trysound/sax@npm:0.2.0"
@@ -3610,6 +3624,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/retry@npm:0.12.2":
+ version: 0.12.2
+ resolution: "@types/retry@npm:0.12.2"
+ checksum: e5675035717b39ce4f42f339657cae9637cf0c0051cf54314a6a2c44d38d91f6544be9ddc0280587789b6afd056be5d99dbe3e9f4df68c286c36321579b1bf4a
+ languageName: node
+ linkType: hard
+
"@types/scheduler@npm:*":
version: 0.16.2
resolution: "@types/scheduler@npm:0.16.2"
@@ -3932,6 +3953,228 @@ __metadata:
languageName: node
linkType: hard
+"@uppy/companion-client@npm:^3.6.0":
+ version: 3.6.0
+ resolution: "@uppy/companion-client@npm:3.6.0"
+ dependencies:
+ "@uppy/utils": ^5.6.0
+ namespace-emitter: ^2.0.1
+ p-retry: ^6.1.0
+ checksum: d22e0c13adc94efbec065943833b6a43926805bed91423a17b4cda3699334e16a1c00197fa59f26f935355bb1b561a8e6faada51baa387d68f21ed3553746989
+ languageName: node
+ linkType: hard
+
+"@uppy/core@npm:^3.7.1":
+ version: 3.7.1
+ resolution: "@uppy/core@npm:3.7.1"
+ dependencies:
+ "@transloadit/prettier-bytes": 0.0.9
+ "@uppy/store-default": ^3.0.5
+ "@uppy/utils": ^5.6.0
+ lodash: ^4.17.21
+ mime-match: ^1.0.2
+ namespace-emitter: ^2.0.1
+ nanoid: ^4.0.0
+ preact: ^10.5.13
+ checksum: 6561cfa0218cf665371ec9caef3486ff216ca2daaafdc7b99567d4e8c1ec360707d275e2ac46769ce1244529cf5f871fe05c35b35f1604af6ec4bb8dfbea1f26
+ languageName: node
+ linkType: hard
+
+"@uppy/dashboard@npm:^3.7.1":
+ version: 3.7.1
+ resolution: "@uppy/dashboard@npm:3.7.1"
+ dependencies:
+ "@transloadit/prettier-bytes": 0.0.7
+ "@uppy/informer": ^3.0.4
+ "@uppy/provider-views": ^3.7.0
+ "@uppy/status-bar": ^3.2.5
+ "@uppy/thumbnail-generator": ^3.0.6
+ "@uppy/utils": ^5.6.0
+ classnames: ^2.2.6
+ is-shallow-equal: ^1.0.1
+ lodash: ^4.17.21
+ memoize-one: ^6.0.0
+ nanoid: ^4.0.0
+ preact: ^10.5.13
+ peerDependencies:
+ "@uppy/core": ^3.7.1
+ checksum: 0e86f6e0cf15034479d22767349f5d3bc92961f73d0f1d68891f68d1e3325b1b27adab838605440c72e9bed87b73cb8a2d9a6781d8eb5177b468987011324e61
+ languageName: node
+ linkType: hard
+
+"@uppy/drag-drop@npm:^3.0.3":
+ version: 3.0.3
+ resolution: "@uppy/drag-drop@npm:3.0.3"
+ dependencies:
+ "@uppy/utils": ^5.4.3
+ preact: ^10.5.13
+ peerDependencies:
+ "@uppy/core": ^3.4.0
+ checksum: e3eacbc6df61e33c22c934a2fcc51865e4cd8e8d6712690c79d24311ee6afab08a4cc40e383cfb404c0e76a93d1810c309a652592faff0c4b9ed4cb673e41e99
+ languageName: node
+ linkType: hard
+
+"@uppy/file-input@npm:^3.0.4":
+ version: 3.0.4
+ resolution: "@uppy/file-input@npm:3.0.4"
+ dependencies:
+ "@uppy/utils": ^5.5.2
+ preact: ^10.5.13
+ peerDependencies:
+ "@uppy/core": ^3.6.0
+ checksum: 4e3e7e44d5651613fe531a2874f578e89060ecafa9a9eb77b8bb7caf16bea921982b55bf1f6930216d29749a5d9229eb48761e0812a6376c68f8cabda13ab915
+ languageName: node
+ linkType: hard
+
+"@uppy/form@npm:^3.0.3":
+ version: 3.0.3
+ resolution: "@uppy/form@npm:3.0.3"
+ dependencies:
+ "@uppy/utils": ^5.5.2
+ get-form-data: ^3.0.0
+ peerDependencies:
+ "@uppy/core": ^3.6.0
+ checksum: 705a323713688312f3efa846cf47d458482b8d301e88fedbe5df873991df795f14ce8044e31b7b5495793d8e3a4530fabb71bf7f1af6cee9dedd9917d5e5ec5c
+ languageName: node
+ linkType: hard
+
+"@uppy/golden-retriever@npm:^3.1.1":
+ version: 3.1.1
+ resolution: "@uppy/golden-retriever@npm:3.1.1"
+ dependencies:
+ "@transloadit/prettier-bytes": 0.0.9
+ "@uppy/utils": ^5.5.2
+ lodash: ^4.17.21
+ peerDependencies:
+ "@uppy/core": ^3.6.0
+ checksum: 6126ff6b1bc87e7f47c89eb68e0d1e458f73ae36d65707529602df10e72d1a4fd893cf969598924e992e32a3e7b8125c8910909cff3142d0db03ea5687617098
+ languageName: node
+ linkType: hard
+
+"@uppy/informer@npm:^3.0.4":
+ version: 3.0.4
+ resolution: "@uppy/informer@npm:3.0.4"
+ dependencies:
+ "@uppy/utils": ^5.5.2
+ preact: ^10.5.13
+ peerDependencies:
+ "@uppy/core": ^3.6.0
+ checksum: e4bffe78fe08601a36e59ef98439e84aa10833b8f286af2e3d85cd60cce6161e691312c8e1ac2f3c5552252e76bc62800e969bbc2f17bb40a728ff473f2495fd
+ languageName: node
+ linkType: hard
+
+"@uppy/progress-bar@npm:^3.0.4":
+ version: 3.0.4
+ resolution: "@uppy/progress-bar@npm:3.0.4"
+ dependencies:
+ "@uppy/utils": ^5.5.2
+ preact: ^10.5.13
+ peerDependencies:
+ "@uppy/core": ^3.6.0
+ checksum: 18cd69471946c11a1575ba760dc7c92cb17c4a72599b2377833ec0d76876e54dc2fdbf38d1fd35404f4b2031e4024c30fe0ad8ff7b91ce4028e9f8cf3334357d
+ languageName: node
+ linkType: hard
+
+"@uppy/provider-views@npm:^3.7.0":
+ version: 3.7.0
+ resolution: "@uppy/provider-views@npm:3.7.0"
+ dependencies:
+ "@uppy/utils": ^5.6.0
+ classnames: ^2.2.6
+ nanoid: ^4.0.0
+ p-queue: ^7.3.4
+ preact: ^10.5.13
+ peerDependencies:
+ "@uppy/core": ^3.7.0
+ checksum: 8f1d499a4f5ca4dd658115ba34358cecba99bc1d67cd7fee01761eef5a74a347739f79c44a35638fe1074175a70640af8a5cfd327e02bcf6f7a446d91a102f52
+ languageName: node
+ linkType: hard
+
+"@uppy/react@npm:^3.2.1":
+ version: 3.2.1
+ resolution: "@uppy/react@npm:3.2.1"
+ dependencies:
+ "@uppy/utils": ^5.6.0
+ prop-types: ^15.6.1
+ peerDependencies:
+ "@uppy/core": ^3.7.1
+ "@uppy/dashboard": ^3.7.1
+ "@uppy/drag-drop": ^3.0.3
+ "@uppy/file-input": ^3.0.4
+ "@uppy/progress-bar": ^3.0.4
+ "@uppy/status-bar": ^3.2.5
+ react: ^16.0.0 || ^17.0.0 || ^18.0.0
+ peerDependenciesMeta:
+ "@uppy/dashboard":
+ optional: true
+ "@uppy/drag-drop":
+ optional: true
+ "@uppy/file-input":
+ optional: true
+ "@uppy/progress-bar":
+ optional: true
+ "@uppy/status-bar":
+ optional: true
+ checksum: 273795bca3619e2e1870c0464cce07c38403a2eded3fdafd962513f4510e3e01bc622b216f0138d59f0070044e27565297a022575e4bad2d8308ec6eac1635e6
+ languageName: node
+ linkType: hard
+
+"@uppy/status-bar@npm:^3.2.5":
+ version: 3.2.5
+ resolution: "@uppy/status-bar@npm:3.2.5"
+ dependencies:
+ "@transloadit/prettier-bytes": 0.0.9
+ "@uppy/utils": ^5.5.2
+ classnames: ^2.2.6
+ preact: ^10.5.13
+ peerDependencies:
+ "@uppy/core": ^3.6.0
+ checksum: 55a494b03885517bc0be8f27c455b122cdda4f823136e3fae7f692494684bf2a931024728f0b57530ece788a22e6b1ebeaedf4e9b58acaa6de439b97c39f76c0
+ languageName: node
+ linkType: hard
+
+"@uppy/store-default@npm:^3.0.5":
+ version: 3.0.5
+ resolution: "@uppy/store-default@npm:3.0.5"
+ checksum: b609574033fd4564938f1db6bbe8eba3be979c55d37fd2f359bf0659e1c813d7254158616199041c6d1cf2ad97f4762c924bd73c28815192dc3ac0dcb5289773
+ languageName: node
+ linkType: hard
+
+"@uppy/thumbnail-generator@npm:^3.0.6":
+ version: 3.0.6
+ resolution: "@uppy/thumbnail-generator@npm:3.0.6"
+ dependencies:
+ "@uppy/utils": ^5.5.2
+ exifr: ^7.0.0
+ peerDependencies:
+ "@uppy/core": ^3.6.0
+ checksum: d78ebc96f99f54d0ec3621f5e2ff8e71a624fbb184633cfd2a350bd9b9bc76eca630d3b5fa28c30424e06f9027d8d85789a88e99ff03244523808d82109cb299
+ languageName: node
+ linkType: hard
+
+"@uppy/tus@npm:^3.4.0":
+ version: 3.4.0
+ resolution: "@uppy/tus@npm:3.4.0"
+ dependencies:
+ "@uppy/companion-client": ^3.6.0
+ "@uppy/utils": ^5.6.0
+ tus-js-client: ^3.0.0
+ peerDependencies:
+ "@uppy/core": ^3.7.0
+ checksum: 0f6393be525937cba1b624fffc0f0c9cb00f7162efb6fe5a98f2b6a446f0f0b22323950a5ceba4c16c473c9a99bbc161511e04f3e134c06cdea4337428ef2962
+ languageName: node
+ linkType: hard
+
+"@uppy/utils@npm:^5.4.3, @uppy/utils@npm:^5.5.2, @uppy/utils@npm:^5.6.0":
+ version: 5.6.0
+ resolution: "@uppy/utils@npm:5.6.0"
+ dependencies:
+ lodash: ^4.17.21
+ preact: ^10.5.13
+ checksum: 51456f691b50efed4aacd6600aad8a2111e57ec2dc8f0f02982a13c863243eeda016edce8b0fabbfbe277b2286e9b56786d74995c76a97a6194a5461ffee77e0
+ languageName: node
+ linkType: hard
+
"@webassemblyjs/ast@npm:1.11.1":
version: 1.11.1
resolution: "@webassemblyjs/ast@npm:1.11.1"
@@ -5167,7 +5410,7 @@ __metadata:
languageName: node
linkType: hard
-"buffer-from@npm:^1.0.0":
+"buffer-from@npm:^1.0.0, buffer-from@npm:^1.1.2":
version: 1.1.2
resolution: "buffer-from@npm:1.1.2"
checksum: 0448524a562b37d4d7ed9efd91685a5b77a50672c556ea254ac9a6d30e3403a517d8981f10e565db24e8339413b43c97ca2951f10e399c6125a0d8911f5679bb
@@ -5484,7 +5727,7 @@ __metadata:
languageName: node
linkType: hard
-"classnames@npm:^2.2.5":
+"classnames@npm:^2.2.5, classnames@npm:^2.2.6":
version: 2.3.2
resolution: "classnames@npm:2.3.2"
checksum: 2c62199789618d95545c872787137262e741f9db13328e216b093eea91c85ef2bfb152c1f9e63027204e2559a006a92eb74147d46c800a9f96297ae1d9f96f4e
@@ -5692,6 +5935,16 @@ __metadata:
languageName: node
linkType: hard
+"combine-errors@npm:^3.0.3":
+ version: 3.0.3
+ resolution: "combine-errors@npm:3.0.3"
+ dependencies:
+ custom-error-instance: 2.1.1
+ lodash.uniqby: 4.5.0
+ checksum: bd0b0d2a4020f9976b8fe8eb7d5aa855b43ecacdcb61ee1fc5664d73ff8c1d7d0bbe4dd948bea7ba1870518bfc5688b89941de7a4967659418b4664cdb02884f
+ languageName: node
+ linkType: hard
+
"combined-stream@npm:^1.0.6, combined-stream@npm:^1.0.8, combined-stream@npm:~1.0.6":
version: 1.0.8
resolution: "combined-stream@npm:1.0.8"
@@ -6290,6 +6543,13 @@ __metadata:
languageName: node
linkType: hard
+"custom-error-instance@npm:2.1.1":
+ version: 2.1.1
+ resolution: "custom-error-instance@npm:2.1.1"
+ checksum: db01483864c9f4356b720b443a1f9b374758745a75199187a0ccc12505cf822bc801a0d8e3f96d727559880024f40e09667d5c08e5de0bff243c6b5ae0bd303c
+ languageName: node
+ linkType: hard
+
"custom-event-polyfill@npm:1.0.7":
version: 1.0.7
resolution: "custom-event-polyfill@npm:1.0.7"
@@ -6409,6 +6669,15 @@ __metadata:
"@types/react-virtualized": 9.21.10
"@typescript-eslint/eslint-plugin": 5.62.0
"@typescript-eslint/parser": 5.62.0
+ "@uppy/core": ^3.7.1
+ "@uppy/dashboard": ^3.7.1
+ "@uppy/drag-drop": ^3.0.3
+ "@uppy/file-input": ^3.0.4
+ "@uppy/form": ^3.0.3
+ "@uppy/golden-retriever": ^3.1.1
+ "@uppy/progress-bar": ^3.0.4
+ "@uppy/react": ^3.2.1
+ "@uppy/tus": ^3.4.0
axios: 1.6.1
connected-react-router: 6.9.1
date-fns: 2.30.0
@@ -8059,6 +8328,13 @@ __metadata:
languageName: node
linkType: hard
+"exifr@npm:^7.0.0":
+ version: 7.1.3
+ resolution: "exifr@npm:7.1.3"
+ checksum: c75a21e700378a29d226dd2c102d43679a68539ff1774b05ca19acce366ad47d056985e4f497afe7da6d0d8966d7dc73efb51acc11ff84fa57c9467e630c67ff
+ languageName: node
+ linkType: hard
+
"exit@npm:^0.1.2":
version: 0.1.2
resolution: "exit@npm:0.1.2"
@@ -8666,6 +8942,13 @@ __metadata:
languageName: node
linkType: hard
+"get-form-data@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "get-form-data@npm:3.0.0"
+ checksum: 3e3f80531cdfa434b04f40e1a25dcf034a56dab8c5dae0f77b5e652b1463448ee43b460acfdd6f9dfe3c6d890628cc770d4070524ed58df11094df012066e23a
+ languageName: node
+ linkType: hard
+
"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3":
version: 1.2.0
resolution: "get-intrinsic@npm:1.2.0"
@@ -9670,6 +9953,13 @@ __metadata:
languageName: node
linkType: hard
+"is-network-error@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "is-network-error@npm:1.0.0"
+ checksum: 2ca2b4b2d420015e0237abe28ebf316fcd26a82304b07432abf155759a3bee6895609ac91e692a72ad61b7fc902c3283b2dece61e1ddb05a6257777a8573e468
+ languageName: node
+ linkType: hard
+
"is-number-object@npm:^1.0.4":
version: 1.0.7
resolution: "is-number-object@npm:1.0.7"
@@ -9761,6 +10051,13 @@ __metadata:
languageName: node
linkType: hard
+"is-shallow-equal@npm:^1.0.1":
+ version: 1.0.1
+ resolution: "is-shallow-equal@npm:1.0.1"
+ checksum: 6bd0981c14c4c9219449c9bea4c73035defc4544394b96392573515046c1165c770a19a87c88457c40f0983f94f6750d14b0d207b27be00727e273dc4e0f5a35
+ languageName: node
+ linkType: hard
+
"is-shared-array-buffer@npm:^1.0.2":
version: 1.0.2
resolution: "is-shared-array-buffer@npm:1.0.2"
@@ -10645,6 +10942,13 @@ __metadata:
languageName: node
linkType: hard
+"js-base64@npm:^3.7.2":
+ version: 3.7.5
+ resolution: "js-base64@npm:3.7.5"
+ checksum: 67a78c8b1c47b73f1c6fba1957e9fe6fd9dc78ac93ac46cc2e43472dcb9cf150d126fb0e593192e88e0497354fa634d17d255add7cc6ee3c7b4d29870faa8e18
+ languageName: node
+ linkType: hard
+
"js-sha3@npm:0.8.0":
version: 0.8.0
resolution: "js-sha3@npm:0.8.0"
@@ -11056,6 +11360,55 @@ __metadata:
languageName: node
linkType: hard
+"lodash._baseiteratee@npm:~4.7.0":
+ version: 4.7.0
+ resolution: "lodash._baseiteratee@npm:4.7.0"
+ dependencies:
+ lodash._stringtopath: ~4.8.0
+ checksum: 814a7125b9e2fa7e436c4402eae842a200189e2839b56bd6cde7cd0a3628b60842f5d39a9f5dceaf8766669b2e4a17a36ce2a213d1d6a891c1bef8a6bda36ea9
+ languageName: node
+ linkType: hard
+
+"lodash._basetostring@npm:~4.12.0":
+ version: 4.12.0
+ resolution: "lodash._basetostring@npm:4.12.0"
+ checksum: ccaf83827f86be5c9daeb7b939f761d6a43f0de0781bc3b6772fcb8568fbcbfa1e1082c66e5e12dd23e00ac40a18349c5a793a6a552e3574cbbcb3e1545fcb4c
+ languageName: node
+ linkType: hard
+
+"lodash._baseuniq@npm:~4.6.0":
+ version: 4.6.0
+ resolution: "lodash._baseuniq@npm:4.6.0"
+ dependencies:
+ lodash._createset: ~4.0.0
+ lodash._root: ~3.0.0
+ checksum: 8c16fe2e80716b18c2f28bbcc902768141d432b0b98e03b30a2fba6a097377fabdc8753da232568375d2aa9502dc6b3a390200aa1467d2f685a582a46a271936
+ languageName: node
+ linkType: hard
+
+"lodash._createset@npm:~4.0.0":
+ version: 4.0.3
+ resolution: "lodash._createset@npm:4.0.3"
+ checksum: fb4450fbf4846aa7b420837ee44400b88664e28499388b7e04b4db38adca1305915f68a245fb2a87e031e7f440b997de4f360de6dea2712952520e97c7898de1
+ languageName: node
+ linkType: hard
+
+"lodash._root@npm:~3.0.0":
+ version: 3.0.1
+ resolution: "lodash._root@npm:3.0.1"
+ checksum: 3e12c6f409ae13164a8db358f44a691f1e038dad4e25463802980d0ed641ed118c147b65657501c51778c885422b913264dfbe33ec0c5d676443dd630a7e685a
+ languageName: node
+ linkType: hard
+
+"lodash._stringtopath@npm:~4.8.0":
+ version: 4.8.0
+ resolution: "lodash._stringtopath@npm:4.8.0"
+ dependencies:
+ lodash._basetostring: ~4.12.0
+ checksum: 00663b317796333e6315ebb4e8b590e68845de10d5d25c7585751fd9d28adf3e60e1ce85a6fbb6a0d440447c841465b91877e761239e358231eed2f52f0a5472
+ languageName: node
+ linkType: hard
+
"lodash.chunk@npm:4.2.0":
version: 4.2.0
resolution: "lodash.chunk@npm:4.2.0"
@@ -11133,6 +11486,13 @@ __metadata:
languageName: node
linkType: hard
+"lodash.throttle@npm:^4.1.1":
+ version: 4.1.1
+ resolution: "lodash.throttle@npm:4.1.1"
+ checksum: 129c0a28cee48b348aef146f638ef8a8b197944d4e9ec26c1890c19d9bf5a5690fe11b655c77a4551268819b32d27f4206343e30c78961f60b561b8608c8c805
+ languageName: node
+ linkType: hard
+
"lodash.uniq@npm:^4.5.0":
version: 4.5.0
resolution: "lodash.uniq@npm:4.5.0"
@@ -11140,6 +11500,16 @@ __metadata:
languageName: node
linkType: hard
+"lodash.uniqby@npm:4.5.0":
+ version: 4.5.0
+ resolution: "lodash.uniqby@npm:4.5.0"
+ dependencies:
+ lodash._baseiteratee: ~4.7.0
+ lodash._baseuniq: ~4.6.0
+ checksum: 40a4fdd4c31323fcb6db91ec3124020333212ca1f13e75cc9939decdd33e8b176d204fb277be36a51a855c2c90e14d67932b3b130b2f0eedc729e4cb9cdcaed1
+ languageName: node
+ linkType: hard
+
"lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.7.0":
version: 4.17.21
resolution: "lodash@npm:4.17.21"
@@ -11357,6 +11727,13 @@ __metadata:
languageName: node
linkType: hard
+"memoize-one@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "memoize-one@npm:6.0.0"
+ checksum: f185ea69f7cceae5d1cb596266dcffccf545e8e7b4106ec6aa93b71ab9d16460dd118ac8b12982c55f6d6322fcc1485de139df07eacffaae94888b9b3ad7675f
+ languageName: node
+ linkType: hard
+
"merge-descriptors@npm:1.0.1":
version: 1.0.1
resolution: "merge-descriptors@npm:1.0.1"
@@ -11416,6 +11793,15 @@ __metadata:
languageName: node
linkType: hard
+"mime-match@npm:^1.0.2":
+ version: 1.0.2
+ resolution: "mime-match@npm:1.0.2"
+ dependencies:
+ wildcard: ^1.1.0
+ checksum: 3e4afd6be98e20bfb421146a14147560941f471886e6d3534372b37d29bb7e35a7462e1f9cee98312f92e44969ae9deca2da7ad91ab5a738af55a7d5f03a6814
+ languageName: node
+ linkType: hard
+
"mime-types@npm:2.1.18":
version: 2.1.18
resolution: "mime-types@npm:2.1.18"
@@ -11659,6 +12045,13 @@ __metadata:
languageName: node
linkType: hard
+"namespace-emitter@npm:^2.0.1":
+ version: 2.0.1
+ resolution: "namespace-emitter@npm:2.0.1"
+ checksum: 49ae49674d5b83e18ad91c549ed264df1fcba02d039617e0932dab618f79e3749de6d878d54962b5cbf2611c0eafc91203a376980399c33c30edf542f19cb034
+ languageName: node
+ linkType: hard
+
"nano-time@npm:1.0.0":
version: 1.0.0
resolution: "nano-time@npm:1.0.0"
@@ -11677,6 +12070,15 @@ __metadata:
languageName: node
linkType: hard
+"nanoid@npm:^4.0.0":
+ version: 4.0.2
+ resolution: "nanoid@npm:4.0.2"
+ bin:
+ nanoid: bin/nanoid.js
+ checksum: 747c399cea4664dd0be1d0ec498ffd1ef8f1f5221676fc8b577e3f46f66d9afcddb9595d63d19a2e78d0bc6cc33984f65e66bf1682c850b9e26288883d96b53f
+ languageName: node
+ linkType: hard
+
"natural-compare-lite@npm:^1.4.0":
version: 1.4.0
resolution: "natural-compare-lite@npm:1.4.0"
@@ -12136,6 +12538,16 @@ __metadata:
languageName: node
linkType: hard
+"p-queue@npm:^7.3.4":
+ version: 7.4.1
+ resolution: "p-queue@npm:7.4.1"
+ dependencies:
+ eventemitter3: ^5.0.1
+ p-timeout: ^5.0.2
+ checksum: 1c6888aa994d399262a9fbdd49c7066f8359732397f7a42ecf03f22875a1d65899797b46413f97e44acc18dddafbcc101eb135c284714c931dbbc83c3967f450
+ languageName: node
+ linkType: hard
+
"p-retry@npm:^4.5.0":
version: 4.6.2
resolution: "p-retry@npm:4.6.2"
@@ -12146,6 +12558,24 @@ __metadata:
languageName: node
linkType: hard
+"p-retry@npm:^6.1.0":
+ version: 6.1.0
+ resolution: "p-retry@npm:6.1.0"
+ dependencies:
+ "@types/retry": 0.12.2
+ is-network-error: ^1.0.0
+ retry: ^0.13.1
+ checksum: 1083b2b72672205680f8a736583e31dce5d4ae472996cd06f4a33cd7ea11798d7712c202d253eb8afbdc80abf52f049651989c59f2e2ccca529e6b64d722b1f7
+ languageName: node
+ linkType: hard
+
+"p-timeout@npm:^5.0.2":
+ version: 5.1.0
+ resolution: "p-timeout@npm:5.1.0"
+ checksum: f5cd4e17301ff1ff1d8dbf2817df0ad88c6bba99349fc24d8d181827176ad4f8aca649190b8a5b1a428dfd6ddc091af4606835d3e0cb0656e04045da5c9e270c
+ languageName: node
+ linkType: hard
+
"p-try@npm:^2.0.0":
version: 2.2.0
resolution: "p-try@npm:2.2.0"
@@ -13232,6 +13662,13 @@ __metadata:
languageName: node
linkType: hard
+"preact@npm:^10.5.13":
+ version: 10.19.2
+ resolution: "preact@npm:10.19.2"
+ checksum: fec27fa3f14ac2d7a5061818d0cf2973ffaece83126047a47e5a075aa8e40ca56b5fcebc36106ee9cf59be0aeb51f3d996760e158d2a2660b42cbfb2e71f37bf
+ languageName: node
+ linkType: hard
+
"prelude-ls@npm:^1.2.1":
version: 1.2.1
resolution: "prelude-ls@npm:1.2.1"
@@ -13365,7 +13802,7 @@ __metadata:
languageName: node
linkType: hard
-"prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.0, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
+"prop-types@npm:^15.6.0, prop-types@npm:^15.6.1, prop-types@npm:^15.6.2, prop-types@npm:^15.7.0, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
version: 15.8.1
resolution: "prop-types@npm:15.8.1"
dependencies:
@@ -13376,6 +13813,17 @@ __metadata:
languageName: node
linkType: hard
+"proper-lockfile@npm:^4.1.2":
+ version: 4.1.2
+ resolution: "proper-lockfile@npm:4.1.2"
+ dependencies:
+ graceful-fs: ^4.2.4
+ retry: ^0.12.0
+ signal-exit: ^3.0.2
+ checksum: 00078ee6a61c216a56a6140c7d2a98c6c733b3678503002dc073ab8beca5d50ca271de4c85fca13b9b8ee2ff546c36674d1850509b84a04a5d0363bcb8638939
+ languageName: node
+ linkType: hard
+
"proxy-addr@npm:~2.0.7":
version: 2.0.7
resolution: "proxy-addr@npm:2.0.7"
@@ -15817,6 +16265,21 @@ __metadata:
languageName: node
linkType: hard
+"tus-js-client@npm:^3.0.0":
+ version: 3.1.1
+ resolution: "tus-js-client@npm:3.1.1"
+ dependencies:
+ buffer-from: ^1.1.2
+ combine-errors: ^3.0.3
+ is-stream: ^2.0.0
+ js-base64: ^3.7.2
+ lodash.throttle: ^4.1.1
+ proper-lockfile: ^4.1.2
+ url-parse: ^1.5.7
+ checksum: 7cb227b8d94e1f48a172f52c0cf8f0d58a7866211157788c6a8c9d7512550d21ef5ed13d074b8ffadbecc7e730273eca9f346d0ea4c89d9da60f351162a68d02
+ languageName: node
+ linkType: hard
+
"tweetnacl@npm:^0.14.3, tweetnacl@npm:~0.14.0":
version: 0.14.5
resolution: "tweetnacl@npm:0.14.5"
@@ -16096,7 +16559,7 @@ __metadata:
languageName: node
linkType: hard
-"url-parse@npm:^1.5.3":
+"url-parse@npm:^1.5.3, url-parse@npm:^1.5.7":
version: 1.5.10
resolution: "url-parse@npm:1.5.10"
dependencies:
@@ -16600,6 +17063,13 @@ __metadata:
languageName: node
linkType: hard
+"wildcard@npm:^1.1.0":
+ version: 1.1.2
+ resolution: "wildcard@npm:1.1.2"
+ checksum: f93bf48a23b7b776f7960fa7f252af55da265b4ce8127852e420f04a907b78073bc0412f74fc662f561667f3277473974f6553a260ece67f53b1975d128320ab
+ languageName: node
+ linkType: hard
+
"wildcard@npm:^2.0.0":
version: 2.0.0
resolution: "wildcard@npm:2.0.0"