diff --git a/cypress/integration/home.spec.js b/cypress/integration/home.spec.js index 8a39ab40..ce5959dc 100644 --- a/cypress/integration/home.spec.js +++ b/cypress/integration/home.spec.js @@ -12,7 +12,6 @@ describe("Home e2e", function () { cy.contains("Your Published Submissions").should("be.visible") cy.get("ul.MuiList-root").eq(0).children().should("have.length.at.most", 5) - cy.get("ul.MuiList-root").eq(1).children().should("have.length.at.most", 5) // Create a new Unpublished folder cy.get("button").contains("Create Submission").click() @@ -82,16 +81,6 @@ describe("Home e2e", function () { cy.contains("Your draft submissions") .should("be.visible") .then($el => $el.click()) - - // Close unpublished folders list - cy.get("div.MuiCardActions-root") - .contains("Close") - .should("be.visible") - .then($btn => $btn.click()) - - // Check Overview submissions page is shown - cy.contains("Your Draft Submissions").should("be.visible") - cy.contains("Your Published Submissions").should("be.visible") }) it("create a published folder, navigate to see folder details, delete object inside folder, navigate back to Overview submissions", () => { @@ -157,15 +146,5 @@ describe("Home e2e", function () { cy.contains("Your published submissions") .should("be.visible") .then($el => $el.click()) - - // Close published folders list - cy.get("div.MuiCardActions-root") - .contains("Close") - .should("be.visible") - .then($btn => $btn.click()) - - // Check Overview submissions page is shown - cy.contains("Your Draft Submissions").should("be.visible") - cy.contains("Your Published Submissions").should("be.visible") }) }) diff --git a/docker-compose.yml b/docker-compose.yml index 95344174..d1e39da2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,8 +16,8 @@ services: tty: true networks: # Change if using different network for backend container - - metadatasubmitter_default + - metadata-submitter_default networks: - metadatasubmitter_default: + metadata-submitter_default: external: true diff --git a/package-lock.json b/package-lock.json index 07224920..86da31f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1899,6 +1899,7 @@ "jest-resolve": "^26.6.2", "jest-util": "^26.6.2", "jest-worker": "^26.6.2", + "node-notifier": "^8.0.0", "slash": "^3.0.0", "source-map": "^0.6.0", "string-length": "^4.0.1", @@ -5575,6 +5576,7 @@ "integrity": "sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==", "dev": true, "dependencies": { + "colors": "^1.1.2", "object-assign": "^4.1.0", "string-width": "^4.2.0" }, @@ -7888,7 +7890,8 @@ "esprima": "^4.0.1", "estraverse": "^4.2.0", "esutils": "^2.0.2", - "optionator": "^0.8.1" + "optionator": "^0.8.1", + "source-map": "~0.6.1" }, "bin": { "escodegen": "bin/escodegen.js", @@ -12526,6 +12529,7 @@ "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", "graceful-fs": "^4.2.4", "jest-regex-util": "^26.0.0", "jest-serializer": "^26.6.2", @@ -18659,6 +18663,7 @@ "eslint-webpack-plugin": "^2.5.2", "file-loader": "6.1.1", "fs-extra": "^9.0.1", + "fsevents": "^2.1.3", "html-webpack-plugin": "4.5.0", "identity-obj-proxy": "3.0.0", "jest": "26.6.0", @@ -20569,6 +20574,7 @@ "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", "graceful-fs": "^4.2.4", "jest-regex-util": "^26.0.0", "jest-serializer": "^26.5.0", @@ -21701,6 +21707,26 @@ "node": ">=10.0.0" } }, + "node_modules/table/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "node_modules/tapable": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", @@ -22759,8 +22785,10 @@ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", "dependencies": { + "chokidar": "^3.4.1", "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" + "neo-async": "^2.5.0", + "watchpack-chokidar2": "^2.0.1" }, "optionalDependencies": { "chokidar": "^3.4.1", @@ -23054,6 +23082,21 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/webpack-dev-server/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/webpack-dev-server/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -23079,6 +23122,7 @@ "anymatch": "^2.0.0", "async-each": "^1.0.1", "braces": "^2.3.2", + "fsevents": "^1.2.7", "glob-parent": "^3.1.0", "inherits": "^2.0.3", "is-binary-path": "^1.0.0", diff --git a/src/App.js b/src/App.js index d8616d78..7d5f5c69 100644 --- a/src/App.js +++ b/src/App.js @@ -6,6 +6,8 @@ import CssBaseline from "@material-ui/core/CssBaseline" import { makeStyles } from "@material-ui/core/styles" import { Switch, Route, useLocation } from "react-router-dom" +import SelectedFolderDetails from "components/Home/SelectedFolderDetails" +import SubmissionFolderList from "components/Home/SubmissionFolderList" import Nav from "components/Nav" import Page401 from "views/ErrorPages/Page401" import Page403 from "views/ErrorPages/Page403" @@ -79,6 +81,26 @@ const App = (): React$Element => { + + + + + + + + + + + + + + + + + + + + diff --git a/src/__tests__/WizardAddObjectStep.test.js b/src/__tests__/WizardAddObjectStep.test.js index deb5e933..b4530dce 100644 --- a/src/__tests__/WizardAddObjectStep.test.js +++ b/src/__tests__/WizardAddObjectStep.test.js @@ -1,12 +1,14 @@ import React from "react" import "@testing-library/jest-dom/extend-expect" +import { ThemeProvider } from "@material-ui/core/styles" import { render, screen, act } from "@testing-library/react" import { Provider } from "react-redux" import configureStore from "redux-mock-store" import { toMatchDiffSnapshot } from "snapshot-diff" import WizardAddObjectStep from "../components/NewDraftWizard/WizardSteps/WizardAddObjectStep" +import CSCtheme from "../theme" import { ObjectSubmissionTypes, ObjectSubmissionsArray, ObjectTypes } from "constants/wizardObject" @@ -72,7 +74,9 @@ describe("WizardAddObjectStep", () => { }) render( - + + + ) expect(screen.getByTestId(typeName)).toBeInTheDocument() diff --git a/src/__tests__/WizardDraftObjectPicker.test.js b/src/__tests__/WizardDraftObjectPicker.test.js index 986cd263..4c83ddbe 100644 --- a/src/__tests__/WizardDraftObjectPicker.test.js +++ b/src/__tests__/WizardDraftObjectPicker.test.js @@ -1,12 +1,14 @@ import React from "react" import "@testing-library/jest-dom/extend-expect" +import { ThemeProvider } from "@material-ui/core/styles" import { render, screen } from "@testing-library/react" import { Provider } from "react-redux" import configureStore from "redux-mock-store" import thunk from "redux-thunk" import WizardDraftObjectPicker from "../components/NewDraftWizard/WizardComponents/WizardDraftObjectPicker" +import CSCtheme from "../theme" import { ObjectSubmissionTypes, ObjectTypes } from "constants/wizardObject" @@ -33,7 +35,9 @@ describe("WizardStepper", () => { it("should have drafts listed for selected object type", async () => { render( - + + + ) expect(screen.getAllByRole("button")).toHaveLength(4) diff --git a/src/__tests__/WizardFillObjectDetailsForm.test.js b/src/__tests__/WizardFillObjectDetailsForm.test.js index 7bf0da63..11b00979 100644 --- a/src/__tests__/WizardFillObjectDetailsForm.test.js +++ b/src/__tests__/WizardFillObjectDetailsForm.test.js @@ -1,11 +1,13 @@ import React from "react" import "@testing-library/jest-dom/extend-expect" +import { ThemeProvider } from "@material-ui/core/styles" import { render, screen, waitFor } from "@testing-library/react" import { Provider } from "react-redux" import configureStore from "redux-mock-store" import WizardFillObjectDetailsForm from "../components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm" +import CSCtheme from "../theme" import { ObjectSubmissionTypes, ObjectTypes } from "constants/wizardObject" @@ -48,7 +50,9 @@ describe("WizardFillObjectDetailsForm", () => { it("should create study form from schema in sessionStorage", async () => { render( - + + + ) await waitFor(() => screen.getByText("Study Description")) @@ -60,7 +64,9 @@ describe("WizardFillObjectDetailsForm", () => { const spy = jest.spyOn(Storage.prototype, "getItem") render( - + + + ) expect(spy).toBeCalledWith("cached_study_schema") diff --git a/src/__tests__/WizardObjectIndex.test.js b/src/__tests__/WizardObjectIndex.test.js index 4569c82f..50df097d 100644 --- a/src/__tests__/WizardObjectIndex.test.js +++ b/src/__tests__/WizardObjectIndex.test.js @@ -31,6 +31,12 @@ describe("WizardObjectIndex", () => { { accessionId: "TESTID0101", schema: `draft-${ObjectTypes.analysis}` }, { accessionId: "TESTID0202", schema: `draft-${ObjectTypes.experiment}` }, ], + metadataObjects: [ + { accessionId: "TESTID1234", schema: ObjectTypes.study }, + { accessionId: "TESTID5678", schema: ObjectTypes.study }, + { accessionId: "TESTID0101", schema: ObjectTypes.analysis }, + { accessionId: "TESTID0202", schema: ObjectTypes.experiment }, + ], }, }) @@ -41,10 +47,10 @@ describe("WizardObjectIndex", () => { ) const badge = await screen.queryAllByTestId("badge") - expect(badge).toHaveLength(8) + expect(badge).toHaveLength(3) const studyBadge = screen.queryAllByTestId("badge")[0] expect(studyBadge).toHaveTextContent(2) - const analysisBadge = screen.queryAllByTestId("badge")[4] + const analysisBadge = screen.queryAllByTestId("badge")[1] expect(analysisBadge).toHaveTextContent(1) const experimentBadge = screen.queryAllByTestId("badge")[2] expect(experimentBadge).toHaveTextContent(1) diff --git a/src/__tests__/WizardSavedObjectsList.test.js b/src/__tests__/WizardSavedObjectsList.test.js index 7debdae8..28f872b2 100644 --- a/src/__tests__/WizardSavedObjectsList.test.js +++ b/src/__tests__/WizardSavedObjectsList.test.js @@ -1,11 +1,13 @@ import React from "react" import "@testing-library/jest-dom/extend-expect" +import { ThemeProvider } from "@material-ui/core/styles" import { render, screen, within } from "@testing-library/react" import { Provider } from "react-redux" import configureStore from "redux-mock-store" import WizardSavedObjectsList from "../components/NewDraftWizard/WizardComponents/WizardSavedObjectsList" +import CSCtheme from "../theme" import { ObjectTypes, ObjectSubmissionTypes } from "constants/wizardObject" @@ -26,7 +28,9 @@ describe("WizardStepper", () => { beforeEach(() => { render( - + + + ) }) diff --git a/src/__tests__/WizardShowSummaryStep.test.js b/src/__tests__/WizardShowSummaryStep.test.js index daa57ca6..ea97a811 100644 --- a/src/__tests__/WizardShowSummaryStep.test.js +++ b/src/__tests__/WizardShowSummaryStep.test.js @@ -1,12 +1,14 @@ import React from "react" import "@testing-library/jest-dom/extend-expect" +import { ThemeProvider } from "@material-ui/core/styles" import { render, screen } from "@testing-library/react" import { Provider } from "react-redux" import configureStore from "redux-mock-store" import { toMatchDiffSnapshot } from "snapshot-diff" import WizardShowSummaryStep from "../components/NewDraftWizard/WizardSteps/WizardShowSummaryStep" +import CSCtheme from "../theme" import { ObjectTypes } from "constants/wizardObject" @@ -43,7 +45,9 @@ describe("WizardShowSummaryStep", () => { }) wrapper = ( - + + + ) }) diff --git a/src/__tests__/WizardUploadObjectXMLForm.test.js b/src/__tests__/WizardUploadObjectXMLForm.test.js index ae885b81..2b8ae2b9 100644 --- a/src/__tests__/WizardUploadObjectXMLForm.test.js +++ b/src/__tests__/WizardUploadObjectXMLForm.test.js @@ -1,6 +1,7 @@ import React from "react" import "@testing-library/jest-dom/extend-expect" +import { ThemeProvider } from "@material-ui/core/styles" import { render, screen } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { Provider } from "react-redux" @@ -8,6 +9,7 @@ import configureStore from "redux-mock-store" import { toMatchDiffSnapshot } from "snapshot-diff" import WizardUploadObjectXMLForm from "../components/NewDraftWizard/WizardForms/WizardUploadObjectXMLForm" +import CSCtheme from "../theme" import { ObjectSubmissionTypes, ObjectTypes } from "constants/wizardObject" @@ -30,7 +32,9 @@ describe("WizardStepper", () => { it("should have send button disabled when there's no validated xml file", async () => { render( - + + + ) const button = await screen.findByRole("button", { name: /submit/i }) @@ -41,7 +45,9 @@ describe("WizardStepper", () => { const file = new File(["test"], "test.xml", { type: "text/xml" }) render( - + + + ) const input = await screen.findByRole("textbox") diff --git a/src/components/Home/SelectedFolderDetails.js b/src/components/Home/SelectedFolderDetails.js new file mode 100644 index 00000000..d61b2901 --- /dev/null +++ b/src/components/Home/SelectedFolderDetails.js @@ -0,0 +1,169 @@ +//@flow +import React, { useEffect, useState } from "react" + +import Breadcrumbs from "@material-ui/core/Breadcrumbs" +import CircularProgress from "@material-ui/core/CircularProgress" +import Grid from "@material-ui/core/Grid" +import Link from "@material-ui/core/Link" +import { makeStyles } from "@material-ui/core/styles" +import Typography from "@material-ui/core/Typography" +import { useLocation, Link as RouterLink } from "react-router-dom" + +import SubmissionDetailTable from "components/Home/SubmissionDetailTable" +import WizardStatusMessageHandler from "components/NewDraftWizard/WizardForms/WizardStatusMessageHandler" +import { FolderSubmissionStatus } from "constants/wizardFolder" +import { ObjectStatus } from "constants/wizardObject" +import { WizardStatus } from "constants/wizardStatus" +import draftAPIService from "services/draftAPI" +import folderAPIService from "services/folderAPI" +import objectAPIService from "services/objectAPI" + +const useStyles = makeStyles(theme => ({ + tableGrid: { + margin: theme.spacing(2, 0), + }, + circularProgress: { + margin: theme.spacing(10, "auto"), + }, +})) + +const SelectedFolderDetails = (): React$Element => { + const classes = useStyles() + + const [isFetchingFolder, setFetchingFolder] = useState(true) + const [connError, setConnError] = useState(false) + const [responseError, setResponseError] = useState({}) + const [errorPrefix, setErrorPrefix] = useState("") + const [selectedFolder, setSelectedFolder] = useState({ + folderTitle: "", + allObjects: [], + published: false, + }) + + const folderId = useLocation().pathname.split("/").pop() + + const objectsArr = [] + + // Fetch folder data and map objects + useEffect(() => { + let isMounted = true + const getFolder = async () => { + const response = await folderAPIService.getFolderById(folderId) + if (isMounted) { + if (response.ok) { + const data = response.data + if (!data.published) { + for (let i = 0; i < data.drafts?.length; i += 1) { + const objectType = data.drafts[i].schema.includes("draft-") + ? data.drafts[i].schema.substr(6) + : data.drafts[i].schema + const response = await draftAPIService.getObjectByAccessionId(objectType, data.drafts[i].accessionId) + if (response.ok) { + const draftObjectDetails = { + accessionId: data.drafts[i].accessionId, + title: response.data.descriptor?.studyTitle, + objectType, + status: ObjectStatus.draft, + lastModified: response.data.dateModified, + } + objectsArr.push(draftObjectDetails) + } else { + setConnError(true) + setResponseError(response) + setErrorPrefix("Fetching folder error.") + } + } + } + for (let j = 0; j < data.metadataObjects?.length; j += 1) { + const objectType = data.metadataObjects[j].schema + const response = await objectAPIService.getObjectByAccessionId( + objectType, + data.metadataObjects[j].accessionId + ) + if (response.ok) { + const submittedObjectDetails = { + accessionId: data.metadataObjects[j].accessionId, + title: response.data.descriptor?.studyTitle, + objectType, + status: ObjectStatus.submitted, + lastModified: response.data.dateModified, + } + objectsArr.push(submittedObjectDetails) + } else { + setConnError(true) + setResponseError(response) + setErrorPrefix("Fetching folder error.") + } + } + setSelectedFolder({ folderTitle: data.name, allObjects: objectsArr, published: data.published }) + + setFetchingFolder(false) + } else { + setConnError(true) + setResponseError(response) + setErrorPrefix("Fetching folders error.") + } + } + } + getFolder() + return () => { + isMounted = false + } + }, []) + + // Delete object from current folder + const handleDeleteObject = async (objectId: string, objectType: string, objectStatus: string) => { + const service = objectStatus === ObjectStatus.draft ? draftAPIService : objectAPIService + const response = await service.deleteObjectByAccessionId(objectType, objectId) + if (response.ok) { + const updatedFolder = { ...selectedFolder } + updatedFolder.allObjects = selectedFolder.allObjects.filter(item => item.accessionId !== objectId) + setSelectedFolder(updatedFolder) + } else { + setConnError(true) + setResponseError(JSON.parse(response)) + setErrorPrefix("Can't delete object") + } + } + + return ( + + {isFetchingFolder && } + {!isFetchingFolder && ( + <> + + + Home + + + {selectedFolder.published ? "Published" : "Drafts"} + + {selectedFolder.folderTitle} + + + + )} + {connError && ( + + )} + + ) +} + +export default SelectedFolderDetails diff --git a/src/components/Home/SubmissionDetailTable.js b/src/components/Home/SubmissionDetailTable.js index 302aed8a..e93b8cc4 100644 --- a/src/components/Home/SubmissionDetailTable.js +++ b/src/components/Home/SubmissionDetailTable.js @@ -5,6 +5,7 @@ import Button from "@material-ui/core/Button" import Card from "@material-ui/core/Card" import CardContent from "@material-ui/core/CardContent" import CardHeader from "@material-ui/core/CardHeader" +import Link from "@material-ui/core/Link" import ListItem from "@material-ui/core/ListItem" import ListItemIcon from "@material-ui/core/ListItemIcon" import ListItemText from "@material-ui/core/ListItemText" @@ -20,6 +21,7 @@ import Typography from "@material-ui/core/Typography" import FolderIcon from "@material-ui/icons/Folder" import FolderOpenIcon from "@material-ui/icons/FolderOpen" import KeyboardBackspaceIcon from "@material-ui/icons/KeyboardBackspace" +import { Link as RouterLink } from "react-router-dom" import { FolderSubmissionStatus } from "constants/wizardFolder" import type { ObjectDetails } from "types" @@ -42,6 +44,9 @@ const useStyles = makeStyles(theme => ({ cursor: "pointer", }, }, + headerLink: { + color: theme.palette.font.main, + }, tableHeader: { padding: theme.spacing(1), backgroundColor: theme.palette.primary.main, @@ -71,14 +76,13 @@ type SubmissionDetailTableProps = { folderTitle: string, bodyRows: Array, folderType: string, - onClickCardHeader: () => void, - onDelete: (objectId: string, objectType: string, objectStatus: string) => void, + location: string, + onDelete: (objectId: string, objectType: string, objectStatus: string) => Promise, } const SubmissionDetailTable = (props: SubmissionDetailTableProps): React.Node => { const classes = useStyles() - const { bodyRows, folderTitle, folderType, onClickCardHeader, onDelete } = props - + const { bodyRows, folderTitle, folderType, location, onDelete } = props const getDateFormat = (date: string) => { const d = new Date(date) const day = d.getDate() @@ -87,7 +91,6 @@ const SubmissionDetailTable = (props: SubmissionDetailTableProps): React.Node => return `${day}.${month}.${year}` } - // Renders when current folder has the object(s) const CurrentFolder = () => ( @@ -166,7 +169,6 @@ const SubmissionDetailTable = (props: SubmissionDetailTableProps): React.Node => ) - // Renders when current folder is empty const EmptyFolder = () => ( @@ -177,13 +179,15 @@ const SubmissionDetailTable = (props: SubmissionDetailTableProps): React.Node => return ( - } - title={`Your ${folderType} submissions`} - titleTypographyProps={{ variant: "subtitle1", fontWeight: "fontWeightBold" }} - onClick={onClickCardHeader} - /> + + } + title={`Your ${folderType} submissions`} + titleTypographyProps={{ variant: "subtitle1", fontWeight: "fontWeightBold" }} + /> + + {bodyRows?.length > 0 ? : } ) diff --git a/src/components/Home/SubmissionFolderList.js b/src/components/Home/SubmissionFolderList.js new file mode 100644 index 00000000..b6a2259a --- /dev/null +++ b/src/components/Home/SubmissionFolderList.js @@ -0,0 +1,116 @@ +//@flow +import React, { useEffect, useState } from "react" + +import Breadcrumbs from "@material-ui/core/Breadcrumbs" +import CircularProgress from "@material-ui/core/CircularProgress" +import Grid from "@material-ui/core/Grid" +import Link from "@material-ui/core/Link" +import { makeStyles } from "@material-ui/core/styles" +import Typography from "@material-ui/core/Typography" +import { useDispatch, useSelector } from "react-redux" +import { useLocation, Link as RouterLink } from "react-router-dom" + +import SubmissionIndexCard from "components/Home/SubmissionIndexCard" +import WizardStatusMessageHandler from "components/NewDraftWizard/WizardForms/WizardStatusMessageHandler" +import { FolderSubmissionStatus } from "constants/wizardFolder" +import { WizardStatus } from "constants/wizardStatus" +import { setPublishedFolders } from "features/publishedFoldersSlice" +import { setUnpublishedFolders } from "features/unpublishedFoldersSlice" +import { fetchUserById } from "features/userSlice" +import folderAPIService from "services/folderAPI" + +const useStyles = makeStyles(theme => ({ + folderGrid: { + margin: theme.spacing(2, 0), + }, + tableCard: { + margin: theme.spacing(1, 0), + }, + loggedUser: { + margin: theme.spacing(2, 0, 0), + }, + circularProgress: { + margin: theme.spacing(10, "auto"), + }, +})) + +const SubmissionFolderList = (): React$Element => { + const dispatch = useDispatch() + + const unpublishedFolders = useSelector(state => state.unpublishedFolders) + const publishedFolders = useSelector(state => state.publishedFolders) + + const classes = useStyles() + + const [isFetchingFolders, setFetchingFolders] = useState(true) + + const [connError, setConnError] = useState(false) + const [responseError, setResponseError] = useState({}) + const [errorPrefix, setErrorPrefix] = useState("") + + const location = useLocation().pathname.split("/").pop() + + useEffect(() => { + dispatch(fetchUserById("current")) + }, []) + + useEffect(() => { + let isMounted = true + const getFolders = async () => { + const response = await folderAPIService.getFolders() + if (isMounted) { + if (response.ok) { + dispatch(setUnpublishedFolders(response.data.folders.filter(folder => folder.published === false))) + dispatch(setPublishedFolders(response.data.folders.filter(folder => folder.published === true))) + setFetchingFolders(false) + } else { + setConnError(true) + setResponseError(response) + setErrorPrefix("Fetching folders error.") + } + } + } + getFolders() + return () => { + isMounted = false + } + }, []) + + // Full list of folders + const Submissions = () => ( + + + + ) + + return ( + + + + Home + + {location.charAt(0).toUpperCase() + location.slice(1)} + + {isFetchingFolders && } + {!isFetchingFolders && ( + <> + + + )} + + {connError && ( + + )} + + ) +} + +export default SubmissionFolderList diff --git a/src/components/Home/SubmissionIndexCard.js b/src/components/Home/SubmissionIndexCard.js index b4e27292..725f8738 100644 --- a/src/components/Home/SubmissionIndexCard.js +++ b/src/components/Home/SubmissionIndexCard.js @@ -7,6 +7,7 @@ import CardActions from "@material-ui/core/CardActions" import CardContent from "@material-ui/core/CardContent" import CardHeader from "@material-ui/core/CardHeader" import Grid from "@material-ui/core/Grid" +import Link from "@material-ui/core/Link" import List from "@material-ui/core/List" import ListItem from "@material-ui/core/ListItem" import ListItemIcon from "@material-ui/core/ListItemIcon" @@ -15,6 +16,7 @@ import { makeStyles } from "@material-ui/core/styles" import Typography from "@material-ui/core/Typography" import FolderIcon from "@material-ui/icons/Folder" import FolderOpenIcon from "@material-ui/icons/FolderOpen" +import { Link as RouterLink } from "react-router-dom" import { FolderSubmissionStatus } from "constants/wizardFolder" import type { FolderDetailsWithId } from "types" @@ -31,9 +33,6 @@ const useStyles = makeStyles(theme => ({ fontSize: "0.5em", padding: 0, marginTop: theme.spacing(1), - "&:hover": { - cursor: "pointer", - }, }, cardContent: { flexGrow: 1, @@ -45,6 +44,7 @@ const useStyles = makeStyles(theme => ({ margin: theme.spacing(1, 0), boxShadow: "0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)", alignItems: "flex-start", + color: theme.palette.font.main, }, submissionsListIcon: { minWidth: 35, @@ -54,15 +54,13 @@ const useStyles = makeStyles(theme => ({ type SubmissionIndexCardProps = { folderType: string, folders: Array, - buttonTitle: string, - onClickHeader?: () => void, - onClickContent: (folderId: string, folderType: string) => Promise, - onClickButton: () => void, + location?: string, + displayButton?: boolean, } const SubmissionIndexCard = (props: SubmissionIndexCardProps): React$Element => { const classes = useStyles() - const { folderType, folders, buttonTitle, onClickHeader, onClickContent, onClickButton } = props + const { folderType, folders, location = "", displayButton } = props // Renders when there is folder list const FolderList = () => ( @@ -71,35 +69,33 @@ const SubmissionIndexCard = (props: SubmissionIndexCardProps): React$Element {folders.map((folder, index) => { return ( - onClickContent(folder.folderId, folderType)} - > - - {folderType === FolderSubmissionStatus.published ? ( - - ) : ( - - )} - - - + + + + {folderType === FolderSubmissionStatus.published ? ( + + ) : ( + + )} + + + + ) })} - - {folders.length > 0 && ( + {displayButton && ( + - + + + - )} - + + )} ) @@ -120,7 +116,6 @@ const SubmissionIndexCard = (props: SubmissionIndexCardProps): React$Element {folders.length > 0 ? : } diff --git a/src/components/Nav.js b/src/components/Nav.js index a713b44e..27d544d5 100644 --- a/src/components/Nav.js +++ b/src/components/Nav.js @@ -20,7 +20,7 @@ const useStyles = makeStyles(theme => ({ appBar: { borderBottom: `1px solid ${theme.palette.divider}`, color: theme.palette.text.primary, - backgroundColor: "#FFF", + backgroundColor: theme.palette.background.default, }, logo: { height: "auto", @@ -64,10 +64,10 @@ const Menu = () => { > - + Open submissions - + Submissions diff --git a/src/components/NewDraftWizard/WizardComponents/WizardDraftObjectPicker.js b/src/components/NewDraftWizard/WizardComponents/WizardDraftObjectPicker.js index 139a0677..73d52b2b 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardDraftObjectPicker.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardDraftObjectPicker.js @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react" import type { Node } from "react" +import Box from "@material-ui/core/Box" import Button from "@material-ui/core/Button" import ButtonGroup from "@material-ui/core/ButtonGroup" import CardHeader from "@material-ui/core/CardHeader" @@ -29,28 +30,16 @@ const useStyles = makeStyles(theme => ({ width: "100%", padding: 0, }, - cardHeader: { - backgroundColor: theme.palette.primary.main, - color: "#FFF", - fontWeight: "bold", - marginBottom: theme.spacing(3), - }, + cardHeader: theme.wizard.cardHeader, objectList: { - padding: "0 1rem", - }, - objectListItems: { - border: "none", - borderRadius: 3, - margin: theme.spacing(1, 0), - boxShadow: "0px 3px 10px -5px rgba(0,0,0,0.49)", - alignItems: "flex-start", - padding: ".5rem", + padding: theme.spacing(0, 2), }, + objectListItem: theme.wizard.objectListItem, buttonContinue: { - color: "#007bff", + color: theme.palette.button.edit, }, buttonDelete: { - color: "#dc3545", + color: theme.palette.button.delete, }, })) @@ -104,7 +93,7 @@ const WizardDraftObjectPicker = (): Node => { } return ( - + { {currentObjectTypeDrafts.map(submission => { return ( - + @@ -141,9 +130,11 @@ const WizardDraftObjectPicker = (): Node => { })} ) : ( - - No {objectType} drafts. - + + + No {objectType} drafts. + + )} {connError && ( diff --git a/src/components/NewDraftWizard/WizardComponents/WizardFooter.js b/src/components/NewDraftWizard/WizardComponents/WizardFooter.js index 2862e7e6..aed07d0d 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardFooter.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardFooter.js @@ -23,7 +23,7 @@ const useStyles = makeStyles(theme => ({ justifyContent: "space-between", flexShrink: 0, borderTop: "solid 1px #ccc", - backgroundColor: "#FFF", + backgroundColor: theme.palette.background.default, position: "fixed", zIndex: 1, left: 0, diff --git a/src/components/NewDraftWizard/WizardComponents/WizardHeader.js b/src/components/NewDraftWizard/WizardComponents/WizardHeader.js index eb257be1..04d5a221 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardHeader.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardHeader.js @@ -10,7 +10,7 @@ const useStyles = makeStyles(theme => ({ color: "#FFF", width: "100%", padding: theme.spacing(3), - backgroundColor: "#9b416b", + backgroundColor: theme.palette.primary.light, }, })) diff --git a/src/components/NewDraftWizard/WizardComponents/WizardObjectIndex.js b/src/components/NewDraftWizard/WizardComponents/WizardObjectIndex.js index 79f59ce0..ed49c1a1 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardObjectIndex.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardObjectIndex.js @@ -9,8 +9,9 @@ import List from "@material-ui/core/List" import ListItem from "@material-ui/core/ListItem" import ListItemText from "@material-ui/core/ListItemText" import { makeStyles, withStyles } from "@material-ui/core/styles" +import Tooltip from "@material-ui/core/Tooltip" import Typography from "@material-ui/core/Typography" -import NoteAddIcon from "@material-ui/icons/NoteAdd" +import DescriptionRoundedIcon from "@material-ui/icons/DescriptionRounded" import { useDispatch, useSelector } from "react-redux" import WizardAlert from "./WizardAlert" @@ -71,6 +72,9 @@ const Accordion = withStyles({ "&$expanded": { margin: "auto", }, + "&:first-of-type": { + borderTop: "none", + }, }, expanded: {}, })(MuiAccordion) @@ -87,9 +91,15 @@ const AccordionSummary = withStyles(theme => ({ color: "#FFF", fontWeight: "bold", "&$expanded": { - margin: `${theme.spacing(2)} 0`, + margin: `${theme.spacing(2)}px 0`, }, "& .MuiSvgIcon-root": { + height: "auto", + }, + "& .MuiTypography-subtitle1": { + alignSelf: "center", + }, + "&:not(.MuiBadge-root) > .MuiSvgIcon-root": { marginRight: theme.spacing(2), }, }, @@ -103,7 +113,7 @@ const AccordionDetails = withStyles({ }, })(MuiAccordionDetails) -const Badge = withStyles(theme => ({ +const ObjectCountBadge = withStyles(theme => ({ badge: { backgroundColor: theme.palette.common.white, color: theme.palette.common.black, @@ -111,6 +121,13 @@ const Badge = withStyles(theme => ({ }, }))(MuiBadge) +const Badge = withStyles(theme => ({ + badge: { + fontWeight: theme.typography.fontWeightBold, + marginRight: theme.spacing(1), + }, +}))(MuiBadge) + /* * Render list of submission types to be used in accordions */ @@ -205,6 +222,11 @@ const SubmissionTypeList = ({ showSkipLink && isCurrentObjectType && currentSubmissionType === submissionType && skipToSubmissionLink() } /> + {submissionType === ObjectSubmissionTypes.existing && draftCount > 0 && ( + + + + )} ))} @@ -234,6 +256,9 @@ const WizardObjectIndex = (): React$Element => { ?.map(draft => draft.schema) .reduce((acc, val) => ((acc[val] = (acc[val] || 0) + 1), acc), {}) + const savedObjects = folder.metadataObjects + ?.map(draft => draft.schema) + .reduce((acc, val) => ((acc[val] = (acc[val] || 0) + 1), acc), {}) // Fetch array of schemas from backend and store it in frontend // Fetch only if the initial array is empty // if there is any errors while fetching, it will return a manually created ObjectsArray instead @@ -277,7 +302,11 @@ const WizardObjectIndex = (): React$Element => { } const getDraftCount = (objectType: string) => { - return draftObjects[objectType] ? draftObjects[objectType] : 0 + return draftObjects && draftObjects[objectType] ? draftObjects[objectType] : 0 + } + + const getSavedObjectCount = (objectType: string) => { + return savedObjects && savedObjects[objectType] ? savedObjects[objectType] : 0 } const handleSubmissionTypeChange = (submissionType: string) => { @@ -325,12 +354,18 @@ const WizardObjectIndex = (): React$Element => { aria-controls="type-content" id="type-header" > - {typeCapitalized} - + {typeCapitalized} + {getSavedObjectCount(objectType) > 0 && ( + + + + + + )} ({ +const useStyles = makeStyles(theme => ({ buttonEdit: { - color: "#007bff", + color: theme.palette.button.edit, }, buttonDelete: { - color: "#dc3545", + color: theme.palette.button.delete, }, })) diff --git a/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js b/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js index 04a9388f..a52e7f2b 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardSavedObjectsList.js @@ -1,6 +1,8 @@ //@flow import React, { useEffect, useRef } from "react" +import Box from "@material-ui/core/Box" +import CardHeader from "@material-ui/core/CardHeader" import List from "@material-ui/core/List" import ListItem from "@material-ui/core/ListItem" import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction" @@ -15,20 +17,15 @@ import type { ObjectInsideFolderWithTags } from "types" const useStyles = makeStyles(theme => ({ objectList: { - padding: "0 1rem", + paddingLeft: theme.spacing(2), width: "25%", + flex: "auto", }, header: { marginBlockEnd: "0", }, - objectListItems: { - border: "none", - borderRadius: 3, - margin: theme.spacing(1, 0), - boxShadow: "0px 3px 10px -5px rgba(0,0,0,0.49)", - alignItems: "flex-start", - padding: ".5rem", - }, + cardHeader: theme.wizard.cardHeader, + objectListItem: theme.wizard.objectListItem, listItemText: { display: "inline-block", maxWidth: "50%", @@ -87,32 +84,36 @@ const WizardSavedObjectsList = ({ submissions }: WizardSavedObjectsListProps): R case ObjectSubmissionTypes.xml: return submission.submittedItems.length >= 2 ? "XML files" : "XML file" default: - break + return "" } } return (
{groupedSubmissions.map(group => ( - -

- Submitted {displayObjectType(objectType)} {displaySubmissionType(group)} -

- {group.submittedItems.map(item => ( - - - - - - - ))} -
+ + + + {group.submittedItems.map(item => ( + + + + + + + ))} + + ))}
) diff --git a/src/components/NewDraftWizard/WizardComponents/WizardStepper.js b/src/components/NewDraftWizard/WizardComponents/WizardStepper.js index 5995f6d7..0f5d991b 100644 --- a/src/components/NewDraftWizard/WizardComponents/WizardStepper.js +++ b/src/components/NewDraftWizard/WizardComponents/WizardStepper.js @@ -26,8 +26,8 @@ import type { CreateFolderFormRef } from "types" const QontoConnector = withStyles(theme => ({ alternativeLabel: { top: 10, - left: "calc(-50% + 16px)", - right: "calc(50% + 16px)", + left: `calc(-50% + ${theme.spacing(2)})`, + right: `calc(50% + ${theme.spacing(2)})`, }, active: { "& $line": { @@ -69,7 +69,7 @@ const useQontoStepIconStyles = makeStyles(theme => ({ }, floating: { border: "solid 1px #000", - backgroundColor: "#FFF", + backgroundColor: theme.palette.background.default, boxShadow: 0, }, })) diff --git a/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js b/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js index 6ed14ca4..6a03fae6 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js +++ b/src/components/NewDraftWizard/WizardForms/WizardFillObjectDetailsForm.js @@ -34,14 +34,7 @@ const useStyles = makeStyles(theme => ({ margin: 0, padding: 0, }, - cardHeader: { - backgroundColor: theme.palette.primary.main, - color: "#FFF", - fontWeight: "bold", - position: "sticky", - top: theme.spacing(8), - zIndex: 2, - }, + cardHeader: { ...theme.wizard.cardHeader, position: "sticky", top: theme.spacing(8), zIndex: 2 }, cardHeaderAction: { marginTop: "-4px", marginBottom: "-4px", @@ -52,12 +45,18 @@ const useStyles = makeStyles(theme => ({ "& > :not(:last-child)": { marginRight: theme.spacing(1), }, + "& button": { + backgroundColor: "#FFF", + }, }, addIcon: { marginRight: theme.spacing(1), }, formComponents: { margin: theme.spacing(3, 2), + "& .MuiTextField-root > .Mui-required": { + color: theme.palette.primary.main, + }, "& .MuiTextField-root": { width: "48%", margin: theme.spacing(1), @@ -69,7 +68,7 @@ const useStyles = makeStyles(theme => ({ }, "& .MuiTypography-h2": { width: "100%", - color: theme.palette.secondary.main, + color: theme.palette.primary.light, borderBottom: `2px solid ${theme.palette.secondary.main}`, }, "& .MuiTypography-h3": { @@ -77,7 +76,6 @@ const useStyles = makeStyles(theme => ({ }, "& .array": { margin: theme.spacing(1), - width: "45%", "& .arrayRow": { display: "flex", alignItems: "center", @@ -87,6 +85,9 @@ const useStyles = makeStyles(theme => ({ width: "95%", }, }, + "& h2, h3, h4": { + margin: theme.spacing(1, 0), + }, }, }, })) @@ -246,7 +247,6 @@ const FormContent = ({ resolver, formSchema, onSubmit, objectType, folderId, cur const clone = cloneDeep(currentObject) const values = JSONSchemaParser.cleanUpFormValues(methods.getValues()) setCleanedValues(values) - if (checkFormCleanedValuesEmpty(values)) { Object.keys(values).forEach(item => (clone[item] = values[item])) @@ -538,7 +538,7 @@ const WizardFillObjectDetailsForm = (): React$Element => { if (states.error) return {states.errorPrefix} return ( - + { + return { + borderColor: theme.palette.primary.main, + borderWidth: 2, + } +} + /* * Solve $ref -references in schema, return new schema instead of modifying passed in-place. */ @@ -275,6 +287,15 @@ const FormOneOfField = ({ path, object }: { path: string[], object: any }) => { ) } +/* + * Highlight required input fields + */ +const ValidationTextField = withStyles(theme => ({ + root: { + "& input:invalid:not(:valid) + fieldset": highlightStyle(theme), + }, +}))(TextField) + /* * FormTextField is the most usual type, rendered for strings, integers and numbers. */ @@ -284,7 +305,7 @@ const FormTextField = ({ name, label, required, type = "string" }: FormFieldBase const error = _.get(errors, name) const multiLineRowIdentifiers = ["description", "abstract", "policy text"] return ( - ) +/* + * Highlight required select fields + */ +const ValidationSelectField = withStyles(theme => ({ + root: { + "& select:required:not(:valid) + svg + fieldset": highlightStyle(theme), + }, +}))(TextField) + /* * FormSelectField is rendered for choosing one from many options */ @@ -309,7 +339,7 @@ const FormSelectField = ({ name, label, required, options }: FormSelectFieldProp {({ register, errors }) => { const error = _.get(errors, name) return ( - ))} - + ) }} @@ -340,12 +370,14 @@ const FormBooleanField = ({ name, label, required }: FormFieldBaseProps) => ( {({ register, errors }) => { const error = _.get(errors, name) return ( - - - } label={label} /> - {error?.message} - - + + + + } label={label} /> + {error?.message} + + + ) }} @@ -355,7 +387,7 @@ const FormBooleanField = ({ name, label, required }: FormFieldBaseProps) => ( * FormSelectField is rendered for selection from options where it's possible to choose many options */ const FormCheckBoxArray = ({ name, label, required, options }: FormSelectFieldProps) => ( -
+

{label} - check from following options

@@ -378,7 +410,7 @@ const FormCheckBoxArray = ({ name, label, required, options }: FormSelectFieldPr ) }} -
+ ) type FormArrayProps = { @@ -421,7 +453,7 @@ const FormArray = ({ object, path }: FormArrayProps) => { const properties = object.items.properties return (
- + {Object.keys(items).map(item => { const pathForThisIndex = [...pathWithoutLastItem, lastPathItemWithIndex, item] return traverseFields(properties[item], pathForThisIndex) diff --git a/src/components/NewDraftWizard/WizardForms/WizardUploadObjectXMLForm.js b/src/components/NewDraftWizard/WizardForms/WizardUploadObjectXMLForm.js index 3bd32fe1..d03befaf 100644 --- a/src/components/NewDraftWizard/WizardForms/WizardUploadObjectXMLForm.js +++ b/src/components/NewDraftWizard/WizardForms/WizardUploadObjectXMLForm.js @@ -26,14 +26,9 @@ const useStyles = makeStyles(theme => ({ container: { padding: 0, }, - cardHeader: { - backgroundColor: theme.palette.primary.main, - color: "#FFF", - fontWeight: "bold", - }, + cardHeader: theme.wizard.cardHeader, cardHeaderAction: { - marginTop: "-4px", - marginBottom: "-4px", + margin: theme.spacing(-0.5, 0), }, root: { display: "flex", @@ -55,6 +50,9 @@ const useStyles = makeStyles(theme => ({ fileField: { display: "inline-flex", }, + submitButton: { + backgroundColor: "#FFF", + }, })) /* @@ -158,6 +156,7 @@ const WizardUploadObjectXMLForm = (): React$Element => {