diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index 65d1e75fd..99345611f 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -9,7 +9,15 @@ jobs: if: ${{ github.actor != 'dependabot[bot]' }} steps: - uses: actions/checkout@v3 + - uses: actions/setup-node@v4 + with: + node-version: 16 + cache: 'npm' + - run: npm ci - uses: ArtiomTr/jest-coverage-report-action@v2.2.4 with: github-token: ${{ secrets.GITHUB_TOKEN }} annotations: failed-tests + skip-step: install + env: + NODE_OPTIONS: --experimental-vm-modules diff --git a/jest.config.js b/jest.config.js index a4c18b937..e03db8337 100644 --- a/jest.config.js +++ b/jest.config.js @@ -17,10 +17,6 @@ module.exports = { "^lodash/(.*)$": "lodash-es/$1", }, coverageThreshold: { - global: { - lines: 25, - statements: 50, - }, "./src/state/*/selectors/index.ts": { branches: 100, functions: 100, diff --git a/src/components/CustomModal/index.tsx b/src/components/CustomModal/index.tsx index c202767cd..5cb73b9d6 100644 --- a/src/components/CustomModal/index.tsx +++ b/src/components/CustomModal/index.tsx @@ -19,6 +19,8 @@ interface CustomModalProps extends Omit { titleText?: string; footerButtons?: React.ReactNode; divider?: boolean; + wrapProps?: { onDragOver: (e: DragEvent) => void }; + wrapClassName?: string; } /** Wrapper to keep styling of modals consistent: @@ -31,6 +33,8 @@ const CustomModal: React.FC = ({ titleText, footerButtons, divider, + wrapProps, + wrapClassName, ...props }) => { const title = ( @@ -66,6 +70,8 @@ const CustomModal: React.FC = ({ footer={footer} open centered + wrapClassName={wrapClassName} + wrapProps={wrapProps} /> ); }; diff --git a/src/components/FileUploadModal/index.tsx b/src/components/FileUploadModal/index.tsx index 846abef00..417ecbdb0 100644 --- a/src/components/FileUploadModal/index.tsx +++ b/src/components/FileUploadModal/index.tsx @@ -1,7 +1,8 @@ import { Form, Tabs } from "antd"; import { RcFile } from "antd/lib/upload"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { ActionCreator } from "redux"; +import classNames from "classnames"; import { ClearSimFileDataAction, @@ -33,6 +34,8 @@ interface FileUploadModalProps { setViewerStatus: ActionCreator; clearSimulariumFile: ActionCreator; setError: ActionCreator; + fileIsDraggedOverViewer: boolean; + handleDragOver: (e: DragEvent) => void; } const FileUploadModal: React.FC = ({ @@ -41,6 +44,8 @@ const FileUploadModal: React.FC = ({ loadLocalFile, setViewerStatus, setError, + fileIsDraggedOverViewer, + handleDragOver, }) => { const [openTab, setOpenTab] = useState(UploadTab.Device); const [noUrlInput, setNoUrlInput] = useState(true); @@ -51,6 +56,17 @@ const FileUploadModal: React.FC = ({ setIsModalVisible(false); }; + useEffect(() => { + if (!fileIsDraggedOverViewer) return; + + window.addEventListener("drop", closeModal); + return () => window.removeEventListener("drop", closeModal); + }, [fileIsDraggedOverViewer]); + + const fileDragClass = fileIsDraggedOverViewer + ? styles.fileDragged + : undefined; + const onUrlInput = (event: React.ChangeEvent) => { // Form subcomponent takes care of its own submit behavior // all we care about is if input is present or not @@ -118,10 +134,15 @@ const FileUploadModal: React.FC = ({ return ( handleDragOver(e), + }} + divider > ; export const Download = ; export const LoopOutlined = ; export const Exclamation = ; +export const Drag = ; export const PurpleArrow = ; export const AicsLogo = ; diff --git a/src/components/LoadFileMenu/index.tsx b/src/components/LoadFileMenu/index.tsx index 5c2d4a774..18750e25d 100644 --- a/src/components/LoadFileMenu/index.tsx +++ b/src/components/LoadFileMenu/index.tsx @@ -35,6 +35,8 @@ interface LoadFileMenuProps { setError: ActionCreator; conversionStatus: ConversionStatus; setConversionStatus: ActionCreator; + fileIsDraggedOverViewer: boolean; + handleDragOver: (e: DragEvent) => void; } const LoadFileMenu = ({ @@ -46,6 +48,8 @@ const LoadFileMenu = ({ setError, conversionStatus, setConversionStatus, + fileIsDraggedOverViewer, + handleDragOver, }: LoadFileMenuProps): JSX.Element => { const [isModalVisible, setIsModalVisible] = useState(false); const location = useLocation(); @@ -142,6 +146,8 @@ const LoadFileMenu = ({ loadLocalFile={loadLocalFile} setViewerStatus={setViewerStatus} setError={setError} + handleDragOver={handleDragOver} + fileIsDraggedOverViewer={fileIsDraggedOverViewer} /> )} diff --git a/src/components/LocalFileUpload/index.tsx b/src/components/LocalFileUpload/index.tsx index c45a8a0aa..58ebe953c 100644 --- a/src/components/LocalFileUpload/index.tsx +++ b/src/components/LocalFileUpload/index.tsx @@ -3,10 +3,12 @@ import { Link } from "react-router-dom"; import { message, Upload, UploadProps } from "antd"; import { CloseOutlined } from "@ant-design/icons"; import { RcFile } from "antd/lib/upload"; +import classNames from "classnames"; import { ButtonClass } from "../../constants/interfaces"; import { VIEWER_PATHNAME } from "../../routes"; import { CustomButton } from "../CustomButton"; +import { Drag } from "../Icons"; import styles from "./style.css"; @@ -35,7 +37,9 @@ const LocalFileUpload: React.FC = ({ ...uploadConfigProps }) => { const uploadPresetProps: UploadProps = { - className: styles.fileUpload, + className: classNames(styles.fileUpload, { + [styles.listEmpty]: fileList.length === 0, + }), showUploadList: { removeIcon: , }, @@ -64,6 +68,11 @@ const LocalFileUpload: React.FC = ({ return ( +
+ {" "} + {Drag} Drag and drop a .simularium file anywhere in this window + or browse to a location.{" "} +
= ({ > {children || ( - Select file + Browse )} diff --git a/src/components/LocalFileUpload/style.css b/src/components/LocalFileUpload/style.css index c005eeafd..c382f8774 100644 --- a/src/components/LocalFileUpload/style.css +++ b/src/components/LocalFileUpload/style.css @@ -1,6 +1,17 @@ +.file-upload :global(.ant-upload){ + display: flex; + flex-direction: column; + gap: 26px; +} + .file-upload :global(.ant-upload-list) { min-height: 30px; } + +.file-upload.list-empty :global(.ant-upload-list) { + display: none; +} + .file-upload :global(.ant-upload-list-item):hover, .file-upload :global(.ant-upload-list-item-list-type-text):focus, .file-upload :global(.ant-upload-list-item-list-type-text) :global(.ant-upload-list-item-info):hover { @@ -28,3 +39,8 @@ width: max-content; } +.file-upload .drag { + border: 1px dashed var(--heather); + font-size: 14px; + padding: 20px 16px; +} diff --git a/src/components/ViewerOverlayTarget/style.css b/src/components/ViewerOverlayTarget/style.css index 365e56cfe..8959ade0a 100644 --- a/src/components/ViewerOverlayTarget/style.css +++ b/src/components/ViewerOverlayTarget/style.css @@ -2,7 +2,7 @@ position: absolute; height: 100%; width: 100%; - z-index: 300; + z-index: 1001; background-color: rgba(181, 159, 246, 0.7); } diff --git a/src/containers/AppHeader/index.tsx b/src/containers/AppHeader/index.tsx index 45b613adf..ca308bd41 100644 --- a/src/containers/AppHeader/index.tsx +++ b/src/containers/AppHeader/index.tsx @@ -24,6 +24,7 @@ import viewerStateBranch from "../../state/viewer"; import { SetViewerStatusAction, SetErrorAction, + DragOverViewerAction, } from "../../state/viewer/types"; import { ButtonClass } from "../../constants/interfaces"; import ShareTrajectoryButton from "../../components/ShareTrajectoryButton"; @@ -43,6 +44,8 @@ interface AppHeaderProps { setError: ActionCreator; conversionStatus: ConversionStatus; setConversionStatus: ActionCreator; + fileIsDraggedOverViewer: boolean; + dragOverViewer: ActionCreator; } const AppHeader: React.FC = ({ @@ -56,6 +59,8 @@ const AppHeader: React.FC = ({ isNetworkedFile, conversionStatus, setConversionStatus, + fileIsDraggedOverViewer, + dragOverViewer, }) => { const history = useHistory(); @@ -113,6 +118,8 @@ const AppHeader: React.FC = ({ setError={setError} conversionStatus={conversionStatus} setConversionStatus={setConversionStatus} + handleDragOver={dragOverViewer} + fileIsDraggedOverViewer={fileIsDraggedOverViewer} /> @@ -129,6 +136,8 @@ function mapStateToProps(state: State) { trajectoryStateBranch.selectors.getIsNetworkedFile(state), conversionStatus: trajectoryStateBranch.selectors.getConversionStatus(state), + fileIsDraggedOverViewer: + viewerStateBranch.selectors.getFileDraggedOver(state), }; } @@ -140,6 +149,7 @@ const dispatchToPropsMap = { setViewerStatus: viewerStateBranch.actions.setStatus, setError: viewerStateBranch.actions.setError, setConversionStatus: trajectoryStateBranch.actions.setConversionStatus, + dragOverViewer: viewerStateBranch.actions.dragOverViewer, }; export default connect(mapStateToProps, dispatchToPropsMap)(AppHeader);