From bfe8f1cff8e34903855228ca130df9362f566f1f Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Wed, 30 Nov 2022 11:36:30 -0600 Subject: [PATCH 001/115] chore: Initial layout --- .../ria/web/projects/ProjectsController.java | 58 ++++++++----------- src/main/webapp/pages/projects/index.html | 17 ++++++ .../js/pages/projects/ProjectSPA.tsx | 23 +++++--- 3 files changed, 58 insertions(+), 40 deletions(-) create mode 100644 src/main/webapp/pages/projects/index.html diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectsController.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectsController.java index ff6e4b254bc..bc25416d243 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectsController.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectsController.java @@ -1,17 +1,19 @@ package ca.corefacility.bioinformatics.irida.ria.web.projects; -import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.security.Principal; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.*; -import java.util.Map.Entry; -import java.util.stream.Collectors; - -import javax.servlet.http.HttpServletResponse; - +import ca.corefacility.bioinformatics.irida.exceptions.ProjectWithoutOwnerException; +import ca.corefacility.bioinformatics.irida.model.joins.Join; +import ca.corefacility.bioinformatics.irida.model.project.Project; +import ca.corefacility.bioinformatics.irida.model.sample.Sample; +import ca.corefacility.bioinformatics.irida.model.user.User; +import ca.corefacility.bioinformatics.irida.ria.utilities.converters.FileSizeConverter; +import ca.corefacility.bioinformatics.irida.ria.web.models.datatables.DTProject; +import ca.corefacility.bioinformatics.irida.service.ProjectService; +import ca.corefacility.bioinformatics.irida.service.TaxonomyService; +import ca.corefacility.bioinformatics.irida.service.sample.SampleService; +import ca.corefacility.bioinformatics.irida.service.user.UserService; +import ca.corefacility.bioinformatics.irida.util.TreeNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; import org.apache.poi.ss.usermodel.Cell; @@ -30,21 +32,16 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; -import ca.corefacility.bioinformatics.irida.exceptions.ProjectWithoutOwnerException; -import ca.corefacility.bioinformatics.irida.model.joins.Join; -import ca.corefacility.bioinformatics.irida.model.project.Project; -import ca.corefacility.bioinformatics.irida.model.sample.Sample; -import ca.corefacility.bioinformatics.irida.model.user.User; -import ca.corefacility.bioinformatics.irida.ria.utilities.converters.FileSizeConverter; -import ca.corefacility.bioinformatics.irida.ria.web.models.datatables.DTProject; -import ca.corefacility.bioinformatics.irida.service.ProjectService; -import ca.corefacility.bioinformatics.irida.service.TaxonomyService; -import ca.corefacility.bioinformatics.irida.service.sample.SampleService; -import ca.corefacility.bioinformatics.irida.service.user.UserService; -import ca.corefacility.bioinformatics.irida.util.TreeNode; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.security.Principal; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; /** * Controller for project related views @@ -129,12 +126,7 @@ public String getAllProjectsPage(Model model) { */ @RequestMapping(value = { "/projects/{projectId}", "/projects/{projectId}/samples", }) public String getProjectSamplesPage(final Model model, final Principal principal, @PathVariable long projectId) { - Project project = projectService.read(projectId); - model.addAttribute("project", project); - - // Set up the template information - projectControllerUtils.getProjectTemplateDetails(model, principal, project); - return "projects/project_samples"; + return "projects/index"; } /** diff --git a/src/main/webapp/pages/projects/index.html b/src/main/webapp/pages/projects/index.html new file mode 100644 index 00000000000..fe36e31ccd9 --- /dev/null +++ b/src/main/webapp/pages/projects/index.html @@ -0,0 +1,17 @@ + + + + + TODO: UPDATE THIS TITLE + + + +
+ + + + diff --git a/src/main/webapp/resources/js/pages/projects/ProjectSPA.tsx b/src/main/webapp/resources/js/pages/projects/ProjectSPA.tsx index 6e4abf450fd..4681e87859b 100644 --- a/src/main/webapp/resources/js/pages/projects/ProjectSPA.tsx +++ b/src/main/webapp/resources/js/pages/projects/ProjectSPA.tsx @@ -12,6 +12,7 @@ import { setBaseUrl } from "../../utilities/url-utilities"; import { loader as detailsLoader } from "../../components/ncbi/details"; import { LoadingOutlined } from "@ant-design/icons"; import { loader as ncbiLoader } from "./ncbi/create"; +import { Layout } from "antd"; const ProjectNCBILayout = React.lazy(() => import("./ncbi")); const NCBIExportDetails = React.lazy( @@ -27,9 +28,15 @@ const DefaultErrorBoundary = React.lazy( __webpack_public_path__ = setBaseUrl(`/dist/`); +const CONTEXT_PATH = document.querySelector("#root").dataset.context; + const router = createBrowserRouter( createRoutesFromElements( - }> + } + > + SAMPLES} /> } @@ -64,12 +71,14 @@ const router = createBrowserRouter( */ function ProjectBase(): JSX.Element { return ( -
- {/* TODO: NAV AND OTHER TOP LEVEL ITEMS HERE */} - }> - - -
+ + NAV HERE + + }> + + + + ); } From 46551e0a5d5aec7c0f21b9bf1f6dcb730c96c327 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Wed, 30 Nov 2022 12:31:42 -0600 Subject: [PATCH 002/115] chore: Initial menu setup --- .../js/components/MainNavigation/index.css | 6 ++ .../js/components/MainNavigation/index.tsx | 82 +++++++++++++++++++ src/main/webapp/resources/js/data/routes.ts | 8 ++ .../js/pages/projects/ProjectSPA.tsx | 11 ++- .../{url-utilities.js => url-utilities.ts} | 14 ++++ 5 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 src/main/webapp/resources/js/components/MainNavigation/index.css create mode 100644 src/main/webapp/resources/js/components/MainNavigation/index.tsx create mode 100644 src/main/webapp/resources/js/data/routes.ts rename src/main/webapp/resources/js/utilities/{url-utilities.js => url-utilities.ts} (71%) diff --git a/src/main/webapp/resources/js/components/MainNavigation/index.css b/src/main/webapp/resources/js/components/MainNavigation/index.css new file mode 100644 index 00000000000..eb0cadadc14 --- /dev/null +++ b/src/main/webapp/resources/js/components/MainNavigation/index.css @@ -0,0 +1,6 @@ +.nav-logo { + float: left; + width: 128px; + height: 28px; + margin: 16px 24px 16px 0; +} diff --git a/src/main/webapp/resources/js/components/MainNavigation/index.tsx b/src/main/webapp/resources/js/components/MainNavigation/index.tsx new file mode 100644 index 00000000000..0247ea5d97e --- /dev/null +++ b/src/main/webapp/resources/js/components/MainNavigation/index.tsx @@ -0,0 +1,82 @@ +import React from "react"; +import { Menu } from "antd"; +import type { MenuProps } from "antd/es"; +import { + CONTEXT_PATH, + ROUTE_HOME, + ROUTE_PROJECTS_ALL, + ROUTE_PROJECTS_PERSONAL, + ROUTE_PROJECTS_SYNC, +} from "../../data/routes"; +import { setBaseUrl } from "../../utilities/url-utilities"; +import { SPACE_LG } from "../../styles/spacing"; +import { theme } from "../../utilities/theme-utilities"; +import "./index.css"; + +type MenuItem = { + key: string; + label?: string | JSX.Element; + type?: "divider"; + children?: MenuItem[]; +}; + +const menuItems: MenuItem[] = [ + { + key: `nav-projects`, + label: `PROJECTS`, + children: [ + { + key: `nav-projects-personal`, + label: ( + {i18n("nav.main.project-list")} + ), + }, + ...[ + { + key: `nav-projects-all`, + label: ( + {i18n("nav.main.project-list-all")} + ), + }, + ], + { type: `divider`, key: `nav-div-1` }, + { + key: `nav-projects-sync`, + label: ( + {i18n("nav.main.project-sync")} + ), + }, + ], + }, +]; + +function renderMenuItem(item: MenuItem): JSX.Element { + if (item.type === `divider`) { + return ; + } else if (!item.children) { + return {item.label}; + } else { + return ( + + {item.children.map(renderMenuItem)} + + ); + } +} + +export default function MainNavigation(): JSX.Element { + return ( + <> + + {i18n("global.title")} + + + {menuItems.map(renderMenuItem)} + + + ); +} diff --git a/src/main/webapp/resources/js/data/routes.ts b/src/main/webapp/resources/js/data/routes.ts new file mode 100644 index 00000000000..df51c68e80a --- /dev/null +++ b/src/main/webapp/resources/js/data/routes.ts @@ -0,0 +1,8 @@ +import { getContextPath } from "../utilities/url-utilities"; + +export const CONTEXT_PATH = getContextPath(); + +export const ROUTE_HOME = CONTEXT_PATH; +export const ROUTE_PROJECTS_PERSONAL = `${CONTEXT_PATH}/projects`; +export const ROUTE_PROJECTS_ALL = `${CONTEXT_PATH}/projects/all`; +export const ROUTE_PROJECTS_SYNC = `${CONTEXT_PATH}/projects/synchronize`; diff --git a/src/main/webapp/resources/js/pages/projects/ProjectSPA.tsx b/src/main/webapp/resources/js/pages/projects/ProjectSPA.tsx index 4681e87859b..49a08954311 100644 --- a/src/main/webapp/resources/js/pages/projects/ProjectSPA.tsx +++ b/src/main/webapp/resources/js/pages/projects/ProjectSPA.tsx @@ -7,8 +7,9 @@ import { Route, RouterProvider, } from "react-router-dom"; +import MainNavigation from "../../components/MainNavigation"; import { loader as exportsLoader } from "../../components/ncbi/export-table"; -import { setBaseUrl } from "../../utilities/url-utilities"; +import { getContextPath } from "../../utilities/url-utilities"; import { loader as detailsLoader } from "../../components/ncbi/details"; import { LoadingOutlined } from "@ant-design/icons"; import { loader as ncbiLoader } from "./ncbi/create"; @@ -26,9 +27,9 @@ const DefaultErrorBoundary = React.lazy( () => import("../../components/DefaultErrorBoundary") ); -__webpack_public_path__ = setBaseUrl(`/dist/`); +const CONTEXT_PATH = getContextPath(); -const CONTEXT_PATH = document.querySelector("#root").dataset.context; +__webpack_public_path__ = `${CONTEXT_PATH}/dist/`; const router = createBrowserRouter( createRoutesFromElements( @@ -72,7 +73,9 @@ const router = createBrowserRouter( function ProjectBase(): JSX.Element { return ( - NAV HERE + + + }> diff --git a/src/main/webapp/resources/js/utilities/url-utilities.js b/src/main/webapp/resources/js/utilities/url-utilities.ts similarity index 71% rename from src/main/webapp/resources/js/utilities/url-utilities.js rename to src/main/webapp/resources/js/utilities/url-utilities.ts index 3d86edce58e..9feaa1496bc 100644 --- a/src/main/webapp/resources/js/utilities/url-utilities.js +++ b/src/main/webapp/resources/js/utilities/url-utilities.ts @@ -46,3 +46,17 @@ export function getProjectIdFromUrl(url = window.location.href) { } } } + +/** + * Get the context path for IRIDA. This expects the root element to have a data attribute + * of context (`data-context`) with the context path. + */ +export function getContextPath(): string { + const element = document.getElementById("root"); + if (element && element.dataset && element.dataset.context) { + return element.dataset.context; + } else { + console.error("No `#root` element with attribute `data-context`"); + return "/"; + } +} From bde6a048dbae2c22025910082085b2bece594dc9 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Thu, 1 Dec 2022 08:10:14 -0600 Subject: [PATCH 003/115] chore: Added global search and cart link --- .../Header/MainNavigation/MainNavigation.jsx | 200 ------------------ .../js/components/MainNavigation/index.css | 6 - .../js/components/MainNavigation/index.tsx | 82 ------- .../main-navigation/components/CartLink.tsx | 17 ++ .../components/GlobalSearch.tsx | 22 ++ .../js/components/main-navigation/index.css | 25 +++ .../js/components/main-navigation/index.tsx | 172 +++++++++++++++ src/main/webapp/resources/js/data/routes.ts | 12 +- .../js/pages/projects/ProjectSPA.tsx | 10 +- .../webapp/resources/js/redux/services/api.ts | 17 ++ .../resources/js/redux/services/cart.ts | 12 ++ src/main/webapp/resources/js/redux/store.ts | 22 ++ 12 files changed, 306 insertions(+), 291 deletions(-) delete mode 100644 src/main/webapp/resources/js/components/MainNavigation/index.css delete mode 100644 src/main/webapp/resources/js/components/MainNavigation/index.tsx create mode 100644 src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx create mode 100644 src/main/webapp/resources/js/components/main-navigation/components/GlobalSearch.tsx create mode 100644 src/main/webapp/resources/js/components/main-navigation/index.css create mode 100644 src/main/webapp/resources/js/components/main-navigation/index.tsx create mode 100644 src/main/webapp/resources/js/redux/services/api.ts create mode 100644 src/main/webapp/resources/js/redux/services/cart.ts create mode 100644 src/main/webapp/resources/js/redux/store.ts diff --git a/src/main/webapp/resources/js/components/Header/MainNavigation/MainNavigation.jsx b/src/main/webapp/resources/js/components/Header/MainNavigation/MainNavigation.jsx index 2c6b714f736..6cd6131fa64 100644 --- a/src/main/webapp/resources/js/components/Header/MainNavigation/MainNavigation.jsx +++ b/src/main/webapp/resources/js/components/Header/MainNavigation/MainNavigation.jsx @@ -30,206 +30,6 @@ export function MainNavigation() { return (
- - {i18n("global.title")} - - {isLargeScreen ? ( - - {i18n("nav.main.projects")} - } - > - - - {i18n("nav.main.project-list")} - - - {isAdmin && ( - - - {i18n("nav.main.project-list-all")} - - - )} - - - - {i18n("nav.main.project-sync")} - - - - - {i18n("nav.main.analysis")} - } - > - - - {i18n("nav.main.analysis-admin-user")} - - - {isAdmin && ( - - - {i18n("nav.main.analysis-admin-all")} - - - )} - - - - {i18n("Analysis.outputFiles")} - - - - - {!isAdmin && isManager && ( - {i18n("nav.main.users")} - } - > - - {i18n("nav.main.users-list")} - - - - {i18n("nav.main.groups-list")} - - - - )} - - {isTechnician && ( - - - - )} - - {!isAdmin && ( - - - - )} - - ) : ( - - {i18n("nav.main.projects")} - } - > - - - {i18n("nav.main.project-list")} - - - {isAdmin && ( - - - {i18n("nav.main.project-list-all")} - - - )} - - - - {i18n("nav.main.project-sync")} - - - - - {i18n("nav.main.analysis")} - } - > - - - {i18n("nav.main.analysis-admin-user")} - - - {isAdmin && ( - - - {i18n("nav.main.analysis-admin-all")} - - - )} - - - - {i18n("Analysis.outputFiles")} - - - - - {!isAdmin && isManager && ( - {i18n("nav.main.users")} - } - > - - {i18n("nav.main.users-list")} - - - - {i18n("nav.main.groups-list")} - - - - )} - - {isTechnician && ( - - - - )} - - {!isAdmin && ( - - - - )} - - )} - -
{isAdmin && (
diff --git a/src/main/webapp/resources/js/components/MainNavigation/index.css b/src/main/webapp/resources/js/components/MainNavigation/index.css deleted file mode 100644 index eb0cadadc14..00000000000 --- a/src/main/webapp/resources/js/components/MainNavigation/index.css +++ /dev/null @@ -1,6 +0,0 @@ -.nav-logo { - float: left; - width: 128px; - height: 28px; - margin: 16px 24px 16px 0; -} diff --git a/src/main/webapp/resources/js/components/MainNavigation/index.tsx b/src/main/webapp/resources/js/components/MainNavigation/index.tsx deleted file mode 100644 index 0247ea5d97e..00000000000 --- a/src/main/webapp/resources/js/components/MainNavigation/index.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from "react"; -import { Menu } from "antd"; -import type { MenuProps } from "antd/es"; -import { - CONTEXT_PATH, - ROUTE_HOME, - ROUTE_PROJECTS_ALL, - ROUTE_PROJECTS_PERSONAL, - ROUTE_PROJECTS_SYNC, -} from "../../data/routes"; -import { setBaseUrl } from "../../utilities/url-utilities"; -import { SPACE_LG } from "../../styles/spacing"; -import { theme } from "../../utilities/theme-utilities"; -import "./index.css"; - -type MenuItem = { - key: string; - label?: string | JSX.Element; - type?: "divider"; - children?: MenuItem[]; -}; - -const menuItems: MenuItem[] = [ - { - key: `nav-projects`, - label: `PROJECTS`, - children: [ - { - key: `nav-projects-personal`, - label: ( - {i18n("nav.main.project-list")} - ), - }, - ...[ - { - key: `nav-projects-all`, - label: ( - {i18n("nav.main.project-list-all")} - ), - }, - ], - { type: `divider`, key: `nav-div-1` }, - { - key: `nav-projects-sync`, - label: ( - {i18n("nav.main.project-sync")} - ), - }, - ], - }, -]; - -function renderMenuItem(item: MenuItem): JSX.Element { - if (item.type === `divider`) { - return ; - } else if (!item.children) { - return {item.label}; - } else { - return ( - - {item.children.map(renderMenuItem)} - - ); - } -} - -export default function MainNavigation(): JSX.Element { - return ( - <> - - {i18n("global.title")} - - - {menuItems.map(renderMenuItem)} - - - ); -} diff --git a/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx b/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx new file mode 100644 index 00000000000..0c952061e78 --- /dev/null +++ b/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { useGetCartCountQuery } from "../../../redux/services/cart"; +import { ShoppingCartOutlined } from "@ant-design/icons"; +import { Badge, Button } from "antd"; +import { ROUTE_CART } from "../../../data/routes"; + +export default function CartLink() { + const { data: count } = useGetCartCountQuery(undefined, {}); + + return ( + + + + ); +} diff --git a/src/main/webapp/resources/js/components/main-navigation/components/GlobalSearch.tsx b/src/main/webapp/resources/js/components/main-navigation/components/GlobalSearch.tsx new file mode 100644 index 00000000000..40d724aeb7d --- /dev/null +++ b/src/main/webapp/resources/js/components/main-navigation/components/GlobalSearch.tsx @@ -0,0 +1,22 @@ +import { Input } from "antd"; +import React from "react"; +import { ROUTE_SEARCH } from "../../../data/routes"; +import { SearchOutlined } from "@ant-design/icons"; + +/** + * React component to render a global search input to the main navigation. + */ +export default function GlobalSearch(): JSX.Element { + return ( +
+ } + placeholder={i18n("nav.main.search")} + /> +
+ ); +} diff --git a/src/main/webapp/resources/js/components/main-navigation/index.css b/src/main/webapp/resources/js/components/main-navigation/index.css new file mode 100644 index 00000000000..1574d0e7ebb --- /dev/null +++ b/src/main/webapp/resources/js/components/main-navigation/index.css @@ -0,0 +1,25 @@ +.nav-logo { + float: left; + width: 128px; + height: 28px; + margin: 16px 24px 16px 0; +} + +.main-nav { + flex: 1; +} + +.main-nav .ant-menu-title-content > a { + color: var(--grey-2); +} + +.global-search { + width: 300px; + background-color: transparent; +} + +.global-search [data-icon="search"], +.global-search .anticon-close-circle svg { + color: var(--grey-4); + font-size: 14px; +} diff --git a/src/main/webapp/resources/js/components/main-navigation/index.tsx b/src/main/webapp/resources/js/components/main-navigation/index.tsx new file mode 100644 index 00000000000..4330ad1fdc4 --- /dev/null +++ b/src/main/webapp/resources/js/components/main-navigation/index.tsx @@ -0,0 +1,172 @@ +import React from "react"; +import { Menu, Space } from "antd"; +import GlobalSearch from "./components/GlobalSearch"; +import { + CONTEXT_PATH, + ROUTE_ADMIN, + ROUTE_ANALYSES, + ROUTE_ANALYSES_ALL, + ROUTE_ANALYSES_OUTPUT, + ROUTE_HOME, + ROUTE_PROJECTS_ALL, + ROUTE_PROJECTS_PERSONAL, + ROUTE_PROJECTS_SYNC, + ROUTE_REMOTE_API, + ROUTE_SEQUENCING_RUNS, + ROUTE_USER_GROUPS, + ROUTE_USERS, +} from "../../data/routes"; +import { theme } from "../../utilities/theme-utilities"; +import "./index.css"; +import CartLink from "./components/CartLink"; + +type MenuItem = { + key: string; + label?: string | JSX.Element; + type?: "divider"; + children?: MenuItem[]; +}; + +const menuItems: MenuItem[] = [ + { + key: `nav-projects`, + label: {i18n("nav.main.projects")}, + children: [ + { + key: `nav-projects-personal`, + label: ( + {i18n("nav.main.project-list")} + ), + }, + ...[ + // ADMIN ONLY + { + key: `nav-projects-all`, + label: ( + {i18n("nav.main.project-list-all")} + ), + }, + ], + { type: `divider`, key: `nav-div-1` }, + { + key: `nav-projects-sync`, + label: ( + {i18n("nav.main.project-sync")} + ), + }, + ], + }, + { + key: `nav-analyses`, + label: {i18n("nav.main.analysis")}, + children: [ + { + key: `nav-analyses-personal`, + label: ( + {i18n("nav.main.analysis-admin-user")} + ), + }, + ...[ + // ADMIN ONLY + { + key: `nav-analyses-all`, + label: ( + + {i18n("nav.main.analysis-admin-all")} + + ), + }, + ], + { type: `divider`, key: `nav-div-2` }, + { + key: `nav-analyses-output`, + label: ( + {i18n("Analysis.outputFiles")} + ), + }, + ], + }, + // not admin but is manager + ...[ + { + key: `nav-users`, + label: {i18n("nav.main.users")}, + children: [ + { + key: `nav-users-list`, + label: {i18n("nav.main.users-list")}, + }, + { + key: `nav-user-groups`, + label: {i18n("nav.main.groups-list")}, + }, + ], + }, + ], + // TECHNICIANS only + ...[ + { + key: `nav-sequencing`, + label: ( + {i18n("nav.main.sequencing-runs")} + ), + }, + ], + // NOT ADMINS + ...[ + { + key: `nav-remote-api`, + label: {i18n("nav.main.remoteapis")}, + }, + ], + // ADMIN ONLY + ...[ + { + key: `nav-admin`, + label: {i18n("MainNavigation.admin")}, + }, + ], +]; + +function renderMenuItem(item: MenuItem): JSX.Element { + if (item.type === `divider`) { + return ; + } else if (!item.children) { + return {item.label}; + } else { + return ( + + {item.children.map(renderMenuItem)} + + ); + } +} + +export default function MainNavigation(): JSX.Element { + return ( + <> + + {i18n("global.title")} + +
+ + {menuItems.map(renderMenuItem)} + + + + + +
+ + ); +} diff --git a/src/main/webapp/resources/js/data/routes.ts b/src/main/webapp/resources/js/data/routes.ts index df51c68e80a..8449e3050a9 100644 --- a/src/main/webapp/resources/js/data/routes.ts +++ b/src/main/webapp/resources/js/data/routes.ts @@ -2,7 +2,17 @@ import { getContextPath } from "../utilities/url-utilities"; export const CONTEXT_PATH = getContextPath(); +export const ROUTE_ADMIN = `${CONTEXT_PATH}/admin`; +export const ROUTE_ANALYSES = `${CONTEXT_PATH}/analysis`; +export const ROUTE_ANALYSES_ALL = `${CONTEXT_PATH}/analysis/all`; +export const ROUTE_ANALYSES_OUTPUT = `${CONTEXT_PATH}/analysis/user/analysis-outputs`; +export const ROUTE_CART = `${CONTEXT_PATH}/cart`; export const ROUTE_HOME = CONTEXT_PATH; -export const ROUTE_PROJECTS_PERSONAL = `${CONTEXT_PATH}/projects`; export const ROUTE_PROJECTS_ALL = `${CONTEXT_PATH}/projects/all`; +export const ROUTE_PROJECTS_PERSONAL = `${CONTEXT_PATH}/projects`; export const ROUTE_PROJECTS_SYNC = `${CONTEXT_PATH}/projects/synchronize`; +export const ROUTE_REMOTE_API = `${CONTEXT_PATH}/remote_api`; +export const ROUTE_SEARCH = `${CONTEXT_PATH}/search`; +export const ROUTE_SEQUENCING_RUNS = `${CONTEXT_PATH}/sequencing-runs`; +export const ROUTE_USERS = `${CONTEXT_PATH}/users`; +export const ROUTE_USER_GROUPS = `${CONTEXT_PATH}/groups`; diff --git a/src/main/webapp/resources/js/pages/projects/ProjectSPA.tsx b/src/main/webapp/resources/js/pages/projects/ProjectSPA.tsx index 49a08954311..829c4835c4d 100644 --- a/src/main/webapp/resources/js/pages/projects/ProjectSPA.tsx +++ b/src/main/webapp/resources/js/pages/projects/ProjectSPA.tsx @@ -7,13 +7,15 @@ import { Route, RouterProvider, } from "react-router-dom"; -import MainNavigation from "../../components/MainNavigation"; +import MainNavigation from "../../components/main-navigation"; import { loader as exportsLoader } from "../../components/ncbi/export-table"; import { getContextPath } from "../../utilities/url-utilities"; import { loader as detailsLoader } from "../../components/ncbi/details"; import { LoadingOutlined } from "@ant-design/icons"; import { loader as ncbiLoader } from "./ncbi/create"; import { Layout } from "antd"; +import { Provider } from "react-redux"; +import { store } from "../../redux/store"; const ProjectNCBILayout = React.lazy(() => import("./ncbi")); const NCBIExportDetails = React.lazy( @@ -92,7 +94,11 @@ function ProjectBase(): JSX.Element { * @constructor */ export default function ProjectSPA(): JSX.Element { - return ; + return ( + + + + ); } render(, document.querySelector("#root")); diff --git a/src/main/webapp/resources/js/redux/services/api.ts b/src/main/webapp/resources/js/redux/services/api.ts new file mode 100644 index 00000000000..55ca493297a --- /dev/null +++ b/src/main/webapp/resources/js/redux/services/api.ts @@ -0,0 +1,17 @@ +import { createApi, fetchBaseQuery, retry } from "@reduxjs/toolkit/query/react"; +import { CONTEXT_PATH } from "../../data/routes"; + +const baseQuery = fetchBaseQuery({ + baseUrl: `${CONTEXT_PATH}/ajax`, +}); + +const baseQueryWithRetry = retry(baseQuery, { maxRetries: 6 }); + +export const api = createApi({ + baseQuery: baseQueryWithRetry, + /** + * All tags must be defined here, not in the injected endpoints + */ + tagTypes: ["CartCount"], + endpoints: () => ({}), +}); diff --git a/src/main/webapp/resources/js/redux/services/cart.ts b/src/main/webapp/resources/js/redux/services/cart.ts new file mode 100644 index 00000000000..893e54d2b0f --- /dev/null +++ b/src/main/webapp/resources/js/redux/services/cart.ts @@ -0,0 +1,12 @@ +import { api } from "./api"; + +export const cartApi = api.injectEndpoints({ + endpoints: (build) => ({ + getCartCount: build.query({ + query: () => "cart/count", + providesTags: ["CartCount"], + }), + }), +}); + +export const { useGetCartCountQuery } = cartApi; diff --git a/src/main/webapp/resources/js/redux/store.ts b/src/main/webapp/resources/js/redux/store.ts new file mode 100644 index 00000000000..2e8ed9b0915 --- /dev/null +++ b/src/main/webapp/resources/js/redux/store.ts @@ -0,0 +1,22 @@ +import type { ConfigureStoreOptions } from "@reduxjs/toolkit"; +import { configureStore } from "@reduxjs/toolkit"; +import type { TypedUseSelectorHook } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; +import { api } from "./services/api"; + +export const createStore = ( + options?: ConfigureStoreOptions["preloadedState"] | undefined +) => + configureStore({ + reducer: { + [api.reducerPath]: api.reducer, + }, + ...options, + }); + +export const store = createStore(); + +export type AppDispatch = typeof store.dispatch; +export const useAppDispatch: () => AppDispatch = useDispatch; +export type RootState = ReturnType; +export const useTypedSelector: TypedUseSelectorHook = useSelector; From 881109e46f54bc3a658f254e00901c96321dda77 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Thu, 1 Dec 2022 12:15:27 -0600 Subject: [PATCH 004/115] chore: Added in user account submenu --- .../irida/ria/web/ajax/dto/CurrentUser.java | 13 +- .../Header/MainNavigation/MainNavigation.jsx | 6 +- .../components/ant.design/menu-utilities.tsx | 29 ++ .../main-navigation/components/CartLink.tsx | 10 +- .../components/CurrentUser.tsx | 28 ++ .../js/components/main-navigation/index.css | 25 +- .../js/components/main-navigation/index.tsx | 316 +++++++++++------- src/main/webapp/resources/js/data/routes.ts | 1 + .../js/redux/{services => endpoints}/api.ts | 3 +- .../js/redux/{services => endpoints}/cart.ts | 3 +- .../resources/js/redux/endpoints/tags.ts | 4 + .../resources/js/redux/endpoints/user.ts | 13 + src/main/webapp/resources/js/redux/store.ts | 4 +- .../resources/js/types/ant-design/index.d.ts | 8 + 14 files changed, 334 insertions(+), 129 deletions(-) create mode 100644 src/main/webapp/resources/js/components/ant.design/menu-utilities.tsx create mode 100644 src/main/webapp/resources/js/components/main-navigation/components/CurrentUser.tsx rename src/main/webapp/resources/js/redux/{services => endpoints}/api.ts (86%) rename src/main/webapp/resources/js/redux/{services => endpoints}/cart.ts (77%) create mode 100644 src/main/webapp/resources/js/redux/endpoints/tags.ts create mode 100644 src/main/webapp/resources/js/redux/endpoints/user.ts diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/ajax/dto/CurrentUser.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/ajax/dto/CurrentUser.java index dc2c7d1569d..2f7bdba6fe8 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/ajax/dto/CurrentUser.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/ajax/dto/CurrentUser.java @@ -12,6 +12,8 @@ public class CurrentUser { private String firstName; private String lastName; private boolean isAdmin; + private boolean isManager; + private boolean isTechnician; public CurrentUser(User user) { this.identifier = user.getId(); @@ -19,7 +21,8 @@ public CurrentUser(User user) { this.firstName = user.getFirstName(); this.lastName = user.getLastName(); this.isAdmin = user.getSystemRole().equals(Role.ROLE_ADMIN); - ; + this.isManager = user.getSystemRole().equals(Role.ROLE_MANAGER); + this.isTechnician = user.getSystemRole().equals(Role.ROLE_TECHNICIAN); } public Long getIdentifier() { @@ -41,4 +44,12 @@ public String getLastName() { public boolean isAdmin() { return isAdmin; } + + public boolean isManager() { + return isManager; + } + + public boolean isTechnician() { + return isTechnician; + } } \ No newline at end of file diff --git a/src/main/webapp/resources/js/components/Header/MainNavigation/MainNavigation.jsx b/src/main/webapp/resources/js/components/Header/MainNavigation/MainNavigation.jsx index 6cd6131fa64..4f96cf1d5ae 100644 --- a/src/main/webapp/resources/js/components/Header/MainNavigation/MainNavigation.jsx +++ b/src/main/webapp/resources/js/components/Header/MainNavigation/MainNavigation.jsx @@ -46,11 +46,7 @@ export function MainNavigation() { }> - - - {i18n("nav.main.account")} - - + ; + } else if (item.type === "group") { + return ( + + {item.children?.map(renderMenuItem)} + + ); + } else if (!item.children) { + return ( + + {item.label} + + ); + } else { + return ( + + {item.children.map(renderMenuItem)} + + ); + } +} diff --git a/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx b/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx index 0c952061e78..66c81ee387a 100644 --- a/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx @@ -1,17 +1,17 @@ import React from "react"; -import { useGetCartCountQuery } from "../../../redux/services/cart"; +import { useGetCartCountQuery } from "../../../redux/endpoints/cart"; import { ShoppingCartOutlined } from "@ant-design/icons"; -import { Badge, Button } from "antd"; +import { Badge } from "antd"; import { ROUTE_CART } from "../../../data/routes"; export default function CartLink() { const { data: count } = useGetCartCountQuery(undefined, {}); return ( - - + ); } diff --git a/src/main/webapp/resources/js/components/main-navigation/components/CurrentUser.tsx b/src/main/webapp/resources/js/components/main-navigation/components/CurrentUser.tsx new file mode 100644 index 00000000000..53d20e38153 --- /dev/null +++ b/src/main/webapp/resources/js/components/main-navigation/components/CurrentUser.tsx @@ -0,0 +1,28 @@ +import React, { useMemo } from "react"; +import { useGetCurrentUserQuery } from "../../../redux/endpoints/user"; +import { Avatar } from "antd"; +import { generateColourForItem } from "../../../utilities/colour-utilities"; + +export default function CurrentUser(): JSX.Element { + const { data: user, isSuccess } = useGetCurrentUserQuery(undefined, {}); + + const colour = useMemo( + () => + isSuccess + ? generateColourForItem({ id: user.identifier, label: user.username }) + : { text: "transparent", background: "transparent" }, + [isSuccess, user?.identifier, user?.username] + ); + + return ( + + {`${user?.firstName.charAt(0)}${user?.lastName.charAt(0)}`} + + ); +} diff --git a/src/main/webapp/resources/js/components/main-navigation/index.css b/src/main/webapp/resources/js/components/main-navigation/index.css index 1574d0e7ebb..0b0a47b6cc3 100644 --- a/src/main/webapp/resources/js/components/main-navigation/index.css +++ b/src/main/webapp/resources/js/components/main-navigation/index.css @@ -9,17 +9,36 @@ flex: 1; } -.main-nav .ant-menu-title-content > a { - color: var(--grey-2); +.main-nav.ant-menu-root li.ant-menu-submenu, +.main-nav.ant-menu-root .ant-menu-item, +.utils-nav.ant-menu-root li.ant-menu-submenu, +.utils-nav.ant-menu-root .ant-menu-item { + display: flex; + height: 64px; + overflow-y: hidden; +} + +.main-nav.ant-menu-root li.ant-menu-submenu a, +.main-nav.ant-menu-root .ant-menu-item a { + color: var(--grey-4); + font-size: 18px; } .global-search { width: 300px; background-color: transparent; + margin: 0 20px; } .global-search [data-icon="search"], .global-search .anticon-close-circle svg { - color: var(--grey-4); + color: #bfbfbf; font-size: 14px; } + +.nav-icon svg { + position: relative; + top: 6px; + color: #f0f0f0; + font-size: 26px; +} diff --git a/src/main/webapp/resources/js/components/main-navigation/index.tsx b/src/main/webapp/resources/js/components/main-navigation/index.tsx index 4330ad1fdc4..d184647da15 100644 --- a/src/main/webapp/resources/js/components/main-navigation/index.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/index.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; import { Menu, Space } from "antd"; import GlobalSearch from "./components/GlobalSearch"; import { @@ -8,6 +8,7 @@ import { ROUTE_ANALYSES_ALL, ROUTE_ANALYSES_OUTPUT, ROUTE_HOME, + ROUTE_LOGOUT, ROUTE_PROJECTS_ALL, ROUTE_PROJECTS_PERSONAL, ROUTE_PROJECTS_SYNC, @@ -19,130 +20,219 @@ import { import { theme } from "../../utilities/theme-utilities"; import "./index.css"; import CartLink from "./components/CartLink"; +import CurrentUser from "./components/CurrentUser"; +import type { MenuItem } from "../../types/ant-design"; +import { renderMenuItem } from "../ant.design/menu-utilities"; +import { useGetCurrentUserQuery } from "../../redux/endpoints/user"; +import { setBaseUrl } from "../../utilities/url-utilities"; -type MenuItem = { - key: string; - label?: string | JSX.Element; - type?: "divider"; - children?: MenuItem[]; -}; +export default function MainNavigation(): JSX.Element { + const { data: user = {}, isSuccess } = useGetCurrentUserQuery(undefined, {}); + console.log(user); -const menuItems: MenuItem[] = [ - { - key: `nav-projects`, - label: {i18n("nav.main.projects")}, - children: [ + const leftMenuItems: MenuItem[] = useMemo( + () => [ { - key: `nav-projects-personal`, + key: `nav-projects`, label: ( - {i18n("nav.main.project-list")} + {i18n("nav.main.projects")} ), + children: [ + { + key: `nav-projects-personal`, + label: ( + + {i18n("nav.main.project-list")} + + ), + }, + ...(isSuccess && user.admin + ? [ + { + key: `nav-projects-all`, + label: ( + + {i18n("nav.main.project-list-all")} + + ), + }, + ] + : []), + { type: `divider`, key: `nav-div-1` }, + { + key: `nav-projects-sync`, + label: ( + {i18n("nav.main.project-sync")} + ), + }, + ], }, - ...[ - // ADMIN ONLY - { - key: `nav-projects-all`, - label: ( - {i18n("nav.main.project-list-all")} - ), - }, - ], - { type: `divider`, key: `nav-div-1` }, { - key: `nav-projects-sync`, - label: ( - {i18n("nav.main.project-sync")} - ), + key: `nav-analyses`, + label: {i18n("nav.main.analysis")}, + children: [ + { + key: `nav-analyses-personal`, + label: ( + + {i18n("nav.main.analysis-admin-user")} + + ), + }, + ...(isSuccess && user.admin + ? [ + { + key: `nav-analyses-all`, + label: ( + + {i18n("nav.main.analysis-admin-all")} + + ), + }, + ] + : []), + { type: `divider`, key: `nav-div-2` }, + { + key: `nav-analyses-output`, + label: ( + {i18n("Analysis.outputFiles")} + ), + }, + ], }, + ...(isSuccess && !user.admin && user.manager + ? [ + { + key: `nav-users`, + label: {i18n("nav.main.users")}, + children: [ + { + key: `nav-users-list`, + label: ( + {i18n("nav.main.users-list")} + ), + }, + { + key: `nav-user-groups`, + label: ( + + {i18n("nav.main.groups-list")} + + ), + }, + ], + }, + ] + : []), + ...(isSuccess && user.technician + ? [ + { + key: `nav-sequencing`, + label: ( + + {i18n("nav.main.sequencing-runs")} + + ), + }, + ] + : []), + ...(isSuccess && !user.admin + ? [ + { + key: `nav-remote-api`, + label: ( + {i18n("nav.main.remoteapis")} + ), + }, + ] + : []), + ...(isSuccess && user.admin + ? [ + { + key: `nav-admin`, + label: {i18n("MainNavigation.admin")}, + }, + ] + : []), ], - }, - { - key: `nav-analyses`, - label: {i18n("nav.main.analysis")}, - children: [ + [isSuccess, user.admin] + ); + + const rightMenuItems: MenuItem[] = useMemo( + () => [ { - key: `nav-analyses-personal`, - label: ( - {i18n("nav.main.analysis-admin-user")} - ), + key: `nav-cart`, + label: , }, - ...[ - // ADMIN ONLY - { - key: `nav-analyses-all`, - label: ( - - {i18n("nav.main.analysis-admin-all")} - - ), - }, - ], - { type: `divider`, key: `nav-div-2` }, { - key: `nav-analyses-output`, - label: ( - {i18n("Analysis.outputFiles")} - ), + key: `nav-user`, + label: , + children: [ + { + key: `nav-user-account`, + label: ( + + {i18n("nav.main.account")} + + ), + }, + { + key: `nav-guides`, + label: `Guides`, + children: [ + { + key: `nav-user-guide`, + label: ( + + {i18n("nav.main.userguide")} + + ), + }, + ...(isSuccess && user.admin + ? [ + { + key: `nav-admin-guide`, + label: ( + + {i18n("nav.main.adminguide")} + + ), + }, + ] + : []), + ], + }, + { + type: "divider", + key: "nav-account-divider", + }, + { + key: `nav-logout`, + label: {i18n("nav.main.logout")}, + }, + { + type: "divider", + key: "nav-logout-divider", + }, + { + key: `nav-version`, + disabled: true, + label: i18n("irida.version"), + }, + ], }, ], - }, - // not admin but is manager - ...[ - { - key: `nav-users`, - label: {i18n("nav.main.users")}, - children: [ - { - key: `nav-users-list`, - label: {i18n("nav.main.users-list")}, - }, - { - key: `nav-user-groups`, - label: {i18n("nav.main.groups-list")}, - }, - ], - }, - ], - // TECHNICIANS only - ...[ - { - key: `nav-sequencing`, - label: ( - {i18n("nav.main.sequencing-runs")} - ), - }, - ], - // NOT ADMINS - ...[ - { - key: `nav-remote-api`, - label: {i18n("nav.main.remoteapis")}, - }, - ], - // ADMIN ONLY - ...[ - { - key: `nav-admin`, - label: {i18n("MainNavigation.admin")}, - }, - ], -]; - -function renderMenuItem(item: MenuItem): JSX.Element { - if (item.type === `divider`) { - return ; - } else if (!item.children) { - return {item.label}; - } else { - return ( - - {item.children.map(renderMenuItem)} - - ); - } -} + [isSuccess, user?.admin] + ); -export default function MainNavigation(): JSX.Element { return ( <> @@ -160,11 +250,13 @@ export default function MainNavigation(): JSX.Element { }} > - {menuItems.map(renderMenuItem)} + {leftMenuItems.map(renderMenuItem)} - + + {rightMenuItems.map(renderMenuItem)} +
diff --git a/src/main/webapp/resources/js/data/routes.ts b/src/main/webapp/resources/js/data/routes.ts index 8449e3050a9..a28799af04c 100644 --- a/src/main/webapp/resources/js/data/routes.ts +++ b/src/main/webapp/resources/js/data/routes.ts @@ -8,6 +8,7 @@ export const ROUTE_ANALYSES_ALL = `${CONTEXT_PATH}/analysis/all`; export const ROUTE_ANALYSES_OUTPUT = `${CONTEXT_PATH}/analysis/user/analysis-outputs`; export const ROUTE_CART = `${CONTEXT_PATH}/cart`; export const ROUTE_HOME = CONTEXT_PATH; +export const ROUTE_LOGOUT = `${CONTEXT_PATH}/logout`; export const ROUTE_PROJECTS_ALL = `${CONTEXT_PATH}/projects/all`; export const ROUTE_PROJECTS_PERSONAL = `${CONTEXT_PATH}/projects`; export const ROUTE_PROJECTS_SYNC = `${CONTEXT_PATH}/projects/synchronize`; diff --git a/src/main/webapp/resources/js/redux/services/api.ts b/src/main/webapp/resources/js/redux/endpoints/api.ts similarity index 86% rename from src/main/webapp/resources/js/redux/services/api.ts rename to src/main/webapp/resources/js/redux/endpoints/api.ts index 55ca493297a..5d641b177f2 100644 --- a/src/main/webapp/resources/js/redux/services/api.ts +++ b/src/main/webapp/resources/js/redux/endpoints/api.ts @@ -1,5 +1,6 @@ import { createApi, fetchBaseQuery, retry } from "@reduxjs/toolkit/query/react"; import { CONTEXT_PATH } from "../../data/routes"; +import { PROVIDED_TAGS } from "./tags"; const baseQuery = fetchBaseQuery({ baseUrl: `${CONTEXT_PATH}/ajax`, @@ -12,6 +13,6 @@ export const api = createApi({ /** * All tags must be defined here, not in the injected endpoints */ - tagTypes: ["CartCount"], + tagTypes: PROVIDED_TAGS, endpoints: () => ({}), }); diff --git a/src/main/webapp/resources/js/redux/services/cart.ts b/src/main/webapp/resources/js/redux/endpoints/cart.ts similarity index 77% rename from src/main/webapp/resources/js/redux/services/cart.ts rename to src/main/webapp/resources/js/redux/endpoints/cart.ts index 893e54d2b0f..618d906e3b5 100644 --- a/src/main/webapp/resources/js/redux/services/cart.ts +++ b/src/main/webapp/resources/js/redux/endpoints/cart.ts @@ -1,10 +1,11 @@ import { api } from "./api"; +import { TAG_COUNT } from "./tags"; export const cartApi = api.injectEndpoints({ endpoints: (build) => ({ getCartCount: build.query({ query: () => "cart/count", - providesTags: ["CartCount"], + providesTags: [TAG_COUNT], }), }), }); diff --git a/src/main/webapp/resources/js/redux/endpoints/tags.ts b/src/main/webapp/resources/js/redux/endpoints/tags.ts new file mode 100644 index 00000000000..c235960d29f --- /dev/null +++ b/src/main/webapp/resources/js/redux/endpoints/tags.ts @@ -0,0 +1,4 @@ +export const TAG_COUNT = "tag-cart-count"; +export const TAG_USER = "tag-user"; + +export const PROVIDED_TAGS = [TAG_COUNT, TAG_USER]; diff --git a/src/main/webapp/resources/js/redux/endpoints/user.ts b/src/main/webapp/resources/js/redux/endpoints/user.ts new file mode 100644 index 00000000000..c6e2ac14fd2 --- /dev/null +++ b/src/main/webapp/resources/js/redux/endpoints/user.ts @@ -0,0 +1,13 @@ +import { api } from "./api"; +import { TAG_USER } from "./tags"; + +export const userApi = api.injectEndpoints({ + endpoints: (build) => ({ + getCurrentUser: build.query({ + query: () => "/users/current", + providesTags: [TAG_USER], + }), + }), +}); + +export const { useGetCurrentUserQuery } = userApi; diff --git a/src/main/webapp/resources/js/redux/store.ts b/src/main/webapp/resources/js/redux/store.ts index 2e8ed9b0915..8c79e37b955 100644 --- a/src/main/webapp/resources/js/redux/store.ts +++ b/src/main/webapp/resources/js/redux/store.ts @@ -2,7 +2,7 @@ import type { ConfigureStoreOptions } from "@reduxjs/toolkit"; import { configureStore } from "@reduxjs/toolkit"; import type { TypedUseSelectorHook } from "react-redux"; import { useDispatch, useSelector } from "react-redux"; -import { api } from "./services/api"; +import { api } from "./endpoints/api"; export const createStore = ( options?: ConfigureStoreOptions["preloadedState"] | undefined @@ -11,6 +11,8 @@ export const createStore = ( reducer: { [api.reducerPath]: api.reducer, }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat(api.middleware), ...options, }); diff --git a/src/main/webapp/resources/js/types/ant-design/index.d.ts b/src/main/webapp/resources/js/types/ant-design/index.d.ts index d31bc08e957..e71b857f66f 100644 --- a/src/main/webapp/resources/js/types/ant-design/index.d.ts +++ b/src/main/webapp/resources/js/types/ant-design/index.d.ts @@ -12,6 +12,14 @@ export interface GridProps { xxl?: number; } +export type MenuItem = { + key: string; + label?: string | JSX.Element; + type?: "divider" | "group"; + children?: MenuItem[]; + disabled?: boolean; +}; + export type TagColor = | "magenta" | "red" From d5c0c44cc1d792d909c27cb7a4b7e56a1821e1fd Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Thu, 1 Dec 2022 13:00:58 -0600 Subject: [PATCH 005/115] chore: Updated announcements link --- .../AnnouncementAjaxController.java | 27 ++++++++++------ .../web/services/UIAnnouncementsService.java | 31 +++++++++++-------- .../components/AnnouncementLink.tsx | 17 ++++++++++ .../main-navigation/components/CartLink.tsx | 2 +- .../js/components/main-navigation/index.tsx | 10 +++++- src/main/webapp/resources/js/data/routes.ts | 1 + .../js/redux/endpoints/announcements.ts | 13 ++++++++ .../resources/js/redux/endpoints/tags.ts | 3 +- 8 files changed, 79 insertions(+), 25 deletions(-) create mode 100644 src/main/webapp/resources/js/components/main-navigation/components/AnnouncementLink.tsx create mode 100644 src/main/webapp/resources/js/redux/endpoints/announcements.ts diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/announcements/AnnouncementAjaxController.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/announcements/AnnouncementAjaxController.java index d2e8c2d37e8..ca70f80bae3 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/announcements/AnnouncementAjaxController.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/announcements/AnnouncementAjaxController.java @@ -1,21 +1,20 @@ package ca.corefacility.bioinformatics.irida.ria.web.announcements; -import java.security.Principal; -import java.util.List; - -import ca.corefacility.bioinformatics.irida.model.announcements.AnnouncementUserJoin; -import ca.corefacility.bioinformatics.irida.ria.web.announcements.dto.AnnouncementUserReadDetails; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - import ca.corefacility.bioinformatics.irida.model.announcements.Announcement; +import ca.corefacility.bioinformatics.irida.model.announcements.AnnouncementUserJoin; import ca.corefacility.bioinformatics.irida.ria.web.announcements.dto.AnnouncementRequest; import ca.corefacility.bioinformatics.irida.ria.web.announcements.dto.AnnouncementTableModel; +import ca.corefacility.bioinformatics.irida.ria.web.announcements.dto.AnnouncementUserReadDetails; import ca.corefacility.bioinformatics.irida.ria.web.announcements.dto.AnnouncementUserTableModel; import ca.corefacility.bioinformatics.irida.ria.web.models.tables.TableRequest; import ca.corefacility.bioinformatics.irida.ria.web.models.tables.TableResponse; import ca.corefacility.bioinformatics.irida.ria.web.services.UIAnnouncementsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; +import java.util.List; /** * Controller for all ajax requests from the UI for announcements. @@ -41,6 +40,16 @@ public TableResponse getAnnouncementsAdmin(@RequestBody return service.getAnnouncementsAdmin(tableRequest); } + /** + * Get the total number of unread announcement for a user. + * + * @return number of unread announcements + */ + @GetMapping("/count") + public int getUnreadAnnouncementCount() { + return service.getUnreadAnnouncementsCount(); + } + /** * Handle request for getting a list of read and unread announcements for a user. * diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIAnnouncementsService.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIAnnouncementsService.java index 678b6de5d03..0b35f5cec35 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIAnnouncementsService.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIAnnouncementsService.java @@ -1,18 +1,5 @@ package ca.corefacility.bioinformatics.irida.ria.web.services; -import java.security.Principal; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -import javax.transaction.Transactional; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Component; - import ca.corefacility.bioinformatics.irida.model.announcements.Announcement; import ca.corefacility.bioinformatics.irida.model.announcements.AnnouncementUserJoin; import ca.corefacility.bioinformatics.irida.model.user.User; @@ -26,6 +13,19 @@ import ca.corefacility.bioinformatics.irida.ria.web.models.tables.TableResponse; import ca.corefacility.bioinformatics.irida.service.AnnouncementService; import ca.corefacility.bioinformatics.irida.service.user.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import javax.transaction.Transactional; +import java.security.Principal; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; /** * A utility class for formatting responses for the announcements page UI. @@ -192,4 +192,9 @@ private AnnouncementUserJoin userHasRead(final User user, final Announcement ann return currentAnnouncement.orElse(null); } + public int getUnreadAnnouncementsCount() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + User user = userService.getUserByUsername(authentication.getName()); + return announcementService.getUnreadAnnouncementsForUser(user).size(); + } } diff --git a/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementLink.tsx b/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementLink.tsx new file mode 100644 index 00000000000..0250963ce69 --- /dev/null +++ b/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementLink.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { Badge } from "antd"; +import { BellOutlined } from "@ant-design/icons"; +import { ROUTE_ANNOUNCEMENTS } from "../../../data/routes"; +import { useGetAnnouncementCountQuery } from "../../../redux/endpoints/announcements"; + +export default function AnnouncementLink() { + const { data: count } = useGetAnnouncementCountQuery(undefined, {}); + + return ( + + + + + + ); +} diff --git a/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx b/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx index 66c81ee387a..71d968e2e19 100644 --- a/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx @@ -9,7 +9,7 @@ export default function CartLink() { return ( - + diff --git a/src/main/webapp/resources/js/components/main-navigation/index.tsx b/src/main/webapp/resources/js/components/main-navigation/index.tsx index d184647da15..d99f41076e6 100644 --- a/src/main/webapp/resources/js/components/main-navigation/index.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/index.tsx @@ -25,10 +25,14 @@ import type { MenuItem } from "../../types/ant-design"; import { renderMenuItem } from "../ant.design/menu-utilities"; import { useGetCurrentUserQuery } from "../../redux/endpoints/user"; import { setBaseUrl } from "../../utilities/url-utilities"; +import AnnouncementLink from "./components/AnnouncementLink"; +/** + * React component to render the main navigation component at the top of the page. + * @constructor + */ export default function MainNavigation(): JSX.Element { const { data: user = {}, isSuccess } = useGetCurrentUserQuery(undefined, {}); - console.log(user); const leftMenuItems: MenuItem[] = useMemo( () => [ @@ -164,6 +168,10 @@ export default function MainNavigation(): JSX.Element { key: `nav-cart`, label: , }, + { + key: `nav-announcements`, + label: , + }, { key: `nav-user`, label: , diff --git a/src/main/webapp/resources/js/data/routes.ts b/src/main/webapp/resources/js/data/routes.ts index a28799af04c..38a93903870 100644 --- a/src/main/webapp/resources/js/data/routes.ts +++ b/src/main/webapp/resources/js/data/routes.ts @@ -6,6 +6,7 @@ export const ROUTE_ADMIN = `${CONTEXT_PATH}/admin`; export const ROUTE_ANALYSES = `${CONTEXT_PATH}/analysis`; export const ROUTE_ANALYSES_ALL = `${CONTEXT_PATH}/analysis/all`; export const ROUTE_ANALYSES_OUTPUT = `${CONTEXT_PATH}/analysis/user/analysis-outputs`; +export const ROUTE_ANNOUNCEMENTS = `${CONTEXT_PATH}/announcements/user/list`; export const ROUTE_CART = `${CONTEXT_PATH}/cart`; export const ROUTE_HOME = CONTEXT_PATH; export const ROUTE_LOGOUT = `${CONTEXT_PATH}/logout`; diff --git a/src/main/webapp/resources/js/redux/endpoints/announcements.ts b/src/main/webapp/resources/js/redux/endpoints/announcements.ts new file mode 100644 index 00000000000..d632655a458 --- /dev/null +++ b/src/main/webapp/resources/js/redux/endpoints/announcements.ts @@ -0,0 +1,13 @@ +import { api } from "./api"; +import { TAG_ANNOUNCEMENT_COUNT } from "./tags"; + +export const announcementsApi = api.injectEndpoints({ + endpoints: (build) => ({ + getAnnouncementCount: build.query({ + query: () => "announcements/count", + providesTags: [TAG_ANNOUNCEMENT_COUNT], + }), + }), +}); + +export const { useGetAnnouncementCountQuery } = announcementsApi; diff --git a/src/main/webapp/resources/js/redux/endpoints/tags.ts b/src/main/webapp/resources/js/redux/endpoints/tags.ts index c235960d29f..0db90e95dae 100644 --- a/src/main/webapp/resources/js/redux/endpoints/tags.ts +++ b/src/main/webapp/resources/js/redux/endpoints/tags.ts @@ -1,4 +1,5 @@ +export const TAG_ANNOUNCEMENT_COUNT = "tag-announcement-count"; export const TAG_COUNT = "tag-cart-count"; export const TAG_USER = "tag-user"; -export const PROVIDED_TAGS = [TAG_COUNT, TAG_USER]; +export const PROVIDED_TAGS = [TAG_ANNOUNCEMENT_COUNT, TAG_COUNT, TAG_USER]; From 0cfa78879be061977dca1ba6416578ad6373b3ed Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Thu, 1 Dec 2022 14:24:47 -0600 Subject: [PATCH 006/115] chore: Updated search form to be more dynamic --- src/main/webapp/pages/projects/index.html | 8 +- src/main/webapp/pages/template/page.html | 141 ++++++++++-------- .../js/components/Header/PageHeader.jsx | 37 ++--- .../main-navigation/components/CartLink.tsx | 2 +- .../components/GlobalSearch.tsx | 1 - .../js/components/main-navigation/index.css | 30 +++- .../js/components/main-navigation/index.tsx | 4 + .../resources/js/utilities/url-utilities.ts | 2 +- 8 files changed, 126 insertions(+), 99 deletions(-) diff --git a/src/main/webapp/pages/projects/index.html b/src/main/webapp/pages/projects/index.html index fe36e31ccd9..490ee7bc803 100644 --- a/src/main/webapp/pages/projects/index.html +++ b/src/main/webapp/pages/projects/index.html @@ -5,12 +5,8 @@ TODO: UPDATE THIS TITLE - -
+ +
diff --git a/src/main/webapp/pages/template/page.html b/src/main/webapp/pages/template/page.html index 96bf429903c..a1c6a871aa4 100644 --- a/src/main/webapp/pages/template/page.html +++ b/src/main/webapp/pages/template/page.html @@ -1,71 +1,80 @@ - - - - - - - -
-
-
-
+ + + + + + + +
+
+
+
+ - window.TL = { - _USER: /*[[${session.CURRENT_USER_DETAILS}]]*/ {}, - _BASE_URL: /*[[@{/}]]*/ '/', - LANGUAGE_TAG: /*[[${#locale.toLanguageTag()}]]*/ "en", - URLS: { - cart: { - add: /*[[@{/cart/add/samples}]]*/ "/cart/add/samples" - } - }, - SESSION_LENGTH: /*[[${#httpSession.getAttribute('SESSION_TIMEOUT')}]]*/ 1800, - lang: { - page: { - "first": /*[[#{table.first}]]*/ 'First', - "prev": /*[[#{table.previous}]]*/ 'Previous', - "next": /*[[#{table.next}]]*/ 'Next', - "last": /*[[#{table.last}]]*/ 'Last' - } - } - }; - + + + + + + - - - - - - - - - - -
- - \ No newline at end of file + + + +
+ + diff --git a/src/main/webapp/resources/js/components/Header/PageHeader.jsx b/src/main/webapp/resources/js/components/Header/PageHeader.jsx index 3e3f8ef4b36..9c5a2b7dc14 100644 --- a/src/main/webapp/resources/js/components/Header/PageHeader.jsx +++ b/src/main/webapp/resources/js/components/Header/PageHeader.jsx @@ -5,13 +5,12 @@ import { Notifications } from "../notifications/Notifications"; import GalaxyAlert from "./GalaxyAlert"; import { Breadcrumbs } from "./Breadcrumbs"; import { setBaseUrl } from "../../utilities/url-utilities"; -import { MainNavigation } from "./MainNavigation"; -import { - AnnouncementProvider -} from "./MainNavigation/components/announcements-context"; -import { - AnnouncementsModal -} from "./MainNavigation/components/AnnouncementsModal"; +import { AnnouncementProvider } from "./MainNavigation/components/announcements-context"; +import { AnnouncementsModal } from "./MainNavigation/components/AnnouncementsModal"; +import { Provider } from "react-redux"; +import { store } from "../../redux/store"; +import MainNavigation from "../main-navigation"; +import { Layout } from "antd"; /* WEBPACK PUBLIC PATH: @@ -29,16 +28,20 @@ export function PageHeader() { }, []); return ( - <> - - - - - - - - {inGalaxy ? : null} - + + + + + + + + + + + + {inGalaxy ? : null} + + ); } diff --git a/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx b/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx index 71d968e2e19..606be0e9c6a 100644 --- a/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx @@ -8,7 +8,7 @@ export default function CartLink() { const { data: count } = useGetCartCountQuery(undefined, {}); return ( - + diff --git a/src/main/webapp/resources/js/components/main-navigation/components/GlobalSearch.tsx b/src/main/webapp/resources/js/components/main-navigation/components/GlobalSearch.tsx index 40d724aeb7d..93cdf0bdccf 100644 --- a/src/main/webapp/resources/js/components/main-navigation/components/GlobalSearch.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/components/GlobalSearch.tsx @@ -11,7 +11,6 @@ export default function GlobalSearch(): JSX.Element {
} diff --git a/src/main/webapp/resources/js/components/main-navigation/index.css b/src/main/webapp/resources/js/components/main-navigation/index.css index 0b0a47b6cc3..a97d574d83f 100644 --- a/src/main/webapp/resources/js/components/main-navigation/index.css +++ b/src/main/webapp/resources/js/components/main-navigation/index.css @@ -21,24 +21,40 @@ .main-nav.ant-menu-root li.ant-menu-submenu a, .main-nav.ant-menu-root .ant-menu-item a { color: var(--grey-4); - font-size: 18px; + font-size: 16px; +} + +.global-search .ant-input-affix-wrapper { + background-color: #001529; + border: none; +} + +.global-search:focus-within .ant-input-affix-wrapper { + background-color: white; } .global-search { - width: 300px; background-color: transparent; - margin: 0 20px; +} + +.global-search input { + width: 0; +} + +.global-search input:focus { + width: 300px; } .global-search [data-icon="search"], .global-search .anticon-close-circle svg { - color: #bfbfbf; - font-size: 14px; + color: #f0f0f0; + font-size: 22px; + cursor: pointer; } .nav-icon svg { position: relative; - top: 6px; + top: 5px; color: #f0f0f0; - font-size: 26px; + font-size: 22px; } diff --git a/src/main/webapp/resources/js/components/main-navigation/index.tsx b/src/main/webapp/resources/js/components/main-navigation/index.tsx index d99f41076e6..7b898269601 100644 --- a/src/main/webapp/resources/js/components/main-navigation/index.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/index.tsx @@ -26,14 +26,18 @@ import { renderMenuItem } from "../ant.design/menu-utilities"; import { useGetCurrentUserQuery } from "../../redux/endpoints/user"; import { setBaseUrl } from "../../utilities/url-utilities"; import AnnouncementLink from "./components/AnnouncementLink"; +import useBreakpoint from "antd/es/grid/hooks/useBreakpoint"; /** * React component to render the main navigation component at the top of the page. * @constructor */ export default function MainNavigation(): JSX.Element { + const screens = useBreakpoint(); const { data: user = {}, isSuccess } = useGetCurrentUserQuery(undefined, {}); + console.log(screens); + const leftMenuItems: MenuItem[] = useMemo( () => [ { diff --git a/src/main/webapp/resources/js/utilities/url-utilities.ts b/src/main/webapp/resources/js/utilities/url-utilities.ts index 9feaa1496bc..7b0db92c2d8 100644 --- a/src/main/webapp/resources/js/utilities/url-utilities.ts +++ b/src/main/webapp/resources/js/utilities/url-utilities.ts @@ -52,7 +52,7 @@ export function getProjectIdFromUrl(url = window.location.href) { * of context (`data-context`) with the context path. */ export function getContextPath(): string { - const element = document.getElementById("root"); + const element = document.querySelector("body"); if (element && element.dataset && element.dataset.context) { return element.dataset.context; } else { From 2c485e4485ea18eef4322b70aeaa77e886ce1667 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Thu, 1 Dec 2022 14:27:47 -0600 Subject: [PATCH 007/115] chore: Cleaned up search icon when used in prefix --- .../resources/js/components/main-navigation/index.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/resources/js/components/main-navigation/index.css b/src/main/webapp/resources/js/components/main-navigation/index.css index a97d574d83f..beab11fc5d4 100644 --- a/src/main/webapp/resources/js/components/main-navigation/index.css +++ b/src/main/webapp/resources/js/components/main-navigation/index.css @@ -45,13 +45,16 @@ width: 300px; } -.global-search [data-icon="search"], -.global-search .anticon-close-circle svg { +.global-search [data-icon="search"] { color: #f0f0f0; font-size: 22px; cursor: pointer; } +.global-search:focus-within [data-icon="search"] { + color: #bfbfbf; +} + .nav-icon svg { position: relative; top: 5px; From ed8b3b21288614a3d4d0f8cb0cce976839c86707 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Thu, 1 Dec 2022 15:05:54 -0600 Subject: [PATCH 008/115] chore: Updated typescript definitions --- .../main-navigation/components/AnnouncementLink.tsx | 6 ++++++ .../resources/js/components/main-navigation/index.tsx | 8 ++------ src/main/webapp/resources/js/redux/endpoints/api.ts | 5 +++++ src/main/webapp/resources/js/redux/endpoints/cart.ts | 2 +- src/main/webapp/resources/js/redux/endpoints/user.ts | 3 ++- src/main/webapp/resources/js/types/irida/index.d.ts | 2 ++ 6 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementLink.tsx b/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementLink.tsx index 0250963ce69..acc91fe334f 100644 --- a/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementLink.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementLink.tsx @@ -4,6 +4,12 @@ import { BellOutlined } from "@ant-design/icons"; import { ROUTE_ANNOUNCEMENTS } from "../../../data/routes"; import { useGetAnnouncementCountQuery } from "../../../redux/endpoints/announcements"; +/** + * React component to render a link in the main navigation to the announcements page, + * and display the number of unread announcements. + * + * @constructor + */ export default function AnnouncementLink() { const { data: count } = useGetAnnouncementCountQuery(undefined, {}); diff --git a/src/main/webapp/resources/js/components/main-navigation/index.tsx b/src/main/webapp/resources/js/components/main-navigation/index.tsx index 7b898269601..5036b557ee2 100644 --- a/src/main/webapp/resources/js/components/main-navigation/index.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/index.tsx @@ -26,17 +26,13 @@ import { renderMenuItem } from "../ant.design/menu-utilities"; import { useGetCurrentUserQuery } from "../../redux/endpoints/user"; import { setBaseUrl } from "../../utilities/url-utilities"; import AnnouncementLink from "./components/AnnouncementLink"; -import useBreakpoint from "antd/es/grid/hooks/useBreakpoint"; /** * React component to render the main navigation component at the top of the page. * @constructor */ export default function MainNavigation(): JSX.Element { - const screens = useBreakpoint(); - const { data: user = {}, isSuccess } = useGetCurrentUserQuery(undefined, {}); - - console.log(screens); + const { data: user, isSuccess } = useGetCurrentUserQuery(undefined, {}); const leftMenuItems: MenuItem[] = useMemo( () => [ @@ -163,7 +159,7 @@ export default function MainNavigation(): JSX.Element { ] : []), ], - [isSuccess, user.admin] + [isSuccess, user?.admin, user?.manager, user?.technician] ); const rightMenuItems: MenuItem[] = useMemo( diff --git a/src/main/webapp/resources/js/redux/endpoints/api.ts b/src/main/webapp/resources/js/redux/endpoints/api.ts index 5d641b177f2..a8aa889bb55 100644 --- a/src/main/webapp/resources/js/redux/endpoints/api.ts +++ b/src/main/webapp/resources/js/redux/endpoints/api.ts @@ -2,6 +2,11 @@ import { createApi, fetchBaseQuery, retry } from "@reduxjs/toolkit/query/react"; import { CONTEXT_PATH } from "../../data/routes"; import { PROVIDED_TAGS } from "./tags"; +/** + * @fileoverview Root api for all redux toolkit in the SPA. All other endpoints should be + * injected into this one. See {@link https://redux-toolkit.js.org/rtk-query/usage/code-splitting} + */ + const baseQuery = fetchBaseQuery({ baseUrl: `${CONTEXT_PATH}/ajax`, }); diff --git a/src/main/webapp/resources/js/redux/endpoints/cart.ts b/src/main/webapp/resources/js/redux/endpoints/cart.ts index 618d906e3b5..378e82ae3a3 100644 --- a/src/main/webapp/resources/js/redux/endpoints/cart.ts +++ b/src/main/webapp/resources/js/redux/endpoints/cart.ts @@ -3,7 +3,7 @@ import { TAG_COUNT } from "./tags"; export const cartApi = api.injectEndpoints({ endpoints: (build) => ({ - getCartCount: build.query({ + getCartCount: build.query({ query: () => "cart/count", providesTags: [TAG_COUNT], }), diff --git a/src/main/webapp/resources/js/redux/endpoints/user.ts b/src/main/webapp/resources/js/redux/endpoints/user.ts index c6e2ac14fd2..94bef2d3cb7 100644 --- a/src/main/webapp/resources/js/redux/endpoints/user.ts +++ b/src/main/webapp/resources/js/redux/endpoints/user.ts @@ -1,9 +1,10 @@ import { api } from "./api"; import { TAG_USER } from "./tags"; +import { CurrentUser } from "../../types/irida"; export const userApi = api.injectEndpoints({ endpoints: (build) => ({ - getCurrentUser: build.query({ + getCurrentUser: build.query({ query: () => "/users/current", providesTags: [TAG_USER], }), diff --git a/src/main/webapp/resources/js/types/irida/index.d.ts b/src/main/webapp/resources/js/types/irida/index.d.ts index 3fd74617ba1..a2463a951a9 100644 --- a/src/main/webapp/resources/js/types/irida/index.d.ts +++ b/src/main/webapp/resources/js/types/irida/index.d.ts @@ -47,6 +47,8 @@ declare namespace IRIDA { firstName: string; identifier: number; lastName: string; + manager: boolean; + technician: boolean; username: string; }; From 30d7faa217fdece4ef66436d9bd01696b7df1342 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Thu, 1 Dec 2022 15:13:03 -0600 Subject: [PATCH 009/115] chore: Removed old main navigation files --- .../Header/MainNavigation/MainNavigation.css | 55 --------- .../Header/MainNavigation/MainNavigation.jsx | 93 -------------- .../components/AnnouncementsModal.jsx | 113 ------------------ .../components/AnnouncementsSubMenu.jsx | 106 ---------------- .../MainNavigation/components/CartLink.jsx | 52 -------- .../components/GlobalSearch.css | 19 --- .../components/GlobalSearch.jsx | 25 ---- .../components/Header/MainNavigation/index.js | 1 - 8 files changed, 464 deletions(-) delete mode 100644 src/main/webapp/resources/js/components/Header/MainNavigation/MainNavigation.css delete mode 100644 src/main/webapp/resources/js/components/Header/MainNavigation/MainNavigation.jsx delete mode 100644 src/main/webapp/resources/js/components/Header/MainNavigation/components/AnnouncementsModal.jsx delete mode 100644 src/main/webapp/resources/js/components/Header/MainNavigation/components/AnnouncementsSubMenu.jsx delete mode 100644 src/main/webapp/resources/js/components/Header/MainNavigation/components/CartLink.jsx delete mode 100644 src/main/webapp/resources/js/components/Header/MainNavigation/components/GlobalSearch.css delete mode 100644 src/main/webapp/resources/js/components/Header/MainNavigation/components/GlobalSearch.jsx delete mode 100644 src/main/webapp/resources/js/components/Header/MainNavigation/index.js diff --git a/src/main/webapp/resources/js/components/Header/MainNavigation/MainNavigation.css b/src/main/webapp/resources/js/components/Header/MainNavigation/MainNavigation.css deleted file mode 100644 index cbafac32a25..00000000000 --- a/src/main/webapp/resources/js/components/Header/MainNavigation/MainNavigation.css +++ /dev/null @@ -1,55 +0,0 @@ -.main-navigation { - display: flex; - align-content: center; - height: 64px; -} - -.dark-theme.main-navigation { - background-color: #001529; -} - -.light-theme.main-navigation { - background-color: #ffffff; -} - -.main-navigation header { - position: fixed; - z-index: 1000; - display: flex; - width: 100%; -} - -.main-navigation .ant-menu-item-active { - background-color: transparent; -} - -.main-navigation .anticon { - font-size: 20px !important; -} - -.dark-theme .main-navigation .anticon, -.dark-theme .main-navigation .ant-menu-title-content a { - color: var(--grey-2); -} - -.light-theme .main-navigation .anticon, -.light-theme .main-navigation .ant-menu-title-content a { - color: var(--grey-11); -} - -.main-navigation .ant-menu-title-content a { - font-weight: 600; - font-size: 1.5rem; -} - -.dark-theme .main-navigation .ant-menu-title-content a:hover { - color: var(--grey-1); -} - -.light-theme .main-navigation .ant-menu-title-content a:hover { - color: var(--grey-8); -} - -.main-navigation .accessory-menu { - margin-left: 30px; -} diff --git a/src/main/webapp/resources/js/components/Header/MainNavigation/MainNavigation.jsx b/src/main/webapp/resources/js/components/Header/MainNavigation/MainNavigation.jsx deleted file mode 100644 index 4f96cf1d5ae..00000000000 --- a/src/main/webapp/resources/js/components/Header/MainNavigation/MainNavigation.jsx +++ /dev/null @@ -1,93 +0,0 @@ -import React from "react"; -import { Button, Layout, Menu } from "antd"; -import { SPACE_LG, SPACE_MD } from "../../../styles/spacing"; -import { theme } from "../../../utilities/theme-utilities"; -import { setBaseUrl } from "../../../utilities/url-utilities"; -import { IconUser } from "../../icons/Icons"; -import { AnnouncementsSubMenu } from "./components/AnnouncementsSubMenu"; -import { CartLink } from "./components/CartLink"; -import { GlobalSearch } from "./components/GlobalSearch"; -import "./MainNavigation.css"; - -const { Header } = Layout; - -const isAdmin = window.TL._USER.systemRole === "ROLE_ADMIN"; -const isManager = isAdmin || window.TL._USER.systemRole === "ROLE_MANAGER"; -const isTechnician = window.TL._USER.systemRole === "ROLE_TECHNICIAN"; - -export function MainNavigation() { - const [isLargeScreen, setIsLargeScreen] = React.useState( - window.innerWidth > 1050 - ); - - React.useEffect(() => { - const handleResize = () => { - setIsLargeScreen(window.innerWidth > 1050); - }; - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); - }, []); - - return ( -
- - {isAdmin && ( -
- -
- )} - - - - }> - - - - - {i18n("nav.main.userguide")} - - - {isAdmin && ( - - - {i18n("nav.main.adminguide")} - - - )} - - - - {i18n("generic.irida.website")} - - - - - {i18n("irida.version")} - - - - {i18n("nav.main.logout")} - - - -
- ); -} diff --git a/src/main/webapp/resources/js/components/Header/MainNavigation/components/AnnouncementsModal.jsx b/src/main/webapp/resources/js/components/Header/MainNavigation/components/AnnouncementsModal.jsx deleted file mode 100644 index c5d7945dd2a..00000000000 --- a/src/main/webapp/resources/js/components/Header/MainNavigation/components/AnnouncementsModal.jsx +++ /dev/null @@ -1,113 +0,0 @@ -import React from "react"; -import { ScrollableModal } from "../../../ant.design/ScrollableModal"; -import { Button, Space, Tag, Typography } from "antd"; -import { PriorityFlag } from "../../../../pages/announcement/components/PriorityFlag"; -import { formatDate } from "../../../../utilities/date-utilities"; -import ReactMarkdown from "react-markdown"; -import { TYPES, useAnnouncements } from "./announcements-context"; -import { - readAndCloseAnnouncement, - readAndNextAnnouncement, - readAndPreviousAnnouncement, -} from "./announcement-dispatch"; - -const { Text } = Typography; - -/** - * React component to display the announcements modal. - * - * @returns {JSX.Element} - * @constructor - */ -export function AnnouncementsModal() { - const [ - { announcements, modalVisible: visible, index, isPriority }, - dispatch, - ] = useAnnouncements(); - - const [newAnnouncements, setNewAnnouncements] = React.useState([]); - - React.useEffect(() => { - if (isPriority) { - setNewAnnouncements(announcements.filter((a) => a.priority)); - } else { - setNewAnnouncements(announcements); - } - }, [announcements]); - - const footer = [ - index > 0 && ( - - ), - (index === 0 || index + 1 === newAnnouncements.length) && ( - - ), - index + 1 < newAnnouncements.length && ( - - ), - ]; - - return visible && newAnnouncements.length ? ( - - - {i18n( - "AnnouncementsModal.tag.details", - newAnnouncements.filter((a) => a.read).length, - newAnnouncements.length - )} - - - - - {newAnnouncements[index].title} - - {i18n( - "AnnouncementsModal.create.details", - newAnnouncements[index].user.username, - formatDate({ date: newAnnouncements[index].createdDate }) - )} - - - - - } - visible={visible} - width="90ch" - onCancel={() => dispatch({ type: TYPES.CLOSE_ANNOUNCEMENT })} - footer={footer} - > -
- {newAnnouncements[index].message} -
-
- ) : null; -} diff --git a/src/main/webapp/resources/js/components/Header/MainNavigation/components/AnnouncementsSubMenu.jsx b/src/main/webapp/resources/js/components/Header/MainNavigation/components/AnnouncementsSubMenu.jsx deleted file mode 100644 index 81079f8d294..00000000000 --- a/src/main/webapp/resources/js/components/Header/MainNavigation/components/AnnouncementsSubMenu.jsx +++ /dev/null @@ -1,106 +0,0 @@ -/** - * @file AnnouncementsSubMenu is the announcements drop down in the main navigation bar. - */ -import React from "react"; -import { Avatar, Badge, Button, List, Popover } from "antd"; -import { blue6, grey6 } from "../../../../styles/colors"; -import { SPACE_MD } from "../../../../styles/spacing"; -import { fromNow } from "../../../../utilities/date-utilities"; -import { setBaseUrl } from "../../../../utilities/url-utilities"; -import { IconBell, IconFlag } from "../../../icons/Icons"; -import { TYPES, useAnnouncements } from "./announcements-context"; - -/** - * React component to display the bell icon and new announcement count badge - * - * @returns {JSX.Element} - * @constructor - */ -export function AnnouncementsSubMenu() { - const [{ announcements }, dispatch] = useAnnouncements(); - - function showAnnouncementModal(index) { - dispatch({ - type: TYPES.SHOW_ANNOUNCEMENT, - payload: { - index, - isPriority: false, - }, - }); - } - - const Announcements = () => ( - - {announcements.map((announcement, index) => ( - - } - style={{ - backgroundColor: announcement.priority ? blue6 : grey6, - }} - /> - } - title={ - showAnnouncementModal(index)} - > - {announcement.title} - - } - description={fromNow({ date: announcement.createdDate })} - /> - - ))} - {announcements.length === 0 && ( - {i18n("AnnouncementsSubMenu.emptyList")} - )} - - - {i18n("AnnouncementsSubMenu.view-all")} - - - - ); - - return ( - - -
- } - trigger="click" - > -
-
- - ); -} diff --git a/src/main/webapp/resources/js/components/Header/MainNavigation/components/CartLink.jsx b/src/main/webapp/resources/js/components/Header/MainNavigation/components/CartLink.jsx deleted file mode 100644 index 35a257d71a9..00000000000 --- a/src/main/webapp/resources/js/components/Header/MainNavigation/components/CartLink.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Badge, Button } from "antd"; -import React from "react"; -import { getCartCount } from "../../../../apis/cart/cart"; -import { SPACE_MD } from "../../../../styles/spacing"; -import { CART } from "../../../../utilities/events-utilities"; -import { setBaseUrl } from "../../../../utilities/url-utilities"; -import { IconShoppingCart } from "../../../icons/Icons"; - -/** - * React component to display the cart icon and current counts in the - * IU header. - * @returns {JSX.Element} - * @constructor - */ -export function CartLink() { - const [count, setCount] = React.useState(0); - /* - If we are inside a galaxy session then the cart should direct to the galaxy. - TODO: Move this logic to the cart not the main navigation - */ - const inGalaxy = typeof window.GALAXY !== "undefined"; - - function updateCount(e) { - const { count: newCount } = e.detail; - setCount(newCount); - } - - // Initialize cart here - React.useEffect(() => { - getCartCount().then(setCount); - }, []); - - React.useEffect(() => { - document.addEventListener(CART.UPDATED, updateCount, false); - return () => document.removeEventListener(CART.UPDATED, updateCount, false); - }, []); - - return ( -
-
- ); -} diff --git a/src/main/webapp/resources/js/components/Header/MainNavigation/components/GlobalSearch.css b/src/main/webapp/resources/js/components/Header/MainNavigation/components/GlobalSearch.css deleted file mode 100644 index 605755da9ed..00000000000 --- a/src/main/webapp/resources/js/components/Header/MainNavigation/components/GlobalSearch.css +++ /dev/null @@ -1,19 +0,0 @@ -.global-search { - width: 300px; - margin-right: 15px; - background-color: transparent; -} - -.light-theme .global-search { - color: var(--grey-1); -} - -.dark-theme .global-search { - color: var(--grey-11); -} - -.global-search .ant-input-prefix svg, -.global-search .anticon-close-circle svg { - color: var(--grey-6); - font-size: 14px; -} diff --git a/src/main/webapp/resources/js/components/Header/MainNavigation/components/GlobalSearch.jsx b/src/main/webapp/resources/js/components/Header/MainNavigation/components/GlobalSearch.jsx deleted file mode 100644 index 81987c97e24..00000000000 --- a/src/main/webapp/resources/js/components/Header/MainNavigation/components/GlobalSearch.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Input } from "antd"; -import React from "react"; -import { setBaseUrl } from "../../../../utilities/url-utilities"; -import { IconSearch } from "../../../icons/Icons"; -import "./GlobalSearch.css"; - -/** - * React component to render a global search input to the main navigation. - * @returns {JSX.Element} - * @constructor - */ -export function GlobalSearch() { - return ( - - } - placeholder={i18n("nav.main.search")} - /> - - ); -} diff --git a/src/main/webapp/resources/js/components/Header/MainNavigation/index.js b/src/main/webapp/resources/js/components/Header/MainNavigation/index.js deleted file mode 100644 index 0d73b4f663f..00000000000 --- a/src/main/webapp/resources/js/components/Header/MainNavigation/index.js +++ /dev/null @@ -1 +0,0 @@ -export { MainNavigation } from "./MainNavigation"; From 2538e23b5c782a1fa130bd68918afd0b6e1b47a3 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Thu, 1 Dec 2022 15:14:02 -0600 Subject: [PATCH 010/115] chore: Removed unused vendor entry and files. --- src/main/webapp/entries.js | 1 - .../vendor/datatables/select.bootstrap.css | 85 ------------------- 2 files changed, 86 deletions(-) delete mode 100644 src/main/webapp/resources/vendor/datatables/select.bootstrap.css diff --git a/src/main/webapp/entries.js b/src/main/webapp/entries.js index 13dc1353ca6..ac1126b9500 100644 --- a/src/main/webapp/entries.js +++ b/src/main/webapp/entries.js @@ -4,7 +4,6 @@ * Webpack will then create the bundle in `resource/js/build/` */ module.exports = { - vendor: ["expose-loader?exposes=$,jQuery!jquery", "./resources/js/vendors"], login: "./resources/js/pages/LoginPage.tsx", "project-spa": "./resources/js/pages/projects/ProjectSPA.tsx", access_confirmation: "./resources/js/pages/oauth/access_confirmation.js", diff --git a/src/main/webapp/resources/vendor/datatables/select.bootstrap.css b/src/main/webapp/resources/vendor/datatables/select.bootstrap.css deleted file mode 100644 index 307e92c98d4..00000000000 --- a/src/main/webapp/resources/vendor/datatables/select.bootstrap.css +++ /dev/null @@ -1,85 +0,0 @@ -table.dataTable tbody > tr.selected, -table.dataTable tbody > tr > .selected { - background-color: #08C; } -table.dataTable.stripe tbody > tr.odd.selected, -table.dataTable.stripe tbody > tr.odd > .selected, table.dataTable.display tbody > tr.odd.selected, -table.dataTable.display tbody > tr.odd > .selected { - background-color: #0085c7; } -table.dataTable.hover tbody > tr.selected:hover, -table.dataTable.hover tbody > tr > .selected:hover, table.dataTable.display tbody > tr.selected:hover, -table.dataTable.display tbody > tr > .selected:hover { - background-color: #0083c5; } -table.dataTable.order-column tbody > tr.selected > .sorting_1, -table.dataTable.order-column tbody > tr.selected > .sorting_2, -table.dataTable.order-column tbody > tr.selected > .sorting_3, -table.dataTable.order-column tbody > tr > .selected, table.dataTable.display tbody > tr.selected > .sorting_1, -table.dataTable.display tbody > tr.selected > .sorting_2, -table.dataTable.display tbody > tr.selected > .sorting_3, -table.dataTable.display tbody > tr > .selected { - background-color: #0085c8; } -table.dataTable.display tbody > tr.odd.selected > .sorting_1, table.dataTable.order-column.stripe tbody > tr.odd.selected > .sorting_1 { - background-color: #0081c1; } -table.dataTable.display tbody > tr.odd.selected > .sorting_2, table.dataTable.order-column.stripe tbody > tr.odd.selected > .sorting_2 { - background-color: #0082c2; } -table.dataTable.display tbody > tr.odd.selected > .sorting_3, table.dataTable.order-column.stripe tbody > tr.odd.selected > .sorting_3 { - background-color: #0083c4; } -table.dataTable.display tbody > tr.even.selected > .sorting_1, table.dataTable.order-column.stripe tbody > tr.even.selected > .sorting_1 { - background-color: #0085c8; } -table.dataTable.display tbody > tr.even.selected > .sorting_2, table.dataTable.order-column.stripe tbody > tr.even.selected > .sorting_2 { - background-color: #0086ca; } -table.dataTable.display tbody > tr.even.selected > .sorting_3, table.dataTable.order-column.stripe tbody > tr.even.selected > .sorting_3 { - background-color: #0087cb; } -table.dataTable.display tbody > tr.odd > .selected, table.dataTable.order-column.stripe tbody > tr.odd > .selected { - background-color: #0081c1; } -table.dataTable.display tbody > tr.even > .selected, table.dataTable.order-column.stripe tbody > tr.even > .selected { - background-color: #0085c8; } -table.dataTable.display tbody > tr.selected:hover > .sorting_1, table.dataTable.order-column.hover tbody > tr.selected:hover > .sorting_1 { - background-color: #007dbb; } -table.dataTable.display tbody > tr.selected:hover > .sorting_2, table.dataTable.order-column.hover tbody > tr.selected:hover > .sorting_2 { - background-color: #007ebd; } -table.dataTable.display tbody > tr.selected:hover > .sorting_3, table.dataTable.order-column.hover tbody > tr.selected:hover > .sorting_3 { - background-color: #007fbf; } -table.dataTable.display tbody > tr:hover > .selected, -table.dataTable.display tbody > tr > .selected:hover, table.dataTable.order-column.hover tbody > tr:hover > .selected, -table.dataTable.order-column.hover tbody > tr > .selected:hover { - background-color: #007dbb; } -table.dataTable td.select-checkbox { - position: relative; } - table.dataTable td.select-checkbox:before, table.dataTable td.select-checkbox:after { - display: block; - position: absolute; - top: 1.2em; - left: 50%; - width: 12px; - height: 12px; - box-sizing: border-box; } - table.dataTable td.select-checkbox:before { - content: ' '; - margin-top: -6px; - margin-left: -6px; - border: 1px solid black; - border-radius: 3px; } -table.dataTable tr.selected td.select-checkbox:after { - content: '\2714'; - margin-top: -11px; - margin-left: -4px; - text-align: center; - text-shadow: 1px 1px #B0BED9, -1px -1px #B0BED9, 1px -1px #B0BED9, -1px 1px #B0BED9; } - -div.dataTables_wrapper span.select-info, -div.dataTables_wrapper span.select-item { - margin-left: 0.5em; } - -@media screen and (max-width: 640px) { - div.dataTables_wrapper span.select-info, - div.dataTables_wrapper span.select-item { - margin-left: 0; - display: block; } } -table.dataTable tbody tr.selected, -table.dataTable tbody th.selected, -table.dataTable tbody td.selected { - color: white; } - table.dataTable tbody tr.selected a, - table.dataTable tbody th.selected a, - table.dataTable tbody td.selected a { - color: #a2d4ed; } From 81f2936b3372b1be464058a3d88a1be765236e1a Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Thu, 1 Dec 2022 15:16:01 -0600 Subject: [PATCH 011/115] chore: File should not have been removed --- .../components/AnnouncementsModal.jsx | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/main/webapp/resources/js/components/Header/MainNavigation/components/AnnouncementsModal.jsx diff --git a/src/main/webapp/resources/js/components/Header/MainNavigation/components/AnnouncementsModal.jsx b/src/main/webapp/resources/js/components/Header/MainNavigation/components/AnnouncementsModal.jsx new file mode 100644 index 00000000000..c5d7945dd2a --- /dev/null +++ b/src/main/webapp/resources/js/components/Header/MainNavigation/components/AnnouncementsModal.jsx @@ -0,0 +1,113 @@ +import React from "react"; +import { ScrollableModal } from "../../../ant.design/ScrollableModal"; +import { Button, Space, Tag, Typography } from "antd"; +import { PriorityFlag } from "../../../../pages/announcement/components/PriorityFlag"; +import { formatDate } from "../../../../utilities/date-utilities"; +import ReactMarkdown from "react-markdown"; +import { TYPES, useAnnouncements } from "./announcements-context"; +import { + readAndCloseAnnouncement, + readAndNextAnnouncement, + readAndPreviousAnnouncement, +} from "./announcement-dispatch"; + +const { Text } = Typography; + +/** + * React component to display the announcements modal. + * + * @returns {JSX.Element} + * @constructor + */ +export function AnnouncementsModal() { + const [ + { announcements, modalVisible: visible, index, isPriority }, + dispatch, + ] = useAnnouncements(); + + const [newAnnouncements, setNewAnnouncements] = React.useState([]); + + React.useEffect(() => { + if (isPriority) { + setNewAnnouncements(announcements.filter((a) => a.priority)); + } else { + setNewAnnouncements(announcements); + } + }, [announcements]); + + const footer = [ + index > 0 && ( + + ), + (index === 0 || index + 1 === newAnnouncements.length) && ( + + ), + index + 1 < newAnnouncements.length && ( + + ), + ]; + + return visible && newAnnouncements.length ? ( + + + {i18n( + "AnnouncementsModal.tag.details", + newAnnouncements.filter((a) => a.read).length, + newAnnouncements.length + )} + + + + + {newAnnouncements[index].title} + + {i18n( + "AnnouncementsModal.create.details", + newAnnouncements[index].user.username, + formatDate({ date: newAnnouncements[index].createdDate }) + )} + + + + + } + visible={visible} + width="90ch" + onCancel={() => dispatch({ type: TYPES.CLOSE_ANNOUNCEMENT })} + footer={footer} + > +
+ {newAnnouncements[index].message} +
+
+ ) : null; +} From 28b79c1e22b00a8fedeb5c7daa795f61def019ed Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 05:25:57 -0600 Subject: [PATCH 012/115] chore: Updated favicon --- src/main/webapp/pages/projects/index.html | 6 ++++++ src/main/webapp/pages/template/page.html | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/pages/projects/index.html b/src/main/webapp/pages/projects/index.html index 490ee7bc803..8ea9645ec1e 100644 --- a/src/main/webapp/pages/projects/index.html +++ b/src/main/webapp/pages/projects/index.html @@ -2,6 +2,12 @@ + TODO: UPDATE THIS TITLE diff --git a/src/main/webapp/pages/template/page.html b/src/main/webapp/pages/template/page.html index e02c0b6a819..3b6d84792dc 100644 --- a/src/main/webapp/pages/template/page.html +++ b/src/main/webapp/pages/template/page.html @@ -17,6 +17,7 @@ @@ -24,7 +25,10 @@ window.breadcrumbs = /*[[${breadcrumbs}]]*/ []; - +
From d4c4e2775764335dbab7d5770e13da1cacd6f43b Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 05:30:41 -0600 Subject: [PATCH 013/115] chore: Added `theme` back as attribute on page body --- src/main/webapp/pages/projects/index.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/pages/projects/index.html b/src/main/webapp/pages/projects/index.html index 8ea9645ec1e..790d6407287 100644 --- a/src/main/webapp/pages/projects/index.html +++ b/src/main/webapp/pages/projects/index.html @@ -11,7 +11,10 @@ TODO: UPDATE THIS TITLE - +
From c7bdcb29aa34ba1480907ae3d2ab87bfde1014dd Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 07:10:04 -0600 Subject: [PATCH 014/115] chore: Fixed the way roles are passed down for the CurrentUser --- .../irida/ria/web/ajax/dto/CurrentUser.java | 6 +++--- .../js/components/main-navigation/index.tsx | 18 +++++++++--------- .../webapp/resources/js/types/irida/index.d.ts | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/ajax/dto/CurrentUser.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/ajax/dto/CurrentUser.java index 2f7bdba6fe8..4cc5d30d623 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/ajax/dto/CurrentUser.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/ajax/dto/CurrentUser.java @@ -41,15 +41,15 @@ public String getLastName() { return lastName; } - public boolean isAdmin() { + public boolean getIsAdmin() { return isAdmin; } - public boolean isManager() { + public boolean getIsManager() { return isManager; } - public boolean isTechnician() { + public boolean getIsTechnician() { return isTechnician; } } \ No newline at end of file diff --git a/src/main/webapp/resources/js/components/main-navigation/index.tsx b/src/main/webapp/resources/js/components/main-navigation/index.tsx index 5036b557ee2..60ac91ca387 100644 --- a/src/main/webapp/resources/js/components/main-navigation/index.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/index.tsx @@ -50,7 +50,7 @@ export default function MainNavigation(): JSX.Element { ), }, - ...(isSuccess && user.admin + ...(isSuccess && user.isAdmin ? [ { key: `nav-projects-all`, @@ -83,7 +83,7 @@ export default function MainNavigation(): JSX.Element { ), }, - ...(isSuccess && user.admin + ...(isSuccess && user.isAdmin ? [ { key: `nav-analyses-all`, @@ -104,7 +104,7 @@ export default function MainNavigation(): JSX.Element { }, ], }, - ...(isSuccess && !user.admin && user.manager + ...(isSuccess && !user.isAdmin && user.isManager ? [ { key: `nav-users`, @@ -128,7 +128,7 @@ export default function MainNavigation(): JSX.Element { }, ] : []), - ...(isSuccess && user.technician + ...(isSuccess && user.isTechnician ? [ { key: `nav-sequencing`, @@ -140,7 +140,7 @@ export default function MainNavigation(): JSX.Element { }, ] : []), - ...(isSuccess && !user.admin + ...(isSuccess && !user.isAdmin ? [ { key: `nav-remote-api`, @@ -150,7 +150,7 @@ export default function MainNavigation(): JSX.Element { }, ] : []), - ...(isSuccess && user.admin + ...(isSuccess && user.isAdmin ? [ { key: `nav-admin`, @@ -159,7 +159,7 @@ export default function MainNavigation(): JSX.Element { ] : []), ], - [isSuccess, user?.admin, user?.manager, user?.technician] + [isSuccess, user?.isAdmin, user?.isManager, user?.isTechnician] ); const rightMenuItems: MenuItem[] = useMemo( @@ -200,7 +200,7 @@ export default function MainNavigation(): JSX.Element { ), }, - ...(isSuccess && user.admin + ...(isSuccess && user.isAdmin ? [ { key: `nav-admin-guide`, @@ -238,7 +238,7 @@ export default function MainNavigation(): JSX.Element { ], }, ], - [isSuccess, user?.admin] + [isSuccess, user?.isAdmin] ); return ( diff --git a/src/main/webapp/resources/js/types/irida/index.d.ts b/src/main/webapp/resources/js/types/irida/index.d.ts index a2463a951a9..bca978efec6 100644 --- a/src/main/webapp/resources/js/types/irida/index.d.ts +++ b/src/main/webapp/resources/js/types/irida/index.d.ts @@ -43,13 +43,13 @@ declare namespace IRIDA { } export type CurrentUser = { - admin: boolean; + isAdmin: boolean; firstName: string; identifier: number; lastName: string; - manager: boolean; + isManager: boolean; technician: boolean; - username: string; + isTechnician: string; }; export type PRIORITY = "LOW" | "MEDIUM" | "HIGH"; From bd503b4ea0ae9e2b3dfc3b95282aa56e377633fd Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 08:58:03 -0600 Subject: [PATCH 015/115] refactor: Moved code to top level so this does not have to be done at a later point --- .../irida/ria/web/DashboardController.java | 2 +- .../irida/ria/web/SPAController.java | 23 + .../ria/web/projects/ProjectsController.java | 728 +++++++++--------- src/main/webapp/entries.js | 2 +- src/main/webapp/package.json | 2 +- src/main/webapp/pages/dashboard.html | 24 + src/main/webapp/pages/index.html | 40 +- src/main/webapp/pages/projects/index.html | 22 - src/main/webapp/pnpm-lock.yaml | 22 +- .../components/AnnouncementLink.tsx | 2 + src/main/webapp/resources/js/index.tsx | 74 ++ .../js/pages/projects/ProjectSPA.tsx | 104 --- 12 files changed, 504 insertions(+), 541 deletions(-) create mode 100644 src/main/java/ca/corefacility/bioinformatics/irida/ria/web/SPAController.java create mode 100644 src/main/webapp/pages/dashboard.html delete mode 100644 src/main/webapp/pages/projects/index.html create mode 100644 src/main/webapp/resources/js/index.tsx delete mode 100644 src/main/webapp/resources/js/pages/projects/ProjectSPA.tsx diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/DashboardController.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/DashboardController.java index 355aceeca1e..9d24a2bbd95 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/DashboardController.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/DashboardController.java @@ -11,7 +11,7 @@ */ @Controller public class DashboardController { - private static final String DASHBOARD_PAGE = "index"; + private static final String DASHBOARD_PAGE = "dashboard"; private static final Logger logger = LoggerFactory.getLogger(DashboardController.class); /** diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/SPAController.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/SPAController.java new file mode 100644 index 00000000000..bb2c083809e --- /dev/null +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/SPAController.java @@ -0,0 +1,23 @@ +package ca.corefacility.bioinformatics.irida.ria.web; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +/** + * Only controller to be used for IRIDA SPA + */ +@Controller +public class SPAController { + + /** + * Entry point for IRIDA SPA. + * + * @return Index page. + */ + @GetMapping("/projects/**") + public String getSPAEntry() { + // TODO: (Josh - 12/2/22) This url will need to get updated as we move higher up the chain of pages. + return "index"; + } + +} diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectsController.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectsController.java index bc25416d243..18d3f25ce4e 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectsController.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectsController.java @@ -1,47 +1,25 @@ package ca.corefacility.bioinformatics.irida.ria.web.projects; -import ca.corefacility.bioinformatics.irida.exceptions.ProjectWithoutOwnerException; -import ca.corefacility.bioinformatics.irida.model.joins.Join; -import ca.corefacility.bioinformatics.irida.model.project.Project; -import ca.corefacility.bioinformatics.irida.model.sample.Sample; -import ca.corefacility.bioinformatics.irida.model.user.User; import ca.corefacility.bioinformatics.irida.ria.utilities.converters.FileSizeConverter; -import ca.corefacility.bioinformatics.irida.ria.web.models.datatables.DTProject; import ca.corefacility.bioinformatics.irida.service.ProjectService; import ca.corefacility.bioinformatics.irida.service.TaxonomyService; import ca.corefacility.bioinformatics.irida.service.sample.SampleService; import ca.corefacility.bioinformatics.irida.service.user.UserService; -import ca.corefacility.bioinformatics.irida.util.TreeNode; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import org.apache.commons.csv.CSVFormat; -import org.apache.commons.csv.CSVPrinter; -import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.Row; -import org.apache.poi.xssf.usermodel.XSSFSheet; -import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Scope; import org.springframework.format.Formatter; import org.springframework.format.datetime.DateFormatter; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.RequestMapping; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.security.Principal; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.*; -import java.util.Map.Entry; -import java.util.stream.Collectors; +import java.util.Date; +import java.util.List; +import java.util.Map; /** * Controller for project related views @@ -116,358 +94,348 @@ public String getAllProjectsPage(Model model) { return LIST_PROJECTS_PAGE; } - /** - * Get the samples for a given project - * - * @param model A model for the sample list view - * @param principal The user reading the project - * @param projectId The ID of the project - * @return Name of the project samples list view - */ - @RequestMapping(value = { "/projects/{projectId}", "/projects/{projectId}/samples", }) - public String getProjectSamplesPage(final Model model, final Principal principal, @PathVariable long projectId) { - return "projects/index"; - } - - /** - * Default page handler for all UI routes loaded through the project SPA endpoint - * - * @param model Default spring model - * @param principal Currently logged in user - * @param projectId Current project id - * @return page that contains the base code for the project SPA - */ - @RequestMapping(value = { "/projects/{projectId}/ncbi", "/projects/{projectId}/export", - "/projects/{projectId}/export/**" }) - public String getProjectSPA(final Model model, final Principal principal, @PathVariable long projectId) { - - // TODO: get rid of this once project object off page. - Project project = projectService.read(projectId); - model.addAttribute("project", project); - - // Set up the template information - projectControllerUtils.getProjectTemplateDetails(model, principal, project); - - return "projects/project-spa"; - } - - /** - * Request for a specific project details page. - * - * @param projectId The id for the project to show details for. - * @param model Spring model to populate the html page. - * @param principal a reference to the logged in user. - * @return The name of the project details page. - */ - @RequestMapping(value = "/projects/{projectId}/activity") - public String getProjectActivityPage(@PathVariable Long projectId, final Model model, final Principal principal) { - Project project = projectService.read(projectId); - model.addAttribute("project", project); - projectControllerUtils.getProjectTemplateDetails(model, principal, project); - return "projects/project_activity"; - } - - /** - * Get the page to synchronize remote projects - * - * @return Name of the project sync page - */ - @RequestMapping(value = "/projects/synchronize", method = RequestMethod.GET) - public String getSynchronizeProjectPage() { - return SYNC_NEW_PROJECT_PAGE; - } - - /** - * Get the page to share samples between projects - * - * @param projectId Identifier for the current project - * @param model Spring model for template variables - * @param principal Currently logged in user - * @return Path to the template for sharing samples - */ - @RequestMapping("/projects/{projectId}/share") - public String getProjectsSharePage(@PathVariable Long projectId, final Model model, final Principal principal) { - Project project = projectService.read(projectId); - projectControllerUtils.getProjectTemplateDetails(model, principal, project); - return "projects/project_share"; - } - - /** - * Get the page for analyses shared with a given {@link Project} - * - * @param projectId the ID of the {@link Project} - * @param principal the logged in user - * @param model model for view variables - * @return name of the analysis view page - */ - @RequestMapping("/projects/{projectId}/analyses/**") - public String getProjectAnalysisList(@PathVariable Long projectId, Principal principal, Model model) { - Project project = projectService.read(projectId); - projectControllerUtils.getProjectTemplateDetails(model, principal, project); - return "projects/project_analyses"; - } - - /** - * Get the project settings page - * - * @param projectId - identifier for the {@link Project} currently being viewed - * @param principal - Currently logged in used - * @param model Spring UI model - * @return path to the html settings page - */ - @GetMapping("/projects/{projectId}/settings/**") - public String getProjectSettingsPage(@PathVariable Long projectId, Principal principal, Model model) { - Project project = projectService.read(projectId); - model.addAttribute("project", project); - model.addAttribute("page", "details"); - projectControllerUtils.getProjectTemplateDetails(model, principal, project); - return "projects/project_settings"; - } - - /** - * Search for taxonomy terms. This method will return a map of found - * taxonomy terms and their child nodes. - *

- * Note: If the search term was not included in the results, it will be - * added as an option - * - * @param searchTerm The term to find taxa for - * @return A {@code List>} which will contain a taxonomic - * tree of matching terms - */ - @RequestMapping("/projects/ajax/taxonomy/search") - @ResponseBody - public List> searchTaxonomy(@RequestParam String searchTerm) { - Collection> search = taxonomyService.search(searchTerm); - - TreeNode searchTermNode = new TreeNode<>(searchTerm); - // add a property to this node to indicate that it's the search term - searchTermNode.addProperty("searchTerm", true); - - List> elements = new ArrayList<>(); - - // get the search term in first if it's not there yet - if (!search.contains(searchTermNode)) { - elements.add(transformTreeNode(searchTermNode)); - } - - for (TreeNode node : search) { - Map transformTreeNode = transformTreeNode(node); - elements.add(transformTreeNode); - } - return elements; - } - - /** - * Export Projects table as either an excel file or CSV - * - * @param type of file to export (csv or excel) - * @param isAdmin if the currently logged in user is an administrator - * @param response {@link HttpServletResponse} - * @param principal {@link Principal} - * @param locale {@link Locale} - * @throws IOException thrown if cannot open the {@link HttpServletResponse} - * {@link OutputStream} - */ - @RequestMapping("/projects/ajax/export") - public void exportProjectsToFile(@RequestParam(value = "dtf") String type, - @RequestParam(required = false, defaultValue = "false", value = "admin") Boolean isAdmin, - HttpServletResponse response, Principal principal, Locale locale) throws IOException { - // Let's make sure the export type is set properly - if (!(type.equalsIgnoreCase("xlsx") || type.equalsIgnoreCase("csv"))) { - throw new IllegalArgumentException( - "No file type sent for downloading all projects. Expecting parameter 'dtf=' xlsx or csv"); - } - - List projects; - // If viewing the admin projects page give the user all the projects. - if (isAdmin) { - projects = (List) projectService.findAll(); - } - // If on the users projects page, give the user their projects. - else { - User user = userService.getUserByUsername(principal.getName()); - projects = projectService.getProjectsForUser(user) - .stream() - .map(Join::getSubject) - .collect(Collectors.toList()); - } - - List dtProjects = projects.stream() - .map(this::createDataTablesProject) - .collect(Collectors.toList()); - List headers = ImmutableList.of("ProjectsTable_th_id", "ProjectsTable_th_name", - "ProjectsTable_th_organism", "ProjectsTable_th_samples", "ProjectsTable_th_created_date", - "ProjectsTable_th_modified_date") - .stream() - .map(h -> messageSource.getMessage(h, new Object[] {}, locale)) - .collect(Collectors.toList()); - - // Create the filename - Date date = new Date(); - DateFormat fileDateFormat = new SimpleDateFormat(messageSource.getMessage("date.iso-8601", null, locale)); - String filename = "IRIDA_projects_" + fileDateFormat.format(date); - - response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "." + type + "\""); - if (type.equals("xlsx")) { - writeProjectsToExcelFile(headers, dtProjects, locale, response); - } else { - writeProjectsToCsvFile(headers, dtProjects, locale, response); - } - } - - /** - * Handle the page request to upload {@link Sample} metadata - * - * @param model {@link Model} - * @param projectId {@link Long} identifier for the current {@link Project} - * @param principal {@link Principal} currently logged in use - * @return {@link String} the path to the metadata import page - */ - @GetMapping("/projects/{projectId}/sample-metadata/upload/*") - public String getProjectSamplesMetadataUploadPage(final Model model, @PathVariable Long projectId, - Principal principal) { - projectControllerUtils.getProjectTemplateDetails(model, principal, projectService.read(projectId)); - return "projects/project_samples_metadata_upload"; - } - - /** - * Write the projects as a CSV file - * - * @param headers {@link List} for {@link String} headers for the information. - * @param projects {@link List} of {@link DTProject} to export - * @param locale {@link Locale} - * @param response {@link HttpServletResponse} - * @throws IOException Thrown if cannot get the {@link PrintWriter} for the response - */ - private void writeProjectsToCsvFile(List headers, List projects, Locale locale, - HttpServletResponse response) throws IOException { - PrintWriter writer = response.getWriter(); - try (CSVPrinter printer = new CSVPrinter(writer, - CSVFormat.DEFAULT.withRecordSeparator(System.lineSeparator()))) { - printer.printRecord(headers); - - DateFormat dateFormat = new SimpleDateFormat(messageSource.getMessage("locale.date.long", null, locale)); - for (DTProject p : projects) { - List record = new ArrayList<>(); - record.add(String.valueOf(p.getId())); - record.add(p.getName()); - record.add(p.getOrganism()); - record.add(String.valueOf(p.getSamples())); - record.add(dateFormat.format(p.getCreatedDate())); - record.add(dateFormat.format(p.getModifiedDate())); - printer.printRecord(record); - } - printer.flush(); - } - } - - /** - * Write the projects as a Excel file - * - * @param headers {@link List} for {@link String} headers for the information. - * @param projects {@link List} of {@link DTProject} to export - * @param locale {@link Locale} - * @param response {@link HttpServletResponse} - * @throws IOException Thrown if cannot get the {@link OutputStream} for the - * response - */ - private void writeProjectsToExcelFile(List headers, List projects, Locale locale, - HttpServletResponse response) throws IOException { - XSSFWorkbook workbook = new XSSFWorkbook(); - XSSFSheet sheet = workbook.createSheet(); - int rowCount = 0; - - // Create the headers - Row headerRow = sheet.createRow(rowCount++); - for (int cellCount = 0; cellCount < headers.size(); cellCount++) { - Cell cell = headerRow.createCell(cellCount); - cell.setCellValue(headers.get(cellCount)); - } - - // Create the rest of the sheet - DateFormat dateFormat = new SimpleDateFormat(messageSource.getMessage("locale.date.long", null, locale)); - for (DTProject p : projects) { - Row row = sheet.createRow(rowCount++); - int cellCount = 0; - row.createCell(cellCount++) - .setCellValue(String.valueOf(p.getId())); - row.createCell(cellCount++) - .setCellValue(p.getName()); - row.createCell(cellCount++) - .setCellValue(p.getOrganism()); - row.createCell(cellCount++) - .setCellValue(String.valueOf(p.getSamples())); - row.createCell(cellCount++) - .setCellValue(dateFormat.format(p.getCreatedDate())); - row.createCell(cellCount) - .setCellValue(dateFormat.format(p.getModifiedDate())); - } - - // Write the file - try (OutputStream stream = response.getOutputStream()) { - workbook.write(stream); - stream.flush(); - } - - workbook.close(); - } - - /** - * Handle a {@link ProjectWithoutOwnerException} error. Returns a forbidden - * error - * - * @param ex the exception to handle. - * @return response entity with FORBIDDEN error - */ - @ExceptionHandler(ProjectWithoutOwnerException.class) - @ResponseBody - public ResponseEntity roleChangeErrorHandler(Exception ex) { - return new ResponseEntity<>(ex.getMessage(), HttpStatus.FORBIDDEN); - } - - /** - * } - *

- * /** Recursively transform a {@link TreeNode} into a json parsable map - * object - * - * @param node The node to transform - * @return A Map which may contain more children - */ - private Map transformTreeNode(TreeNode node) { - Map current = new HashMap<>(); - - // add the node properties to the map - for (Entry property : node.getProperties() - .entrySet()) { - current.put(property.getKey(), property.getValue()); - } - - current.put("id", node.getValue()); - current.put("text", node.getValue()); - - List children = new ArrayList<>(); - for (TreeNode child : node.getChildren()) { - Map transformTreeNode = transformTreeNode(child); - children.add(transformTreeNode); - } - - if (!children.isEmpty()) { - current.put("children", children); - } - - return current; - } - - /** - * Extract the details of the a {@link Project} into a {@link DTProject} - * which is consumable by the UI - * - * @param project {@link Project} - * @return {@link DTProject} - */ - private DTProject createDataTablesProject(Project project) { - return new DTProject(project, sampleService.getNumberOfSamplesForProject(project)); - } + // TODO: (Josh - 12/2/22) Remove code below as it gets implemented in SPA + +// +// /** +// * Default page handler for all UI routes loaded through the project SPA endpoint +// * +// * @param model Default spring model +// * @param principal Currently logged in user +// * @param projectId Current project id +// * @return page that contains the base code for the project SPA +// */ +// @RequestMapping(value = { "/projects/{projectId}/ncbi", "/projects/{projectId}/export", +// "/projects/{projectId}/export/**" }) +// public String getProjectSPA(final Model model, final Principal principal, @PathVariable long projectId) { +// +// // TODO: get rid of this once project object off page. +// Project project = projectService.read(projectId); +// model.addAttribute("project", project); +// +// // Set up the template information +// projectControllerUtils.getProjectTemplateDetails(model, principal, project); +// +// return "projects/project-spa"; +// } +// +// /** +// * Request for a specific project details page. +// * +// * @param projectId The id for the project to show details for. +// * @param model Spring model to populate the html page. +// * @param principal a reference to the logged in user. +// * @return The name of the project details page. +// */ +// @RequestMapping(value = "/projects/{projectId}/activity") +// public String getProjectActivityPage(@PathVariable Long projectId, final Model model, final Principal principal) { +// Project project = projectService.read(projectId); +// model.addAttribute("project", project); +// projectControllerUtils.getProjectTemplateDetails(model, principal, project); +// return "projects/project_activity"; +// } +// +// /** +// * Get the page to synchronize remote projects +// * +// * @return Name of the project sync page +// */ +// @RequestMapping(value = "/projects/synchronize", method = RequestMethod.GET) +// public String getSynchronizeProjectPage() { +// return SYNC_NEW_PROJECT_PAGE; +// } +// +// /** +// * Get the page to share samples between projects +// * +// * @param projectId Identifier for the current project +// * @param model Spring model for template variables +// * @param principal Currently logged in user +// * @return Path to the template for sharing samples +// */ +// @RequestMapping("/projects/{projectId}/share") +// public String getProjectsSharePage(@PathVariable Long projectId, final Model model, final Principal principal) { +// Project project = projectService.read(projectId); +// projectControllerUtils.getProjectTemplateDetails(model, principal, project); +// return "projects/project_share"; +// } +// +// /** +// * Get the page for analyses shared with a given {@link Project} +// * +// * @param projectId the ID of the {@link Project} +// * @param principal the logged in user +// * @param model model for view variables +// * @return name of the analysis view page +// */ +// @RequestMapping("/projects/{projectId}/analyses/**") +// public String getProjectAnalysisList(@PathVariable Long projectId, Principal principal, Model model) { +// Project project = projectService.read(projectId); +// projectControllerUtils.getProjectTemplateDetails(model, principal, project); +// return "projects/project_analyses"; +// } +// +// /** +// * Get the project settings page +// * +// * @param projectId - identifier for the {@link Project} currently being viewed +// * @param principal - Currently logged in used +// * @param model Spring UI model +// * @return path to the html settings page +// */ +// @GetMapping("/projects/{projectId}/settings/**") +// public String getProjectSettingsPage(@PathVariable Long projectId, Principal principal, Model model) { +// Project project = projectService.read(projectId); +// model.addAttribute("project", project); +// model.addAttribute("page", "details"); +// projectControllerUtils.getProjectTemplateDetails(model, principal, project); +// return "projects/project_settings"; +// } +// +// /** +// * Search for taxonomy terms. This method will return a map of found +// * taxonomy terms and their child nodes. +// *

+// * Note: If the search term was not included in the results, it will be +// * added as an option +// * +// * @param searchTerm The term to find taxa for +// * @return A {@code List>} which will contain a taxonomic +// * tree of matching terms +// */ +// @RequestMapping("/projects/ajax/taxonomy/search") +// @ResponseBody +// public List> searchTaxonomy(@RequestParam String searchTerm) { +// Collection> search = taxonomyService.search(searchTerm); +// +// TreeNode searchTermNode = new TreeNode<>(searchTerm); +// // add a property to this node to indicate that it's the search term +// searchTermNode.addProperty("searchTerm", true); +// +// List> elements = new ArrayList<>(); +// +// // get the search term in first if it's not there yet +// if (!search.contains(searchTermNode)) { +// elements.add(transformTreeNode(searchTermNode)); +// } +// +// for (TreeNode node : search) { +// Map transformTreeNode = transformTreeNode(node); +// elements.add(transformTreeNode); +// } +// return elements; +// } +// +// /** +// * Export Projects table as either an excel file or CSV +// * +// * @param type of file to export (csv or excel) +// * @param isAdmin if the currently logged in user is an administrator +// * @param response {@link HttpServletResponse} +// * @param principal {@link Principal} +// * @param locale {@link Locale} +// * @throws IOException thrown if cannot open the {@link HttpServletResponse} +// * {@link OutputStream} +// */ +// @RequestMapping("/projects/ajax/export") +// public void exportProjectsToFile(@RequestParam(value = "dtf") String type, +// @RequestParam(required = false, defaultValue = "false", value = "admin") Boolean isAdmin, +// HttpServletResponse response, Principal principal, Locale locale) throws IOException { +// // Let's make sure the export type is set properly +// if (!(type.equalsIgnoreCase("xlsx") || type.equalsIgnoreCase("csv"))) { +// throw new IllegalArgumentException( +// "No file type sent for downloading all projects. Expecting parameter 'dtf=' xlsx or csv"); +// } +// +// List projects; +// // If viewing the admin projects page give the user all the projects. +// if (isAdmin) { +// projects = (List) projectService.findAll(); +// } +// // If on the users projects page, give the user their projects. +// else { +// User user = userService.getUserByUsername(principal.getName()); +// projects = projectService.getProjectsForUser(user) +// .stream() +// .map(Join::getSubject) +// .collect(Collectors.toList()); +// } +// +// List dtProjects = projects.stream() +// .map(this::createDataTablesProject) +// .collect(Collectors.toList()); +// List headers = ImmutableList.of("ProjectsTable_th_id", "ProjectsTable_th_name", +// "ProjectsTable_th_organism", "ProjectsTable_th_samples", "ProjectsTable_th_created_date", +// "ProjectsTable_th_modified_date") +// .stream() +// .map(h -> messageSource.getMessage(h, new Object[] {}, locale)) +// .collect(Collectors.toList()); +// +// // Create the filename +// Date date = new Date(); +// DateFormat fileDateFormat = new SimpleDateFormat(messageSource.getMessage("date.iso-8601", null, locale)); +// String filename = "IRIDA_projects_" + fileDateFormat.format(date); +// +// response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "." + type + "\""); +// if (type.equals("xlsx")) { +// writeProjectsToExcelFile(headers, dtProjects, locale, response); +// } else { +// writeProjectsToCsvFile(headers, dtProjects, locale, response); +// } +// } +// +// /** +// * Handle the page request to upload {@link Sample} metadata +// * +// * @param model {@link Model} +// * @param projectId {@link Long} identifier for the current {@link Project} +// * @param principal {@link Principal} currently logged in use +// * @return {@link String} the path to the metadata import page +// */ +// @GetMapping("/projects/{projectId}/sample-metadata/upload/*") +// public String getProjectSamplesMetadataUploadPage(final Model model, @PathVariable Long projectId, +// Principal principal) { +// projectControllerUtils.getProjectTemplateDetails(model, principal, projectService.read(projectId)); +// return "projects/project_samples_metadata_upload"; +// } +// +// /** +// * Write the projects as a CSV file +// * +// * @param headers {@link List} for {@link String} headers for the information. +// * @param projects {@link List} of {@link DTProject} to export +// * @param locale {@link Locale} +// * @param response {@link HttpServletResponse} +// * @throws IOException Thrown if cannot get the {@link PrintWriter} for the response +// */ +// private void writeProjectsToCsvFile(List headers, List projects, Locale locale, +// HttpServletResponse response) throws IOException { +// PrintWriter writer = response.getWriter(); +// try (CSVPrinter printer = new CSVPrinter(writer, +// CSVFormat.DEFAULT.withRecordSeparator(System.lineSeparator()))) { +// printer.printRecord(headers); +// +// DateFormat dateFormat = new SimpleDateFormat(messageSource.getMessage("locale.date.long", null, locale)); +// for (DTProject p : projects) { +// List record = new ArrayList<>(); +// record.add(String.valueOf(p.getId())); +// record.add(p.getName()); +// record.add(p.getOrganism()); +// record.add(String.valueOf(p.getSamples())); +// record.add(dateFormat.format(p.getCreatedDate())); +// record.add(dateFormat.format(p.getModifiedDate())); +// printer.printRecord(record); +// } +// printer.flush(); +// } +// } +// +// /** +// * Write the projects as a Excel file +// * +// * @param headers {@link List} for {@link String} headers for the information. +// * @param projects {@link List} of {@link DTProject} to export +// * @param locale {@link Locale} +// * @param response {@link HttpServletResponse} +// * @throws IOException Thrown if cannot get the {@link OutputStream} for the +// * response +// */ +// private void writeProjectsToExcelFile(List headers, List projects, Locale locale, +// HttpServletResponse response) throws IOException { +// XSSFWorkbook workbook = new XSSFWorkbook(); +// XSSFSheet sheet = workbook.createSheet(); +// int rowCount = 0; +// +// // Create the headers +// Row headerRow = sheet.createRow(rowCount++); +// for (int cellCount = 0; cellCount < headers.size(); cellCount++) { +// Cell cell = headerRow.createCell(cellCount); +// cell.setCellValue(headers.get(cellCount)); +// } +// +// // Create the rest of the sheet +// DateFormat dateFormat = new SimpleDateFormat(messageSource.getMessage("locale.date.long", null, locale)); +// for (DTProject p : projects) { +// Row row = sheet.createRow(rowCount++); +// int cellCount = 0; +// row.createCell(cellCount++) +// .setCellValue(String.valueOf(p.getId())); +// row.createCell(cellCount++) +// .setCellValue(p.getName()); +// row.createCell(cellCount++) +// .setCellValue(p.getOrganism()); +// row.createCell(cellCount++) +// .setCellValue(String.valueOf(p.getSamples())); +// row.createCell(cellCount++) +// .setCellValue(dateFormat.format(p.getCreatedDate())); +// row.createCell(cellCount) +// .setCellValue(dateFormat.format(p.getModifiedDate())); +// } +// +// // Write the file +// try (OutputStream stream = response.getOutputStream()) { +// workbook.write(stream); +// stream.flush(); +// } +// +// workbook.close(); +// } +// +// /** +// * Handle a {@link ProjectWithoutOwnerException} error. Returns a forbidden +// * error +// * +// * @param ex the exception to handle. +// * @return response entity with FORBIDDEN error +// */ +// @ExceptionHandler(ProjectWithoutOwnerException.class) +// @ResponseBody +// public ResponseEntity roleChangeErrorHandler(Exception ex) { +// return new ResponseEntity<>(ex.getMessage(), HttpStatus.FORBIDDEN); +// } +// +// /** +// * } +// *

+// * /** Recursively transform a {@link TreeNode} into a json parsable map +// * object +// * +// * @param node The node to transform +// * @return A Map which may contain more children +// */ +// private Map transformTreeNode(TreeNode node) { +// Map current = new HashMap<>(); +// +// // add the node properties to the map +// for (Entry property : node.getProperties() +// .entrySet()) { +// current.put(property.getKey(), property.getValue()); +// } +// +// current.put("id", node.getValue()); +// current.put("text", node.getValue()); +// +// List children = new ArrayList<>(); +// for (TreeNode child : node.getChildren()) { +// Map transformTreeNode = transformTreeNode(child); +// children.add(transformTreeNode); +// } +// +// if (!children.isEmpty()) { +// current.put("children", children); +// } +// +// return current; +// } +// +// /** +// * Extract the details of the a {@link Project} into a {@link DTProject} +// * which is consumable by the UI +// * +// * @param project {@link Project} +// * @return {@link DTProject} +// */ +// private DTProject createDataTablesProject(Project project) { +// return new DTProject(project, sampleService.getNumberOfSamplesForProject(project)); +// } } diff --git a/src/main/webapp/entries.js b/src/main/webapp/entries.js index ac1126b9500..3acd95f125e 100644 --- a/src/main/webapp/entries.js +++ b/src/main/webapp/entries.js @@ -5,7 +5,7 @@ */ module.exports = { login: "./resources/js/pages/LoginPage.tsx", - "project-spa": "./resources/js/pages/projects/ProjectSPA.tsx", + main: "./resources/js/index.tsx", access_confirmation: "./resources/js/pages/oauth/access_confirmation.js", cart: "./resources/js/pages/cart/index.tsx", announcements: "./resources/js/pages/announcement", diff --git a/src/main/webapp/package.json b/src/main/webapp/package.json index 0629429dadb..28ebdbd1c03 100644 --- a/src/main/webapp/package.json +++ b/src/main/webapp/package.json @@ -65,7 +65,7 @@ "react-markdown": "^8.0.2", "react-mde": "^11.5.0", "react-redux": "^7.2.8", - "react-router-dom": "^6.4.3", + "react-router-dom": "^6.4.4", "react-virtualized-auto-sizer": "^1.0.6", "react-window": "^1.8.6", "reactour": "^1.18.7", diff --git a/src/main/webapp/pages/dashboard.html b/src/main/webapp/pages/dashboard.html new file mode 100644 index 00000000000..cf06bd66d69 --- /dev/null +++ b/src/main/webapp/pages/dashboard.html @@ -0,0 +1,24 @@ + + + + + + + +
+ +
+ + + + + + + diff --git a/src/main/webapp/pages/index.html b/src/main/webapp/pages/index.html index cf06bd66d69..1a50b9cd3bf 100644 --- a/src/main/webapp/pages/index.html +++ b/src/main/webapp/pages/index.html @@ -1,24 +1,22 @@ - - - - - - -
- -
+ + + + + TODO: UPDATE THIS TITLE + + + +
- - - - + + - diff --git a/src/main/webapp/pages/projects/index.html b/src/main/webapp/pages/projects/index.html deleted file mode 100644 index 790d6407287..00000000000 --- a/src/main/webapp/pages/projects/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - TODO: UPDATE THIS TITLE - - - -
- - - - diff --git a/src/main/webapp/pnpm-lock.yaml b/src/main/webapp/pnpm-lock.yaml index 939276c17f7..8aa88afd3aa 100644 --- a/src/main/webapp/pnpm-lock.yaml +++ b/src/main/webapp/pnpm-lock.yaml @@ -99,7 +99,7 @@ specifiers: react-markdown: ^8.0.2 react-mde: ^11.5.0 react-redux: ^7.2.8 - react-router-dom: ^6.4.3 + react-router-dom: ^6.4.4 react-virtualized-auto-sizer: ^1.0.6 react-window: ^1.8.6 reactour: ^1.18.7 @@ -166,7 +166,7 @@ dependencies: react-markdown: 8.0.2_hx2b44akkvgcgvvtmk7ds2qk6q react-mde: 11.5.0_sfoxds7t5ydpegc3knd667wn6m react-redux: 7.2.8_sfoxds7t5ydpegc3knd667wn6m - react-router-dom: 6.4.3_sfoxds7t5ydpegc3knd667wn6m + react-router-dom: 6.4.4_sfoxds7t5ydpegc3knd667wn6m react-virtualized-auto-sizer: 1.0.6_sfoxds7t5ydpegc3knd667wn6m react-window: 1.8.6_sfoxds7t5ydpegc3knd667wn6m reactour: 1.18.7_c7dgqabs4tvn3myjgibvwpzy7e @@ -2764,8 +2764,8 @@ packages: reselect: 4.1.5 dev: false - /@remix-run/router/1.0.3: - resolution: {integrity: sha512-ceuyTSs7PZ/tQqi19YZNBc5X7kj1f8p+4DIyrcIYFY9h+hd1OKm4RqtiWldR9eGEvIiJfsqwM4BsuCtRIuEw6Q==} + /@remix-run/router/1.0.4: + resolution: {integrity: sha512-gTL8H5USTAKOyVA4xczzDJnC3HMssdFa3tRlwBicXynx9XfiXwneHnYQogwSKpdCkjXISrEKSTtX62rLpNEVQg==} engines: {node: '>=14'} dev: false @@ -8656,26 +8656,26 @@ packages: react-is: 17.0.2 dev: false - /react-router-dom/6.4.3_sfoxds7t5ydpegc3knd667wn6m: - resolution: {integrity: sha512-MiaYQU8CwVCaOfJdYvt84KQNjT78VF0TJrA17SIQgNHRvLnXDJO6qsFqq8F/zzB1BWZjCFIrQpu4QxcshitziQ==} + /react-router-dom/6.4.4_sfoxds7t5ydpegc3knd667wn6m: + resolution: {integrity: sha512-0Axverhw5d+4SBhLqLpzPhNkmv7gahUwlUVIOrRLGJ4/uwt30JVajVJXqv2Qr/LCwyvHhQc7YyK1Do8a9Jj7qA==} engines: {node: '>=14'} peerDependencies: react: '>=16.8' react-dom: '>=16.8' dependencies: - '@remix-run/router': 1.0.3 + '@remix-run/router': 1.0.4 react: 17.0.2 react-dom: 17.0.2_react@17.0.2 - react-router: 6.4.3_react@17.0.2 + react-router: 6.4.4_react@17.0.2 dev: false - /react-router/6.4.3_react@17.0.2: - resolution: {integrity: sha512-BT6DoGn6aV1FVP5yfODMOiieakp3z46P1Fk0RNzJMACzE7C339sFuHebfvWtnB4pzBvXXkHP2vscJzWRuUjTtA==} + /react-router/6.4.4_react@17.0.2: + resolution: {integrity: sha512-SA6tSrUCRfuLWeYsTJDuriRqfFIsrSvuH7SqAJHegx9ZgxadE119rU8oOX/rG5FYEthpdEaEljdjDlnBxvfr+Q==} engines: {node: '>=14'} peerDependencies: react: '>=16.8' dependencies: - '@remix-run/router': 1.0.3 + '@remix-run/router': 1.0.4 react: 17.0.2 dev: false diff --git a/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementLink.tsx b/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementLink.tsx index acc91fe334f..10227060b2b 100644 --- a/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementLink.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementLink.tsx @@ -13,6 +13,8 @@ import { useGetAnnouncementCountQuery } from "../../../redux/endpoints/announcem export default function AnnouncementLink() { const { data: count } = useGetAnnouncementCountQuery(undefined, {}); + // TODO: (Josh - 12/2/22) Re-implement modal for high priority messages only + return ( diff --git a/src/main/webapp/resources/js/index.tsx b/src/main/webapp/resources/js/index.tsx new file mode 100644 index 00000000000..11a0bce3188 --- /dev/null +++ b/src/main/webapp/resources/js/index.tsx @@ -0,0 +1,74 @@ +import React, { Suspense } from "react"; +import { render } from "react-dom"; +import { + createBrowserRouter, + createRoutesFromElements, + Outlet, + Route, + RouterProvider, +} from "react-router-dom"; +import { Provider } from "react-redux"; +import { store } from "./redux/store"; +import { getContextPath } from "./utilities/url-utilities"; +import { Layout } from "antd"; +import MainNavigation from "./components/main-navigation"; +import { LoadingOutlined } from "@ant-design/icons"; + +const CONTEXT_PATH = getContextPath(); + +__webpack_public_path__ = `${CONTEXT_PATH}/dist/`; + +const AppLayout = (): JSX.Element => ( + + + + + + }> + + + + +); + +// TODO: (Josh - 12/2/22) Build up from the root here so we can easy add +const router = createBrowserRouter( + createRoutesFromElements( + }> + SAMPLES}> + SAMPLES} /> + {/*}*/} + {/* loader={ncbiLoader}*/} + {/* errorElement={}*/} + {/*/>*/} + {/*}*/} + {/* errorElement={}*/} + {/*>*/} + {/* }*/} + {/* loader={exportsLoader}*/} + {/* errorElement={}*/} + {/* />*/} + {/* }*/} + {/* loader={detailsLoader}*/} + {/* errorElement={}*/} + {/* />*/} + {/**/} + + + ) +); + +render( + + + , + document.querySelector("#root") +); diff --git a/src/main/webapp/resources/js/pages/projects/ProjectSPA.tsx b/src/main/webapp/resources/js/pages/projects/ProjectSPA.tsx deleted file mode 100644 index 829c4835c4d..00000000000 --- a/src/main/webapp/resources/js/pages/projects/ProjectSPA.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { Suspense } from "react"; -import { render } from "react-dom"; -import { - createBrowserRouter, - createRoutesFromElements, - Outlet, - Route, - RouterProvider, -} from "react-router-dom"; -import MainNavigation from "../../components/main-navigation"; -import { loader as exportsLoader } from "../../components/ncbi/export-table"; -import { getContextPath } from "../../utilities/url-utilities"; -import { loader as detailsLoader } from "../../components/ncbi/details"; -import { LoadingOutlined } from "@ant-design/icons"; -import { loader as ncbiLoader } from "./ncbi/create"; -import { Layout } from "antd"; -import { Provider } from "react-redux"; -import { store } from "../../redux/store"; - -const ProjectNCBILayout = React.lazy(() => import("./ncbi")); -const NCBIExportDetails = React.lazy( - () => import("../../components/ncbi/details") -); -const NcbiExportTable = React.lazy( - () => import("../../components/ncbi/export-table") -); -const NcbiCreateExport = React.lazy(() => import("./ncbi/create")); -const DefaultErrorBoundary = React.lazy( - () => import("../../components/DefaultErrorBoundary") -); - -const CONTEXT_PATH = getContextPath(); - -__webpack_public_path__ = `${CONTEXT_PATH}/dist/`; - -const router = createBrowserRouter( - createRoutesFromElements( - } - > - SAMPLES} /> - } - loader={ncbiLoader} - errorElement={} - /> - } - errorElement={} - > - } - loader={exportsLoader} - errorElement={} - /> - } - loader={detailsLoader} - errorElement={} - /> - - - ) -); - -/** - * Default layout for the Project Single Page Application - * @constructor - */ -function ProjectBase(): JSX.Element { - return ( - - - - - - }> - - - - - ); -} - -/** - * Base component for the Project SPA. - * - Routes - * - Any Redux store should be added here - * @constructor - */ -export default function ProjectSPA(): JSX.Element { - return ( - - - - ); -} - -render(, document.querySelector("#root")); From 4d343dd273cde32e4b6a88f667c4160dcd905592 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 10:35:11 -0600 Subject: [PATCH 016/115] chore: Removed TODO statement --- .../corefacility/bioinformatics/irida/ria/web/SPAController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/SPAController.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/SPAController.java index bb2c083809e..6a58e46d8f9 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/SPAController.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/SPAController.java @@ -16,7 +16,6 @@ public class SPAController { */ @GetMapping("/projects/**") public String getSPAEntry() { - // TODO: (Josh - 12/2/22) This url will need to get updated as we move higher up the chain of pages. return "index"; } From 5f3d914e3e7938c8173f71916668e00d6eb75d5d Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 10:36:28 -0600 Subject: [PATCH 017/115] chore: Updated JavaDoc --- .../irida/ria/web/services/UIAnnouncementsService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIAnnouncementsService.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIAnnouncementsService.java index 0b35f5cec35..9644afd46d0 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIAnnouncementsService.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIAnnouncementsService.java @@ -192,6 +192,11 @@ private AnnouncementUserJoin userHasRead(final User user, final Announcement ann return currentAnnouncement.orElse(null); } + /** + * Get the number of unread announcements for a user. + * + * @return the number of unread announcements + */ public int getUnreadAnnouncementsCount() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); User user = userService.getUserByUsername(authentication.getName()); From e18b7ab8301bba74eecfb3d649e56944061144cc Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 10:40:27 -0600 Subject: [PATCH 018/115] chore: Added comment on where the root SPA file found --- src/main/webapp/pages/index.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/pages/index.html b/src/main/webapp/pages/index.html index 1a50b9cd3bf..e36f0cbb3ec 100644 --- a/src/main/webapp/pages/index.html +++ b/src/main/webapp/pages/index.html @@ -15,7 +15,9 @@ th:class="${session.siteTheme + '-theme'}" th:attr="data-context=${#request.contextPath}" > -
+
+ +
From 482d1a728493edb6b76ad0aab5b429463be4f559 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 10:43:12 -0600 Subject: [PATCH 019/115] chore: Added JSDoc --- .../resources/js/components/ant.design/menu-utilities.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/webapp/resources/js/components/ant.design/menu-utilities.tsx b/src/main/webapp/resources/js/components/ant.design/menu-utilities.tsx index b55bec14a62..652f93cebc6 100644 --- a/src/main/webapp/resources/js/components/ant.design/menu-utilities.tsx +++ b/src/main/webapp/resources/js/components/ant.design/menu-utilities.tsx @@ -4,6 +4,11 @@ import { Menu } from "antd"; const { Item, Divider, SubMenu, ItemGroup } = Menu; +/** + * Generate Ant Design menu items. + * TODO: (Josh - 12/2/22) This can be removed once updated to >4.20.0 + * @param item + */ export function renderMenuItem(item: MenuItem): JSX.Element { if (item.type === `divider`) { return ; From 6e71fe6b34c0822ed22735068c498b970560ea92 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 10:57:26 -0600 Subject: [PATCH 020/115] chore: Added JSDoc --- .../js/components/main-navigation/components/CartLink.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx b/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx index 606be0e9c6a..d0e914f354c 100644 --- a/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/components/CartLink.tsx @@ -4,6 +4,11 @@ import { ShoppingCartOutlined } from "@ant-design/icons"; import { Badge } from "antd"; import { ROUTE_CART } from "../../../data/routes"; +/** + * React component to render the cart icon in the main navigation and display + * the number of samples in the cart. + * @constructor + */ export default function CartLink() { const { data: count } = useGetCartCountQuery(undefined, {}); From e123fb440cf25e26a782cf516850731b01aead4d Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 10:59:54 -0600 Subject: [PATCH 021/115] chore: Added JSDoc --- .../js/components/main-navigation/components/CurrentUser.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/webapp/resources/js/components/main-navigation/components/CurrentUser.tsx b/src/main/webapp/resources/js/components/main-navigation/components/CurrentUser.tsx index 53d20e38153..ea3f2ce809c 100644 --- a/src/main/webapp/resources/js/components/main-navigation/components/CurrentUser.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/components/CurrentUser.tsx @@ -3,6 +3,11 @@ import { useGetCurrentUserQuery } from "../../../redux/endpoints/user"; import { Avatar } from "antd"; import { generateColourForItem } from "../../../utilities/colour-utilities"; +/** + * React component to render an avatar with the users initials. + * The colour of the avatar is generated from the user id and username + * @constructor + */ export default function CurrentUser(): JSX.Element { const { data: user, isSuccess } = useGetCurrentUserQuery(undefined, {}); From b820a286de02c0f292aafb884aaa7e6bf5c0dec5 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 11:00:07 -0600 Subject: [PATCH 022/115] chore: Added username to CurrentUser type --- src/main/webapp/resources/js/types/irida/index.d.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/resources/js/types/irida/index.d.ts b/src/main/webapp/resources/js/types/irida/index.d.ts index bca978efec6..915a2a3d3be 100644 --- a/src/main/webapp/resources/js/types/irida/index.d.ts +++ b/src/main/webapp/resources/js/types/irida/index.d.ts @@ -43,13 +43,14 @@ declare namespace IRIDA { } export type CurrentUser = { - isAdmin: boolean; firstName: string; identifier: number; - lastName: string; + isAdmin: boolean; isManager: boolean; - technician: boolean; isTechnician: string; + lastName: string; + technician: boolean; + username: string; }; export type PRIORITY = "LOW" | "MEDIUM" | "HIGH"; From b8d115ab8a772e30a41016fe8776245709b05622 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 11:02:35 -0600 Subject: [PATCH 023/115] chore: Added internationalization for guides --- src/main/resources/i18n/messages.properties | 1 + .../webapp/resources/js/components/main-navigation/index.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index b66b5f032ca..ee2d3b66369 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -225,6 +225,7 @@ nav.main.analysis-admin-all=All Analyses nav.main.sequencingruns=Sequencing Runs nav.main.ncbi-uploads=NCBI Export Uploads nav.main.help=Help +nav.main.guides=Guides nav.main.userguide=User Guide nav.main.adminguide=Admin Guide nav.main.announcement=Announcements diff --git a/src/main/webapp/resources/js/components/main-navigation/index.tsx b/src/main/webapp/resources/js/components/main-navigation/index.tsx index 60ac91ca387..03b2b9d35e8 100644 --- a/src/main/webapp/resources/js/components/main-navigation/index.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/index.tsx @@ -186,7 +186,7 @@ export default function MainNavigation(): JSX.Element { }, { key: `nav-guides`, - label: `Guides`, + label: i18n("nav.main.guides"), children: [ { key: `nav-user-guide`, From 5c6d11164199b50b16bb1067d2f7b1386ae56da4 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 11:05:12 -0600 Subject: [PATCH 024/115] chore: Added JSDoc --- src/main/webapp/resources/js/data/routes.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/webapp/resources/js/data/routes.ts b/src/main/webapp/resources/js/data/routes.ts index 38a93903870..fc8b80d2fe2 100644 --- a/src/main/webapp/resources/js/data/routes.ts +++ b/src/main/webapp/resources/js/data/routes.ts @@ -1,5 +1,10 @@ import { getContextPath } from "../utilities/url-utilities"; +/** + * @fileoverview This file contains all the high-level route, along with the properly set context path, + * used in IRIDA. + */ + export const CONTEXT_PATH = getContextPath(); export const ROUTE_ADMIN = `${CONTEXT_PATH}/admin`; From 52231ad2b06dbb607ff45bf105642795fe4462e7 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 11:06:30 -0600 Subject: [PATCH 025/115] chore: Added JSDoc --- src/main/webapp/resources/js/index.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/webapp/resources/js/index.tsx b/src/main/webapp/resources/js/index.tsx index 11a0bce3188..b15157bfc91 100644 --- a/src/main/webapp/resources/js/index.tsx +++ b/src/main/webapp/resources/js/index.tsx @@ -14,6 +14,11 @@ import { Layout } from "antd"; import MainNavigation from "./components/main-navigation"; import { LoadingOutlined } from "@ant-design/icons"; +/** + * @fileoverview This is the highest level React component in IRIDA. It is responsible + * for the global layout, and routing. + */ + const CONTEXT_PATH = getContextPath(); __webpack_public_path__ = `${CONTEXT_PATH}/dist/`; From cccbd8ddae4df3225f2d6e45cdf3e748906d5f49 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 11:09:40 -0600 Subject: [PATCH 026/115] chore: Added JSDoc --- .../webapp/resources/js/redux/endpoints/announcements.ts | 4 ++++ src/main/webapp/resources/js/redux/endpoints/cart.ts | 4 ++++ src/main/webapp/resources/js/redux/endpoints/tags.ts | 5 +++++ src/main/webapp/resources/js/redux/endpoints/user.ts | 4 ++++ 4 files changed, 17 insertions(+) diff --git a/src/main/webapp/resources/js/redux/endpoints/announcements.ts b/src/main/webapp/resources/js/redux/endpoints/announcements.ts index d632655a458..299f2f25303 100644 --- a/src/main/webapp/resources/js/redux/endpoints/announcements.ts +++ b/src/main/webapp/resources/js/redux/endpoints/announcements.ts @@ -1,6 +1,10 @@ import { api } from "./api"; import { TAG_ANNOUNCEMENT_COUNT } from "./tags"; +/** + * @fileoverview Announcement API for redux-toolkit. + */ + export const announcementsApi = api.injectEndpoints({ endpoints: (build) => ({ getAnnouncementCount: build.query({ diff --git a/src/main/webapp/resources/js/redux/endpoints/cart.ts b/src/main/webapp/resources/js/redux/endpoints/cart.ts index 378e82ae3a3..0344585696f 100644 --- a/src/main/webapp/resources/js/redux/endpoints/cart.ts +++ b/src/main/webapp/resources/js/redux/endpoints/cart.ts @@ -1,6 +1,10 @@ import { api } from "./api"; import { TAG_COUNT } from "./tags"; +/** + * @fileoverview Cart API for redux-toolkit. + */ + export const cartApi = api.injectEndpoints({ endpoints: (build) => ({ getCartCount: build.query({ diff --git a/src/main/webapp/resources/js/redux/endpoints/tags.ts b/src/main/webapp/resources/js/redux/endpoints/tags.ts index 0db90e95dae..467da1e8aa6 100644 --- a/src/main/webapp/resources/js/redux/endpoints/tags.ts +++ b/src/main/webapp/resources/js/redux/endpoints/tags.ts @@ -1,3 +1,8 @@ +/** + * @fileoverview All `Tags` used in redux-toolkit should be listed here and export in the `PROVIDED_TAGS` array. + * This is consumed by the top level API. + */ + export const TAG_ANNOUNCEMENT_COUNT = "tag-announcement-count"; export const TAG_COUNT = "tag-cart-count"; export const TAG_USER = "tag-user"; diff --git a/src/main/webapp/resources/js/redux/endpoints/user.ts b/src/main/webapp/resources/js/redux/endpoints/user.ts index 94bef2d3cb7..efefaa256f0 100644 --- a/src/main/webapp/resources/js/redux/endpoints/user.ts +++ b/src/main/webapp/resources/js/redux/endpoints/user.ts @@ -2,6 +2,10 @@ import { api } from "./api"; import { TAG_USER } from "./tags"; import { CurrentUser } from "../../types/irida"; +/** + * @fileoverview Announcement API for redux-toolkit. + */ + export const userApi = api.injectEndpoints({ endpoints: (build) => ({ getCurrentUser: build.query({ From 70a28ac22ac30374a9539ec98e6cb9f6d6783db0 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 11:10:52 -0600 Subject: [PATCH 027/115] chore: Added JSDoc --- src/main/webapp/resources/js/redux/endpoints/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/resources/js/redux/endpoints/user.ts b/src/main/webapp/resources/js/redux/endpoints/user.ts index efefaa256f0..505ef4a2da1 100644 --- a/src/main/webapp/resources/js/redux/endpoints/user.ts +++ b/src/main/webapp/resources/js/redux/endpoints/user.ts @@ -3,7 +3,7 @@ import { TAG_USER } from "./tags"; import { CurrentUser } from "../../types/irida"; /** - * @fileoverview Announcement API for redux-toolkit. + * @fileoverview Current User API for redux-toolkit. */ export const userApi = api.injectEndpoints({ From d4920646b048ccacd3873c351ed7a32fcb465b98 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 11:12:01 -0600 Subject: [PATCH 028/115] chore: Added JSDoc --- src/main/webapp/resources/js/redux/store.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/webapp/resources/js/redux/store.ts b/src/main/webapp/resources/js/redux/store.ts index 8c79e37b955..01264b49bea 100644 --- a/src/main/webapp/resources/js/redux/store.ts +++ b/src/main/webapp/resources/js/redux/store.ts @@ -4,6 +4,10 @@ import type { TypedUseSelectorHook } from "react-redux"; import { useDispatch, useSelector } from "react-redux"; import { api } from "./endpoints/api"; +/** + * @fileoverview This should be the only redux store in the IRIDA SPA. + */ + export const createStore = ( options?: ConfigureStoreOptions["preloadedState"] | undefined ) => From 8cb8720b4181f3e7303073549bf1505a02dd7876 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 11:13:46 -0600 Subject: [PATCH 029/115] chore: Sorted properties of MenuItem type --- src/main/webapp/resources/js/types/ant-design/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/resources/js/types/ant-design/index.d.ts b/src/main/webapp/resources/js/types/ant-design/index.d.ts index e71b857f66f..00c6f706d27 100644 --- a/src/main/webapp/resources/js/types/ant-design/index.d.ts +++ b/src/main/webapp/resources/js/types/ant-design/index.d.ts @@ -13,11 +13,11 @@ export interface GridProps { } export type MenuItem = { + children?: MenuItem[]; + disabled?: boolean; key: string; label?: string | JSX.Element; type?: "divider" | "group"; - children?: MenuItem[]; - disabled?: boolean; }; export type TagColor = From acd88961c0c59220447e9e4356bd2a8ac39b3043 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 11:15:43 -0600 Subject: [PATCH 030/115] chore: Added JSDoc for where to find the context --- src/main/webapp/resources/js/utilities/url-utilities.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/resources/js/utilities/url-utilities.ts b/src/main/webapp/resources/js/utilities/url-utilities.ts index 7b0db92c2d8..31e23e5a691 100644 --- a/src/main/webapp/resources/js/utilities/url-utilities.ts +++ b/src/main/webapp/resources/js/utilities/url-utilities.ts @@ -48,7 +48,7 @@ export function getProjectIdFromUrl(url = window.location.href) { } /** - * Get the context path for IRIDA. This expects the root element to have a data attribute + * Get the context path for IRIDA. This expects the body element to have a data attribute * of context (`data-context`) with the context path. */ export function getContextPath(): string { @@ -56,7 +56,7 @@ export function getContextPath(): string { if (element && element.dataset && element.dataset.context) { return element.dataset.context; } else { - console.error("No `#root` element with attribute `data-context`"); + console.error("The body element is missing the attribute `data-context`"); return "/"; } } From 5b877f50dc5eeb0ca626ac4a7dc2a00595a8e5dd Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 13:49:07 -0600 Subject: [PATCH 031/115] chore: Updated name of cart count tag --- src/main/webapp/resources/js/redux/endpoints/cart.ts | 4 ++-- src/main/webapp/resources/js/redux/endpoints/tags.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/resources/js/redux/endpoints/cart.ts b/src/main/webapp/resources/js/redux/endpoints/cart.ts index 0344585696f..45384617b3c 100644 --- a/src/main/webapp/resources/js/redux/endpoints/cart.ts +++ b/src/main/webapp/resources/js/redux/endpoints/cart.ts @@ -1,5 +1,5 @@ import { api } from "./api"; -import { TAG_COUNT } from "./tags"; +import { TAG_CART_COUNT } from "./tags"; /** * @fileoverview Cart API for redux-toolkit. @@ -9,7 +9,7 @@ export const cartApi = api.injectEndpoints({ endpoints: (build) => ({ getCartCount: build.query({ query: () => "cart/count", - providesTags: [TAG_COUNT], + providesTags: [TAG_CART_COUNT], }), }), }); diff --git a/src/main/webapp/resources/js/redux/endpoints/tags.ts b/src/main/webapp/resources/js/redux/endpoints/tags.ts index 467da1e8aa6..572802923bf 100644 --- a/src/main/webapp/resources/js/redux/endpoints/tags.ts +++ b/src/main/webapp/resources/js/redux/endpoints/tags.ts @@ -4,7 +4,7 @@ */ export const TAG_ANNOUNCEMENT_COUNT = "tag-announcement-count"; -export const TAG_COUNT = "tag-cart-count"; +export const TAG_CART_COUNT = "tag-cart-count"; export const TAG_USER = "tag-user"; -export const PROVIDED_TAGS = [TAG_ANNOUNCEMENT_COUNT, TAG_COUNT, TAG_USER]; +export const PROVIDED_TAGS = [TAG_ANNOUNCEMENT_COUNT, TAG_CART_COUNT, TAG_USER]; From 14d55df43153bf1503f4e81dd191791d3bc5591b Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 2 Dec 2022 14:00:35 -0600 Subject: [PATCH 032/115] chore: Used useMemo for the user initials --- .../components/main-navigation/components/CurrentUser.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/resources/js/components/main-navigation/components/CurrentUser.tsx b/src/main/webapp/resources/js/components/main-navigation/components/CurrentUser.tsx index ea3f2ce809c..c6fe5f4fd82 100644 --- a/src/main/webapp/resources/js/components/main-navigation/components/CurrentUser.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/components/CurrentUser.tsx @@ -19,6 +19,11 @@ export default function CurrentUser(): JSX.Element { [isSuccess, user?.identifier, user?.username] ); + const initials = useMemo( + () => `${user?.firstName.charAt(0)}${user?.lastName.charAt(0)}`, + [user?.firstName, user?.lastName] + ); + return ( - {`${user?.firstName.charAt(0)}${user?.lastName.charAt(0)}`} + {initials} ); } From 2f1102df3d7e01ace73a7735b5169b9e64181451 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Mon, 5 Dec 2022 10:57:01 -0600 Subject: [PATCH 033/115] refactor: Moved AppLayout to it's own file --- src/main/webapp/resources/js/index.tsx | 16 ++----------- .../resources/js/layouts/app-layout.tsx | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 src/main/webapp/resources/js/layouts/app-layout.tsx diff --git a/src/main/webapp/resources/js/index.tsx b/src/main/webapp/resources/js/index.tsx index b15157bfc91..dad20bc1510 100644 --- a/src/main/webapp/resources/js/index.tsx +++ b/src/main/webapp/resources/js/index.tsx @@ -13,9 +13,10 @@ import { getContextPath } from "./utilities/url-utilities"; import { Layout } from "antd"; import MainNavigation from "./components/main-navigation"; import { LoadingOutlined } from "@ant-design/icons"; +import AppLayout from "./layouts/app-layout"; /** - * @fileoverview This is the highest level React component in IRIDA. It is responsible + * @fileoverview This is the highest level React component in IRIDA. it's responsible * for the global layout, and routing. */ @@ -23,19 +24,6 @@ const CONTEXT_PATH = getContextPath(); __webpack_public_path__ = `${CONTEXT_PATH}/dist/`; -const AppLayout = (): JSX.Element => ( - - - - - - }> - - - - -); - // TODO: (Josh - 12/2/22) Build up from the root here so we can easy add const router = createBrowserRouter( createRoutesFromElements( diff --git a/src/main/webapp/resources/js/layouts/app-layout.tsx b/src/main/webapp/resources/js/layouts/app-layout.tsx new file mode 100644 index 00000000000..d889f1bccca --- /dev/null +++ b/src/main/webapp/resources/js/layouts/app-layout.tsx @@ -0,0 +1,24 @@ +import { Layout } from "antd"; +import MainNavigation from "../components/main-navigation"; +import React, { Suspense } from "react"; +import { LoadingOutlined } from "@ant-design/icons"; +import { Outlet } from "react-router-dom"; + +/** + * Global layout component + * @constructor + */ +const AppLayout = (): JSX.Element => ( + + + + + + }> + + + + +); + +export default AppLayout; From 805ff5eec76bfef0809c270599529b5e561e3f64 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Mon, 5 Dec 2022 12:51:35 -0600 Subject: [PATCH 034/115] refactor: Added in project navigation --- src/main/webapp/resources/js/index.tsx | 36 ++++++++--- .../resources/js/layouts/project-layout.tsx | 62 +++++++++++++++++++ .../resources/js/redux/endpoints/project.ts | 15 +++++ .../resources/js/redux/endpoints/tags.ts | 8 ++- 4 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 src/main/webapp/resources/js/layouts/project-layout.tsx create mode 100644 src/main/webapp/resources/js/redux/endpoints/project.ts diff --git a/src/main/webapp/resources/js/index.tsx b/src/main/webapp/resources/js/index.tsx index dad20bc1510..8ad90b5c14e 100644 --- a/src/main/webapp/resources/js/index.tsx +++ b/src/main/webapp/resources/js/index.tsx @@ -1,19 +1,16 @@ -import React, { Suspense } from "react"; +import React from "react"; import { render } from "react-dom"; import { createBrowserRouter, createRoutesFromElements, - Outlet, Route, RouterProvider, } from "react-router-dom"; import { Provider } from "react-redux"; import { store } from "./redux/store"; import { getContextPath } from "./utilities/url-utilities"; -import { Layout } from "antd"; -import MainNavigation from "./components/main-navigation"; -import { LoadingOutlined } from "@ant-design/icons"; import AppLayout from "./layouts/app-layout"; +import ProjectLayout from "./layouts/project-layout"; /** * @fileoverview This is the highest level React component in IRIDA. it's responsible @@ -28,8 +25,33 @@ __webpack_public_path__ = `${CONTEXT_PATH}/dist/`; const router = createBrowserRouter( createRoutesFromElements( }> - SAMPLES}> - SAMPLES} /> + }> + SAMPLES} id={`project-samples`} /> + LINELIST} + id={`project-linelist`} + /> + ANALYSES} + id={`project-analyses`} + /> + EXPORTS} + id={`project-exports`} + /> + ACTIVITY} + id={`project-activity`} + /> + SETTINGS} + id={`project-settings`} + /> {/*}*/} diff --git a/src/main/webapp/resources/js/layouts/project-layout.tsx b/src/main/webapp/resources/js/layouts/project-layout.tsx new file mode 100644 index 00000000000..9aba794a6be --- /dev/null +++ b/src/main/webapp/resources/js/layouts/project-layout.tsx @@ -0,0 +1,62 @@ +import React, { useMemo } from "react"; +import { Link, Outlet, useMatches, useParams } from "react-router-dom"; +import { Menu, PageHeader } from "antd"; +import { useGetProjectDetailsQuery } from "../redux/endpoints/project"; +import { MenuItem } from "../types/ant-design"; +import { renderMenuItem } from "../components/ant.design/menu-utilities"; + +export default function ProjectLayout(): JSX.Element { + const { projectId } = useParams(); + const matches = useMatches(); + + const path = useMemo(() => { + const match = matches.find((match) => match.id.startsWith(`project-`)); + return match?.id; + }, [matches]); + + const { data: details = {} } = useGetProjectDetailsQuery(projectId); + + const menuItems: MenuItem[] = [ + { + key: `project-samples`, + label: {i18n("project.nav.samples")}, + }, + { + key: `project-linelist`, + label: {i18n("project.nav.linelist")}, + }, + { + key: `project-analyses`, + label: {i18n("project.nav.analysis")}, + }, + { + key: `project-exports`, + label: {i18n("project.nav.exports")}, + }, + { + key: `project-activity`, + label: {i18n("project.nav.activity")}, + }, + { + key: `project-settings`, + label: {i18n("project.nav.settings")}, + }, + ]; + + return ( + +
+ + {menuItems.map(renderMenuItem)} + +
+ +
+
+
+ ); +} diff --git a/src/main/webapp/resources/js/redux/endpoints/project.ts b/src/main/webapp/resources/js/redux/endpoints/project.ts new file mode 100644 index 00000000000..474ddb75ef6 --- /dev/null +++ b/src/main/webapp/resources/js/redux/endpoints/project.ts @@ -0,0 +1,15 @@ +import { api } from "./api"; +import { TAG_PROJECT } from "./tags"; + +export const projectApi = api.injectEndpoints({ + endpoints: (build) => ({ + getProjectDetails: build.query({ + query: (projectId) => `project/details?projectId=${projectId}`, + providesTags: (_result, _err, projectId) => [ + { type: TAG_PROJECT, projectId }, + ], + }), + }), +}); + +export const { useGetProjectDetailsQuery } = projectApi; diff --git a/src/main/webapp/resources/js/redux/endpoints/tags.ts b/src/main/webapp/resources/js/redux/endpoints/tags.ts index 572802923bf..1981781e878 100644 --- a/src/main/webapp/resources/js/redux/endpoints/tags.ts +++ b/src/main/webapp/resources/js/redux/endpoints/tags.ts @@ -5,6 +5,12 @@ export const TAG_ANNOUNCEMENT_COUNT = "tag-announcement-count"; export const TAG_CART_COUNT = "tag-cart-count"; +export const TAG_PROJECT = "tag-project"; export const TAG_USER = "tag-user"; -export const PROVIDED_TAGS = [TAG_ANNOUNCEMENT_COUNT, TAG_CART_COUNT, TAG_USER]; +export const PROVIDED_TAGS = [ + TAG_ANNOUNCEMENT_COUNT, + TAG_CART_COUNT, + TAG_PROJECT, + TAG_USER, +]; From 5caa9c715e737202d4c829d632e4151bf1a9dbfb Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Mon, 5 Dec 2022 14:47:30 -0600 Subject: [PATCH 035/115] refactor: Extracted project navigation into it's own component --- .../components/project/project-navigation.tsx | 51 +++++++++++++++++++ .../resources/js/layouts/project-layout.tsx | 50 ++++-------------- 2 files changed, 60 insertions(+), 41 deletions(-) create mode 100644 src/main/webapp/resources/js/components/project/project-navigation.tsx diff --git a/src/main/webapp/resources/js/components/project/project-navigation.tsx b/src/main/webapp/resources/js/components/project/project-navigation.tsx new file mode 100644 index 00000000000..ad2ae3dc3af --- /dev/null +++ b/src/main/webapp/resources/js/components/project/project-navigation.tsx @@ -0,0 +1,51 @@ +import React, { useMemo } from "react"; +import { MenuItem } from "../../types/ant-design"; +import { Link, useMatches } from "react-router-dom"; +import { Menu } from "antd"; +import { renderMenuItem } from "../ant.design/menu-utilities"; + +export default function ProjectNavigation() { + const matches = useMatches(); + + /** + * Determine which path of the project pages the user is on to + * select the appropriate menu item. + */ + const path = useMemo(() => { + const match = matches.find((match) => match.id.startsWith(`project-`)); + return match?.id || "project-samples"; + }, [matches]); + + const menuItems: MenuItem[] = [ + { + key: `project-samples`, + label: {i18n("project.nav.samples")}, + }, + { + key: `project-linelist`, + label: {i18n("project.nav.linelist")}, + }, + { + key: `project-analyses`, + label: {i18n("project.nav.analysis")}, + }, + { + key: `project-exports`, + label: {i18n("project.nav.exports")}, + }, + { + key: `project-activity`, + label: {i18n("project.nav.activity")}, + }, + { + key: `project-settings`, + label: {i18n("project.nav.settings")}, + }, + ]; + + return ( + + {menuItems.map(renderMenuItem)} + + ); +} diff --git a/src/main/webapp/resources/js/layouts/project-layout.tsx b/src/main/webapp/resources/js/layouts/project-layout.tsx index 9aba794a6be..fc6c977dda9 100644 --- a/src/main/webapp/resources/js/layouts/project-layout.tsx +++ b/src/main/webapp/resources/js/layouts/project-layout.tsx @@ -1,48 +1,18 @@ -import React, { useMemo } from "react"; -import { Link, Outlet, useMatches, useParams } from "react-router-dom"; -import { Menu, PageHeader } from "antd"; +import React from "react"; +import { Outlet, useParams } from "react-router-dom"; +import { PageHeader } from "antd"; import { useGetProjectDetailsQuery } from "../redux/endpoints/project"; -import { MenuItem } from "../types/ant-design"; -import { renderMenuItem } from "../components/ant.design/menu-utilities"; +import ProjectNavigation from "../components/project/project-navigation"; +/** + * React component for the layout of the project specific pages + * @constructor + */ export default function ProjectLayout(): JSX.Element { const { projectId } = useParams(); - const matches = useMatches(); - - const path = useMemo(() => { - const match = matches.find((match) => match.id.startsWith(`project-`)); - return match?.id; - }, [matches]); const { data: details = {} } = useGetProjectDetailsQuery(projectId); - const menuItems: MenuItem[] = [ - { - key: `project-samples`, - label: {i18n("project.nav.samples")}, - }, - { - key: `project-linelist`, - label: {i18n("project.nav.linelist")}, - }, - { - key: `project-analyses`, - label: {i18n("project.nav.analysis")}, - }, - { - key: `project-exports`, - label: {i18n("project.nav.exports")}, - }, - { - key: `project-activity`, - label: {i18n("project.nav.activity")}, - }, - { - key: `project-settings`, - label: {i18n("project.nav.settings")}, - }, - ]; - return (
- - {menuItems.map(renderMenuItem)} - +
From 405ec904c0cf8eba96a9b8b75125ecef184b07ab Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Mon, 5 Dec 2022 14:51:04 -0600 Subject: [PATCH 036/115] chore: Updated JSDoc --- .../resources/js/components/project/project-navigation.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/webapp/resources/js/components/project/project-navigation.tsx b/src/main/webapp/resources/js/components/project/project-navigation.tsx index ad2ae3dc3af..e80bfab9e83 100644 --- a/src/main/webapp/resources/js/components/project/project-navigation.tsx +++ b/src/main/webapp/resources/js/components/project/project-navigation.tsx @@ -4,6 +4,10 @@ import { Link, useMatches } from "react-router-dom"; import { Menu } from "antd"; import { renderMenuItem } from "../ant.design/menu-utilities"; +/** + * React component to render the project navigation + * @constructor + */ export default function ProjectNavigation() { const matches = useMatches(); From 5ef5821d53e871e7ff8f60a5fe73f5465e760f04 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Mon, 5 Dec 2022 15:43:53 -0600 Subject: [PATCH 037/115] chore: Updated JSDoc --- src/main/webapp/resources/js/redux/endpoints/project.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/webapp/resources/js/redux/endpoints/project.ts b/src/main/webapp/resources/js/redux/endpoints/project.ts index 474ddb75ef6..869feefce3e 100644 --- a/src/main/webapp/resources/js/redux/endpoints/project.ts +++ b/src/main/webapp/resources/js/redux/endpoints/project.ts @@ -1,6 +1,10 @@ import { api } from "./api"; import { TAG_PROJECT } from "./tags"; +/** + * @fileoverview Project API for redux-toolkit. + */ + export const projectApi = api.injectEndpoints({ endpoints: (build) => ({ getProjectDetails: build.query({ From cf3b8dff43d2bac27a2aee659f973df717b750a0 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Tue, 6 Dec 2022 05:21:31 -0600 Subject: [PATCH 038/115] chore: Disabled all tests that require the project details pages --- .../irida/ria/integration/CartPageIT.java | 39 +++++++++---------- .../pipelines/AssemblyPipelinePageIT.java | 2 + .../pipelines/BioHanselPipelinePageIT.java | 3 +- .../PipelinesPhylogenomicsPageIT.java | 2 + .../projects/AssociatedProjectsPageIT.java | 3 +- .../integration/projects/CreateProjectIT.java | 2 + .../projects/NcbiExportPageIT.java | 2 + .../projects/NcbiExportsListingPageIT.java | 2 + .../projects/ProjectActivityPageIT.java | 6 ++- .../projects/ProjectAnalysisPageIT.java | 2 + .../projects/ProjectDeletePageIT.java | 2 + .../projects/ProjectDetailsPageIT.java | 2 + .../projects/ProjectLineListPageIT.java | 8 +++- .../projects/ProjectMembersPageIT.java | 6 ++- .../projects/ProjectMetadataIT.java | 11 +++--- .../projects/ProjectReferenceFilePageIT.java | 1 + .../projects/ProjectRemoteSettingsIT.java | 7 +++- .../ProjectSampleMetadataImportPageIT.java | 9 ++--- .../projects/ProjectSamplesPageIT.java | 16 +++++--- .../projects/ProjectSettingsPageIT.java | 6 +-- .../projects/ProjectShareSamplesIT.java | 18 ++++----- .../projects/ProjectSyncPageIT.java | 2 + .../projects/ProjectUserGroupsPageIT.java | 2 + .../integration/projects/ProjectsPageIT.java | 2 + 24 files changed, 99 insertions(+), 56 deletions(-) diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/CartPageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/CartPageIT.java index e718bba1839..7ca99abc4b9 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/CartPageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/CartPageIT.java @@ -1,5 +1,19 @@ package ca.corefacility.bioinformatics.irida.ria.integration; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.JavascriptExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; + import ca.corefacility.bioinformatics.irida.ria.integration.components.FastQCModal; import ca.corefacility.bioinformatics.irida.ria.integration.components.SampleDetailsViewer; import ca.corefacility.bioinformatics.irida.ria.integration.pages.LoginPage; @@ -7,23 +21,13 @@ import ca.corefacility.bioinformatics.irida.ria.integration.pages.projects.ProjectSamplesPage; import ca.corefacility.bioinformatics.irida.ria.integration.pages.projects.TableSummary; import ca.corefacility.bioinformatics.irida.ria.integration.utilities.FileUtilities; + import com.github.springtestdbunit.annotation.DatabaseSetup; import com.google.common.collect.ImmutableList; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.openqa.selenium.JavascriptExecutor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; import static org.junit.jupiter.api.Assertions.*; +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/CartView.xml") public class CartPageIT extends AbstractIridaUIITChromeDriver { private FileUtilities fileUtilities = new FileUtilities(); @@ -32,8 +36,7 @@ public class CartPageIT extends AbstractIridaUIITChromeDriver { "02-2222_S1_L001_R2_001.fastq", "04-4444_S1_L001_R1_001.fastq", "04-4444_S1_L001_R2_001.fastq", "test_file.fasta", "test_file_2.fasta")); - private List singleFileNames = new ArrayList<>( - List.of("test_file.fastq", "test_file_1.fastq", "test_file_2.fastq")); + private List singleFileNames = new ArrayList<>(List.of("test_file.fastq", "test_file_1.fastq", "test_file_2.fastq")); private List pairedFileNames = new ArrayList<>( List.of("01-1111_S1_L001_R1_001.fastq", "02-2222_S1_L001_R2_001.fastq", "04-4444_S1_L001_R1_001.fastq", @@ -76,8 +79,7 @@ public void setFile() throws IOException { } for (String pFileName : pairedFileNames) { - fileUtilities.copyFileToDirectory(sequenceFileBaseDirectory, - "src/test/resources/files/sequence-files/" + pFileName); + fileUtilities.copyFileToDirectory(sequenceFileBaseDirectory, "src/test/resources/files/sequence-files/" + pFileName); } for (String aFileName : assemblyFileNames) { @@ -258,7 +260,6 @@ public void testCartPageAsAdmin() { sampleDetailsViewer.clickRemoveSampleFromCartButton(); sampleDetailsViewer.clickSampleDetailsViewerCloseButton(); - samplesPage.selectSampleByName("sample5fg44"); samplesPage.selectSampleByName("sample5fdgr"); samplesPage.selectSampleByName("sample554sg5"); @@ -279,7 +280,6 @@ public void testCartPageAsAdmin() { final String projectName = "project"; page.viewSampleDetailsFor(sampleName); - assertFalse(sampleDetailsViewer.isAddSampleToCartButtonVisible(), "The add cart to sample button should not be displayed"); assertTrue(sampleDetailsViewer.isRemoveSampleFromCartButtonVisible(), @@ -464,8 +464,7 @@ void addAndRemoveAssociatedProjectSamplesToCart() { ProjectSamplesPage samplesPage = ProjectSamplesPage.goToPage(driver(), 1); samplesPage.toggleAssociatedProject(PROJECT_NAME); TableSummary summary = samplesPage.getTableSummary(); - assertEquals(22, summary.getTotal(), - "Should have more samples visible with another project selected"); + assertEquals(22, summary.getTotal(), "Should have more samples visible with another project selected"); samplesPage.selectSampleByName(ASSOCIATED_SAMPLE_NAME); samplesPage.selectSampleByName(SAMPLE_NAME); samplesPage.addSelectedSamplesToCart(); diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/pipelines/AssemblyPipelinePageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/pipelines/AssemblyPipelinePageIT.java index 0307561fd76..2c5cacb9a38 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/pipelines/AssemblyPipelinePageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/pipelines/AssemblyPipelinePageIT.java @@ -4,6 +4,7 @@ import java.time.Duration; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; @@ -21,6 +22,7 @@ /** * Testing for launching an assembly pipeline. */ +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/pipelines/AssemblyPipelinePageIT.xml") public class AssemblyPipelinePageIT extends AbstractIridaUIITChromeDriver { protected LaunchPipelinePage page; diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/pipelines/BioHanselPipelinePageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/pipelines/BioHanselPipelinePageIT.java index 03e07e4d538..4fbb639324a 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/pipelines/BioHanselPipelinePageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/pipelines/BioHanselPipelinePageIT.java @@ -4,6 +4,7 @@ import java.time.Duration; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; @@ -21,7 +22,7 @@ /** * Testing for launching a biohansel pipeline. */ - +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/pipelines/BioHanselPipelinePageIT.xml") public class BioHanselPipelinePageIT extends AbstractIridaUIITChromeDriver { protected LaunchPipelinePage page; diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/pipelines/PipelinesPhylogenomicsPageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/pipelines/PipelinesPhylogenomicsPageIT.java index f90b54d1205..6b296f0de45 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/pipelines/PipelinesPhylogenomicsPageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/pipelines/PipelinesPhylogenomicsPageIT.java @@ -4,6 +4,7 @@ import java.time.Duration; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; @@ -23,6 +24,7 @@ * Testing for launching a phylogenomics pipeline. *

*/ +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/pipelines/PipelinePhylogenomicsView.xml") public class PipelinesPhylogenomicsPageIT extends AbstractIridaUIITChromeDriver { protected LaunchPipelinePage page; diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/AssociatedProjectsPageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/AssociatedProjectsPageIT.java index aade5b4d645..e07c046127a 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/AssociatedProjectsPageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/AssociatedProjectsPageIT.java @@ -1,5 +1,6 @@ package ca.corefacility.bioinformatics.irida.ria.integration.projects; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver; @@ -10,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/ProjectsPageIT.xml") public class AssociatedProjectsPageIT extends AbstractIridaUIITChromeDriver { Long PROJECT_ID = 1L; @@ -35,6 +37,5 @@ public void hasTheCorrectAssociatedProjects() { driver().navigate().refresh(); assertEquals(2, page.getNumberOfAssociatedProject(), "There should still be 2 projects selected"); - } } diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/CreateProjectIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/CreateProjectIT.java index a274769368d..91a2576410d 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/CreateProjectIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/CreateProjectIT.java @@ -1,6 +1,7 @@ package ca.corefacility.bioinformatics.irida.ria.integration.projects; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver; @@ -21,6 +22,7 @@ * Integration test to ensure that the ProjectsNew Page. *

*/ +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/ProjectsPageIT.xml") public class CreateProjectIT extends AbstractIridaUIITChromeDriver { diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/NcbiExportPageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/NcbiExportPageIT.java index cf6680cb340..61cef85ae02 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/NcbiExportPageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/NcbiExportPageIT.java @@ -1,5 +1,6 @@ package ca.corefacility.bioinformatics.irida.ria.integration.projects; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver; @@ -11,6 +12,7 @@ import static org.junit.jupiter.api.Assertions.*; +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/projects/NcbiExportPageIT.xml") class NcbiExportPageIT extends AbstractIridaUIITChromeDriver { private final NcbiExportPage page = NcbiExportPage.init(driver()); diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/NcbiExportsListingPageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/NcbiExportsListingPageIT.java index b3e4271d061..a827d74c7b2 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/NcbiExportsListingPageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/NcbiExportsListingPageIT.java @@ -1,5 +1,6 @@ package ca.corefacility.bioinformatics.irida.ria.integration.projects; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver; @@ -10,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/projects/NcbiExportsListingPageIT.xml") public class NcbiExportsListingPageIT extends AbstractIridaUIITChromeDriver { @Test diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectActivityPageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectActivityPageIT.java index b2678944ca1..b696e35552c 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectActivityPageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectActivityPageIT.java @@ -1,6 +1,6 @@ package ca.corefacility.bioinformatics.irida.ria.integration.projects; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver; @@ -9,9 +9,13 @@ import com.github.springtestdbunit.annotation.DatabaseSetup; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + /** *

Integration test to ensure that the Project Details Page.

*/ +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/ProjectsPageIT.xml") public class ProjectActivityPageIT extends AbstractIridaUIITChromeDriver { diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectAnalysisPageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectAnalysisPageIT.java index 3b6a4b12de6..72a89d117f2 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectAnalysisPageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectAnalysisPageIT.java @@ -1,5 +1,6 @@ package ca.corefacility.bioinformatics.irida.ria.integration.projects; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver; @@ -11,6 +12,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/ProjectsPageIT.xml") public class ProjectAnalysisPageIT extends AbstractIridaUIITChromeDriver { private ProjectAnalysesPage projectAnalysesPage; diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectDeletePageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectDeletePageIT.java index 476d4665daa..340262606aa 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectDeletePageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectDeletePageIT.java @@ -1,5 +1,6 @@ package ca.corefacility.bioinformatics.irida.ria.integration.projects; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver; @@ -14,6 +15,7 @@ /** * Test class for deleting a project */ +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/ProjectsPageIT.xml") public class ProjectDeletePageIT extends AbstractIridaUIITChromeDriver { diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectDetailsPageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectDetailsPageIT.java index bfc39ab3852..bfcfc3e1411 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectDetailsPageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectDetailsPageIT.java @@ -1,5 +1,6 @@ package ca.corefacility.bioinformatics.irida.ria.integration.projects; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver; @@ -15,6 +16,7 @@ * Integration test to ensure that the Project Details Page. *

*/ +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/ProjectsPageIT.xml") public class ProjectDetailsPageIT extends AbstractIridaUIITChromeDriver { diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectLineListPageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectLineListPageIT.java index 9eee9ef248d..d0668e22340 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectLineListPageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectLineListPageIT.java @@ -1,12 +1,15 @@ package ca.corefacility.bioinformatics.irida.ria.integration.projects; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.Dimension; + import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver; import ca.corefacility.bioinformatics.irida.ria.integration.pages.LoginPage; import ca.corefacility.bioinformatics.irida.ria.integration.pages.projects.ProjectLineListPage; import ca.corefacility.bioinformatics.irida.ria.integration.pages.projects.ShareSamplesPage; + import com.github.springtestdbunit.annotation.DatabaseSetup; -import org.junit.jupiter.api.Test; -import org.openqa.selenium.Dimension; import static org.junit.jupiter.api.Assertions.*; @@ -15,6 +18,7 @@ * Integration test to ensure that the Project Line List Page is working. *

*/ +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectLineListView.xml") public class ProjectLineListPageIT extends AbstractIridaUIITChromeDriver { private final String TEMPLATE_1 = "Testing Template 1"; diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectMembersPageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectMembersPageIT.java index 25444e22c2a..4a262f656dc 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectMembersPageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectMembersPageIT.java @@ -1,5 +1,8 @@ package ca.corefacility.bioinformatics.irida.ria.integration.projects; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + import ca.corefacility.bioinformatics.irida.model.enums.ProjectMetadataRole; import ca.corefacility.bioinformatics.irida.model.enums.ProjectRole; import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver; @@ -9,8 +12,8 @@ import ca.corefacility.bioinformatics.irida.ria.integration.pages.projects.ProjectDetailsPage; import ca.corefacility.bioinformatics.irida.ria.integration.pages.projects.ProjectSyncPage; import ca.corefacility.bioinformatics.irida.ria.integration.utilities.RemoteApiUtilities; + import com.github.springtestdbunit.annotation.DatabaseSetup; -import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -19,6 +22,7 @@ * Integration test to ensure that the Project Collaborators Page. *

*/ +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/ProjectsPageIT.xml") public class ProjectMembersPageIT extends AbstractIridaUIITChromeDriver { diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectMetadataIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectMetadataIT.java index 62cf00b94b2..4620f44606c 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectMetadataIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectMetadataIT.java @@ -1,6 +1,6 @@ package ca.corefacility.bioinformatics.irida.ria.integration.projects; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver; @@ -9,6 +9,9 @@ import com.github.springtestdbunit.annotation.DatabaseSetup; +import static org.junit.jupiter.api.Assertions.*; + +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectMetadataView.xml") public class ProjectMetadataIT extends AbstractIridaUIITChromeDriver { @@ -18,14 +21,12 @@ public void testAdminProjectMetadata() { ProjectMetadataPage page = ProjectMetadataPage.goTo(driver()); // FIELDS - assertEquals(5, page.getNumberOfMetadataFields(), - "Expected to display all metadata fields in the project"); + assertEquals(5, page.getNumberOfMetadataFields(), "Expected to display all metadata fields in the project"); // TEST FIELD RESTRICTIONS assertTrue(page.areFieldRestrictionSettingsVisible(), "Fields restrictions settings should be visible to managers"); - assertEquals("Level 1", page.getFieldRestrictionForRow(0), - "Should currently be set to level 1 by default"); + assertEquals("Level 1", page.getFieldRestrictionForRow(0), "Should currently be set to level 1 by default"); page.updateFieldRestrictionToLevel(0, 3); assertEquals("Level 4", page.getFieldRestrictionForRow(0), "Field should now be restricted to level 4"); diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectReferenceFilePageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectReferenceFilePageIT.java index 407f39cc349..ba41120a816 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectReferenceFilePageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectReferenceFilePageIT.java @@ -13,6 +13,7 @@ /** */ +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectReferenceFileIT.xml") public class ProjectReferenceFilePageIT extends AbstractIridaUIITChromeDriver { private static final Long PROJECT_ID_WITH_REFERENCE_FILES = 1L; diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectRemoteSettingsIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectRemoteSettingsIT.java index f6248e8f57c..c7e45fb82d0 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectRemoteSettingsIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectRemoteSettingsIT.java @@ -1,5 +1,8 @@ package ca.corefacility.bioinformatics.irida.ria.integration.projects; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver; import ca.corefacility.bioinformatics.irida.ria.integration.pages.LoginPage; import ca.corefacility.bioinformatics.irida.ria.integration.pages.admin.AdminClientsPage; @@ -7,8 +10,8 @@ import ca.corefacility.bioinformatics.irida.ria.integration.pages.projects.ProjectSamplesPage; import ca.corefacility.bioinformatics.irida.ria.integration.pages.projects.ProjectSyncPage; import ca.corefacility.bioinformatics.irida.ria.integration.utilities.RemoteApiUtilities; + import com.github.springtestdbunit.annotation.DatabaseSetup; -import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -18,7 +21,7 @@ * is displayed with the correct elements. *

*/ - +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/ProjectsPageIT.xml") public class ProjectRemoteSettingsIT extends AbstractIridaUIITChromeDriver { ProjectSyncPage page; diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectSampleMetadataImportPageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectSampleMetadataImportPageIT.java index 63a5d944c87..12170a2c267 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectSampleMetadataImportPageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectSampleMetadataImportPageIT.java @@ -6,6 +6,7 @@ import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver; @@ -18,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectSampleMetadataView.xml") public class ProjectSampleMetadataImportPageIT extends AbstractIridaUIITChromeDriver { private static final String GOOD_FILE_PATH = "src/test/resources/files/metadata-upload/good.xlsx"; @@ -48,13 +50,10 @@ public void testGoodFileAndHeaders() { */ List values = ImmutableList.of(2.222222, 2.3666, 1.5689, 63.89756, 59.6666) .stream() - .map(num -> BigDecimal.valueOf(num) - .setScale(2, RoundingMode.HALF_UP) - .doubleValue()) + .map(num -> BigDecimal.valueOf(num).setScale(2, RoundingMode.HALF_UP).doubleValue()) .collect(Collectors.toList()); List formattedNumbers = page.getValuesForColumnByName("Numbers"); - formattedNumbers.forEach(num -> assertTrue(values.contains(Double.valueOf(num)), - "Found " + num + " that was not formatted properly")); + formattedNumbers.forEach(num -> assertTrue(values.contains(Double.valueOf(num)), "Found " + num + " that was not formatted properly")); } diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectSamplesPageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectSamplesPageIT.java index 19080dde050..b2e3b22b2d1 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectSamplesPageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectSamplesPageIT.java @@ -1,18 +1,21 @@ package ca.corefacility.bioinformatics.irida.ria.integration.projects; +import java.time.Duration; +import java.util.List; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver; import ca.corefacility.bioinformatics.irida.ria.integration.pages.LoginPage; import ca.corefacility.bioinformatics.irida.ria.integration.pages.projects.ProjectSamplesPage; import ca.corefacility.bioinformatics.irida.ria.integration.pages.projects.ShareSamplesPage; import ca.corefacility.bioinformatics.irida.ria.integration.pages.projects.TableSummary; + import com.github.springtestdbunit.annotation.DatabaseSetup; import com.google.common.collect.ImmutableList; -import org.junit.jupiter.api.Test; -import org.openqa.selenium.support.ui.ExpectedConditions; -import org.openqa.selenium.support.ui.WebDriverWait; - -import java.time.Duration; -import java.util.List; import static org.junit.jupiter.api.Assertions.*; @@ -21,6 +24,7 @@ * Integration test to ensure that the Project Details Page. *

*/ +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectSamplesPage.xml") public class ProjectSamplesPageIT extends AbstractIridaUIITChromeDriver { final String FIRST_SAMPLE_NAME = "sample55422r"; diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectSettingsPageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectSettingsPageIT.java index d442e6208f4..8902264a99f 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectSettingsPageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectSettingsPageIT.java @@ -1,5 +1,6 @@ package ca.corefacility.bioinformatics.irida.ria.integration.projects; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver; @@ -9,13 +10,12 @@ import com.github.springtestdbunit.annotation.DatabaseSetup; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.*; /** * Test for the project settings processing page */ +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/ProjectsPageIT.xml") public class ProjectSettingsPageIT extends AbstractIridaUIITChromeDriver { diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectShareSamplesIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectShareSamplesIT.java index 70e5f5e4155..d15a2206b90 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectShareSamplesIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectShareSamplesIT.java @@ -1,15 +1,19 @@ package ca.corefacility.bioinformatics.irida.ria.integration.projects; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver; import ca.corefacility.bioinformatics.irida.ria.integration.pages.LoginPage; import ca.corefacility.bioinformatics.irida.ria.integration.pages.projects.ProjectSamplesPage; import ca.corefacility.bioinformatics.irida.ria.integration.pages.projects.ShareSamplesPage; + import com.github.springtestdbunit.annotation.DatabaseSetup; -import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectShareSamples.xml") public class ProjectShareSamplesIT extends AbstractIridaUIITChromeDriver { private ShareSamplesPage shareSamplesPage = ShareSamplesPage.initPage(driver()); @@ -48,8 +52,7 @@ public void testShareSamplesAsManager() { samplesPage.selectSampleByName("sample57567"); samplesPage.shareSamples(); - assertFalse(shareSamplesPage.isNextButtonEnabled(), - "Share button should be disabled without a project selected"); + assertFalse(shareSamplesPage.isNextButtonEnabled(), "Share button should be disabled without a project selected"); shareSamplesPage.searchForProject("project2"); shareSamplesPage.gotToNextStep(); assertEquals(3, shareSamplesPage.getNumberOfSamplesDisplayed(), "Should display the 3 samples selected"); @@ -73,8 +76,7 @@ public void testShareSamplesAsManager() { samplesPage.selectSampleByName("sample5fg44"); samplesPage.shareSamples(); - assertFalse(shareSamplesPage.isNextButtonEnabled(), - "Share button should be disabled without a project selected"); + assertFalse(shareSamplesPage.isNextButtonEnabled(), "Share button should be disabled without a project selected"); shareSamplesPage.searchForProject("project4"); assertTrue(shareSamplesPage.isNextButtonEnabled(), "Share button should be enabled after selecting a project"); shareSamplesPage.gotToNextStep(); @@ -91,8 +93,7 @@ public void testShareSamplesAsManager() { samplesPage.selectSampleByName("sample5dt5"); samplesPage.selectSampleByName("sample55422r"); samplesPage.shareSamples(); - assertFalse(shareSamplesPage.isNextButtonEnabled(), - "Share button should be disabled without a project selected"); + assertFalse(shareSamplesPage.isNextButtonEnabled(), "Share button should be disabled without a project selected"); shareSamplesPage.searchForProject("project2"); shareSamplesPage.gotToNextStep(); assertEquals(4, shareSamplesPage.getNumberOfSamplesDisplayed(), "Should be 4 samples displayed"); @@ -104,8 +105,7 @@ public void testShareSamplesAsManager() { samplesPage = ProjectSamplesPage.goToPage(driver(), 2); samplesPage.selectSampleByName("sample5fg44"); samplesPage.shareSamples(); - assertFalse(shareSamplesPage.isNextButtonEnabled(), - "Share button should be disabled without a project selected"); + assertFalse(shareSamplesPage.isNextButtonEnabled(), "Share button should be disabled without a project selected"); shareSamplesPage.searchForProject("project8"); shareSamplesPage.gotToNextStep(); diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectSyncPageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectSyncPageIT.java index df6b737f1a7..2f724d279c4 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectSyncPageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectSyncPageIT.java @@ -1,5 +1,6 @@ package ca.corefacility.bioinformatics.irida.ria.integration.projects; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver; @@ -14,6 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/ProjectsPageIT.xml") public class ProjectSyncPageIT extends AbstractIridaUIITChromeDriver { diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectUserGroupsPageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectUserGroupsPageIT.java index 1fdae061721..751f8af6312 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectUserGroupsPageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectUserGroupsPageIT.java @@ -1,5 +1,6 @@ package ca.corefacility.bioinformatics.irida.ria.integration.projects; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import ca.corefacility.bioinformatics.irida.model.enums.ProjectRole; @@ -15,6 +16,7 @@ import static org.junit.jupiter.api.Assertions.*; +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/ProjectsPageIT.xml") public class ProjectUserGroupsPageIT extends AbstractIridaUIITChromeDriver { diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectsPageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectsPageIT.java index 98e2e8f4a83..7afd8cc50c0 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectsPageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/projects/ProjectsPageIT.java @@ -2,6 +2,7 @@ import java.util.List; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import ca.corefacility.bioinformatics.irida.ria.integration.AbstractIridaUIITChromeDriver; @@ -18,6 +19,7 @@ *

Integration test to ensure that the Projects Page.

* */ +@Disabled @DatabaseSetup("/ca/corefacility/bioinformatics/irida/ria/web/ProjectsPageIT.xml") public class ProjectsPageIT extends AbstractIridaUIITChromeDriver { From 9a1e6eb925e00ea5553b3a5482c604046ed751cc Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Tue, 6 Dec 2022 07:14:58 -0600 Subject: [PATCH 039/115] chore: Added back taxonomy request --- .../ria/web/projects/ProjectsController.java | 126 +++++++++--------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectsController.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectsController.java index 18d3f25ce4e..7dbcac22d85 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectsController.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectsController.java @@ -1,12 +1,9 @@ package ca.corefacility.bioinformatics.irida.ria.web.projects; -import ca.corefacility.bioinformatics.irida.ria.utilities.converters.FileSizeConverter; -import ca.corefacility.bioinformatics.irida.service.ProjectService; -import ca.corefacility.bioinformatics.irida.service.TaxonomyService; -import ca.corefacility.bioinformatics.irida.service.sample.SampleService; -import ca.corefacility.bioinformatics.irida.service.user.UserService; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; +import java.util.Date; +import java.util.List; +import java.util.Map; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Scope; @@ -16,10 +13,17 @@ import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; -import java.util.Date; -import java.util.List; -import java.util.Map; +import ca.corefacility.bioinformatics.irida.ria.utilities.converters.FileSizeConverter; +import ca.corefacility.bioinformatics.irida.service.ProjectService; +import ca.corefacility.bioinformatics.irida.service.TaxonomyService; +import ca.corefacility.bioinformatics.irida.service.sample.SampleService; +import ca.corefacility.bioinformatics.irida.service.user.UserService; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; /** * Controller for project related views @@ -183,58 +187,56 @@ public String getAllProjectsPage(Model model) { // * @param model Spring UI model // * @return path to the html settings page // */ -// @GetMapping("/projects/{projectId}/settings/**") -// public String getProjectSettingsPage(@PathVariable Long projectId, Principal principal, Model model) { -// Project project = projectService.read(projectId); -// model.addAttribute("project", project); -// model.addAttribute("page", "details"); -// projectControllerUtils.getProjectTemplateDetails(model, principal, project); -// return "projects/project_settings"; -// } -// -// /** -// * Search for taxonomy terms. This method will return a map of found -// * taxonomy terms and their child nodes. -// *

-// * Note: If the search term was not included in the results, it will be -// * added as an option -// * -// * @param searchTerm The term to find taxa for -// * @return A {@code List>} which will contain a taxonomic -// * tree of matching terms -// */ -// @RequestMapping("/projects/ajax/taxonomy/search") -// @ResponseBody -// public List> searchTaxonomy(@RequestParam String searchTerm) { -// Collection> search = taxonomyService.search(searchTerm); -// -// TreeNode searchTermNode = new TreeNode<>(searchTerm); -// // add a property to this node to indicate that it's the search term -// searchTermNode.addProperty("searchTerm", true); -// -// List> elements = new ArrayList<>(); -// -// // get the search term in first if it's not there yet -// if (!search.contains(searchTermNode)) { -// elements.add(transformTreeNode(searchTermNode)); -// } -// -// for (TreeNode node : search) { -// Map transformTreeNode = transformTreeNode(node); -// elements.add(transformTreeNode); -// } -// return elements; -// } -// -// /** -// * Export Projects table as either an excel file or CSV -// * -// * @param type of file to export (csv or excel) -// * @param isAdmin if the currently logged in user is an administrator -// * @param response {@link HttpServletResponse} -// * @param principal {@link Principal} -// * @param locale {@link Locale} -// * @throws IOException thrown if cannot open the {@link HttpServletResponse} + // @GetMapping("/projects/{projectId}/settings/**") + // public String getProjectSettingsPage(@PathVariable Long projectId, Principal principal, Model model) { + // Project project = projectService.read(projectId); + // model.addAttribute("project", project); + // model.addAttribute("page", "details"); + // projectControllerUtils.getProjectTemplateDetails(model, principal, project); + // return "projects/project_settings"; + // } + // + + /** + * Search for taxonomy terms. This method will return a map of found taxonomy terms and their child nodes. + *

+ * Note: If the search term was not included in the results, it will be added as an option + * + * @param searchTerm The term to find taxa for + * @return A {@code List>} which will contain a taxonomic tree of matching terms + */ + @RequestMapping("/projects/ajax/taxonomy/search") + @ResponseBody + public List> searchTaxonomy(@RequestParam String searchTerm) { + Collection> search = taxonomyService.search(searchTerm); + + TreeNode searchTermNode = new TreeNode<>(searchTerm); + // add a property to this node to indicate that it's the search term + searchTermNode.addProperty("searchTerm", true); + + List> elements = new ArrayList<>(); + + // get the search term in first if it's not there yet + if (!search.contains(searchTermNode)) { + elements.add(transformTreeNode(searchTermNode)); + } + + for (TreeNode node : search) { + Map transformTreeNode = transformTreeNode(node); + elements.add(transformTreeNode); + } + return elements; + } + // + // /** + // * Export Projects table as either an excel file or CSV + // * + // * @param type of file to export (csv or excel) + // * @param isAdmin if the currently logged in user is an administrator + // * @param response {@link HttpServletResponse} + // * @param principal {@link Principal} + // * @param locale {@link Locale} + // * @throws IOException thrown if cannot open the {@link HttpServletResponse} // * {@link OutputStream} // */ // @RequestMapping("/projects/ajax/export") From 5cb8f44e8dcb69e94c65fc3805f318acc22d731f Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Tue, 6 Dec 2022 07:18:54 -0600 Subject: [PATCH 040/115] chore: Re-add missing functions --- .../ria/web/projects/ProjectsController.java | 170 +++++++++--------- 1 file changed, 83 insertions(+), 87 deletions(-) diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectsController.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectsController.java index 7dbcac22d85..fcaa0caa06c 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectsController.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectsController.java @@ -1,8 +1,6 @@ package ca.corefacility.bioinformatics.irida.ria.web.projects; -import java.util.Date; -import java.util.List; -import java.util.Map; +import java.util.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; @@ -21,6 +19,7 @@ import ca.corefacility.bioinformatics.irida.service.TaxonomyService; import ca.corefacility.bioinformatics.irida.service.sample.SampleService; import ca.corefacility.bioinformatics.irida.service.user.UserService; +import ca.corefacility.bioinformatics.irida.util.TreeNode; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -356,88 +355,85 @@ public List> searchTaxonomy(@RequestParam String searchTerm) // } // // // Create the rest of the sheet -// DateFormat dateFormat = new SimpleDateFormat(messageSource.getMessage("locale.date.long", null, locale)); -// for (DTProject p : projects) { -// Row row = sheet.createRow(rowCount++); -// int cellCount = 0; -// row.createCell(cellCount++) -// .setCellValue(String.valueOf(p.getId())); -// row.createCell(cellCount++) -// .setCellValue(p.getName()); -// row.createCell(cellCount++) -// .setCellValue(p.getOrganism()); -// row.createCell(cellCount++) -// .setCellValue(String.valueOf(p.getSamples())); -// row.createCell(cellCount++) -// .setCellValue(dateFormat.format(p.getCreatedDate())); -// row.createCell(cellCount) -// .setCellValue(dateFormat.format(p.getModifiedDate())); -// } -// -// // Write the file -// try (OutputStream stream = response.getOutputStream()) { -// workbook.write(stream); -// stream.flush(); -// } -// -// workbook.close(); -// } -// -// /** -// * Handle a {@link ProjectWithoutOwnerException} error. Returns a forbidden -// * error -// * -// * @param ex the exception to handle. -// * @return response entity with FORBIDDEN error -// */ -// @ExceptionHandler(ProjectWithoutOwnerException.class) -// @ResponseBody -// public ResponseEntity roleChangeErrorHandler(Exception ex) { -// return new ResponseEntity<>(ex.getMessage(), HttpStatus.FORBIDDEN); -// } -// -// /** -// * } -// *

-// * /** Recursively transform a {@link TreeNode} into a json parsable map -// * object -// * -// * @param node The node to transform -// * @return A Map which may contain more children -// */ -// private Map transformTreeNode(TreeNode node) { -// Map current = new HashMap<>(); -// -// // add the node properties to the map -// for (Entry property : node.getProperties() -// .entrySet()) { -// current.put(property.getKey(), property.getValue()); -// } -// -// current.put("id", node.getValue()); -// current.put("text", node.getValue()); -// -// List children = new ArrayList<>(); -// for (TreeNode child : node.getChildren()) { -// Map transformTreeNode = transformTreeNode(child); -// children.add(transformTreeNode); -// } -// -// if (!children.isEmpty()) { -// current.put("children", children); -// } -// -// return current; -// } -// -// /** -// * Extract the details of the a {@link Project} into a {@link DTProject} -// * which is consumable by the UI -// * -// * @param project {@link Project} -// * @return {@link DTProject} -// */ -// private DTProject createDataTablesProject(Project project) { -// return new DTProject(project, sampleService.getNumberOfSamplesForProject(project)); -// } + // DateFormat dateFormat = new SimpleDateFormat(messageSource.getMessage("locale.date.long", null, locale)); + // for (DTProject p : projects) { + // Row row = sheet.createRow(rowCount++); + // int cellCount = 0; + // row.createCell(cellCount++) + // .setCellValue(String.valueOf(p.getId())); + // row.createCell(cellCount++) + // .setCellValue(p.getName()); + // row.createCell(cellCount++) + // .setCellValue(p.getOrganism()); + // row.createCell(cellCount++) + // .setCellValue(String.valueOf(p.getSamples())); + // row.createCell(cellCount++) + // .setCellValue(dateFormat.format(p.getCreatedDate())); + // row.createCell(cellCount) + // .setCellValue(dateFormat.format(p.getModifiedDate())); + // } + // + // // Write the file + // try (OutputStream stream = response.getOutputStream()) { + // workbook.write(stream); + // stream.flush(); + // } + // + // workbook.close(); + // } + // + // /** + // * Handle a {@link ProjectWithoutOwnerException} error. Returns a forbidden + // * error + // * + // * @param ex the exception to handle. + // * @return response entity with FORBIDDEN error + // */ + // @ExceptionHandler(ProjectWithoutOwnerException.class) + // @ResponseBody + // public ResponseEntity roleChangeErrorHandler(Exception ex) { + // return new ResponseEntity<>(ex.getMessage(), HttpStatus.FORBIDDEN); + // } + // + + /** + * /** Recursively transform a {@link TreeNode} into a json parsable map object + * + * @param node The node to transform + * @return A Map which may contain more children + */ + private Map transformTreeNode(TreeNode node) { + Map current = new HashMap<>(); + + // add the node properties to the map + for (Map.Entry property : node.getProperties().entrySet()) { + current.put(property.getKey(), property.getValue()); + } + + current.put("id", node.getValue()); + current.put("text", node.getValue()); + + List children = new ArrayList<>(); + for (TreeNode child : node.getChildren()) { + Map transformTreeNode = transformTreeNode(child); + children.add(transformTreeNode); + } + + if (!children.isEmpty()) { + current.put("children", children); + } + + return current; + } + // + // /** + // * Extract the details of the a {@link Project} into a {@link DTProject} + // * which is consumable by the UI + // * + // * @param project {@link Project} + // * @return {@link DTProject} + // */ + // private DTProject createDataTablesProject(Project project) { + // return new DTProject(project, sampleService.getNumberOfSamplesForProject(project)); + // } } From 42959126aba0954a097f6ac1b41808e251962cf8 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Tue, 6 Dec 2022 11:27:43 -0600 Subject: [PATCH 041/115] chore: Working on typing Samples --- .../js/components/project/project-samples.tsx | 17 ++ src/main/webapp/resources/js/index.tsx | 6 +- .../resources/js/layouts/project-layout.tsx | 6 +- .../resources/js/redux/endpoints/project.ts | 13 + .../js/redux/reducers/project-sample.ts | 263 ++++++++++++++++++ .../resources/js/types/ant-design/index.d.ts | 2 + 6 files changed, 304 insertions(+), 3 deletions(-) create mode 100644 src/main/webapp/resources/js/components/project/project-samples.tsx create mode 100644 src/main/webapp/resources/js/redux/reducers/project-sample.ts diff --git a/src/main/webapp/resources/js/components/project/project-samples.tsx b/src/main/webapp/resources/js/components/project/project-samples.tsx new file mode 100644 index 00000000000..eff837cc747 --- /dev/null +++ b/src/main/webapp/resources/js/components/project/project-samples.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { useParams } from "react-router-dom"; +import { useGetProjectDetailsQuery } from "../../redux/endpoints/project"; +import { SamplesTable } from "../../pages/projects/samples/components/SamplesTable"; + +export default function ProjectSamples() { + const { projectId } = useParams(); + const { data: details } = useGetProjectDetailsQuery(projectId); + console.log(details); + + return ( +
+ SAMPLES SHIT + {/**/} +
+ ); +} diff --git a/src/main/webapp/resources/js/index.tsx b/src/main/webapp/resources/js/index.tsx index 8ad90b5c14e..98202549560 100644 --- a/src/main/webapp/resources/js/index.tsx +++ b/src/main/webapp/resources/js/index.tsx @@ -12,6 +12,10 @@ import { getContextPath } from "./utilities/url-utilities"; import AppLayout from "./layouts/app-layout"; import ProjectLayout from "./layouts/project-layout"; +const ProjectSamples = React.lazy( + () => import("./components/project/project-samples") +); + /** * @fileoverview This is the highest level React component in IRIDA. it's responsible * for the global layout, and routing. @@ -26,7 +30,7 @@ const router = createBrowserRouter( createRoutesFromElements( }> }> - SAMPLES} id={`project-samples`} /> + } id={`project-samples`} /> LINELIST} diff --git a/src/main/webapp/resources/js/layouts/project-layout.tsx b/src/main/webapp/resources/js/layouts/project-layout.tsx index fc6c977dda9..127f5b2460e 100644 --- a/src/main/webapp/resources/js/layouts/project-layout.tsx +++ b/src/main/webapp/resources/js/layouts/project-layout.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { Suspense } from "react"; import { Outlet, useParams } from "react-router-dom"; import { PageHeader } from "antd"; import { useGetProjectDetailsQuery } from "../redux/endpoints/project"; @@ -22,7 +22,9 @@ export default function ProjectLayout(): JSX.Element {
- + LOADING...
}> + +
diff --git a/src/main/webapp/resources/js/redux/endpoints/project.ts b/src/main/webapp/resources/js/redux/endpoints/project.ts index 869feefce3e..4099725ea0e 100644 --- a/src/main/webapp/resources/js/redux/endpoints/project.ts +++ b/src/main/webapp/resources/js/redux/endpoints/project.ts @@ -13,6 +13,19 @@ export const projectApi = api.injectEndpoints({ { type: TAG_PROJECT, projectId }, ], }), + listSamples: build.query({ + query: ({ + projectId, + body, + }: { + projectId: number | string; + body: {}; + }) => ({ + url: `projects/${projectId}/samples`, + method: "POST", + body, + }), + }), }), }); diff --git a/src/main/webapp/resources/js/redux/reducers/project-sample.ts b/src/main/webapp/resources/js/redux/reducers/project-sample.ts new file mode 100644 index 00000000000..cd3014741f6 --- /dev/null +++ b/src/main/webapp/resources/js/redux/reducers/project-sample.ts @@ -0,0 +1,263 @@ +import { + createAction, + createAsyncThunk, + createReducer, +} from "@reduxjs/toolkit"; +import type { Action } from "@reduxjs/toolkit"; +import { INITIAL_TABLE_STATE } from "../samples/services/constants"; +import { getMinimalSampleDetailsForFilteredProject } from "../../../apis/projects/samples"; +import { putSampleInCart } from "../../../apis/cart/cart"; +import { downloadPost } from "../../../utilities/file-utilities"; +import { formatFilterBySampleNames } from "../../../utilities/table-utilities"; +import isEqual from "lodash/isEqual"; +import { getProjectIdFromUrl, setBaseUrl } from "../../utilities/url-utilities"; +import { TableFilter } from "../../types/ant-design"; +import { Sample } from "../../types/irida"; + +const reloadTable = createAction("samples/table/reload"); +const addSelectedSample = createAction("samples/table/selected/add"); +const removeSelectedSample = createAction("samples/table/selected/remove"); +const clearSelectedSamples = createAction("samples/table/selected/clear"); +const clearFilterByFile = createAction("samples/table/clearFilterByFile"); + +type UpdateTableProps = { + filters: TableFilter; +}; + +type SelectedSample = Pick; + +/** + * Updates the state of the table filters and search, which triggers + * the re-render of the samples table. + * @type {AsyncThunk} + */ +const updateTable = createAsyncThunk< + UpdateTableProps, + string, + { + state: { + samples: { + selected: SelectedSample[]; + options: { filters: TableFilter }; + selectedCount: number; + }; + }; + } +>("samples/table/update", async (values, { getState }) => { + const { options, selected, selectedCount } = getState().samples; + if ( + isEqual(values?.search, options.search) && + isEqual(values?.filters, options.filters) + ) { + // Just a page change, don't update selected + return { options: values, selected, selectedCount }; + } + // Filters applied therefore need to clear any selections + return { options: values, selected: {}, selectedCount: 0 }; +}); + +/** + * Called when selecting all samples from the Samples Table. + * + * This will trigger a "long load" since there might be a little of samples in + * the table that data needs to be gathered for from the server. + */ +const selectAllSamples = createAsyncThunk( + "/samples/table/selected/all", + async (_, { getState }) => { + const { samples } = getState(); + return await getMinimalSampleDetailsForFilteredProject( + samples.options + ).then((data) => { + const selected = data.reduce( + (accumulator, value) => ({ ...accumulator, [value.key]: value }), + {} + ); + return { selected, selectedCount: data.length }; + }); + } +); + +/** + * Called when adding samples to the cart + */ +const addToCart = createAsyncThunk( + "/samples/table/selected/cart", + async (_, { getState }) => { + const { samples } = getState(); + // Sort by project id + const samplesList = Object.values(samples.selected); + const projects = samplesList.reduce((prev, current) => { + if (!prev[current.projectId]) prev[current.projectId] = []; + prev[current.projectId].push(current); + return prev; + }, {}); + + const promises = []; + for (const projectId in projects) { + promises.push(putSampleInCart(projectId, projects[projectId])); + } + + return Promise.all(promises).then((responses) => responses.pop()); + } +); + +/** + * Called when downloading samples (sequence files) from the server. + */ +const downloadSamples = createAsyncThunk( + "/samples/table/export/download", + async (_, { getState }) => { + const { samples } = getState(); + const sampleIds = Object.values(samples.selected).map((s) => s.id); + return await downloadPost( + setBaseUrl(`/ajax/projects/${samples.projectId}/samples/download`), + { sampleIds } + ); + } +); + +/** + * Called when exporting the current state of the samples' table to either + * a CSV of Excel file. + */ +const exportSamplesToFile = createAsyncThunk( + "/samples/table/export", + async (type, { getState }) => { + const { samples } = getState(); + const options = { ...samples.options }; + if (samples.selectedCount > 0) { + const sampleNamesFilter = formatFilterBySampleNames( + Object.values(samples.selected) + ); + options.search = [...options.search, sampleNamesFilter]; + } + + return await downloadPost( + setBaseUrl( + `/ajax/projects/${samples.projectId}/samples/export?type=${type}` + ), + options + ); + } +); + +const filterByFile = createAction( + `samples/table/filterByFile`, + ({ samples, filename }) => { + return { + payload: { + filename, + fileFilter: formatFilterBySampleNames(samples), + }, + }; + } +); + +/** + * Since the initial table props may need to be reset at some point, we store + * them in a string so they cannot be mutated. When the table needs to be reset + * to it's default state, just re-parse by calling this. + * @returns {object} - default table state + */ +const getInitialTableOptions = () => JSON.parse(INITIAL_TABLE_STATE); + +/** + * Called to format a sample when a sample is selected. + * Needs to be converted to this format so that it can be used by the share + * samples page and the cart. + * @param projectSample - Sample details object returned as part of the table data + * @returns {{sampleName: (Document.mergeForm.sampleName|Document.sampleName|string), owner: *, id: string, projectId: *, key: *}} + */ +const formatSelectedSample = (projectSample) => ({ + key: projectSample.key, + id: projectSample.sample.id, + projectId: projectSample.project.id, + sampleName: projectSample.sample.sampleName, + owner: projectSample.owner, +}); + +const initialState = { + projectId: getProjectIdFromUrl(), + options: getInitialTableOptions(), + selected: {}, + selectedCount: 0, + loadingLong: false, +}; + +export default createReducer(initialState, (builder) => { + builder + .addCase(updateTable.fulfilled, (state, action) => { + state.options = action.payload.options; + state.selected = action.payload.selected; + state.selectedCount = action.payload.selectedCount; + }) + .addCase(reloadTable, (state) => { + const newOptions = getInitialTableOptions(); + newOptions.pagination.pageSize = state.options.pagination.pageSize; + newOptions.reload = Math.floor(Math.random() * 90000) + 10000; // Unique 5 digit number to trigger reload + state.options = newOptions; + state.selected = {}; + state.selectedCount = 0; + }) + .addCase(addSelectedSample, (state, action) => { + state.selected[action.payload.key] = formatSelectedSample(action.payload); + state.selectedCount++; + }) + .addCase(removeSelectedSample, (state, action) => { + delete state.selected[action.payload]; + state.selectedCount--; + }) + .addCase(clearSelectedSamples, (state) => { + state.selected = {}; + state.selectedCount = 0; + }) + .addCase(selectAllSamples.pending, (state) => { + state.loadingLong = true; + }) + .addCase(selectAllSamples.fulfilled, (state, action) => { + state.selected = action.payload.selected; + state.selectedCount = action.payload.selectedCount; + state.loadingLong = false; + }) + .addCase(addToCart.fulfilled, (state) => { + state.selected = {}; + state.selectedCount = 0; + }) + .addCase(downloadSamples.fulfilled, (state) => { + state.selected = {}; + state.selectedCount = 0; + }) + .addCase(exportSamplesToFile.pending, (state) => { + state.loadingLong = true; + }) + .addCase(exportSamplesToFile.fulfilled, (state) => { + state.loadingLong = false; + }) + .addCase(filterByFile, (state, action) => { + state.options.search.push(action.payload.fileFilter); + state.filterByFile = action.payload; + }) + .addCase(clearFilterByFile, (state) => { + state.filterByFile = null; + // Need to specifically remove the filter by file from the search filters. + state.options.search = state.options.search.filter( + (filter) => !filter._file + ); + state.options.reload = Math.floor(Math.random() * 90000) + 10000; + }); +}); + +export { + updateTable, + reloadTable, + addSelectedSample, + removeSelectedSample, + clearSelectedSamples, + filterByFile, + clearFilterByFile, + selectAllSamples, + addToCart, + downloadSamples, + exportSamplesToFile, +}; diff --git a/src/main/webapp/resources/js/types/ant-design/index.d.ts b/src/main/webapp/resources/js/types/ant-design/index.d.ts index 00c6f706d27..51f7e6a0ebb 100644 --- a/src/main/webapp/resources/js/types/ant-design/index.d.ts +++ b/src/main/webapp/resources/js/types/ant-design/index.d.ts @@ -20,6 +20,8 @@ export type MenuItem = { type?: "divider" | "group"; }; +export type TableFilter = {}; + export type TagColor = | "magenta" | "red" From e4674ea0c1f9c9a454acd303f921ce54850b8e05 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Tue, 6 Dec 2022 14:50:57 -0600 Subject: [PATCH 042/115] chore: Updated dashboard test since file name changed --- .../irida/ria/unit/web/DashboardControllerTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/DashboardControllerTest.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/DashboardControllerTest.java index e77906a4604..085c599ee71 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/DashboardControllerTest.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/DashboardControllerTest.java @@ -1,11 +1,11 @@ package ca.corefacility.bioinformatics.irida.ria.unit.web; -import static org.junit.jupiter.api.Assertions.assertEquals; - import org.junit.jupiter.api.Test; import ca.corefacility.bioinformatics.irida.ria.web.DashboardController; +import static org.junit.jupiter.api.Assertions.assertEquals; + /** * Unit Test for {@link DashboardController} * @@ -16,6 +16,6 @@ public class DashboardControllerTest { @Test public void indexPageNormal() { - assertEquals("index", controller.showIndex()); + assertEquals("dashboard", controller.showIndex()); } } From 8cb499b4f612e830f47034d56efe713318e7b0cd Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Tue, 6 Dec 2022 16:17:09 -0600 Subject: [PATCH 043/115] chore: More sample types --- .../projects/samples/services/constants.js | 13 --------- .../js/redux/reducers/project-sample.ts | 27 +++++++++++++------ .../resources/js/types/ant-design/index.d.ts | 19 ++++++++++++- 3 files changed, 37 insertions(+), 22 deletions(-) delete mode 100644 src/main/webapp/resources/js/pages/projects/samples/services/constants.js diff --git a/src/main/webapp/resources/js/pages/projects/samples/services/constants.js b/src/main/webapp/resources/js/pages/projects/samples/services/constants.js deleted file mode 100644 index abafb8fa85f..00000000000 --- a/src/main/webapp/resources/js/pages/projects/samples/services/constants.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Initial state of the sample table - * @type {string} - */ -export const INITIAL_TABLE_STATE = JSON.stringify({ - filters: { associated: null }, - pagination: { - current: 1, - pageSize: 10, - }, - order: [{ property: "sample.modifiedDate", direction: "desc" }], - search: [], -}); diff --git a/src/main/webapp/resources/js/redux/reducers/project-sample.ts b/src/main/webapp/resources/js/redux/reducers/project-sample.ts index cd3014741f6..dd62d5d8163 100644 --- a/src/main/webapp/resources/js/redux/reducers/project-sample.ts +++ b/src/main/webapp/resources/js/redux/reducers/project-sample.ts @@ -3,17 +3,28 @@ import { createAsyncThunk, createReducer, } from "@reduxjs/toolkit"; -import type { Action } from "@reduxjs/toolkit"; -import { INITIAL_TABLE_STATE } from "../samples/services/constants"; import { getMinimalSampleDetailsForFilteredProject } from "../../../apis/projects/samples"; import { putSampleInCart } from "../../../apis/cart/cart"; import { downloadPost } from "../../../utilities/file-utilities"; import { formatFilterBySampleNames } from "../../../utilities/table-utilities"; import isEqual from "lodash/isEqual"; -import { getProjectIdFromUrl, setBaseUrl } from "../../utilities/url-utilities"; -import { TableFilter } from "../../types/ant-design"; +import { setBaseUrl } from "../../utilities/url-utilities"; +import { TableFilters, TableOptions } from "../../types/ant-design"; import { Sample } from "../../types/irida"; +/** + * Initial state of the sample table + */ +export const INITIAL_TABLE_STATE = JSON.stringify({ + filters: { associated: null }, + pagination: { + current: 1, + pageSize: 10, + }, + order: [{ property: "sample.modifiedDate", direction: "desc" }], + search: [], +}); + const reloadTable = createAction("samples/table/reload"); const addSelectedSample = createAction("samples/table/selected/add"); const removeSelectedSample = createAction("samples/table/selected/remove"); @@ -21,7 +32,7 @@ const clearSelectedSamples = createAction("samples/table/selected/clear"); const clearFilterByFile = createAction("samples/table/clearFilterByFile"); type UpdateTableProps = { - filters: TableFilter; + filters: TableFilters; }; type SelectedSample = Pick; @@ -38,7 +49,7 @@ const updateTable = createAsyncThunk< state: { samples: { selected: SelectedSample[]; - options: { filters: TableFilter }; + options: { filters: TableFilters }; selectedCount: number; }; }; @@ -160,7 +171,8 @@ const filterByFile = createAction( * to it's default state, just re-parse by calling this. * @returns {object} - default table state */ -const getInitialTableOptions = () => JSON.parse(INITIAL_TABLE_STATE); +const getInitialTableOptions = (): TableOptions => + JSON.parse(INITIAL_TABLE_STATE); /** * Called to format a sample when a sample is selected. @@ -178,7 +190,6 @@ const formatSelectedSample = (projectSample) => ({ }); const initialState = { - projectId: getProjectIdFromUrl(), options: getInitialTableOptions(), selected: {}, selectedCount: 0, diff --git a/src/main/webapp/resources/js/types/ant-design/index.d.ts b/src/main/webapp/resources/js/types/ant-design/index.d.ts index 51f7e6a0ebb..51506f363f7 100644 --- a/src/main/webapp/resources/js/types/ant-design/index.d.ts +++ b/src/main/webapp/resources/js/types/ant-design/index.d.ts @@ -20,7 +20,24 @@ export type MenuItem = { type?: "divider" | "group"; }; -export type TableFilter = {}; +export type TableFilters = { + [name: string]: string; // TODO: (Josh - 12/6/22) UPDATE THIS +}; + +export type TableOptions = { + filters: TableFilters; + pagination: TablePagination; + order: { + property: string; + direction: "asc" | "desc"; + }; + search: string[]; +}; + +export type TablePagination = { + current: number; + pageSize: number; +}; export type TagColor = | "magenta" From dee742fe21061de27fd2860f97b9a1462565a6d1 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Tue, 6 Dec 2022 21:16:08 -0600 Subject: [PATCH 044/115] chore: Fixed link class to admin panel --- .../resources/js/components/main-navigation/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/resources/js/components/main-navigation/index.tsx b/src/main/webapp/resources/js/components/main-navigation/index.tsx index 03b2b9d35e8..69fe9c7c564 100644 --- a/src/main/webapp/resources/js/components/main-navigation/index.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/index.tsx @@ -154,7 +154,11 @@ export default function MainNavigation(): JSX.Element { ? [ { key: `nav-admin`, - label: {i18n("MainNavigation.admin")}, + label: ( + + {i18n("MainNavigation.admin")} + + ), }, ] : []), From ef121c0f9795ef47b125c8ceb2f4c4205d91f124 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Wed, 7 Dec 2022 05:26:28 -0600 Subject: [PATCH 045/115] chore: Fixed search after variable name change --- .../resources/js/pages/search/SearchLayout.tsx | 4 ++-- .../irida/ria/integration/SearchResultPageIT.java | 4 +++- .../irida/ria/integration/pages/AbstractPage.java | 15 +++++++++------ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/main/webapp/resources/js/pages/search/SearchLayout.tsx b/src/main/webapp/resources/js/pages/search/SearchLayout.tsx index 4dac3c952f4..88a831f50fd 100644 --- a/src/main/webapp/resources/js/pages/search/SearchLayout.tsx +++ b/src/main/webapp/resources/js/pages/search/SearchLayout.tsx @@ -82,7 +82,7 @@ export default function SearchLayout() { setSearchParams(value); }, 500); const [type, setType] = useState("projects"); - const [global, setGlobal] = useState(user.admin); + const [global, setGlobal] = useState(user.isAdmin); const [projects, setProjects] = useState<{ content: SearchProject[]; @@ -253,7 +253,7 @@ export default function SearchLayout() { {i18n("SearchLayout.tooltip")} Date: Wed, 7 Dec 2022 05:34:46 -0600 Subject: [PATCH 046/115] chore: More sample types --- src/main/webapp/resources/js/redux/reducers/project-sample.ts | 4 ++-- src/main/webapp/resources/js/types/ant-design/index.d.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/resources/js/redux/reducers/project-sample.ts b/src/main/webapp/resources/js/redux/reducers/project-sample.ts index dd62d5d8163..8afb911264a 100644 --- a/src/main/webapp/resources/js/redux/reducers/project-sample.ts +++ b/src/main/webapp/resources/js/redux/reducers/project-sample.ts @@ -25,7 +25,7 @@ export const INITIAL_TABLE_STATE = JSON.stringify({ search: [], }); -const reloadTable = createAction("samples/table/reload"); +const reloadTable = createAction("samples/table/reload"); const addSelectedSample = createAction("samples/table/selected/add"); const removeSelectedSample = createAction("samples/table/selected/remove"); const clearSelectedSamples = createAction("samples/table/selected/clear"); @@ -43,8 +43,8 @@ type SelectedSample = Pick; * @type {AsyncThunk} */ const updateTable = createAsyncThunk< - UpdateTableProps, string, + UpdateTableProps, { state: { samples: { diff --git a/src/main/webapp/resources/js/types/ant-design/index.d.ts b/src/main/webapp/resources/js/types/ant-design/index.d.ts index 51506f363f7..7c946093111 100644 --- a/src/main/webapp/resources/js/types/ant-design/index.d.ts +++ b/src/main/webapp/resources/js/types/ant-design/index.d.ts @@ -32,6 +32,7 @@ export type TableOptions = { direction: "asc" | "desc"; }; search: string[]; + reload?: number; }; export type TablePagination = { From 703cda6be0ca35c7a64e1695b4ce84b7fdc94194 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Wed, 7 Dec 2022 07:21:29 -0600 Subject: [PATCH 047/115] refactor: Removed thunk from update table --- .../js/pages/projects/redux/samplesSlice.js | 42 +++++++------------ 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.js b/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.js index a419f2858b2..7c63c2eb859 100644 --- a/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.js +++ b/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.js @@ -14,35 +14,13 @@ import { downloadPost } from "../../../utilities/file-utilities"; import { formatFilterBySampleNames } from "../../../utilities/table-utilities"; import isEqual from "lodash/isEqual"; +const updateTable = createAction("samples/table/update"); const reloadTable = createAction("samples/table/reload"); const addSelectedSample = createAction("samples/table/selected/add"); const removeSelectedSample = createAction("samples/table/selected/remove"); const clearSelectedSamples = createAction("samples/table/selected/clear"); const clearFilterByFile = createAction("samples/table/clearFilterByFile"); -/** - * Updates the state of the table filters and search, which triggers - * the re-render of the samples table. - * @type {AsyncThunk} - */ -const updateTable = createAsyncThunk( - "samples/table/update", - async (values, { getState }) => { - const { - samples: { options, selected, selectedCount }, - } = getState(); - if ( - isEqual(values?.search, options.search) && - isEqual(values?.filters, options.filters) - ) { - // Just a page change, don't update selected - return { options: values, selected, selectedCount }; - } - // Filters applied therefore need to clear any selections - return { options: values, selected: {}, selectedCount: 0 }; - } -); - /** * Called when selecting all samples from the Samples Table. * @@ -174,10 +152,20 @@ const initialState = { export default createReducer(initialState, (builder) => { builder - .addCase(updateTable.fulfilled, (state, action) => { - state.options = action.payload.options; - state.selected = action.payload.selected; - state.selectedCount = action.payload.selectedCount; + .addCase(updateTable, (state, { payload }) => { + const { options, selected, selectedCount } = state; + + if ( + isEqual(payload.search, options.search) && + isEqual(payload.filters && options.filters) + ) { + // Just a page change, don't update selected + state.options = payload; + } else { + state.options = payload; + state.selected = {}; + state.selectedCount = 0; + } }) .addCase(reloadTable, (state) => { const newOptions = getInitialTableOptions(); From 4861115545147096924157ad193ec8de9a3797a9 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Wed, 7 Dec 2022 07:35:54 -0600 Subject: [PATCH 048/115] refactor: Changed to typescript and updated updateTable with types --- .../redux/{samplesSlice.js => samplesSlice.ts} | 16 ++++++++++++---- .../resources/js/types/ant-design/index.d.ts | 12 ++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) rename src/main/webapp/resources/js/pages/projects/redux/{samplesSlice.js => samplesSlice.ts} (93%) diff --git a/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.js b/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.ts similarity index 93% rename from src/main/webapp/resources/js/pages/projects/redux/samplesSlice.js rename to src/main/webapp/resources/js/pages/projects/redux/samplesSlice.ts index 7c63c2eb859..8b15d2064c5 100644 --- a/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.js +++ b/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.ts @@ -13,8 +13,16 @@ import { putSampleInCart } from "../../../apis/cart/cart"; import { downloadPost } from "../../../utilities/file-utilities"; import { formatFilterBySampleNames } from "../../../utilities/table-utilities"; import isEqual from "lodash/isEqual"; +import { TableOptions } from "../../../types/ant-design"; -const updateTable = createAction("samples/table/update"); +export type SamplesTableState = { + projectId: string | number; // TODO: (Josh - 12/7/22) This will be removed in subsequent PR + options: TableOptions; + selectedCount: number; + selected: { [id: number]: string }; +}; + +const updateTable = createAction("samples/table/update"); const reloadTable = createAction("samples/table/reload"); const addSelectedSample = createAction("samples/table/selected/add"); const removeSelectedSample = createAction("samples/table/selected/remove"); @@ -142,7 +150,7 @@ const formatSelectedSample = (projectSample) => ({ owner: projectSample.owner, }); -const initialState = { +const initialState: SamplesTableState = { projectId: getProjectIdFromUrl(), options: getInitialTableOptions(), selected: {}, @@ -153,11 +161,11 @@ const initialState = { export default createReducer(initialState, (builder) => { builder .addCase(updateTable, (state, { payload }) => { - const { options, selected, selectedCount } = state; + const { options } = state; if ( isEqual(payload.search, options.search) && - isEqual(payload.filters && options.filters) + isEqual(payload.filters, options.filters) ) { // Just a page change, don't update selected state.options = payload; diff --git a/src/main/webapp/resources/js/types/ant-design/index.d.ts b/src/main/webapp/resources/js/types/ant-design/index.d.ts index d31bc08e957..b613089fef9 100644 --- a/src/main/webapp/resources/js/types/ant-design/index.d.ts +++ b/src/main/webapp/resources/js/types/ant-design/index.d.ts @@ -12,6 +12,18 @@ export interface GridProps { xxl?: number; } +export type TableOptions = { + filters: { + [column: string]: string; + }; + pagination: { + current: number; + pageSize: number; + }; + order: { property: string; direction: "asc" | "desc" }[]; + search: string[]; +}; + export type TagColor = | "magenta" | "red" From 2d38af0af49ccb5e284d3cd0a0499ee6b66599cc Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Wed, 7 Dec 2022 09:19:33 -0600 Subject: [PATCH 049/115] refactor: Fixed typings for selectAllSamples --- .../resources/js/apis/projects/samples.ts | 10 ++-- src/main/webapp/resources/js/apis/requests.ts | 2 +- .../js/pages/projects/redux/samplesSlice.ts | 53 ++++++++++--------- .../resources/js/types/irida/index.d.ts | 5 ++ 4 files changed, 39 insertions(+), 31 deletions(-) diff --git a/src/main/webapp/resources/js/apis/projects/samples.ts b/src/main/webapp/resources/js/apis/projects/samples.ts index 12792fcf2c5..bf46c981849 100644 --- a/src/main/webapp/resources/js/apis/projects/samples.ts +++ b/src/main/webapp/resources/js/apis/projects/samples.ts @@ -1,6 +1,8 @@ import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; +import { TableOptions } from "../../types/ant-design"; import { PairedEndSequenceFile, + SelectedSample, SingleEndSequenceFile, } from "../../types/irida"; import { getProjectIdFromUrl, setBaseUrl } from "../../utilities/url-utilities"; @@ -138,10 +140,10 @@ export async function shareSamplesWithProject({ * @param {object} options - current table filters * @returns {Promise<*>} */ -export async function getMinimalSampleDetailsForFilteredProject(options: { - [key: string]: string | string[]; -}) { - return post(`${URL}/${PROJECT_ID}/samples/ids`, options); +export async function getMinimalSampleDetailsForFilteredProject( + options: TableOptions +): Promise { + return post(`${URL}/${PROJECT_ID}/samples/ids`, options); } /** diff --git a/src/main/webapp/resources/js/apis/requests.ts b/src/main/webapp/resources/js/apis/requests.ts index 7a5ba6ee72b..54e9d7b9fd7 100644 --- a/src/main/webapp/resources/js/apis/requests.ts +++ b/src/main/webapp/resources/js/apis/requests.ts @@ -22,7 +22,7 @@ export async function get( export async function post( url: string, - params?: T, + params?: Record | undefined, config?: AxiosRequestConfig ): Promise { try { diff --git a/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.ts b/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.ts index 8b15d2064c5..cc60aa956fc 100644 --- a/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.ts +++ b/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.ts @@ -3,31 +3,32 @@ import { createAsyncThunk, createReducer, } from "@reduxjs/toolkit"; +import isEqual from "lodash/isEqual"; +import { putSampleInCart } from "../../../apis/cart/cart"; +import { getMinimalSampleDetailsForFilteredProject } from "../../../apis/projects/samples"; +import { TableOptions } from "../../../types/ant-design"; +import { SelectedSample } from "../../../types/irida"; +import { downloadPost } from "../../../utilities/file-utilities"; +import { formatFilterBySampleNames } from "../../../utilities/table-utilities"; import { getProjectIdFromUrl, setBaseUrl, } from "../../../utilities/url-utilities"; import { INITIAL_TABLE_STATE } from "../samples/services/constants"; -import { getMinimalSampleDetailsForFilteredProject } from "../../../apis/projects/samples"; -import { putSampleInCart } from "../../../apis/cart/cart"; -import { downloadPost } from "../../../utilities/file-utilities"; -import { formatFilterBySampleNames } from "../../../utilities/table-utilities"; -import isEqual from "lodash/isEqual"; -import { TableOptions } from "../../../types/ant-design"; export type SamplesTableState = { projectId: string | number; // TODO: (Josh - 12/7/22) This will be removed in subsequent PR options: TableOptions; selectedCount: number; - selected: { [id: number]: string }; + selected: { [key: string]: SelectedSample }; }; -const updateTable = createAction("samples/table/update"); -const reloadTable = createAction("samples/table/reload"); const addSelectedSample = createAction("samples/table/selected/add"); -const removeSelectedSample = createAction("samples/table/selected/remove"); -const clearSelectedSamples = createAction("samples/table/selected/clear"); const clearFilterByFile = createAction("samples/table/clearFilterByFile"); +const clearSelectedSamples = createAction("samples/table/selected/clear"); +const reloadTable = createAction("samples/table/reload"); +const removeSelectedSample = createAction("samples/table/selected/remove"); +const updateTable = createAction("samples/table/update"); /** * Called when selecting all samples from the Samples Table. @@ -35,21 +36,21 @@ const clearFilterByFile = createAction("samples/table/clearFilterByFile"); * This will trigger a "long load" since there might be a little of samples in * the table that data needs to be gathered for from the server. */ -const selectAllSamples = createAsyncThunk( - "/samples/table/selected/all", - async (_, { getState }) => { - const { samples } = getState(); - return await getMinimalSampleDetailsForFilteredProject( - samples.options - ).then((data) => { - const selected = data.reduce( - (accumulator, value) => ({ ...accumulator, [value.key]: value }), - {} - ); - return { selected, selectedCount: data.length }; - }); - } -); +const selectAllSamples = createAsyncThunk< + Pick, + void, + { state: { samples: SamplesTableState } } +>("/samples/table/selected/all", async (_, { getState }) => { + const { options } = getState().samples; + + const data = await getMinimalSampleDetailsForFilteredProject(options); + + const selected = data.reduce( + (accumulator, value) => ({ ...accumulator, [value.key]: value }), + {} + ); + return { selected, selectedCount: data.length }; +}); /** * Called when adding samples to the cart diff --git a/src/main/webapp/resources/js/types/irida/index.d.ts b/src/main/webapp/resources/js/types/irida/index.d.ts index 3fd74617ba1..f9f323854a1 100644 --- a/src/main/webapp/resources/js/types/irida/index.d.ts +++ b/src/main/webapp/resources/js/types/irida/index.d.ts @@ -220,6 +220,11 @@ declare namespace IRIDA { bioSampleFiles: NcbiBioSampleFiles[]; } + export type SelectedSample = Pick & { + owner: boolean; + projectId: number; + }; + interface SequenceFile extends BaseModel { fileSize: string; } From efe6798b14c705181f601a9042d44f09935e15d2 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Wed, 7 Dec 2022 11:26:05 -0600 Subject: [PATCH 050/115] refactor: Moved add samples to cart into cartApi --- .../webapp/resources/js/apis/cart/cart.ts | 24 ++++++++++++++- .../js/pages/projects/redux/samplesSlice.ts | 29 ------------------- .../samples/components/SamplesMenu.jsx | 15 +++++++++- .../js/pages/projects/samples/index.js | 5 +++- 4 files changed, 41 insertions(+), 32 deletions(-) diff --git a/src/main/webapp/resources/js/apis/cart/cart.ts b/src/main/webapp/resources/js/apis/cart/cart.ts index 4690698fdc2..c77bbb469b4 100644 --- a/src/main/webapp/resources/js/apis/cart/cart.ts +++ b/src/main/webapp/resources/js/apis/cart/cart.ts @@ -1,9 +1,9 @@ import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; import { notification } from "antd"; import axios from "axios"; +import { Sample, SelectedSample } from "../../types/irida"; import { cartUpdated } from "../../utilities/events-utilities"; import { setBaseUrl } from "../../utilities/url-utilities"; -import { Sample } from "../../types/irida"; const AJAX_URL = setBaseUrl(`/ajax/cart`); @@ -110,6 +110,27 @@ export const cartApi = createApi({ { type: "Samples", id: sampleId }, ], }), + addSamplesToCart: build.mutation({ + query: ({ + samples, + projectId, + }: { + samples: SelectedSample[]; + projectId: number; + }) => ({ + url: "", + body: { + sampleIds: samples.map((s) => s.id || s.identifier), + projectId, + }, + method: "POST", + }), + invalidatesTags: ["CartCount"], + transformResponse: (response) => { + updateCart(response); + return response; + }, + }), }), }); @@ -120,6 +141,7 @@ export const { useEmptyMutation, useRemoveProjectMutation, useRemoveSampleMutation, + useAddSamplesToCartMutation, } = cartApi; export const updateCart = (data: CartUpdated) => { diff --git a/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.ts b/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.ts index cc60aa956fc..91b5f4f7551 100644 --- a/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.ts +++ b/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.ts @@ -52,30 +52,6 @@ const selectAllSamples = createAsyncThunk< return { selected, selectedCount: data.length }; }); -/** - * Called when adding samples to the cart - */ -const addToCart = createAsyncThunk( - "/samples/table/selected/cart", - async (_, { getState }) => { - const { samples } = getState(); - // Sort by project id - const samplesList = Object.values(samples.selected); - const projects = samplesList.reduce((prev, current) => { - if (!prev[current.projectId]) prev[current.projectId] = []; - prev[current.projectId].push(current); - return prev; - }, {}); - - const promises = []; - for (const projectId in projects) { - promises.push(putSampleInCart(projectId, projects[projectId])); - } - - return Promise.all(promises).then((responses) => responses.pop()); - } -); - /** * Called when downloading samples (sequence files) from the server. */ @@ -204,10 +180,6 @@ export default createReducer(initialState, (builder) => { state.selectedCount = action.payload.selectedCount; state.loadingLong = false; }) - .addCase(addToCart.fulfilled, (state) => { - state.selected = {}; - state.selectedCount = 0; - }) .addCase(downloadSamples.fulfilled, (state) => { state.selected = {}; state.selectedCount = 0; @@ -241,7 +213,6 @@ export { filterByFile, clearFilterByFile, selectAllSamples, - addToCart, downloadSamples, exportSamplesToFile, }; diff --git a/src/main/webapp/resources/js/pages/projects/samples/components/SamplesMenu.jsx b/src/main/webapp/resources/js/pages/projects/samples/components/SamplesMenu.jsx index a3a1a2f8dfc..6fba0d0adc9 100644 --- a/src/main/webapp/resources/js/pages/projects/samples/components/SamplesMenu.jsx +++ b/src/main/webapp/resources/js/pages/projects/samples/components/SamplesMenu.jsx @@ -34,6 +34,10 @@ import { } from "@ant-design/icons"; import { useGetProjectDetailsQuery } from "../../../../apis/projects/project"; import { storeSamples } from "../../../../utilities/session-utilities"; +import { + updateCart, + useAddSamplesToCartMutation, +} from "../../../../apis/cart/cart"; const MergeModal = lazy(() => import("./MergeModal")); const RemoveModal = lazy(() => import("./RemoveModal")); @@ -49,6 +53,7 @@ const FilterByFileModal = lazy(() => import("./FilterByFileModal")); */ export default function SamplesMenu() { const dispatch = useDispatch(); + const [addSamplesToCart] = useAddSamplesToCartMutation(); const { projectId, @@ -109,7 +114,15 @@ export default function SamplesMenu() { }; const onAddToCart = () => { - dispatch(addToCart()); + const samplesList = Object.values(selected); + const projects = samplesList.reduce((prev, current) => { + if (!prev[current.projectId]) prev[current.projectId] = []; + prev[current.projectId].push(current); + return prev; + }, {}); + for (const projectId in projects) { + addSamplesToCart({ projectId, samples: projects[projectId] }); + } }; const onDownload = () => { diff --git a/src/main/webapp/resources/js/pages/projects/samples/index.js b/src/main/webapp/resources/js/pages/projects/samples/index.js index 28c9f6230e7..dba1c2af77c 100644 --- a/src/main/webapp/resources/js/pages/projects/samples/index.js +++ b/src/main/webapp/resources/js/pages/projects/samples/index.js @@ -12,6 +12,7 @@ import { associatedProjectsApi } from "../../../apis/projects/associated-project import samplesReducer from "../redux/samplesSlice"; import { projectApi } from "../../../apis/projects/project"; import { setBaseUrl } from "../../../utilities/url-utilities"; +import { cartApi } from "../../../apis/cart/cart"; /* WEBPACK PUBLIC PATH: @@ -30,12 +31,14 @@ export const store = configureStore({ [projectApi.reducerPath]: projectApi.reducer, [samplesApi.reducerPath]: samplesApi.reducer, [associatedProjectsApi.reducerPath]: associatedProjectsApi.reducer, + [cartApi.reducerPath]: cartApi.reducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat( samplesApi.middleware, projectApi.middleware, - associatedProjectsApi.middleware + associatedProjectsApi.middleware, + cartApi.middleware ), devTools: process.env.NODE_ENV !== "production", }); From 3d56d7b3fd06fafadcb6ab997f412408538f467a Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Wed, 7 Dec 2022 12:42:24 -0600 Subject: [PATCH 051/115] refactor: Cleaned up remaining types --- .../js/pages/projects/redux/samplesSlice.ts | 116 +++++++++++------- .../resources/js/types/ant-design/index.d.ts | 10 +- .../js/utilities/export-utilities.js | 4 +- ...{table-utilities.js => table-utilities.ts} | 12 +- 4 files changed, 94 insertions(+), 48 deletions(-) rename src/main/webapp/resources/js/utilities/{table-utilities.js => table-utilities.ts} (88%) diff --git a/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.ts b/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.ts index 91b5f4f7551..03b67d0e129 100644 --- a/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.ts +++ b/src/main/webapp/resources/js/pages/projects/redux/samplesSlice.ts @@ -4,10 +4,9 @@ import { createReducer, } from "@reduxjs/toolkit"; import isEqual from "lodash/isEqual"; -import { putSampleInCart } from "../../../apis/cart/cart"; import { getMinimalSampleDetailsForFilteredProject } from "../../../apis/projects/samples"; -import { TableOptions } from "../../../types/ant-design"; -import { SelectedSample } from "../../../types/irida"; +import { TableOptions, TableSearch } from "../../../types/ant-design"; +import { ProjectMinimal, Sample, SelectedSample } from "../../../types/irida"; import { downloadPost } from "../../../utilities/file-utilities"; import { formatFilterBySampleNames } from "../../../utilities/table-utilities"; import { @@ -17,24 +16,44 @@ import { import { INITIAL_TABLE_STATE } from "../samples/services/constants"; export type SamplesTableState = { - projectId: string | number; // TODO: (Josh - 12/7/22) This will be removed in subsequent PR + projectId: string | number | undefined; // TODO: (Josh - 12/7/22) This will be removed in subsequent PR options: TableOptions; selectedCount: number; selected: { [key: string]: SelectedSample }; + loadingLong?: boolean; + filterByFile?: FilterByFile | null; }; -const addSelectedSample = createAction("samples/table/selected/add"); +export type TableSample = { + sample: Sample; + project: ProjectMinimal; + coverage: number; + key: string; + owner: boolean; + qcStatus: string; +}; + +export type FilterByFile = { + filename: string; + fileFilter: TableSearch; +}; + +const addSelectedSample = createAction( + "samples/table/selected/add" +); const clearFilterByFile = createAction("samples/table/clearFilterByFile"); const clearSelectedSamples = createAction("samples/table/selected/clear"); const reloadTable = createAction("samples/table/reload"); -const removeSelectedSample = createAction("samples/table/selected/remove"); +const removeSelectedSample = createAction( + "samples/table/selected/remove" +); const updateTable = createAction("samples/table/update"); /** * Called when selecting all samples from the Samples Table. * * This will trigger a "long load" since there might be a little of samples in - * the table that data needs to be gathered for from the server. + * the table that data need to be gathered for from the server. */ const selectAllSamples = createAsyncThunk< Pick, @@ -55,46 +74,58 @@ const selectAllSamples = createAsyncThunk< /** * Called when downloading samples (sequence files) from the server. */ -const downloadSamples = createAsyncThunk( - "/samples/table/export/download", - async (_, { getState }) => { - const { samples } = getState(); - const sampleIds = Object.values(samples.selected).map((s) => s.id); - return await downloadPost( - setBaseUrl(`/ajax/projects/${samples.projectId}/samples/download`), - { sampleIds } - ); - } -); +const downloadSamples = createAsyncThunk< + void, + void, + { state: { samples: SamplesTableState } } +>("/samples/table/export/download", async (_, { getState }) => { + // TODO: (Josh - 12/7/22) This should not be in here, move out in samples page refactor. + const { selected, projectId } = getState().samples; + const sampleIds = Object.values(selected).map((s) => s.id); + await downloadPost( + setBaseUrl(`/ajax/projects/${projectId}/samples/download`), + { sampleIds } + ); +}); /** * Called when exporting the current state of the samples' table to either * a CSV of Excel file. */ -const exportSamplesToFile = createAsyncThunk( - "/samples/table/export", - async (type, { getState }) => { - const { samples } = getState(); - const options = { ...samples.options }; - if (samples.selectedCount > 0) { - const sampleNamesFilter = formatFilterBySampleNames( - Object.values(samples.selected) - ); - options.search = [...options.search, sampleNamesFilter]; - } - - return await downloadPost( - setBaseUrl( - `/ajax/projects/${samples.projectId}/samples/export?type=${type}` - ), - options +const exportSamplesToFile = createAsyncThunk< + void, + string, + { state: { samples: SamplesTableState } } +>("/samples/table/export", async (type, { getState }) => { + // TODO: (Josh - 12/7/22) This should not be in here, move out in samples page refactor. + const { samples } = getState(); + const options = { ...samples.options }; + if (samples.selectedCount > 0) { + const sampleNamesFilter = formatFilterBySampleNames( + Object.values(samples.selected) ); + options.search = [...options.search, sampleNamesFilter]; } -); + + await downloadPost( + setBaseUrl( + `/ajax/projects/${samples.projectId}/samples/export?type=${type}` + ), + options + ); +}); const filterByFile = createAction( `samples/table/filterByFile`, - ({ samples, filename }) => { + ({ + samples, + filename, + }: { + samples: SelectedSample[]; + filename: string; + }): { + payload: FilterByFile; + } => { return { payload: { filename, @@ -105,9 +136,9 @@ const filterByFile = createAction( ); /** - * Since the initial table props may need to be reset at some point, we store - * them in a string so they cannot be mutated. When the table needs to be reset - * to it's default state, just re-parse by calling this. + * Since the initial table props may need to be reset at some point, store + * them in a string, so they can't mutate. When the table needs to be reset + * to it's default state, just reparse by calling this. * @returns {object} - default table state */ const getInitialTableOptions = () => JSON.parse(INITIAL_TABLE_STATE); @@ -117,9 +148,8 @@ const getInitialTableOptions = () => JSON.parse(INITIAL_TABLE_STATE); * Needs to be converted to this format so that it can be used by the share * samples page and the cart. * @param projectSample - Sample details object returned as part of the table data - * @returns {{sampleName: (Document.mergeForm.sampleName|Document.sampleName|string), owner: *, id: string, projectId: *, key: *}} */ -const formatSelectedSample = (projectSample) => ({ +const formatSelectedSample = (projectSample: TableSample): SelectedSample => ({ key: projectSample.key, id: projectSample.sample.id, projectId: projectSample.project.id, @@ -128,7 +158,7 @@ const formatSelectedSample = (projectSample) => ({ }); const initialState: SamplesTableState = { - projectId: getProjectIdFromUrl(), + projectId: getProjectIdFromUrl(), // TODO: (Josh - 12/7/22) This will get cleaned up in future PR options: getInitialTableOptions(), selected: {}, selectedCount: 0, diff --git a/src/main/webapp/resources/js/types/ant-design/index.d.ts b/src/main/webapp/resources/js/types/ant-design/index.d.ts index b613089fef9..09f91ac04c3 100644 --- a/src/main/webapp/resources/js/types/ant-design/index.d.ts +++ b/src/main/webapp/resources/js/types/ant-design/index.d.ts @@ -21,7 +21,15 @@ export type TableOptions = { pageSize: number; }; order: { property: string; direction: "asc" | "desc" }[]; - search: string[]; + search: TableSearch[]; + reload?: number; +}; + +export type TableSearch = { + property: string; + value: string | string[]; + operation: "IN"; + _file?: boolean; }; export type TagColor = diff --git a/src/main/webapp/resources/js/utilities/export-utilities.js b/src/main/webapp/resources/js/utilities/export-utilities.js index 8b583cd62b7..a4960fc0147 100644 --- a/src/main/webapp/resources/js/utilities/export-utilities.js +++ b/src/main/webapp/resources/js/utilities/export-utilities.js @@ -6,6 +6,6 @@ import * as XLSX from "xlsx"; * @param {string} data - csv representation of the table data. */ export default ({ filename, data }) => { - const workbook = XLSX.read(data, { type: 'binary', raw: true, dense: true }); - XLSX.writeFile(workbook, filename, { bookType: 'xlsx', type: 'base64' }); + const workbook = XLSX.read(data, { type: "binary", raw: true, dense: true }); + XLSX.writeFile(workbook, filename, { bookType: "xlsx", type: "base64" }); }; diff --git a/src/main/webapp/resources/js/utilities/table-utilities.js b/src/main/webapp/resources/js/utilities/table-utilities.ts similarity index 88% rename from src/main/webapp/resources/js/utilities/table-utilities.js rename to src/main/webapp/resources/js/utilities/table-utilities.ts index f7721b85f39..637e78472d8 100644 --- a/src/main/webapp/resources/js/utilities/table-utilities.js +++ b/src/main/webapp/resources/js/utilities/table-utilities.ts @@ -1,4 +1,6 @@ import moment from "moment"; +import { TableSearch } from "../types/ant-design"; +import { SelectedSample } from "../types/irida"; /** * Format Sort Order from the Ant Design sorter object @@ -64,14 +66,20 @@ export function formatSearch(filters) { return formattedSearch; } -export const formatFilterBySampleNames = (samples) => { +/** + * Format the filter by samples names value to send to the server + * @param samples + */ +export function formatFilterBySampleNames( + samples: SelectedSample[] +): TableSearch { return { property: "sample.sampleName", value: samples.map((sample) => sample.sampleName), operation: "IN", _file: true, }; -}; +} export const stringSorter = (property) => (a, b) => a[property].localeCompare(b[property], window.TL.LANGUAGE_TAG, { From f7607979dae0ee5efa8fcd515368cea7e6c10dce Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Thu, 8 Dec 2022 13:23:39 -0600 Subject: [PATCH 052/115] refactor: Updated announcement modal for high priority announcements --- .../components/AnnouncementsModal.jsx | 113 ---------------- .../components/announcement-dispatch.js | 82 ------------ .../components/announcements-context.js | 124 ------------------ .../js/components/Header/PageHeader.jsx | 7 +- ...crollableModal.jsx => ScrollableModal.tsx} | 23 +++- .../components/AnnouncementLink.tsx | 18 +-- .../components/AnnouncementsModal.tsx | 114 ++++++++++++++++ .../js/components/main-navigation/index.tsx | 7 +- .../js/redux/endpoints/announcements.ts | 29 +++- .../resources/js/redux/endpoints/tags.ts | 2 + .../resources/js/types/irida/index.d.ts | 7 +- .../resources/js/utilities/date-utilities.ts | 2 +- .../ria/integration/DashboardPageIT.java | 4 - .../integration/components/Announcements.java | 14 +- 14 files changed, 181 insertions(+), 365 deletions(-) delete mode 100644 src/main/webapp/resources/js/components/Header/MainNavigation/components/AnnouncementsModal.jsx delete mode 100644 src/main/webapp/resources/js/components/Header/MainNavigation/components/announcement-dispatch.js delete mode 100644 src/main/webapp/resources/js/components/Header/MainNavigation/components/announcements-context.js rename src/main/webapp/resources/js/components/ant.design/{ScrollableModal.jsx => ScrollableModal.tsx} (55%) create mode 100644 src/main/webapp/resources/js/components/main-navigation/components/AnnouncementsModal.tsx diff --git a/src/main/webapp/resources/js/components/Header/MainNavigation/components/AnnouncementsModal.jsx b/src/main/webapp/resources/js/components/Header/MainNavigation/components/AnnouncementsModal.jsx deleted file mode 100644 index c5d7945dd2a..00000000000 --- a/src/main/webapp/resources/js/components/Header/MainNavigation/components/AnnouncementsModal.jsx +++ /dev/null @@ -1,113 +0,0 @@ -import React from "react"; -import { ScrollableModal } from "../../../ant.design/ScrollableModal"; -import { Button, Space, Tag, Typography } from "antd"; -import { PriorityFlag } from "../../../../pages/announcement/components/PriorityFlag"; -import { formatDate } from "../../../../utilities/date-utilities"; -import ReactMarkdown from "react-markdown"; -import { TYPES, useAnnouncements } from "./announcements-context"; -import { - readAndCloseAnnouncement, - readAndNextAnnouncement, - readAndPreviousAnnouncement, -} from "./announcement-dispatch"; - -const { Text } = Typography; - -/** - * React component to display the announcements modal. - * - * @returns {JSX.Element} - * @constructor - */ -export function AnnouncementsModal() { - const [ - { announcements, modalVisible: visible, index, isPriority }, - dispatch, - ] = useAnnouncements(); - - const [newAnnouncements, setNewAnnouncements] = React.useState([]); - - React.useEffect(() => { - if (isPriority) { - setNewAnnouncements(announcements.filter((a) => a.priority)); - } else { - setNewAnnouncements(announcements); - } - }, [announcements]); - - const footer = [ - index > 0 && ( - - ), - (index === 0 || index + 1 === newAnnouncements.length) && ( - - ), - index + 1 < newAnnouncements.length && ( - - ), - ]; - - return visible && newAnnouncements.length ? ( - - - {i18n( - "AnnouncementsModal.tag.details", - newAnnouncements.filter((a) => a.read).length, - newAnnouncements.length - )} - - - - - {newAnnouncements[index].title} - - {i18n( - "AnnouncementsModal.create.details", - newAnnouncements[index].user.username, - formatDate({ date: newAnnouncements[index].createdDate }) - )} - - - - - } - visible={visible} - width="90ch" - onCancel={() => dispatch({ type: TYPES.CLOSE_ANNOUNCEMENT })} - footer={footer} - > -
- {newAnnouncements[index].message} -
-
- ) : null; -} diff --git a/src/main/webapp/resources/js/components/Header/MainNavigation/components/announcement-dispatch.js b/src/main/webapp/resources/js/components/Header/MainNavigation/components/announcement-dispatch.js deleted file mode 100644 index f310834f79c..00000000000 --- a/src/main/webapp/resources/js/components/Header/MainNavigation/components/announcement-dispatch.js +++ /dev/null @@ -1,82 +0,0 @@ -import { - markAnnouncementRead -} from "../../../../apis/announcements/announcements"; -import { TYPES } from "./announcements-context"; - -/** - * Marks the announcement as read, if not already done so, and goes to the next announcement. - * - * @param {function} dispatch - triggers the next state change - * @param {object} announcement - the announcement that is to be read - */ -export function readAndNextAnnouncement(dispatch, announcement) { - if (announcement.read) { - dispatch({ - type: TYPES.READ_AND_NEXT, - payload: { - announcement: announcement, - }, - }); - } else { - markAnnouncementRead({ aID: announcement.identifier }).then(() => { - dispatch({ - type: TYPES.READ_AND_NEXT, - payload: { - announcement: { ...announcement, read: true }, - }, - }); - }); - } -} - -/** - * Marks the announcement as read, if not already done so, and goes to the previous announcement. - * - * @param {function} dispatch - triggers the next state change - * @param {object} announcement - the announcement that is to be read - */ -export function readAndPreviousAnnouncement(dispatch, announcement) { - if (announcement.read) { - dispatch({ - type: TYPES.READ_AND_PREVIOUS, - payload: { - announcement: announcement, - }, - }); - } else { - markAnnouncementRead({ aID: announcement.identifier }).then(() => { - dispatch({ - type: TYPES.READ_AND_PREVIOUS, - payload: { - announcement: { ...announcement, read: true }, - }, - }); - }); - } -} - -/** - * Marks the announcement as read, if not already done so, and closes the modal. - * - * @param {function} dispatch - triggers the next state change - * @param {object} announcement - the announcement that is to be read - */ -export function readAndCloseAnnouncement(dispatch, announcement) { - if (announcement.read) { - dispatch({ - type: TYPES.READ_AND_CLOSE, - payload: { - announcement: announcement, - }, - }); - } else { - markAnnouncementRead({ aID: announcement.identifier }).then(() => { - dispatch({ - type: TYPES.READ_AND_CLOSE, - payload: { - announcement: { ...announcement, read: true }, - }, - }); - }); - } -} diff --git a/src/main/webapp/resources/js/components/Header/MainNavigation/components/announcements-context.js b/src/main/webapp/resources/js/components/Header/MainNavigation/components/announcements-context.js deleted file mode 100644 index bf7d6e00e31..00000000000 --- a/src/main/webapp/resources/js/components/Header/MainNavigation/components/announcements-context.js +++ /dev/null @@ -1,124 +0,0 @@ -import React from "react"; -import { - getUnreadAnnouncements -} from "../../../../apis/announcements/announcements"; - -/** - * The context provides access to shared announcement data and actions. - * These unread announcements are displayed in a modal that's triggered at login - * and from the drop down submenu at the bell icon on the main navigation bar. - */ -const AnnouncementContext = React.createContext(); -AnnouncementContext.displayName = "Announcement Context"; - -export const TYPES = { - LOADED: "ANNOUNCEMENTS_LOADED", - SHOW_ANNOUNCEMENT: "SHOW_ANNOUNCEMENT", - CLOSE_ANNOUNCEMENT: "CLOSE_ANNOUNCEMENT", - READ_AND_NEXT: "READ_AND_NEXT", - READ_AND_PREVIOUS: "READ_AND_PREVIOUS", - READ_AND_CLOSE: "READ_AND_CLOSE", -}; - -/** - * Using a reducer to hold all the announcement data for each action that is used to display the unread announcements in a modal. - */ -const reducer = (state, action) => { - switch (action.type) { - case TYPES.LOADED: - const isPriority = - action.payload.announcements.filter((a) => a.priority).length > 0; - return { - ...state, - announcements: action.payload.announcements, - modalVisible: isPriority, - index: 0, - isPriority, - }; - case TYPES.SHOW_ANNOUNCEMENT: - return { - ...state, - modalVisible: true, - index: action.payload.index, - isPriority: action.payload.isPriority, - }; - case TYPES.CLOSE_ANNOUNCEMENT: - return { - ...state, - modalVisible: false, - index: null, - isPriority: null, - announcements: state.announcements.filter((a) => !a.read), - }; - case TYPES.READ_AND_NEXT: - const newNextAnnouncements = [...state.announcements]; - newNextAnnouncements[state.index] = action.payload.announcement; - return { - ...state, - index: state.index + 1, - announcements: newNextAnnouncements, - }; - case TYPES.READ_AND_PREVIOUS: - const newPreviousAnnouncements = [...state.announcements]; - newPreviousAnnouncements[state.index] = action.payload.announcement; - return { - ...state, - index: state.index - 1, - announcements: newPreviousAnnouncements, - }; - case TYPES.READ_AND_CLOSE: - const newCloseAnnouncements = [...state.announcements]; - newCloseAnnouncements[state.index] = action.payload.announcement; - return { - ...state, - modalVisible: false, - index: null, - isPriority: null, - announcements: newCloseAnnouncements.filter((a) => !a.read), - }; - default: - return { ...state }; - } -}; - -/** - * The provider for displaying the unread announcements in a modal. - */ -function AnnouncementProvider({ children }) { - const [state, dispatch] = React.useReducer(reducer, { announcements: [] }); - - React.useEffect(() => { - getUnreadAnnouncements().then((data) => { - const announcements = data - ? data.map((a) => ({ - ...a, - id: `announcement-${a.identifier}`, - })) - : []; - dispatch({ - type: TYPES.LOADED, - payload: { announcements }, - }); - }); - }, []); - - const value = [state, dispatch]; - return ( - - {children} - - ); -} - -/** - * The consumer gets the provided context from within an AnnouncementsProvider. - */ -function useAnnouncements() { - const context = React.useContext(AnnouncementContext); - if (context === undefined) { - throw new Error(`useAnnouncements requires AnnouncementsProvider`); - } - return context; -} - -export { AnnouncementProvider, useAnnouncements }; diff --git a/src/main/webapp/resources/js/components/Header/PageHeader.jsx b/src/main/webapp/resources/js/components/Header/PageHeader.jsx index 9c5a2b7dc14..4380ca9f90f 100644 --- a/src/main/webapp/resources/js/components/Header/PageHeader.jsx +++ b/src/main/webapp/resources/js/components/Header/PageHeader.jsx @@ -5,8 +5,6 @@ import { Notifications } from "../notifications/Notifications"; import GalaxyAlert from "./GalaxyAlert"; import { Breadcrumbs } from "./Breadcrumbs"; import { setBaseUrl } from "../../utilities/url-utilities"; -import { AnnouncementProvider } from "./MainNavigation/components/announcements-context"; -import { AnnouncementsModal } from "./MainNavigation/components/AnnouncementsModal"; import { Provider } from "react-redux"; import { store } from "../../redux/store"; import MainNavigation from "../main-navigation"; @@ -31,10 +29,7 @@ export function PageHeader() { - - - - + diff --git a/src/main/webapp/resources/js/components/ant.design/ScrollableModal.jsx b/src/main/webapp/resources/js/components/ant.design/ScrollableModal.tsx similarity index 55% rename from src/main/webapp/resources/js/components/ant.design/ScrollableModal.jsx rename to src/main/webapp/resources/js/components/ant.design/ScrollableModal.tsx index 56558df72d9..a2519314bb8 100644 --- a/src/main/webapp/resources/js/components/ant.design/ScrollableModal.jsx +++ b/src/main/webapp/resources/js/components/ant.design/ScrollableModal.tsx @@ -3,10 +3,11 @@ */ import React from "react"; -import styled from "styled-components"; +import type { ModalProps } from "antd"; import { Modal } from "antd"; +import styled from "styled-components"; -const ScrollBodyModal = styled(Modal)` +const ScrollBodyModal = styled(Modal)<{ maxHeight: number }>` .ant-modal-body { padding: 15px 25px; overflow-y: auto; @@ -14,15 +15,23 @@ const ScrollBodyModal = styled(Modal)` } `; +type ScrollableModalProps = ModalProps & { + maxHeight?: number; + children: JSX.Element; +}; + /** * React component to be used when you want a scrollable content in your ant design modal. * - * @param {string} maxHeight - maximum height of the scrollable body - * @param {element} children - the modal content - * @returns {*} - * @constructor + * @param maxHeight - maximum height of the scrollable body + * @param children - the modal content + * @param props - modal props */ -export function ScrollableModal({ maxHeight = 600, children, ...props }) { +export function ScrollableModal({ + maxHeight = 600, + children, + ...props +}: ScrollableModalProps): JSX.Element { return ( {children} diff --git a/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementLink.tsx b/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementLink.tsx index 10227060b2b..1b68296d93e 100644 --- a/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementLink.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementLink.tsx @@ -3,6 +3,7 @@ import { Badge } from "antd"; import { BellOutlined } from "@ant-design/icons"; import { ROUTE_ANNOUNCEMENTS } from "../../../data/routes"; import { useGetAnnouncementCountQuery } from "../../../redux/endpoints/announcements"; +import AnnouncementsModal from "./AnnouncementsModal"; /** * React component to render a link in the main navigation to the announcements page, @@ -11,15 +12,16 @@ import { useGetAnnouncementCountQuery } from "../../../redux/endpoints/announcem * @constructor */ export default function AnnouncementLink() { - const { data: count } = useGetAnnouncementCountQuery(undefined, {}); - - // TODO: (Josh - 12/2/22) Re-implement modal for high priority messages only + const { data: count = 0 } = useGetAnnouncementCountQuery(undefined, {}); return ( - - - - - + <> + + + + + + + ); } diff --git a/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementsModal.tsx b/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementsModal.tsx new file mode 100644 index 00000000000..086302492a1 --- /dev/null +++ b/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementsModal.tsx @@ -0,0 +1,114 @@ +import React, { useEffect, useState } from "react"; +import { Button, Space, Tag, Typography } from "antd"; +import { formatDate } from "../../../utilities/date-utilities"; +import { + useGetUnreadAnnouncementsQuery, + useMarkAnnouncementAsReadMutation, +} from "../../../redux/endpoints/announcements"; +import ReactMarkdown from "react-markdown"; +import { FlagTwoTone } from "@ant-design/icons"; +import { ScrollableModal } from "../../ant.design/ScrollableModal"; + +const { Text } = Typography; + +export default function AnnouncementsModal(): JSX.Element | null { + const { data: announcements, isSuccess } = useGetUnreadAnnouncementsQuery( + undefined, + {} + ); + const [markAnnouncementAsRead] = useMarkAnnouncementAsReadMutation(); + + const [visible, setVisible] = useState(false); + const [index, setIndex] = useState(0); + + useEffect(() => { + if (isSuccess) { + setVisible(true); + } + }, [isSuccess]); + + if (!isSuccess || announcements === undefined || announcements.length === 0) { + return null; + } + + const footer = visible && [ + index > 0 && ( + + ), + (index === 0 || index + 1 === announcements.length) && ( + + ), + index + 1 < announcements.length && ( + + ), + ]; + + return ( + + + {i18n( + "AnnouncementsModal.tag.details", + index, + announcements.length + )} + + + + + {announcements[index].title} + + {i18n( + "AnnouncementsModal.create.details", + announcements[index].user.username, + formatDate({ date: announcements[index].createdDate }) + )} + + + + + } + visible={visible} + width="90ch" + onCancel={() => setVisible(false)} + footer={footer} + > +
+ {announcements[index].message} +
+
+ ); +} diff --git a/src/main/webapp/resources/js/components/main-navigation/index.tsx b/src/main/webapp/resources/js/components/main-navigation/index.tsx index 69fe9c7c564..c5e2e64f4df 100644 --- a/src/main/webapp/resources/js/components/main-navigation/index.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/index.tsx @@ -266,7 +266,12 @@ export default function MainNavigation(): JSX.Element { - + {rightMenuItems.map(renderMenuItem)} diff --git a/src/main/webapp/resources/js/redux/endpoints/announcements.ts b/src/main/webapp/resources/js/redux/endpoints/announcements.ts index 299f2f25303..b011258e50f 100644 --- a/src/main/webapp/resources/js/redux/endpoints/announcements.ts +++ b/src/main/webapp/resources/js/redux/endpoints/announcements.ts @@ -1,5 +1,7 @@ import { api } from "./api"; -import { TAG_ANNOUNCEMENT_COUNT } from "./tags"; +import { TAG_ANNOUNCEMENT_COUNT, TAG_ANNOUNCEMENTS_UNREAD } from "./tags"; +import { Announcement } from "../../types/irida"; +import { method } from "lodash"; /** * @fileoverview Announcement API for redux-toolkit. @@ -11,7 +13,30 @@ export const announcementsApi = api.injectEndpoints({ query: () => "announcements/count", providesTags: [TAG_ANNOUNCEMENT_COUNT], }), + getUnreadAnnouncements: build.query({ + query: () => "announcements/user/unread", + transformResponse: (data: Announcement[]) => + data.filter((announcement) => announcement.priority), + providesTags: (result = []) => [ + ...result.map( + ({ id }: { id: number }) => + ({ type: TAG_ANNOUNCEMENTS_UNREAD, id } as const) + ), + { type: TAG_ANNOUNCEMENTS_UNREAD, id: "LIST" }, + ], + }), + markAnnouncementAsRead: build.mutation({ + query: (id: number) => ({ + url: `announcements/read/${id}`, + method: "POST", + }), + invalidatesTags: [TAG_ANNOUNCEMENT_COUNT], + }), }), }); -export const { useGetAnnouncementCountQuery } = announcementsApi; +export const { + useGetAnnouncementCountQuery, + useGetUnreadAnnouncementsQuery, + useMarkAnnouncementAsReadMutation, +} = announcementsApi; diff --git a/src/main/webapp/resources/js/redux/endpoints/tags.ts b/src/main/webapp/resources/js/redux/endpoints/tags.ts index 1981781e878..1b932e0e0d1 100644 --- a/src/main/webapp/resources/js/redux/endpoints/tags.ts +++ b/src/main/webapp/resources/js/redux/endpoints/tags.ts @@ -4,12 +4,14 @@ */ export const TAG_ANNOUNCEMENT_COUNT = "tag-announcement-count"; +export const TAG_ANNOUNCEMENTS_UNREAD = "tag-announcements-unread"; export const TAG_CART_COUNT = "tag-cart-count"; export const TAG_PROJECT = "tag-project"; export const TAG_USER = "tag-user"; export const PROVIDED_TAGS = [ TAG_ANNOUNCEMENT_COUNT, + TAG_ANNOUNCEMENTS_UNREAD, TAG_CART_COUNT, TAG_PROJECT, TAG_USER, diff --git a/src/main/webapp/resources/js/types/irida/index.d.ts b/src/main/webapp/resources/js/types/irida/index.d.ts index 915a2a3d3be..3ba03789399 100644 --- a/src/main/webapp/resources/js/types/irida/index.d.ts +++ b/src/main/webapp/resources/js/types/irida/index.d.ts @@ -8,8 +8,8 @@ declare namespace IRIDA { id: number; key: string; name: string; - createdDate: Date; - modifiedDate: Date; + createdDate: string; + modifiedDate: string; identifier: number; } @@ -38,8 +38,7 @@ declare namespace IRIDA { title: string; message: string; priority: boolean; - createdBy: User; - users: User[]; + user: User; } export type CurrentUser = { diff --git a/src/main/webapp/resources/js/utilities/date-utilities.ts b/src/main/webapp/resources/js/utilities/date-utilities.ts index 7d83647e9bc..fd0f9b74646 100644 --- a/src/main/webapp/resources/js/utilities/date-utilities.ts +++ b/src/main/webapp/resources/js/utilities/date-utilities.ts @@ -47,7 +47,7 @@ export function fromNow({ date }: { date: string | number }) { * @param {String} format defaults to "lll" which is mmm dd, yyyy h:mm AM * @return {string} formatted date */ -export function formatDate({ date, format }: { date: Date; format: any }) { +export function formatDate({ date, format }: { date: string; format?: any }) { return formatInternationalizedDateTime(date, format); } diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/DashboardPageIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/DashboardPageIT.java index b49e3cdbcf9..1ca207276b8 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/DashboardPageIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/DashboardPageIT.java @@ -31,12 +31,8 @@ public void testReadingHighPriorityAnnouncements() { announcements.getNextAnnouncement(); assertEquals(1, announcements.getTotalReadAnnouncements(), "The total read priority announcements count does not match"); - assertEquals(5, announcements.getBadgeCount(), "The announcements badge count does not match"); - announcements.getSubmenuAnnouncement(); - assertEquals("No cake", announcements.getSubmenuAnnouncementTitle(2), - "The announcements title in the submenu does not match"); } @Test diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/components/Announcements.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/components/Announcements.java index 05d3489f92c..0ca6f08f8af 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/components/Announcements.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/components/Announcements.java @@ -35,7 +35,7 @@ public class Announcements { @FindBy(css = ".t-announcements-modal button.t-close-announcement-button") private WebElement closeButton; - @FindBy(css = ".t-announcements-badge") + @FindBy(css = "[data-menu-id*=nav-announcements]") private WebElement badge; @FindBy(className = "t-announcements-submenu") @@ -112,18 +112,6 @@ public int getBadgeCount() { return Integer.parseInt(badge.findElement(By.className("ant-scroll-number-only-unit")).getText()); } - public String getSubmenuAnnouncementTitle(int position) { - return submenu.findElements(By.className("ant-list-item")) - .get(position) - .findElement(By.cssSelector(".ant-list-item-meta-title a")) - .getText(); - } - - public void getSubmenuAnnouncement() { - badge.click(); - waitForSubmenu(); - } - public void getNextAnnouncement() { clickNextButton(); waitForPreviousButton(); From 4a3008c49a31fe7ccd3c34cfdb18db8a87ba6bbd Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Thu, 8 Dec 2022 13:30:14 -0600 Subject: [PATCH 053/115] refactor: Updated name of query to reflect high priority --- .../components/AnnouncementsModal.tsx | 12 +++++++----- .../resources/js/redux/endpoints/announcements.ts | 5 ++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementsModal.tsx b/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementsModal.tsx index 086302492a1..0b03e96ac64 100644 --- a/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementsModal.tsx +++ b/src/main/webapp/resources/js/components/main-navigation/components/AnnouncementsModal.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { Button, Space, Tag, Typography } from "antd"; import { formatDate } from "../../../utilities/date-utilities"; import { - useGetUnreadAnnouncementsQuery, + useGetUnreadHighPriorityAnnouncementsQuery, useMarkAnnouncementAsReadMutation, } from "../../../redux/endpoints/announcements"; import ReactMarkdown from "react-markdown"; @@ -11,11 +11,13 @@ import { ScrollableModal } from "../../ant.design/ScrollableModal"; const { Text } = Typography; +/** + * React component to render a modal if high priority announcements are available + * @constructor + */ export default function AnnouncementsModal(): JSX.Element | null { - const { data: announcements, isSuccess } = useGetUnreadAnnouncementsQuery( - undefined, - {} - ); + const { data: announcements, isSuccess } = + useGetUnreadHighPriorityAnnouncementsQuery(undefined, {}); const [markAnnouncementAsRead] = useMarkAnnouncementAsReadMutation(); const [visible, setVisible] = useState(false); diff --git a/src/main/webapp/resources/js/redux/endpoints/announcements.ts b/src/main/webapp/resources/js/redux/endpoints/announcements.ts index b011258e50f..b8b872454f6 100644 --- a/src/main/webapp/resources/js/redux/endpoints/announcements.ts +++ b/src/main/webapp/resources/js/redux/endpoints/announcements.ts @@ -1,7 +1,6 @@ import { api } from "./api"; import { TAG_ANNOUNCEMENT_COUNT, TAG_ANNOUNCEMENTS_UNREAD } from "./tags"; import { Announcement } from "../../types/irida"; -import { method } from "lodash"; /** * @fileoverview Announcement API for redux-toolkit. @@ -13,7 +12,7 @@ export const announcementsApi = api.injectEndpoints({ query: () => "announcements/count", providesTags: [TAG_ANNOUNCEMENT_COUNT], }), - getUnreadAnnouncements: build.query({ + getUnreadHighPriorityAnnouncements: build.query({ query: () => "announcements/user/unread", transformResponse: (data: Announcement[]) => data.filter((announcement) => announcement.priority), @@ -37,6 +36,6 @@ export const announcementsApi = api.injectEndpoints({ export const { useGetAnnouncementCountQuery, - useGetUnreadAnnouncementsQuery, + useGetUnreadHighPriorityAnnouncementsQuery, useMarkAnnouncementAsReadMutation, } = announcementsApi; From b768d8960cfcf5307c46f7d837ed53ba5373415d Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 9 Dec 2022 09:37:16 -0600 Subject: [PATCH 054/115] refactor: Initial working table with only sample name --- src/main/webapp/entries.js | 1 - .../js/components/project/project-samples.tsx | 17 -- .../project/samples-table/samples-table.tsx | 106 ++++++++ .../samples-table/useSamplesTableState.ts | 105 ++++++++ src/main/webapp/resources/js/index.tsx | 4 +- .../resources/js/layouts/project-samples.tsx | 14 + .../js/pages/projects/redux/samplesSlice.ts | 14 +- .../js/redux/endpoints/project-samples.ts | 39 +++ .../js/redux/reducers/project-sample.ts | 240 +++++++++--------- .../resources/js/types/ant-design/index.d.ts | 30 ++- .../resources/js/types/irida/index.d.ts | 5 + .../resources/js/utilities/table-utilities.ts | 10 +- 12 files changed, 420 insertions(+), 165 deletions(-) delete mode 100644 src/main/webapp/resources/js/components/project/project-samples.tsx create mode 100644 src/main/webapp/resources/js/components/project/samples-table/samples-table.tsx create mode 100644 src/main/webapp/resources/js/components/project/samples-table/useSamplesTableState.ts create mode 100644 src/main/webapp/resources/js/layouts/project-samples.tsx create mode 100644 src/main/webapp/resources/js/redux/endpoints/project-samples.ts diff --git a/src/main/webapp/entries.js b/src/main/webapp/entries.js index 3acd95f125e..ea838690796 100644 --- a/src/main/webapp/entries.js +++ b/src/main/webapp/entries.js @@ -18,7 +18,6 @@ module.exports = { projects: "./resources/js/pages/projects/list/index.js", "samples-metadata-import": "./resources/js/pages/projects/samples-metadata-import", - samples: "./resources/js/pages/projects/samples", "project-linelist": "./resources/js/pages/projects/linelist/index.js", "project-settings": "./resources/js/pages/projects/settings", "project-share": "./resources/js/pages/projects/share", diff --git a/src/main/webapp/resources/js/components/project/project-samples.tsx b/src/main/webapp/resources/js/components/project/project-samples.tsx deleted file mode 100644 index eff837cc747..00000000000 --- a/src/main/webapp/resources/js/components/project/project-samples.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; -import { useParams } from "react-router-dom"; -import { useGetProjectDetailsQuery } from "../../redux/endpoints/project"; -import { SamplesTable } from "../../pages/projects/samples/components/SamplesTable"; - -export default function ProjectSamples() { - const { projectId } = useParams(); - const { data: details } = useGetProjectDetailsQuery(projectId); - console.log(details); - - return ( -
- SAMPLES SHIT - {/**/} -
- ); -} diff --git a/src/main/webapp/resources/js/components/project/samples-table/samples-table.tsx b/src/main/webapp/resources/js/components/project/samples-table/samples-table.tsx new file mode 100644 index 00000000000..18306c7de79 --- /dev/null +++ b/src/main/webapp/resources/js/components/project/samples-table/samples-table.tsx @@ -0,0 +1,106 @@ +import React from "react"; +import { useParams } from "react-router-dom"; +import { useFetchPagedSamplesQuery } from "../../../redux/endpoints/project-samples"; +import { Button, Select, Space, Table } from "antd"; +import type { TableColumnProps } from "antd/es"; +import { Project, Sample } from "../../../types/irida"; +import { SampleDetailViewer } from "../../samples/SampleDetailViewer"; +import { IconSearch } from "../../icons/Icons"; +import { blue6 } from "../../../styles/colors"; +import { FilterConfirmProps } from "antd/es/table/interface"; +import { SearchOutlined } from "@ant-design/icons"; +import useSamplesTableState, { ProjectSample } from "./useSamplesTableState"; + +/** + * React component to render the project samples table + * @constructor + */ +export default function SamplesTable(): JSX.Element { + const [samples, pagination, api] = useSamplesTableState(); + + const getColumnSearchProps = ( + dataIndex: string | string[], + filterName = "", + placeholder = "" + ) => ({ + filterDropdown: ({ + setSelectedKeys, + selectedKeys, + confirm, + clearFilters, + }: { + setSelectedKeys: (param: string | (string & any[])[]) => void; + selectedKeys: string[]; + confirm: (param: FilterConfirmProps) => void; + clearFilters: () => void; + }) => ( +
+ { + const values = Array.isArray(e) && e.length > 0 ? [e] : e; + setSelectedKeys(values as Key[]); + confirm({ closeDropdown: false }); + }} + style={{ marginBottom: 8, display: "block" }} + /> + + + + +
+ ), + filterIcon: (filtered: boolean) => ( + + ), + }); + + const handleChange: TableProps["onChange"] = ( + pagination, + tableFilters, + sorter + ): void => { + const { associated, ...otherSearch } = tableFilters; + const search = formatSearch(otherSearch as TableFilters); + const order = formatSort(sorter); + const filters = associated === undefined ? {} : { associated }; + if (filterByFile) search.push(filterByFile.fileFilter); + + if ( + !( + isEqual(tableOptions.search, search) && + isEqual(tableOptions.filters, filters) + ) + ) { + setSelected({}); + } + + setTableOptions({ + filters, // TODO: (Josh - 12/9/22) Why is this wrong? + pagination, + order, + search, + }); + }; + + const handleSearch = ( + selectedKeys: Key[], + confirm: TableFilterConfirmFn + ): void => { + confirm(); + }; + + /** + * Handle clearing a table column dropdown filter + * @param clearFilters + * @param confirm + */ + const handleClearSearch = ( + clearFilters: (() => void) | undefined, + confirm: TableFilterConfirmFn + ): void | undefined => { + if (typeof clearFilters === "function") { + clearFilters(); + } + confirm({ closeDropdown: false }); + }; + + return [ + data?.content, + isSuccess ? getPaginationOptions(data.total) : undefined, + { + handleClearSearch, + handleSearch, + handleChange, + getColumnSearchProps, + getDateColumnSearchProps, + }, + ]; +} diff --git a/src/main/webapp/resources/js/components/sample-quality/index.js b/src/main/webapp/resources/js/components/sample-quality/index.js index 8de1bfc1476..40e3824976d 100644 --- a/src/main/webapp/resources/js/components/sample-quality/index.js +++ b/src/main/webapp/resources/js/components/sample-quality/index.js @@ -7,7 +7,8 @@ import { IconCheck, IconWarning } from "../icons/Icons"; /** * React component to render the quality data for a sample. * - * @param {array} qualities - list of qc issues associated with the sample + * @param qcStatus + * @param qualities - list of qc issues associated with the sample * @returns {JSX.Element} * @constructor */ diff --git a/src/main/webapp/resources/js/redux/endpoints/project-samples.ts b/src/main/webapp/resources/js/redux/endpoints/project-samples.ts index 8ad31c73e5c..d973e0d17c0 100644 --- a/src/main/webapp/resources/js/redux/endpoints/project-samples.ts +++ b/src/main/webapp/resources/js/redux/endpoints/project-samples.ts @@ -7,7 +7,7 @@ import { TableOptions } from "../../types/ant-design"; import { Project, Sample, TableResponse } from "../../types/irida"; export type ProjectSample = { - key: number; // This should probably be a string + key: string; owner: boolean; coverage: any; // TODO: (Josh - 12/9/22) Figure this one out project: Project; diff --git a/src/main/webapp/resources/js/types/ant-design/index.d.ts b/src/main/webapp/resources/js/types/ant-design/index.d.ts index d547cc74f29..72aa50d1bcf 100644 --- a/src/main/webapp/resources/js/types/ant-design/index.d.ts +++ b/src/main/webapp/resources/js/types/ant-design/index.d.ts @@ -1,7 +1,6 @@ import { FilterConfirmProps, FilterValue, - SortOrder, TablePaginationConfig, } from "antd/es/table/interface"; @@ -33,21 +32,28 @@ export type TableFilterConfirmFn = (param?: FilterConfirmProps) => void; export type TableSortOrder = { property: string; - direction: SortOrder; -}[]; + direction: "asc" | "desc"; +}; export type TableOptions = { filters: TableFilters; pagination: TablePaginationConfig; - order: TableSortOrder | undefined; - search: string[]; + order: TableSortOrder[] | undefined; + search: TableSearch[]; reload?: number; }; +export type TableOperation = + | "IN" + | "MATCH" + | "MATCH_IN" + | "GREATER_THAN_EQUAL" + | "LESS_THAN_EQUAL"; + export type TableSearch = { property: string; value: string | string[]; - operation: "IN"; + operation: TableOperation; _file?: boolean; }; diff --git a/src/main/webapp/resources/js/utilities/table-utilities.ts b/src/main/webapp/resources/js/utilities/table-utilities.ts index 42e06254a9d..161b60d5e21 100644 --- a/src/main/webapp/resources/js/utilities/table-utilities.ts +++ b/src/main/webapp/resources/js/utilities/table-utilities.ts @@ -1,8 +1,14 @@ import moment from "moment"; -import { TableFilters, TableSearch, TableSortOrder } from "../types/ant-design"; +import { + TableFilters, + TableOperation, + TableSearch, + TableSortOrder, +} from "../types/ant-design"; import { SelectedSample } from "../types/irida"; import { SorterResult } from "antd/es/table/interface"; import { ProjectSample } from "../redux/endpoints/project-samples"; +import { direction } from "@antv/matrix-util/lib/ext"; /** * Format Sort Order from the Ant Design sorter object @@ -11,12 +17,14 @@ import { ProjectSample } from "../redux/endpoints/project-samples"; */ export function formatSort( sorter: SorterResult | SorterResult[] -): TableSortOrder | undefined { - const order = { ascend: "asc", descend: "desc" }; - const formatProperty = (property) => property.join("."); - const fromSorter = (item) => ({ - property: formatProperty(item.field), - direction: order[item.order], +): TableSortOrder[] | undefined { + const formatProperty = (property: string[]): string => property.join("."); + + const fromSorter = (item: SorterResult): TableSortOrder => ({ + property: Array.isArray(item.field) + ? formatProperty(item.field) + : (item.field as string), + direction: item.order === "descend" ? "desc" : "asc", }); if (Array.isArray(sorter)) { @@ -32,13 +40,14 @@ export function formatSort( * @param {array | object} filters Ant Design filters object * @returns array of Search objects */ -export function formatSearch(filters: TableFilters): string[] { - const defaultOperation = "MATCH"; +export function formatSearch(filters: TableFilters): TableSearch[] { + const defaultOperation: TableOperation = "MATCH"; const formattedSearch = []; for (const filter in filters) { for (const index in filters[filter]) { const value = filters[filter][index]; + // if we have two values, and they are both moment objects then add searches for date range. if ( Array.isArray(value) && @@ -67,6 +76,8 @@ export function formatSearch(filters: TableFilters): string[] { } } + console.log(formattedSearch); + return formattedSearch; } From cb7c774b1e40d208f4df6cb0735e8a4b454f31dd Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Mon, 12 Dec 2022 11:07:20 -0600 Subject: [PATCH 056/115] refactor: Updating types for useSamplesTableState --- src/main/webapp/package.json | 2 +- src/main/webapp/pnpm-lock.yaml | 14 +- .../{samples-table.tsx => SamplesTable.tsx} | 62 +------- .../components/column-search.tsx | 75 +++++++++ .../components/date-column-search.tsx | 25 ++- .../hooks/useAssociatedProjects.tsx | 16 ++ .../{ => hooks}/useSamplesTableState.tsx | 148 +++++++----------- .../resources/js/layouts/project-samples.tsx | 2 +- .../resources/js/redux/endpoints/project.ts | 18 ++- .../resources/js/types/ant-design/index.d.ts | 11 ++ 10 files changed, 206 insertions(+), 167 deletions(-) rename src/main/webapp/resources/js/components/project/samples-table/{samples-table.tsx => SamplesTable.tsx} (64%) create mode 100644 src/main/webapp/resources/js/components/project/samples-table/components/column-search.tsx create mode 100644 src/main/webapp/resources/js/components/project/samples-table/hooks/useAssociatedProjects.tsx rename src/main/webapp/resources/js/components/project/samples-table/{ => hooks}/useSamplesTableState.tsx (50%) diff --git a/src/main/webapp/package.json b/src/main/webapp/package.json index 0fff3ffe90c..702bc7aa8d0 100644 --- a/src/main/webapp/package.json +++ b/src/main/webapp/package.json @@ -47,7 +47,7 @@ "i": "^0.3.7", "immutability-helper": "^3.1.1", "lodash": "^4.17.21", - "moment": "^2.29.2", + "moment": "^2.29.4", "postcss-nested": "^5.0.6", "process": "^0.11.10", "qs": "^6.10.3", diff --git a/src/main/webapp/pnpm-lock.yaml b/src/main/webapp/pnpm-lock.yaml index 5d400d91f12..3378a180ca7 100644 --- a/src/main/webapp/pnpm-lock.yaml +++ b/src/main/webapp/pnpm-lock.yaml @@ -74,7 +74,7 @@ specifiers: less-loader: ^10.2.0 lodash: ^4.17.21 mini-css-extract-plugin: ^2.6.0 - moment: ^2.29.2 + moment: ^2.29.4 postcss: ^8.4.12 postcss-import: ^14.1.0 postcss-loader: ^6.2.1 @@ -145,7 +145,7 @@ dependencies: i: 0.3.7 immutability-helper: 3.1.1 lodash: 4.17.21 - moment: 2.29.2 + moment: 2.29.4 postcss-nested: 5.0.6_postcss@8.4.12 process: 0.11.10 qs: 6.10.3 @@ -3496,7 +3496,7 @@ packages: copy-to-clipboard: 3.3.1 lodash: 4.17.21 memoize-one: 6.0.0 - moment: 2.29.2 + moment: 2.29.4 rc-cascader: 3.2.6_sfoxds7t5ydpegc3knd667wn6m rc-checkbox: 2.3.2_sfoxds7t5ydpegc3knd667wn6m rc-collapse: 3.1.0_sfoxds7t5ydpegc3knd667wn6m @@ -6854,11 +6854,11 @@ packages: /moment-timezone/0.5.34: resolution: {integrity: sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==} dependencies: - moment: 2.29.2 + moment: 2.29.4 dev: false - /moment/2.29.2: - resolution: {integrity: sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==} + /moment/2.29.4: + resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} dev: false /mri/1.2.0: @@ -8219,7 +8219,7 @@ packages: classnames: 2.3.1 date-fns: 2.28.0 dayjs: 1.11.1 - moment: 2.29.2 + moment: 2.29.4 rc-trigger: 5.2.10_sfoxds7t5ydpegc3knd667wn6m rc-util: 5.20.1_sfoxds7t5ydpegc3knd667wn6m react: 17.0.2 diff --git a/src/main/webapp/resources/js/components/project/samples-table/samples-table.tsx b/src/main/webapp/resources/js/components/project/samples-table/SamplesTable.tsx similarity index 64% rename from src/main/webapp/resources/js/components/project/samples-table/samples-table.tsx rename to src/main/webapp/resources/js/components/project/samples-table/SamplesTable.tsx index 8e4fff755b3..02092bd3c42 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/samples-table.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/SamplesTable.tsx @@ -3,13 +3,12 @@ import { Button, Select, Space, Table } from "antd"; import type { TableColumnProps } from "antd/es"; import { SampleDetailViewer } from "../../samples/SampleDetailViewer"; import { SearchOutlined } from "@ant-design/icons"; -import useSamplesTableState from "./useSamplesTableState"; +import useSamplesTableState from "./hooks/useSamplesTableState"; import { TableFilterConfirmFn } from "../../../types/ant-design"; import { ProjectSample } from "../../../redux/endpoints/project-samples"; import SampleQuality from "./components/SampleQuality"; -import ProjectTag from "../../../pages/search/ProjectTag"; import { formatInternationalizedDateTime } from "../../../utilities/date-utilities"; -import getDateColumnSearchProps from "./components/date-column-search"; +import getColumnSearchProps from "./components/column-search"; /** * React component to render the project samples table @@ -18,61 +17,6 @@ import getDateColumnSearchProps from "./components/date-column-search"; export default function SamplesTable(): JSX.Element { const [samples, pagination, api] = useSamplesTableState(); - const getColumnSearchProps = ( - dataIndex: string | string[], - filterName = "", - placeholder = "" - ) => ({ - filterDropdown: ({ - setSelectedKeys, - selectedKeys, - confirm, - clearFilters, - }: { - setSelectedKeys: (selectedKeys: string[]) => void; - selectedKeys: string[]; - confirm: TableFilterConfirmFn; - clearFilters: () => void; - }) => ( -
- { + const values = Array.isArray(e) && e.length > 0 ? [e] : e; + setSelectedKeys(values as string[]); + confirm({ closeDropdown: false }); + }} + style={{ marginBottom: 8, display: "block" }} + /> + + + + +
+ ), + filterIcon: (filtered: boolean) => ( + + ), + }; +} diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/date-column-search.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/date-column-search.tsx index 47887cad4a3..0b5e044be5c 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/components/date-column-search.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/components/date-column-search.tsx @@ -1,12 +1,27 @@ import React from "react"; import { Button, DatePicker, Space } from "antd"; -import { IconSearch } from "../../../icons/Icons"; -import { FilterDropdownProps } from "antd/lib/table/interface"; +import type { FilterDropdownProps } from "antd/lib/table/interface"; import { SearchOutlined } from "@ant-design/icons"; +import type { + HandleClearSearchFn, + HandleSearchFn, +} from "../hooks/useSamplesTableState"; +import { ColumnSearchReturn } from "../../../../types/ant-design"; +import * as moment from "moment"; const { RangePicker } = DatePicker; -export default function getDateColumnSearchProps(filterName) { +export type DateColumnSearchFn = ( + filterName: string, + handleSearch: HandleSearchFn, + handleClearSearch: HandleClearSearchFn +) => ColumnSearchReturn; + +export default function getDateColumnSearchProps( + filterName: string, + handleSearch: HandleSearchFn, + handleClearSearch: HandleClearSearchFn +): ColumnSearchReturn { return { filterDropdown: ({ setSelectedKeys, @@ -17,7 +32,7 @@ export default function getDateColumnSearchProps(filterName) {
{ if (dates !== null) { setSelectedKeys([ @@ -43,7 +58,7 @@ export default function getDateColumnSearchProps(filterName) { - - -
- ), + }: FilterDropdownProps) => { + function onClear() { + if (typeof clearFilters === `function`) clearFilters(); + confirm({ closeDropdown: true }); + } + + function onFilter() { + confirm({ closeDropdown: true }); + } + + return ( +
+ + // + // + // + // + // + // ) : ( + // + // + // + // )} + // {samples.locked.length ? ( + // + // + // {i18n("LockedSamplesList.header")} + // + // + // + // ) : null} + // + // + // ); +} diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/SampleTools.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/SampleTools.tsx index 0124056e301..d30c2f3a5a1 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/components/SampleTools.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/components/SampleTools.tsx @@ -11,8 +11,15 @@ import { ShareAltOutlined, } from "@ant-design/icons"; import { IconDropDown } from "../../../icons/Icons"; -import { setBaseUrl } from "../../../../utilities/url-utilities"; +import { CONTEXT_PATH } from "../../../../data/routes"; +import MergeTrigger from "./merge/MergeTrigger"; +/** + * React component to render a dropdown list of actions that can be performed + * on samples. + * + * @constructor + */ export default function SampleTools() { const { projectId } = useParams(); const { data: details = {} } = useGetProjectDetailsQuery(projectId); @@ -22,14 +29,15 @@ export default function SampleTools() { () => ( {!details.remote ? ( - } - className={"t-merge"} - > - {i18n("SamplesMenu.merge")} - + + } + className={"t-merge"} + > + {i18n("SamplesMenu.merge")} + + ) : null} }> {i18n("SamplesMenu.import")} diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx new file mode 100644 index 00000000000..563d870c626 --- /dev/null +++ b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { useProjectSamples } from "../../useProjectSamplesContext"; +import { seperateLockedAndUnlockedSamples } from "../../../../../utilities/sample-utilities"; + +type MergeTriggerProps = { + children: JSX.Element; +}; + +export default function MergeTrigger({ + children, +}: MergeTriggerProps): JSX.Element { + const { state } = useProjectSamples(); + + function onClick() { + const [locked, unlocked] = seperateLockedAndUnlockedSamples( + Object.values(state.selection.selected) + ); + + if (unlocked.length > 2) { + } + } + + return React.cloneElement(children, { + onClick, + disabled: state.selection.count < 2, + }); +} diff --git a/src/main/webapp/resources/js/layouts/app-layout.tsx b/src/main/webapp/resources/js/layouts/app-layout.tsx index d889f1bccca..74b7373b442 100644 --- a/src/main/webapp/resources/js/layouts/app-layout.tsx +++ b/src/main/webapp/resources/js/layouts/app-layout.tsx @@ -13,7 +13,7 @@ const AppLayout = (): JSX.Element => ( - + }> diff --git a/src/main/webapp/resources/js/utilities/sample-utilities.ts b/src/main/webapp/resources/js/utilities/sample-utilities.ts new file mode 100644 index 00000000000..d6c7ff8095a --- /dev/null +++ b/src/main/webapp/resources/js/utilities/sample-utilities.ts @@ -0,0 +1,74 @@ +import { SelectedSample } from "../types/irida"; + +/** + * Determine which samples are locked and which are unlocked based on the + */ +export function seperateLockedAndUnlockedSamples( + samples: Array +) { + const unlocked: Array = []; + const locked: Array = []; + + samples.forEach((sample) => { + if (sample.owner) { + unlocked.push(sample); + } else { + locked.push(sample); + } + }); + return [unlocked, locked]; +} + +/** + * Determine if samples are valid, locked, or associated + * valid => samples user has ownership + * locked => sample user does not have ownership + * associated => samples that do not belong to the current project. + * @param {array} samples + * @param {number | string} projectId + * @returns {{valid: *[], associated: *[]}} + */ +export function validateSamplesForRemove(samples, projectId) { + const values = Object.values(samples), + valid = [], + associated = []; + values?.forEach((sample) => { + if (!isSampleFromCurrentProject(sample.projectId, projectId)) { + associated.push(sample); + } else { + valid.push(sample); + } + }); + return { valid, associated }; +} + +/** + * Determine if samples are valid or associated for using the linker command + * Valid => Not associated + * Associated => Belongs to a different project + * @param {array} samples + * @param {number | string} projectId + * @returns {{valid: *[], associated: *[]}} + */ +export function validateSamplesForLinker(samples, projectId) { + const values = Object.values(samples), + valid = [], + associated = []; + values.forEach((sample) => { + if (isSampleFromCurrentProject(sample.projectId, projectId)) { + valid.push(sample.id); + } else { + associated.push(sample); + } + }); + return { valid, associated }; +} + +/** + * Checks id from a sample against the current project + * @param {number | string} sampleProjectId + * @param {number | string} projectId + * @returns {boolean} + */ +const isSampleFromCurrentProject = (sampleProjectId, projectId) => + Number(sampleProjectId) === Number(projectId); From 525a6b4b3d86be2241873d788afd7503105a11dd Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Thu, 5 Jan 2023 05:19:29 -0600 Subject: [PATCH 079/115] chore: Working on updating merge modal code --- .../samples-table/components/MergeModal.tsx | 205 ------------------ .../components/merge/MergeModal.tsx | 199 +++++++++++++++++ .../components/merge/MergeTrigger.tsx | 30 ++- 3 files changed, 224 insertions(+), 210 deletions(-) delete mode 100644 src/main/webapp/resources/js/components/project/samples-table/components/MergeModal.tsx create mode 100644 src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeModal.tsx diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/MergeModal.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/MergeModal.tsx deleted file mode 100644 index a76ef273924..00000000000 --- a/src/main/webapp/resources/js/components/project/samples-table/components/MergeModal.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import { - Alert, - Checkbox, - Col, - Form, - Input, - Modal, - notification, - Radio, - Row, - Space, - Typography, -} from "antd"; -import React from "react"; -import { serverValidateSampleName } from "../../../../utilities/validation-utilities"; -import { useMergeMutation } from "../../../../apis/projects/samples"; -import LockedSamplesList from "./LockedSamplesList"; -import { useParams } from "react-router-dom"; -import { useGetProjectDetailsQuery } from "../../../../redux/endpoints/project"; -import { useProjectSamples } from "../useProjectSamplesContext"; -import { seperateLockedAndUnlockedSamples } from "../../../../utilities/sample-utilities"; -import ReactMarkdown from "react-markdown"; -import children = ReactMarkdown.propTypes.children; - -/** - * React element to display a modal to merge multiple samples into a single one. - * @constructor - */ -export default function MergeModal({ - children, -}: { - children: JSX.Element; -}): JSX.Element { - const { projectId } = useParams(); - const { data: details = {} } = useGetProjectDetailsQuery(projectId); - const { state, dispatch } = useProjectSamples(); - - const [locked, unlocked] = seperateLockedAndUnlockedSamples( - Object.values(state.selection.selected) - ); - - // const [merge, { isLoading }] = useMergeMutation(); - // - // const [renameSample, setRenameSample] = React.useState(false); - // const [error, setError] = React.useState(undefined); - // const [form] = Form.useForm(); - // - // const initialValues = { - // primary: samples.valid[0]?.id, - // newName: "", - // }; - // - // React.useEffect(() => { - // if (!renameSample) { - // form.setFieldsValue({ - // newName: "", - // }); - // } - // }, [form, renameSample]); - // - // // Server validate new name - // const validateName = async (name: string): Promise => { - // if (renameSample) { - // return serverValidateSampleName(name); - // } else { - // return Promise.resolve(); - // } - // }; - // - // const onSubmit = async () => { - // let values; - // - // try { - // values = await form.validateFields(); - // } catch { - // /* - // If the form is in an invalid state it will hit here. This will prevent the - // invalid date from being submitted and display the errors (if not already displayed) - // to the user. - // */ - // return; - // } - // const ids = samples.valid - // .map((sample) => sample.id) - // .filter((id) => id !== values.primary); - // - // const { message } = await merge({ - // projectId, - // request: { - // ...values, - // ids, - // }, - // }).unwrap(); - // - // notification.success({ - // message: i18n("MergeModal.success"), - // description: message, - // }); - // onComplete(); - // }; - - return React.cloneElement(children, { - onClick: () => alert("JELLO"), - disabled: state.selection.count < 2, - }); - - // return ( - // - // - // {samples.valid.length >= 2 ? ( - // <> - // - // - // - // {error !== undefined ? ( - // - // setError(undefined)} - // /> - // - // ) : null} - // - //
- // - // - // - // {samples.valid.map((sample) => { - // return ( - // - // {sample.sampleName} - // - // ); - // })} - // - // - // - // - // setRenameSample(e.target.checked)} - // > - // {i18n("MergeModal.rename")} - // - // ({ - // validator(_, value) { - // return validateName(value); - // }, - // }), - // ]} - // > - // - // - // - //
- // - // - // ) : ( - // - // - // - // )} - // {samples.locked.length ? ( - // - // - // {i18n("LockedSamplesList.header")} - // - // - // - // ) : null} - //
- //
- // ); -} diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeModal.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeModal.tsx new file mode 100644 index 00000000000..1073e20c4b5 --- /dev/null +++ b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeModal.tsx @@ -0,0 +1,199 @@ +import { + Alert, + Checkbox, + Col, + Form, + Input, + Modal, + notification, + Radio, + Row, + Space, + Typography, +} from "antd"; +import React from "react"; +import { serverValidateSampleName } from "../../../../../utilities/validation-utilities"; +import { useMergeMutation } from "../../../../../apis/projects/samples"; +import LockedSamplesList from "../LockedSamplesList"; +import { useParams } from "react-router-dom"; +import { useGetProjectDetailsQuery } from "../../../../../redux/endpoints/project"; +import { useProjectSamples } from "../../useProjectSamplesContext"; +import { seperateLockedAndUnlockedSamples } from "../../../../../utilities/sample-utilities"; +import ReactMarkdown from "react-markdown"; +import children = ReactMarkdown.propTypes.children; +import { SelectedSample } from "../../../../../types/irida"; + +/** + * React element to display a modal to merge multiple samples into a single one. + * @constructor + */ +export default function MergeModal({ + visible, + locked, + unlocked, +}: { + visible: boolean; + locked: Array; + unlocked: Array; +}): JSX.Element { + const { projectId } = useParams(); + + // const [merge, { isLoading }] = useMergeMutation(); + // + // const [renameSample, setRenameSample] = React.useState(false); + // const [error, setError] = React.useState(undefined); + // const [form] = Form.useForm(); + // + // const initialValues = { + // primary: samples.valid[0]?.id, + // newName: "", + // }; + // + // React.useEffect(() => { + // if (!renameSample) { + // form.setFieldsValue({ + // newName: "", + // }); + // } + // }, [form, renameSample]); + // + // // Server validate new name + // const validateName = async (name: string): Promise => { + // if (renameSample) { + // return serverValidateSampleName(name); + // } else { + // return Promise.resolve(); + // } + // }; + // + // const onSubmit = async () => { + // let values; + // + // try { + // values = await form.validateFields(); + // } catch { + // /* + // If the form is in an invalid state it will hit here. This will prevent the + // invalid date from being submitted and display the errors (if not already displayed) + // to the user. + // */ + // return; + // } + // const ids = samples.valid + // .map((sample) => sample.id) + // .filter((id) => id !== values.primary); + // + // const { message } = await merge({ + // projectId, + // request: { + // ...values, + // ids, + // }, + // }).unwrap(); + // + // notification.success({ + // message: i18n("MergeModal.success"), + // description: message, + // }); + // onComplete(); + // }; + + return ( + + + {samples.valid.length >= 2 ? ( + <> + + + + {error !== undefined ? ( + + setError(undefined)} + /> + + ) : null} + +
+ + + + {samples.valid.map((sample) => { + return ( + + {sample.sampleName} + + ); + })} + + + + + setRenameSample(e.target.checked)} + > + {i18n("MergeModal.rename")} + + ({ + validator(_, value) { + return validateName(value); + }, + }), + ]} + > + + + +
+ + + ) : ( + + + + )} + {samples.locked.length ? ( + + + {i18n("LockedSamplesList.header")} + + + + ) : null} +
+
+ ); +} diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx index 563d870c626..1e52ae36262 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx @@ -1,6 +1,9 @@ -import React from "react"; +import React, { Suspense, useState } from "react"; import { useProjectSamples } from "../../useProjectSamplesContext"; import { seperateLockedAndUnlockedSamples } from "../../../../../utilities/sample-utilities"; +import { SelectedSample } from "../../../../../types/irida"; + +const MergeModal = React.lazy(() => import("./MergeModal")); type MergeTriggerProps = { children: JSX.Element; @@ -10,6 +13,11 @@ export default function MergeTrigger({ children, }: MergeTriggerProps): JSX.Element { const { state } = useProjectSamples(); + const [samples, setSamples] = + useState<[Array> | undefined]>( + undefined + ); + const [visible, setVisible] = useState(false); function onClick() { const [locked, unlocked] = seperateLockedAndUnlockedSamples( @@ -17,11 +25,23 @@ export default function MergeTrigger({ ); if (unlocked.length > 2) { + setSamples([locked, unlocked]); + } else { + alert("NOT ENOUGHT SMAPLES"); } } - return React.cloneElement(children, { - onClick, - disabled: state.selection.count < 2, - }); + return ( + <> + {React.cloneElement(children, { + onClick, + disabled: state.selection.count < 2, + })} + {visible ? ( + }> + + + ) : null} + + ); } From fc4400fc1b8693e6c978a2fa3e481d8bad4dcdda Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Wed, 11 Jan 2023 09:21:25 -0600 Subject: [PATCH 080/115] chore: Getting merge modal to work with new code --- .../components/merge/MergeModal.tsx | 92 ++++++++++--------- .../components/merge/MergeTrigger.tsx | 33 ++++--- .../js/redux/endpoints/project-samples.ts | 12 ++- .../js/utilities/sample-utilities.ts | 2 +- ...n-utilities.js => validation-utilities.ts} | 9 +- 5 files changed, 86 insertions(+), 62 deletions(-) rename src/main/webapp/resources/js/utilities/{validation-utilities.js => validation-utilities.ts} (95%) diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeModal.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeModal.tsx index 1073e20c4b5..b6813a7a34c 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeModal.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeModal.tsx @@ -1,3 +1,5 @@ +import React, { useCallback, useState } from "react"; + import { Alert, Checkbox, @@ -5,23 +7,17 @@ import { Form, Input, Modal, - notification, Radio, Row, Space, Typography, } from "antd"; -import React from "react"; -import { serverValidateSampleName } from "../../../../../utilities/validation-utilities"; -import { useMergeMutation } from "../../../../../apis/projects/samples"; -import LockedSamplesList from "../LockedSamplesList"; +import type { Samples } from "./MergeTrigger"; import { useParams } from "react-router-dom"; -import { useGetProjectDetailsQuery } from "../../../../../redux/endpoints/project"; -import { useProjectSamples } from "../../useProjectSamplesContext"; -import { seperateLockedAndUnlockedSamples } from "../../../../../utilities/sample-utilities"; -import ReactMarkdown from "react-markdown"; -import children = ReactMarkdown.propTypes.children; +import { useMergeSamplesMutation } from "../../../../../redux/endpoints/project-samples"; import { SelectedSample } from "../../../../../types/irida"; +import LockedSamplesList from "../LockedSamplesList"; +import { serverValidateSampleName } from "../../../../../utilities/validation-utilities"; /** * React element to display a modal to merge multiple samples into a single one. @@ -29,25 +25,27 @@ import { SelectedSample } from "../../../../../types/irida"; */ export default function MergeModal({ visible, - locked, - unlocked, + samples, + hideModal, }: { visible: boolean; - locked: Array; - unlocked: Array; + samples: NonNullable; + hideModal: () => void; }): JSX.Element { + console.log(samples); + const [unlocked, locked] = samples; const { projectId } = useParams(); - // const [merge, { isLoading }] = useMergeMutation(); - // - // const [renameSample, setRenameSample] = React.useState(false); - // const [error, setError] = React.useState(undefined); - // const [form] = Form.useForm(); - // - // const initialValues = { - // primary: samples.valid[0]?.id, - // newName: "", - // }; + const [merge, { isLoading }] = useMergeSamplesMutation(); + const [renameSample, setRenameSample] = useState(false); + + const [error, setError] = useState(undefined); + const [form] = Form.useForm(); + + const initialValues = { + primary: unlocked[0]?.id, + newName: "", + }; // // React.useEffect(() => { // if (!renameSample) { @@ -56,15 +54,15 @@ export default function MergeModal({ // }); // } // }, [form, renameSample]); - // - // // Server validate new name - // const validateName = async (name: string): Promise => { - // if (renameSample) { - // return serverValidateSampleName(name); - // } else { - // return Promise.resolve(); - // } - // }; + + // Server validate new name + const validateName = async (name: string): Promise => { + if (renameSample) { + return serverValidateSampleName(name); + } else { + return Promise.resolve(); + } + }; // // const onSubmit = async () => { // let values; @@ -79,7 +77,7 @@ export default function MergeModal({ // */ // return; // } - // const ids = samples.valid + // const ids = unlocked // .map((sample) => sample.id) // .filter((id) => id !== values.primary); // @@ -98,22 +96,28 @@ export default function MergeModal({ // onComplete(); // }; + const onSubmit = useCallback(() => hideModal(), [hideModal]); + const clearError = useCallback(() => setError(undefined), []); + const toggleRenameSample = useCallback( + (e) => setRenameSample(e.target.checked), + [] + ); + return ( - {samples.valid.length >= 2 ? ( + {unlocked.length >= 2 ? ( <> setError(undefined)} + onClose={clearError} /> ) : null} @@ -142,7 +146,7 @@ export default function MergeModal({ > - {samples.valid.map((sample) => { + {unlocked.map((sample) => { return ( {sample.sampleName} @@ -156,14 +160,14 @@ export default function MergeModal({ setRenameSample(e.target.checked)} + onChange={toggleRenameSample} > {i18n("MergeModal.rename")} ({ + () => ({ validator(_, value) { return validateName(value); }, @@ -185,12 +189,12 @@ export default function MergeModal({ /> )} - {samples.locked.length ? ( + {locked.length ? ( {i18n("LockedSamplesList.header")} - + ) : null} diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx index 1e52ae36262..62c532e044f 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx @@ -1,7 +1,8 @@ -import React, { Suspense, useState } from "react"; -import { useProjectSamples } from "../../useProjectSamplesContext"; -import { seperateLockedAndUnlockedSamples } from "../../../../../utilities/sample-utilities"; +import React, { Suspense, useCallback, useState } from "react"; + import { SelectedSample } from "../../../../../types/irida"; +import { separateLockedAndUnlockedSamples } from "../../../../../utilities/sample-utilities"; +import { useProjectSamples } from "../../useProjectSamplesContext"; const MergeModal = React.lazy(() => import("./MergeModal")); @@ -9,28 +10,32 @@ type MergeTriggerProps = { children: JSX.Element; }; +export type Samples = + | [Array, Array] + | undefined; + export default function MergeTrigger({ children, }: MergeTriggerProps): JSX.Element { const { state } = useProjectSamples(); - const [samples, setSamples] = - useState<[Array> | undefined]>( - undefined - ); + const [samples, setSamples] = useState(undefined); const [visible, setVisible] = useState(false); function onClick() { - const [locked, unlocked] = seperateLockedAndUnlockedSamples( + const [unlocked, locked] = separateLockedAndUnlockedSamples( Object.values(state.selection.selected) ); - if (unlocked.length > 2) { - setSamples([locked, unlocked]); + if (unlocked.length >= 2) { + setSamples([unlocked, locked]); + setVisible(true); } else { - alert("NOT ENOUGHT SMAPLES"); + alert("NOT ENOUGH SAMPLES"); } } + const hideModal = useCallback(() => setVisible(false), []); + return ( <> {React.cloneElement(children, { @@ -39,7 +44,11 @@ export default function MergeTrigger({ })} {visible ? ( }> - + ) : null} diff --git a/src/main/webapp/resources/js/redux/endpoints/project-samples.ts b/src/main/webapp/resources/js/redux/endpoints/project-samples.ts index 405c6c6e233..c5d3b6450e6 100644 --- a/src/main/webapp/resources/js/redux/endpoints/project-samples.ts +++ b/src/main/webapp/resources/js/redux/endpoints/project-samples.ts @@ -1,14 +1,16 @@ +1; /** * @fileoverview Project > Samples AP for redux-toolkit */ -import { TableOptions } from "../../types/ant-design"; import { Project, Sample, SelectedSample, TableResponse, } from "../../types/irida"; + +import { TableOptions } from "../../types/ant-design"; import { api } from "./api"; export type ProjectSample = { @@ -48,10 +50,18 @@ export const projectSamplesApi = api.injectEndpoints({ body, }), }), + mergeSamples: build.mutation({ + query: ({ projectId, body }) => ({ + url: `/projects/${projectId}/samples/merge`, + method: `POST`, + body, + }), + }), }), }); export const { useFetchPagedSamplesQuery, useLazyFetchMinimalSamplesForFilteredProjectQuery, + useMergeSamplesMutation, } = projectSamplesApi; diff --git a/src/main/webapp/resources/js/utilities/sample-utilities.ts b/src/main/webapp/resources/js/utilities/sample-utilities.ts index d6c7ff8095a..5b6a3bbbff9 100644 --- a/src/main/webapp/resources/js/utilities/sample-utilities.ts +++ b/src/main/webapp/resources/js/utilities/sample-utilities.ts @@ -3,7 +3,7 @@ import { SelectedSample } from "../types/irida"; /** * Determine which samples are locked and which are unlocked based on the */ -export function seperateLockedAndUnlockedSamples( +export function separateLockedAndUnlockedSamples( samples: Array ) { const unlocked: Array = []; diff --git a/src/main/webapp/resources/js/utilities/validation-utilities.js b/src/main/webapp/resources/js/utilities/validation-utilities.ts similarity index 95% rename from src/main/webapp/resources/js/utilities/validation-utilities.js rename to src/main/webapp/resources/js/utilities/validation-utilities.ts index 6af973a6a60..30c7aaa3f5b 100644 --- a/src/main/webapp/resources/js/utilities/validation-utilities.js +++ b/src/main/webapp/resources/js/utilities/validation-utilities.ts @@ -83,17 +83,18 @@ export const validateEmail = (email) => /** * Server validate a sample name asynchronously - * @param {string} name - Sample name to validate - * @returns {Promise} + * @param name - Sample name to validate */ -export const serverValidateSampleName = async (name) => { +export async function serverValidateSampleName( + name: string +): Promise { const data = await validateSampleName(name); if (data.status === "success") { return Promise.resolve(); } else { return Promise.reject(new Error(data.help)); } -}; +} /** * Validate a password From 3d4319440f4fbd2e761e3ca9b63d5914467fbf28 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Wed, 11 Jan 2023 10:55:44 -0600 Subject: [PATCH 081/115] chore: Working merge samples --- .../project/samples-table/SamplesTable.tsx | 31 ++++--- .../components/merge/MergeModal.tsx | 90 ++++++++++--------- .../js/redux/endpoints/project-samples.ts | 13 +++ .../resources/js/redux/endpoints/tags.ts | 2 + 4 files changed, 78 insertions(+), 58 deletions(-) diff --git a/src/main/webapp/resources/js/components/project/samples-table/SamplesTable.tsx b/src/main/webapp/resources/js/components/project/samples-table/SamplesTable.tsx index 56075dd1300..1e7a9ca8072 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/SamplesTable.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/SamplesTable.tsx @@ -6,7 +6,7 @@ import type { TablePaginationConfig, } from "antd/es/table/interface"; import type { CheckboxChangeEvent } from "antd/lib/checkbox"; -import React from "react"; +import React, { useCallback } from "react"; import { useParams } from "react-router-dom"; import ProjectTag from "../../../pages/search/ProjectTag"; import { useGetAssociatedProjectsQuery } from "../../../redux/endpoints/project"; @@ -88,20 +88,23 @@ export default function SamplesTable(): JSX.Element { * * @param e Event fired when the checkbox is clicked */ - async function updateSelectAll(e: CheckboxChangeEvent) { - if (e.target.checked) { - // Need to get all the associated projects - const { data } = await trigger({ - projectId: Number(projectId), - body: state.options, - }); - if (data) { - dispatch({ type: "selectAllSamples", payload: { samples: data } }); + const updateSelectAll = useCallback( + async function updateSelectAll(e: CheckboxChangeEvent) { + if (e.target.checked) { + // Need to get all the associated projects + const { data } = await trigger({ + projectId: Number(projectId), + body: state.options, + }); + if (data) { + dispatch({ type: "selectAllSamples", payload: { samples: data } }); + } + } else { + dispatch({ type: "deselectAllSamples" }); } - } else { - dispatch({ type: "deselectAllSamples" }); - } - } + }, + [dispatch, projectId, state.options, trigger] + ); const columns: TableColumnProps[] = [ { diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeModal.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeModal.tsx index b6813a7a34c..ba49aca7454 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeModal.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeModal.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { Alert, @@ -7,6 +7,7 @@ import { Form, Input, Modal, + notification, Radio, Row, Space, @@ -15,9 +16,9 @@ import { import type { Samples } from "./MergeTrigger"; import { useParams } from "react-router-dom"; import { useMergeSamplesMutation } from "../../../../../redux/endpoints/project-samples"; -import { SelectedSample } from "../../../../../types/irida"; import LockedSamplesList from "../LockedSamplesList"; import { serverValidateSampleName } from "../../../../../utilities/validation-utilities"; +import { useProjectSamples } from "../../useProjectSamplesContext"; /** * React element to display a modal to merge multiple samples into a single one. @@ -32,7 +33,7 @@ export default function MergeModal({ samples: NonNullable; hideModal: () => void; }): JSX.Element { - console.log(samples); + const { dispatch } = useProjectSamples(); const [unlocked, locked] = samples; const { projectId } = useParams(); @@ -46,14 +47,14 @@ export default function MergeModal({ primary: unlocked[0]?.id, newName: "", }; - // - // React.useEffect(() => { - // if (!renameSample) { - // form.setFieldsValue({ - // newName: "", - // }); - // } - // }, [form, renameSample]); + + useEffect(() => { + if (!renameSample) { + form.setFieldsValue({ + newName: "", + }); + } + }, [form, renameSample]); // Server validate new name const validateName = async (name: string): Promise => { @@ -63,40 +64,40 @@ export default function MergeModal({ return Promise.resolve(); } }; - // - // const onSubmit = async () => { - // let values; - // - // try { - // values = await form.validateFields(); - // } catch { - // /* - // If the form is in an invalid state it will hit here. This will prevent the - // invalid date from being submitted and display the errors (if not already displayed) - // to the user. - // */ - // return; - // } - // const ids = unlocked - // .map((sample) => sample.id) - // .filter((id) => id !== values.primary); - // - // const { message } = await merge({ - // projectId, - // request: { - // ...values, - // ids, - // }, - // }).unwrap(); - // - // notification.success({ - // message: i18n("MergeModal.success"), - // description: message, - // }); - // onComplete(); - // }; - const onSubmit = useCallback(() => hideModal(), [hideModal]); + const onSubmit = useCallback(async () => { + let values; + + try { + values = await form.validateFields(); + } catch { + /* + If the form is in an invalid state it will hit here. This will prevent the + invalid date from being submitted and display the errors (if not already displayed) + to the user. + */ + return; + } + const ids = unlocked + .map((sample) => sample.id) + .filter((id) => id !== values.primary); + + const { message } = await merge({ + projectId, + body: { + ...values, + ids, + }, + }).unwrap(); + + notification.success({ + message: i18n("MergeModal.success"), + description: message, + }); + dispatch({ type: `deselectAllSamples` }); + hideModal(); + }, [dispatch, form, hideModal, merge, projectId, unlocked]); + const clearError = useCallback(() => setError(undefined), []); const toggleRenameSample = useCallback( (e) => setRenameSample(e.target.checked), @@ -113,6 +114,7 @@ export default function MergeModal({ okButtonProps={{ loading: isLoading, }} + onOk={onSubmit} cancelText={i18n("MergeModal.cancelText")} width={600} > diff --git a/src/main/webapp/resources/js/redux/endpoints/project-samples.ts b/src/main/webapp/resources/js/redux/endpoints/project-samples.ts index c5d3b6450e6..0db56ce9de6 100644 --- a/src/main/webapp/resources/js/redux/endpoints/project-samples.ts +++ b/src/main/webapp/resources/js/redux/endpoints/project-samples.ts @@ -1,3 +1,5 @@ +import { TAG_PROJECT_SAMPLES } from "./tags"; + 1; /** * @fileoverview Project > Samples AP for redux-toolkit @@ -39,6 +41,16 @@ export const projectSamplesApi = api.injectEndpoints({ method: `POST`, body, }), + providesTags: (result) => + result + ? [ + ...result.content.map(({ key }) => ({ + type: TAG_PROJECT_SAMPLES, + id: key, + })), + { type: TAG_PROJECT_SAMPLES, id: "LIST" }, + ] + : [{ type: TAG_PROJECT_SAMPLES, id: "LIST" }], }), fetchMinimalSamplesForFilteredProject: build.query< Array, @@ -56,6 +68,7 @@ export const projectSamplesApi = api.injectEndpoints({ method: `POST`, body, }), + invalidatesTags: [{ type: TAG_PROJECT_SAMPLES, id: "LIST" }], }), }), }); diff --git a/src/main/webapp/resources/js/redux/endpoints/tags.ts b/src/main/webapp/resources/js/redux/endpoints/tags.ts index 1b932e0e0d1..9d35dff9906 100644 --- a/src/main/webapp/resources/js/redux/endpoints/tags.ts +++ b/src/main/webapp/resources/js/redux/endpoints/tags.ts @@ -7,6 +7,7 @@ export const TAG_ANNOUNCEMENT_COUNT = "tag-announcement-count"; export const TAG_ANNOUNCEMENTS_UNREAD = "tag-announcements-unread"; export const TAG_CART_COUNT = "tag-cart-count"; export const TAG_PROJECT = "tag-project"; +export const TAG_PROJECT_SAMPLES = "tag-project-sample"; export const TAG_USER = "tag-user"; export const PROVIDED_TAGS = [ @@ -14,5 +15,6 @@ export const PROVIDED_TAGS = [ TAG_ANNOUNCEMENTS_UNREAD, TAG_CART_COUNT, TAG_PROJECT, + TAG_PROJECT_SAMPLES, TAG_USER, ]; From 39dba7dbf816e0ac1a6972d3436050a367cde818 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Wed, 11 Jan 2023 15:13:04 -0600 Subject: [PATCH 082/115] chore: Cleaned up type gaurd --- .../components/LockedSamplesList.tsx | 66 +++++++++++-------- .../components/merge/MergeModal.tsx | 13 ++-- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/LockedSamplesList.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/LockedSamplesList.tsx index 5282f78ac2c..e2468e77fe9 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/components/LockedSamplesList.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/components/LockedSamplesList.tsx @@ -1,14 +1,20 @@ -import React from "react"; +import React, { useCallback } from "react"; import { LockTwoTone } from "@ant-design/icons"; import { Avatar, Button, List } from "antd"; -import { red6 } from "../../../../styles/colors"; -import { SampleDetailViewer } from "../../../../components/samples/SampleDetailViewer"; +import { SampleDetailViewer } from "../../../samples/SampleDetailViewer"; import { ProjectSample } from "../../../../redux/endpoints/project-samples"; +import { SelectedSample } from "../../../../types/irida"; type LockedSamplesListParams = { - locked: Array; + locked: Array | Array; }; +function isItemProjectSample( + item: ProjectSample | SelectedSample +): item is ProjectSample { + return "project" in item; +} + /** * React Element to render a list of locked samples. Use this when they * cannot be used in the requested action (e.g. remove). @@ -18,35 +24,41 @@ type LockedSamplesListParams = { */ export default function LockedSamplesList({ locked, -}: LockedSamplesListParams): JSX.Element { +}: LockedSamplesListParams): JSX.Element | null { + const renderItem = useCallback((item: SelectedSample | ProjectSample) => { + const isProjectSample = isItemProjectSample(item); + + return ( + + } + style={{ backgroundColor: "transparent" }} + /> + } + title={ + + + + } + /> + + ); + }, []); + return ( ( - - } - style={{ backgroundColor: "transparent" }} - /> - } - title={ - - - - } - /> - - )} + renderItem={renderItem} /> ); } diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeModal.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeModal.tsx index ba49aca7454..de85a448ef3 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeModal.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeModal.tsx @@ -20,6 +20,11 @@ import LockedSamplesList from "../LockedSamplesList"; import { serverValidateSampleName } from "../../../../../utilities/validation-utilities"; import { useProjectSamples } from "../../useProjectSamplesContext"; +type FormValues = { + primary: number; + newName: string; +}; + /** * React element to display a modal to merge multiple samples into a single one. * @constructor @@ -66,16 +71,12 @@ export default function MergeModal({ }; const onSubmit = useCallback(async () => { - let values; + let values: FormValues; try { values = await form.validateFields(); } catch { - /* - If the form is in an invalid state it will hit here. This will prevent the - invalid date from being submitted and display the errors (if not already displayed) - to the user. - */ + // Invalid state, preventing the invalid data from submitting and display the errors to the user. return; } const ids = unlocked From 2e2c27689b52f81549584d9a5083350fd58f710b Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Thu, 12 Jan 2023 10:45:29 -0600 Subject: [PATCH 083/115] chore: Clean up typings for data-column-search.tsx --- .../components/TableDateRangeFilter.tsx | 70 +++++++++++++++++++ .../components/date-column-search.tsx | 59 ++-------------- .../useProjectSamplesContext.tsx | 4 +- .../js/redux/endpoints/project-samples.ts | 4 +- .../resources/js/redux/endpoints/project.ts | 16 +---- 5 files changed, 79 insertions(+), 74 deletions(-) create mode 100644 src/main/webapp/resources/js/components/project/samples-table/components/TableDateRangeFilter.tsx diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/TableDateRangeFilter.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/TableDateRangeFilter.tsx new file mode 100644 index 00000000000..5f95d03552f --- /dev/null +++ b/src/main/webapp/resources/js/components/project/samples-table/components/TableDateRangeFilter.tsx @@ -0,0 +1,70 @@ +import React, { Key, useCallback } from "react"; +import { Moment } from "moment/moment"; +import { FilterDropdownProps } from "antd/lib/table/interface"; +import { SearchOutlined } from "@ant-design/icons"; +import { Button, DatePicker, Space } from "antd"; +import { RangePickerProps } from "antd/es/date-picker"; + +const { RangePicker } = DatePicker; + +/** + * React component for date range filter for ant design tables. + */ +export default function TableDateRangeFilter({ + filterClassName, + setSelectedKeys, + selectedKeys, + confirm, + clearFilters, +}: FilterDropdownProps & { filterClassName: string }): JSX.Element { + const onChange: RangePickerProps["onChange"] = (dates) => { + const [firstDate, secondDate] = dates as [Moment, Moment]; + const startOf = firstDate.startOf("day"); + const endOf = secondDate.endOf("day"); + const range = [startOf, endOf] as unknown as Key; + setSelectedKeys([range]); + confirm({ closeDropdown: false }); + }; + + const onClear = () => { + if (typeof clearFilters === `function`) clearFilters(); + confirm({ closeDropdown: true }); + }; + + const onFilter = () => confirm({ closeDropdown: true }); + + const onChangeCallback = useCallback(onChange, [confirm, setSelectedKeys]); + const onClearCallback = useCallback(onClear, [clearFilters, confirm]); + const onFilterCallback = useCallback(onFilter, [confirm]); + + const values = selectedKeys[0] as unknown as [Moment, Moment]; + + return ( +
+
+ +
+ + + + +
+ ); +} diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/date-column-search.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/date-column-search.tsx index 18c2876ed7f..524a2923089 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/components/date-column-search.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/components/date-column-search.tsx @@ -1,13 +1,8 @@ -import { SearchOutlined } from "@ant-design/icons"; -import { Button, DatePicker, Space } from "antd"; import type { FilterDropdownProps } from "antd/lib/table/interface"; -import { Moment } from "moment"; import React from "react"; import { ColumnSearchReturn } from "../../../../types/ant-design"; -import { RangePickerDateProps } from "antd/es/date-picker/generatePicker"; import TableSearchFilter from "./TableSearchFilter"; - -const { RangePicker } = DatePicker; +import TableDateRangeFilter from "./TableDateRangeFilter"; export type DateColumnSearchFn = (filterName: string) => ColumnSearchReturn; @@ -19,55 +14,9 @@ export default function getDateColumnSearchProps( filterName: string ): ColumnSearchReturn { return { - filterDropdown: ({ - setSelectedKeys, - selectedKeys, - confirm, - clearFilters, - }: FilterDropdownProps) => { - function onChange(dates: RangePickerDateProps) { - setSelectedKeys([[dates[0].startOf("day"), dates[1].endOf("day")]]); - confirm({ closeDropdown: false }); - } - - function onClear() { - if (typeof clearFilters === `function`) clearFilters(); - confirm({ closeDropdown: true }); - } - - function onFilter() { - confirm({ closeDropdown: true }); - } - - return ( -
-
- -
- - - - -
- ); - }, + filterDropdown: (props: FilterDropdownProps) => ( + + ), filterIcon: (filtered) => , }; } diff --git a/src/main/webapp/resources/js/components/project/samples-table/useProjectSamplesContext.tsx b/src/main/webapp/resources/js/components/project/samples-table/useProjectSamplesContext.tsx index 07b93a177c4..cc4f887ef06 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/useProjectSamplesContext.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/useProjectSamplesContext.tsx @@ -119,7 +119,7 @@ function formatTableOptions( * @param state - current state of the page * @param selected - whether the item is selected * @param item - the project sample - * @returns new state of the application + * @returns new state of the app */ function rowSelectionChange( state: State, @@ -140,7 +140,7 @@ function rowSelectionChange( * Select all samples in a project, including visible associated projects. * @param state - current state of the page * @param samples - minimal representation of samples - * @returns new state of the application + * @returns new state of the app */ function selectAllSamples( state: State, diff --git a/src/main/webapp/resources/js/redux/endpoints/project-samples.ts b/src/main/webapp/resources/js/redux/endpoints/project-samples.ts index 0db56ce9de6..d2a71f1be8d 100644 --- a/src/main/webapp/resources/js/redux/endpoints/project-samples.ts +++ b/src/main/webapp/resources/js/redux/endpoints/project-samples.ts @@ -36,7 +36,7 @@ export const projectSamplesApi = api.injectEndpoints({ TableResponse, FetchPagedSamplesParams >({ - query: ({ projectId, body }) => ({ + query: ({ projectId, body }: FetchPagedSamplesParams) => ({ url: `/projects/${projectId}/samples`, method: `POST`, body, @@ -56,7 +56,7 @@ export const projectSamplesApi = api.injectEndpoints({ Array, FetchPagedSamplesParams >({ - query: ({ projectId, body }) => ({ + query: ({ projectId, body }: FetchPagedSamplesParams) => ({ url: `/projects/${projectId}/samples/ids`, method: `POST`, body, diff --git a/src/main/webapp/resources/js/redux/endpoints/project.ts b/src/main/webapp/resources/js/redux/endpoints/project.ts index 18c47f1f844..844e6e98483 100644 --- a/src/main/webapp/resources/js/redux/endpoints/project.ts +++ b/src/main/webapp/resources/js/redux/endpoints/project.ts @@ -1,6 +1,5 @@ import { api } from "./api"; import { TAG_PROJECT } from "./tags"; -import { BaseQueryArg } from "@reduxjs/toolkit/dist/query/baseQueryTypes"; import { AssociatedProjectsResponse } from "../../apis/projects/associated-projects"; /** @@ -23,21 +22,8 @@ export const projectApi = api.injectEndpoints({ { type: TAG_PROJECT, projectId }, ], }), - listSamples: build.query({ - query: ({ - projectId, - body, - }: { - projectId: number | string; - body: {}; - }) => ({ - url: `projects/${projectId}/samples`, - method: "POST", - body, - }), - }), getAssociatedProjects: build.query({ - query: (projectId: number): BaseQueryArg => ({ + query: (projectId: number) => ({ url: `projects/associated/list`, params: { projectId }, }), From bec1fba9d4fe9fb8b0b5a869a884feae39f34336 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Thu, 12 Jan 2023 11:48:46 -0600 Subject: [PATCH 084/115] chore: Comment cleanup --- .../components/LockedSamplesList.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/LockedSamplesList.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/LockedSamplesList.tsx index e2468e77fe9..2476902c611 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/components/LockedSamplesList.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/components/LockedSamplesList.tsx @@ -1,14 +1,20 @@ +import { Avatar, Button, List } from "antd"; import React, { useCallback } from "react"; + import { LockTwoTone } from "@ant-design/icons"; -import { Avatar, Button, List } from "antd"; -import { SampleDetailViewer } from "../../../samples/SampleDetailViewer"; import { ProjectSample } from "../../../../redux/endpoints/project-samples"; import { SelectedSample } from "../../../../types/irida"; +import { SampleDetailViewer } from "../../../samples/SampleDetailViewer"; type LockedSamplesListParams = { - locked: Array | Array; + locked: Array | Array | undefined; }; +/** + * Type guard to see if item is a project sample or selected sample, + * making this component more reusable. + * @param item + */ function isItemProjectSample( item: ProjectSample | SelectedSample ): item is ProjectSample { @@ -18,9 +24,7 @@ function isItemProjectSample( /** * React Element to render a list of locked samples. Use this when they * cannot be used in the requested action (e.g. remove). - * @param {array} locked - list of samples that are locked from modification - * @returns {JSX.Element} - * @constructor + * @param locked - list of samples that are locked from modification */ export default function LockedSamplesList({ locked, From d20baf2921b7b0cd3c1784e178a62b200a72ba80 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Thu, 12 Jan 2023 14:47:52 -0600 Subject: [PATCH 085/115] chore: Make sure samples in populated before opening the modal --- .../project/samples-table/components/merge/MergeTrigger.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx index 62c532e044f..a1120efeb5a 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx @@ -42,7 +42,7 @@ export default function MergeTrigger({ onClick, disabled: state.selection.count < 2, })} - {visible ? ( + {visible && samples !== undefined ? ( }> Date: Thu, 12 Jan 2023 14:59:47 -0600 Subject: [PATCH 086/115] chore: Fixed typing issue --- .../components/LockedSamplesList.tsx | 34 +++++-------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/LockedSamplesList.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/LockedSamplesList.tsx index 2476902c611..9bbbb866317 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/components/LockedSamplesList.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/components/LockedSamplesList.tsx @@ -2,25 +2,13 @@ import { Avatar, Button, List } from "antd"; import React, { useCallback } from "react"; import { LockTwoTone } from "@ant-design/icons"; -import { ProjectSample } from "../../../../redux/endpoints/project-samples"; import { SelectedSample } from "../../../../types/irida"; import { SampleDetailViewer } from "../../../samples/SampleDetailViewer"; type LockedSamplesListParams = { - locked: Array | Array | undefined; + locked: Array | undefined; }; -/** - * Type guard to see if item is a project sample or selected sample, - * making this component more reusable. - * @param item - */ -function isItemProjectSample( - item: ProjectSample | SelectedSample -): item is ProjectSample { - return "project" in item; -} - /** * React Element to render a list of locked samples. Use this when they * cannot be used in the requested action (e.g. remove). @@ -29,10 +17,8 @@ function isItemProjectSample( export default function LockedSamplesList({ locked, }: LockedSamplesListParams): JSX.Element | null { - const renderItem = useCallback((item: SelectedSample | ProjectSample) => { - const isProjectSample = isItemProjectSample(item); - - return ( + const renderItem = useCallback( + (item: SelectedSample) => ( } title={ - - + + } /> - ); - }, []); + ), + [] + ); return ( Date: Fri, 13 Jan 2023 09:12:52 -0600 Subject: [PATCH 087/115] chore: Added info modal if incorrect samples selected to merge --- src/main/resources/i18n/messages.properties | 12 ++++------ .../components/merge/MergeTrigger.tsx | 24 +++++++++++++++---- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 95ac6eeb1be..75f9541a78a 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -710,12 +710,6 @@ project.samples.select.selectAll.warning.message=If this project contains a larg project.samples.rename-success=Successfully updated sample name project.samples.remove-warning=You are about to remove samples project.samples.remove-success=Samples successfully removed from project -project.samples.combine-title=Merge Samples -project.samples.combine-button=Merge -project.samples.combine-samples-heading=The following {{mergeCtrl.samples.length}} samples will be merged into a single sample. -project.samples.combine-samples-note=The samples will be merged into the selected sample, retaining the selected sample's metadata. -project.samples.combine-new-name=Create a new name for the combines sample. -project.samples.combine-success={0} samples have been successfully combined into {1} project.samples.files.not-available=Files are not currently available. project.samples.copy=Copy project.samples.move=Move @@ -779,14 +773,14 @@ project.samples.nav.add-to-cart=Add to Cart # ========================================================================================== # # Project Samples - Merge Modal # # ========================================================================================== # -project.samples.modal.merge.title= Merge Samples +project.samples.modal.merge.title=Merge Samples project.samples.modal.merge.intro=The following {0} samples will be merged into the selected sample. project.samples.modal.merge.select=Select the sample that the other samples will be merged into. project.samples.modal.merge.warning=All samples will be merged into a single sample. Currently only the metadata from the sample selected will retain its metadata. project.samples.modal.merge.complete=Complete Merge project.samples.modal.merge.select-label=Select a sample to merge into: project.samples.modal.merge.name-label=Rename sample: -project.samples.modal.merge.name.info=(Only letters, numbers and - _ ! @ # $ % ~ `, No spaces or tabs) +project.samples.modal.merge.name.info=(Only letters, numbers, and - _ ! @ # $ % ~ `, No spaces or tabs) project.samples.modal.merge.name.placeholder=Optional, leave empty to retain sample name project.samples.modal.error-length=Name must be at least 5 characters long. project.samples.modal.error-format=Name must not contain any spaces. @@ -2850,6 +2844,8 @@ MergeModal.locked-samples=Locked samples cannot be merged MergeModal.loading=Validating samples MergeModal.success=Merge Success MergeModal.rename=Rename Samples +MergeTrigger.error.locked.title=In order to merge samples, you must have at least 2 unlocked samples. +MergeTrigger.error.locked.content=You don't have permission to modify {0} of the selected sample(s). server.MergeModal.merged-single=Merged {0} into {1}. server.MergeModal.merged-plural=Merged {0} samples into {1}. server.MergeModal.merged-error=Error merging samples. diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx index a1120efeb5a..102b7b6455b 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx @@ -3,6 +3,7 @@ import React, { Suspense, useCallback, useState } from "react"; import { SelectedSample } from "../../../../../types/irida"; import { separateLockedAndUnlockedSamples } from "../../../../../utilities/sample-utilities"; import { useProjectSamples } from "../../useProjectSamplesContext"; +import { Modal, notification } from "antd"; const MergeModal = React.lazy(() => import("./MergeModal")); @@ -14,6 +15,13 @@ export type Samples = | [Array, Array] | undefined; +/** + * Wrapper for React Element which adds a click handler to it to open the merge samples modal. + * Also checks to ensure that the correct number of samples are modifiable by the user (user must + * be the owner). + * @param children + * @constructor + */ export default function MergeTrigger({ children, }: MergeTriggerProps): JSX.Element { @@ -21,7 +29,7 @@ export default function MergeTrigger({ const [samples, setSamples] = useState(undefined); const [visible, setVisible] = useState(false); - function onClick() { + function handleClick() { const [unlocked, locked] = separateLockedAndUnlockedSamples( Object.values(state.selection.selected) ); @@ -30,16 +38,22 @@ export default function MergeTrigger({ setSamples([unlocked, locked]); setVisible(true); } else { - alert("NOT ENOUGH SAMPLES"); + Modal.info({ + title: i18n("MergeTrigger.error.locked.title"), + content: i18n("MergeTrigger.error.locked.content", locked.length), + }); } } - const hideModal = useCallback(() => setVisible(false), []); + const handleClickCallback = useCallback(handleClick, [ + state.selection.selected, + ]); + const hideModalCallback = useCallback(() => setVisible(false), []); return ( <> {React.cloneElement(children, { - onClick, + onClick: handleClickCallback, disabled: state.selection.count < 2, })} {visible && samples !== undefined ? ( @@ -47,7 +61,7 @@ export default function MergeTrigger({ ) : null} From a05cc44ffa39e95a4e6b03f57606ae45d7ac2393 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 13 Jan 2023 09:56:05 -0600 Subject: [PATCH 088/115] chore: Updated i18n of error modal --- src/main/resources/i18n/messages.properties | 2 +- .../project/samples-table/components/SampleTools.tsx | 10 ++++++---- .../samples-table/components/merge/MergeTrigger.tsx | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 75f9541a78a..19cd9cb6994 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -2845,7 +2845,7 @@ MergeModal.loading=Validating samples MergeModal.success=Merge Success MergeModal.rename=Rename Samples MergeTrigger.error.locked.title=In order to merge samples, you must have at least 2 unlocked samples. -MergeTrigger.error.locked.content=You don't have permission to modify {0} of the selected sample(s). +MergeTrigger.error.locked.content=You don't have permission to modify {0} of the selected samples. server.MergeModal.merged-single=Merged {0} into {1}. server.MergeModal.merged-plural=Merged {0} samples into {1}. server.MergeModal.merged-error=Error merging samples. diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/SampleTools.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/SampleTools.tsx index d30c2f3a5a1..4bdff4b2ddf 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/components/SampleTools.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/components/SampleTools.tsx @@ -23,7 +23,9 @@ import MergeTrigger from "./merge/MergeTrigger"; export default function SampleTools() { const { projectId } = useParams(); const { data: details = {} } = useGetProjectDetailsQuery(projectId); - const { state, dispatch } = useProjectSamples(); + const { + state: { selection }, + } = useProjectSamples(); const menu = useMemo( () => ( @@ -40,7 +42,7 @@ export default function SampleTools() { ) : null} } className="t-share" @@ -48,7 +50,7 @@ export default function SampleTools() { {i18n("SamplesMenu.share")} } className="t-remove" @@ -73,7 +75,7 @@ export default function SampleTools() {
), - [details.remote, projectId, state.selection.count] + [details.remote, projectId, selection.count] ); return ( diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx index 102b7b6455b..5b47b9cd424 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/components/merge/MergeTrigger.tsx @@ -3,7 +3,7 @@ import React, { Suspense, useCallback, useState } from "react"; import { SelectedSample } from "../../../../../types/irida"; import { separateLockedAndUnlockedSamples } from "../../../../../utilities/sample-utilities"; import { useProjectSamples } from "../../useProjectSamplesContext"; -import { Modal, notification } from "antd"; +import { Modal } from "antd"; const MergeModal = React.lazy(() => import("./MergeModal")); From 8854b1be267b6ad8db5c42cc7fe8f686ef1f5d84 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 13 Jan 2023 11:29:50 -0600 Subject: [PATCH 089/115] chore: Added proper spacing for dropdown --- .../project/samples-table/components/SampleTools.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/resources/js/components/project/samples-table/components/SampleTools.tsx b/src/main/webapp/resources/js/components/project/samples-table/components/SampleTools.tsx index 4bdff4b2ddf..253efc77235 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/components/SampleTools.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/components/SampleTools.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from "react"; -import { Button, Dropdown, Menu } from "antd"; +import { Button, Dropdown, Menu, Space } from "antd"; import { useProjectSamples } from "../useProjectSamplesContext"; import { useParams } from "react-router-dom"; import { useGetProjectDetailsQuery } from "../../../../redux/endpoints/project"; @@ -81,7 +81,10 @@ export default function SampleTools() { return ( ); From 14540a4d9b979c59a6cc5036b759c7be66609633 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 13 Jan 2023 14:05:45 -0600 Subject: [PATCH 090/115] chore: Fixed typo --- .../js/components/project/samples-table/SamplesTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/resources/js/components/project/samples-table/SamplesTable.tsx b/src/main/webapp/resources/js/components/project/samples-table/SamplesTable.tsx index 1e7a9ca8072..424227e3cf4 100644 --- a/src/main/webapp/resources/js/components/project/samples-table/SamplesTable.tsx +++ b/src/main/webapp/resources/js/components/project/samples-table/SamplesTable.tsx @@ -82,7 +82,7 @@ export default function SamplesTable(): JSX.Element { } /** - * Updated the state of the entire table when the select all checkbox + * Updated the state of the entire table when the select all checkboxes * when clicked. * This can be to select all or none. * From c7777a76d06e9ccafb409c4c0fe9d7657eb44948 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 13 Jan 2023 14:13:18 -0600 Subject: [PATCH 091/115] chore: Removed unused `buildCreateApi` import --- .../resources/js/apis/projects/associated-projects.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/webapp/resources/js/apis/projects/associated-projects.ts b/src/main/webapp/resources/js/apis/projects/associated-projects.ts index bafc647e522..9bcd147a9dc 100644 --- a/src/main/webapp/resources/js/apis/projects/associated-projects.ts +++ b/src/main/webapp/resources/js/apis/projects/associated-projects.ts @@ -1,11 +1,7 @@ /** * @file API the ProjectSettingsAssociatedProjectsController */ -import { - buildCreateApi, - createApi, - fetchBaseQuery, -} from "@reduxjs/toolkit/query/react"; +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; import { setBaseUrl } from "../../utilities/url-utilities"; const BASE_URL = setBaseUrl(`/ajax/projects/associated`); From 7fce997faf5748fbe7942d71105d36fff9ab52db Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 13 Jan 2023 14:18:52 -0600 Subject: [PATCH 092/115] chore: Removed unused import for `direction` --- src/main/webapp/resources/js/utilities/table-utilities.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/webapp/resources/js/utilities/table-utilities.ts b/src/main/webapp/resources/js/utilities/table-utilities.ts index a5e90f05015..0c507e8464f 100644 --- a/src/main/webapp/resources/js/utilities/table-utilities.ts +++ b/src/main/webapp/resources/js/utilities/table-utilities.ts @@ -8,7 +8,6 @@ import { import { SelectedSample } from "../types/irida"; import { SorterResult } from "antd/es/table/interface"; import { ProjectSample } from "../redux/endpoints/project-samples"; -import { direction } from "@antv/matrix-util/lib/ext"; /** * Format Sort Order from the Ant Design sorter object From c074b069ad27d8713e243fb8ab2f08638c222b6f Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 13 Jan 2023 14:19:44 -0600 Subject: [PATCH 093/115] chore: Changed type for converage from `any` to `unknown` --- .../resources/js/redux/endpoints/project-samples.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/webapp/resources/js/redux/endpoints/project-samples.ts b/src/main/webapp/resources/js/redux/endpoints/project-samples.ts index d2a71f1be8d..3f00656950b 100644 --- a/src/main/webapp/resources/js/redux/endpoints/project-samples.ts +++ b/src/main/webapp/resources/js/redux/endpoints/project-samples.ts @@ -1,10 +1,4 @@ import { TAG_PROJECT_SAMPLES } from "./tags"; - -1; -/** - * @fileoverview Project > Samples AP for redux-toolkit - */ - import { Project, Sample, @@ -15,10 +9,15 @@ import { import { TableOptions } from "../../types/ant-design"; import { api } from "./api"; +1; +/** + * @fileoverview Project > Samples AP for redux-toolkit + */ + export type ProjectSample = { key: string; owner: boolean; - coverage: any; // TODO: (Josh - 12/9/22) Figure this one out + coverage: unknown; // TODO: (Josh - 12/9/22) Figure this one out project: Project; qcStatus: string; quality: string[]; From 16a19d2101ebac110ec5fa2dbe872c2c683d9024 Mon Sep 17 00:00:00 2001 From: Josh Adam Date: Fri, 13 Jan 2023 14:20:43 -0600 Subject: [PATCH 094/115] chore: Removed old project sample files --- .../components/AssociatedSamplesList.jsx | 33 -- .../samples/components/CreateNewSample.jsx | 120 ------ .../samples/components/FilterByFileModal.jsx | 156 ------- .../samples/components/LinkerModal.jsx | 120 ------ .../samples/components/LockedSamplesList.jsx | 45 -- .../samples/components/MergeModal.jsx | 189 --------- .../samples/components/ProjectSamples.jsx | 24 -- .../samples/components/RemoveModal.jsx | 83 ---- .../samples/components/SampleIcons.jsx | 27 -- .../samples/components/SamplesMenu.jsx | 397 ------------------ .../samples/components/SamplesTable.jsx | 346 --------------- .../js/pages/projects/samples/index.js | 52 --- .../samples/services/sample.utilities.js | 72 ---- 13 files changed, 1664 deletions(-) delete mode 100644 src/main/webapp/resources/js/pages/projects/samples/components/AssociatedSamplesList.jsx delete mode 100644 src/main/webapp/resources/js/pages/projects/samples/components/CreateNewSample.jsx delete mode 100644 src/main/webapp/resources/js/pages/projects/samples/components/FilterByFileModal.jsx delete mode 100644 src/main/webapp/resources/js/pages/projects/samples/components/LinkerModal.jsx delete mode 100644 src/main/webapp/resources/js/pages/projects/samples/components/LockedSamplesList.jsx delete mode 100644 src/main/webapp/resources/js/pages/projects/samples/components/MergeModal.jsx delete mode 100644 src/main/webapp/resources/js/pages/projects/samples/components/ProjectSamples.jsx delete mode 100644 src/main/webapp/resources/js/pages/projects/samples/components/RemoveModal.jsx delete mode 100644 src/main/webapp/resources/js/pages/projects/samples/components/SampleIcons.jsx delete mode 100644 src/main/webapp/resources/js/pages/projects/samples/components/SamplesMenu.jsx delete mode 100644 src/main/webapp/resources/js/pages/projects/samples/components/SamplesTable.jsx delete mode 100644 src/main/webapp/resources/js/pages/projects/samples/index.js delete mode 100644 src/main/webapp/resources/js/pages/projects/samples/services/sample.utilities.js diff --git a/src/main/webapp/resources/js/pages/projects/samples/components/AssociatedSamplesList.jsx b/src/main/webapp/resources/js/pages/projects/samples/components/AssociatedSamplesList.jsx deleted file mode 100644 index 1372679de84..00000000000 --- a/src/main/webapp/resources/js/pages/projects/samples/components/AssociatedSamplesList.jsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from "react"; -import { WarningOutlined } from "@ant-design/icons"; -import { List, Space, Typography } from "antd"; - -/** - * React Element to render a list of associated samples. Use this when they - * cannot be used in the requested action (e.g. remove). - * @param {array} associatedSamples - list of samples belonging to associated projects - * @returns {JSX.Element} - * @constructor - */ -export default function AssociatedSamplesList({ associatedSamples }) { - return ( - - - - {i18n("AssociatedSamplesList.header")} - - - } - dataSource={associatedSamples} - renderItem={(sample) => ( - - - - )} - /> - ); -} diff --git a/src/main/webapp/resources/js/pages/projects/samples/components/CreateNewSample.jsx b/src/main/webapp/resources/js/pages/projects/samples/components/CreateNewSample.jsx deleted file mode 100644 index 5e60a48f584..00000000000 --- a/src/main/webapp/resources/js/pages/projects/samples/components/CreateNewSample.jsx +++ /dev/null @@ -1,120 +0,0 @@ -import React from "react"; -import { AutoComplete, Form, Input, Modal } from "antd"; -import { - createNewSample, - validateSampleName, -} from "../../../../apis/projects/samples"; -import searchOntology from "../../../../apis/ontology/taxonomy/query"; - -/** - * React component to create a new sample within a project. - * @returns {JSX.Element} - * @constructor - */ -export default function CreateNewSample({ visible, onCreate, onCancel }) { - const [form] = Form.useForm(); - const nameRef = React.useRef(); - const [organisms, setOrganisms] = React.useState([]); - - React.useEffect(() => { - if (visible) { - nameRef.current.focus(); - } - }, [visible]); - - /** - * Reducer: Create the dropdown contents from the taxonomy. - * - * @param {array} accumulator - * @param {object} current - * @returns {*} - */ - function optionsReducer(accumulator, current) { - accumulator.push({ value: current.value }); - /* - Recursively check to see if there are any children to add - */ - if (current.children) { - accumulator.push(...current.children.reduce(optionsReducer, [])); - } - return accumulator; - } - - const handleCancel = () => { - form.resetFields(); - onCancel(); - }; - - /** - * This is used by Ant Design's input validation system to server side validate the - * sample name. This includes name length, special characters, and if the name is already used. - * @param rule - * @param {string} value - the current value of the input - * @returns {Promise} - */ - const validateName = async (value) => { - await validateSampleName(value).then((response) => { - if (response.status === "success") { - return Promise.resolve(); - } else { - return Promise.reject(response.help); - } - }); - }; - - const searchOrganism = async (term) => { - const data = await searchOntology({ - query: term, - ontology: "taxonomy", - }); - setOrganisms(data.reduce(optionsReducer, [])); - }; - - const createSample = () => { - form.validateFields().then((values) => { - createNewSample(values).then(() => { - form.resetFields(); - onCreate(); - }); - }); - }; - - return ( - -
- ({ - validator(_, value) { - return validateName(value); - }, - }), - ]} - > - - - - - -
-
- ); -} diff --git a/src/main/webapp/resources/js/pages/projects/samples/components/FilterByFileModal.jsx b/src/main/webapp/resources/js/pages/projects/samples/components/FilterByFileModal.jsx deleted file mode 100644 index d1a932dc509..00000000000 --- a/src/main/webapp/resources/js/pages/projects/samples/components/FilterByFileModal.jsx +++ /dev/null @@ -1,156 +0,0 @@ -import React from "react"; -import { Col, Form, Input, List, Modal, Row, Typography } from "antd"; -import { useSelector } from "react-redux"; -import { CheckCircleTwoTone, WarningTwoTone } from "@ant-design/icons"; -import { green6, red6 } from "../../../../styles/colors"; -import VirtualList from "rc-virtual-list"; -import { SPACE_SM } from "../../../../styles/spacing"; -import { useValidateSamplesMutation } from "../../../../apis/projects/samples"; - -const ROW_HEIGHT = 43; - -/** - * React component to render a modal to allow the user to select a file containing comma or new - * line seperated sample names to be used to filter the samples' table by. - * - * @param {boolean} visible - whether the modal is currently visible - * @param {function} onComplete - function to call when the sample names are available - * @param {function} onCancel - function to call when cancel the modal - * @returns {JSX.Element} - * @constructor - */ -export default function FilterByFileModal({ visible, onComplete, onCancel }) { - const { options, projectId } = useSelector((state) => state.samples); - const [contents, setContents] = React.useState(""); - const [filename, setFilename] = React.useState(""); - const [valid, setValid] = React.useState([]); - const [invalid, setInvalid] = React.useState([]); - const [validateSamples] = useValidateSamplesMutation(); - - const onFileAdded = async (e) => { - const [file] = e.target.files; - setFilename(file.name); - - const fileContent = await file.text(); - setContents(fileContent); - }; - - React.useEffect(() => { - if (contents.length) { - const associated = options.filters.associated || []; - // Split the contents of the file on either new line or coma, and filter empty entries. - let parsed = contents.split(/[\s,]+/).filter(Boolean); - validateSamples({ - projectId: projectId, - body: { - samples: parsed.map((sample) => ({ - name: sample, - })), - associatedProjectIds: associated, - }, - }).then((response) => { - let valid = response.data.samples.filter((sample) => sample.ids); - let invalid = response.data.samples.filter((sample) => !sample.ids); - - setValid( - valid.map((sample) => { - return { sampleName: sample.name }; - }) - ); - setInvalid( - invalid.map((sample) => { - return sample.name; - }) - ); - }); - } else { - setValid([]); - setInvalid([]); - } - }, [contents, options.filters.associated, projectId]); - - const onOk = () => { - onComplete({ samples: valid, filename }); - }; - - return ( - - <> -
- - - -
- - {valid.length > 0 && ( - - - - - {valid.length === 1 - ? i18n("FilterByFile.valid.single") - : i18n("FilterByFile.valid.plural", valid.length)} - - - - )} - {invalid.length > 0 && ( - - - - - - {i18n("FilterByFile.invalid")} - - - - - { - return item; - }} - > - {(item) => ( - - {item} - } - /> - - )} - - - - - - )} - - -
- ); -} diff --git a/src/main/webapp/resources/js/pages/projects/samples/components/LinkerModal.jsx b/src/main/webapp/resources/js/pages/projects/samples/components/LinkerModal.jsx deleted file mode 100644 index ad263a094ae..00000000000 --- a/src/main/webapp/resources/js/pages/projects/samples/components/LinkerModal.jsx +++ /dev/null @@ -1,120 +0,0 @@ -import React from "react"; -import { - Alert, - Button, - Checkbox, - Form, - Input, - Modal, - Space, - Typography, -} from "antd"; -import styled from "styled-components"; -import { SPACE_SM } from "../../../../styles/spacing"; -import { BORDER_RADIUS, BORDERED_LIGHT } from "../../../../styles/borders"; -import { grey2 } from "../../../../styles/colors"; -import { LoadingOutlined } from "@ant-design/icons"; -import { getNGSLinkerCode } from "../../../../apis/linker/linker"; - -const CommandText = styled(Typography.Paragraph)` - margin: 0px !important; - font-family: monospace; - font-size: 14px; -`; - -const CommandWrapper = styled.div` - margin-top: ${SPACE_SM}; - padding: 2px; - background-color: ${grey2}; - border: ${BORDERED_LIGHT}; - border-radius: ${BORDER_RADIUS}; -`; - -export default function LinkerModal({ - visible, - sampleIds, - projectId, - onFinish, -}) { - const [form] = Form.useForm(); - const [scriptString, setScriptString] = React.useState(); - const [command, setCommand] = React.useState(); - const [error, setError] = React.useState(false); - - const updateCommand = () => { - const types = form.getFieldValue("type"); - setCommand( - types.length ? `${scriptString} -t ${types.join(",")}` : scriptString - ); - }; - - const options = [ - { label: i18n("Linker.fastq"), value: "fastq" }, - { label: i18n("Linker.assembly"), value: "assembly" }, - ]; - - React.useEffect(() => { - getNGSLinkerCode({ sampleIds, projectId }) - .then(({ data }) => { - // Post data to the server to get the linker command. - setScriptString(data); - }) - .catch(() => setError(true)); - }, [projectId, sampleIds]); - - React.useEffect(updateCommand, [scriptString]); - - return ( - - -
- } - > - {i18n("Linker.details")} - - - -
- - - - -
- {error ? ( - - ) : ( - - {scriptString === undefined ? ( - - - {i18n("Linker.loading")} - - ) : ( - - {command} - - )} - - )} - - ); -} diff --git a/src/main/webapp/resources/js/pages/projects/samples/components/LockedSamplesList.jsx b/src/main/webapp/resources/js/pages/projects/samples/components/LockedSamplesList.jsx deleted file mode 100644 index 8820d31e98c..00000000000 --- a/src/main/webapp/resources/js/pages/projects/samples/components/LockedSamplesList.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from "react"; -import { LockTwoTone } from "@ant-design/icons"; -import { Avatar, Button, List } from "antd"; -import { red6 } from "../../../../styles/colors"; -import { SampleDetailViewer } from "../../../../components/samples/SampleDetailViewer"; - -/** - * React Element to render a list of locked samples. Use this when they - * cannot be used in the requested action (e.g. remove). - * @param {array} locked - list of samples that are locked from modification - * @returns {JSX.Element} - * @constructor - */ -export default function LockedSamplesList({ locked }) { - return ( - ( - - } - style={{ backgroundColor: "transparent" }} - /> - } - title={ - - - - } - /> - - )} - /> - ); -} diff --git a/src/main/webapp/resources/js/pages/projects/samples/components/MergeModal.jsx b/src/main/webapp/resources/js/pages/projects/samples/components/MergeModal.jsx deleted file mode 100644 index dad3891809c..00000000000 --- a/src/main/webapp/resources/js/pages/projects/samples/components/MergeModal.jsx +++ /dev/null @@ -1,189 +0,0 @@ -import { - Alert, - Checkbox, - Col, - Form, - Input, - Modal, - notification, - Radio, - Row, - Space, - Typography, -} from "antd"; -import React from "react"; -import { useSelector } from "react-redux"; -import { serverValidateSampleName } from "../../../../utilities/validation-utilities"; -import { useMergeMutation } from "../../../../apis/projects/samples"; -import LockedSamplesList from "./LockedSamplesList"; - -/** - * React element to display a modal to merge multiple samples into a single one. - * @param {array} samples - list of samples to merge together - * @param {boolean} visible - whether the modal is currently visible on the page - * @param {function} onComplete - function to call when the merge is complete - * @param {function} onCancel - function to call when the merge is cancelled. - * @returns {JSX.Element} - * @constructor - */ -export default function MergeModal({ samples, visible, onComplete, onCancel }) { - const { projectId } = useSelector((state) => state.samples); - const [merge, { isLoading }] = useMergeMutation(); - - const [renameSample, setRenameSample] = React.useState(false); - const [error, setError] = React.useState(undefined); - const [form] = Form.useForm(); - - const initialValues = { - primary: samples.valid[0]?.id, - newName: "", - }; - - React.useEffect(() => { - if (!renameSample) { - form.setFieldsValue({ - newName: "", - }); - } - }, [form, renameSample]); - - // Server validate new name - const validateName = async (name) => { - if (renameSample) { - return serverValidateSampleName(name); - } else { - return Promise.resolve(); - } - }; - - const onSubmit = async () => { - let values; - - try { - values = await form.validateFields(); - } catch { - /* - If the form is in an invalid state it will hit here. This will prevent the - invalid date from being submitted and display the errors (if not already displayed) - to the user. - */ - return; - } - const ids = samples.valid - .map((sample) => sample.id) - .filter((id) => id !== values.primary); - - const { message } = await merge({ - projectId, - request: { - ...values, - ids, - }, - }).unwrap(); - - notification.success({ - message: i18n("MergeModal.success"), - description: message, - }); - onComplete(); - }; - - return ( - - - {samples.valid.length >= 2 ? ( - <> - - - - {error !== undefined ? ( - - setError(undefined)} - /> - - ) : null} - -
- - - - {samples.valid.map((sample) => { - return ( - - {sample.sampleName} - - ); - })} - - - - - setRenameSample(e.target.checked)} - > - {i18n("MergeModal.rename")} - - ({ - validator(_, value) { - return validateName(value); - }, - }), - ]} - > - - - -
- - - ) : ( - - - - )} - {samples.locked.length ? ( - - - {i18n("LockedSamplesList.header")} - - - - ) : null} -
-
- ); -} diff --git a/src/main/webapp/resources/js/pages/projects/samples/components/ProjectSamples.jsx b/src/main/webapp/resources/js/pages/projects/samples/components/ProjectSamples.jsx deleted file mode 100644 index 7e84e5e5c88..00000000000 --- a/src/main/webapp/resources/js/pages/projects/samples/components/ProjectSamples.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; -import { SamplesTable } from "./SamplesTable"; -import { Col, Row } from "antd"; -import SamplesMenu from "./SamplesMenu"; - -/** - * React component to handle the layout and higher order functions of the project - * samples page. - * - * @returns {JSX.Element} - * @constructor - */ -export default function ProjectSamples() { - return ( - - - - - - - - - ); -} diff --git a/src/main/webapp/resources/js/pages/projects/samples/components/RemoveModal.jsx b/src/main/webapp/resources/js/pages/projects/samples/components/RemoveModal.jsx deleted file mode 100644 index e2d7a585d4e..00000000000 --- a/src/main/webapp/resources/js/pages/projects/samples/components/RemoveModal.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from "react"; -import { Alert, Col, Divider, List, Modal, Row, Typography } from "antd"; -import { useRemoveMutation } from "../../../../apis/projects/samples"; -import LockedSamplesList from "./LockedSamplesList"; -import AssociatedSamplesList from "./AssociatedSamplesList"; - -/** - * React Element to display a modal with sample to be removed from the current - * project. - * - Will display samples that are locked - cannot be removed. - * - Will display associated samples that cannot be removed from this project. - * @param {array} samples - list of samples to remove from the current project - * @param {boolean} visible - whether the modal is currently visible on the page - * @param {function} onComplete - action to perform after the remove is complete - * @param {function} onCancel - action to perform if the remove is cancelled. - * @returns {JSX.Element} - * @constructor - */ -export default function RemoveModal({ - samples, - visible, - onComplete, - onCancel, -}) { - const [removeSamples, { isLoading, error }] = useRemoveMutation(); - - const onOk = async () => { - try { - await removeSamples(samples.valid.map((sample) => sample.id)); - onComplete(); - } catch (e) { - // Do nothing, handled by mutation - } - }; - - return ( - - - - {i18n("RemoveModal.valid")} - } - dataSource={samples.valid} - renderItem={(sample) => ( - - - - )} - /> - - {samples.associated.length > 0 && ( - - - {i18n("RemoveModal.divider")} - - - )} - {samples.associated.length > 0 && ( - - - - )} - {error && ( - - )} - - - ); -} diff --git a/src/main/webapp/resources/js/pages/projects/samples/components/SampleIcons.jsx b/src/main/webapp/resources/js/pages/projects/samples/components/SampleIcons.jsx deleted file mode 100644 index 52eb3c254f2..00000000000 --- a/src/main/webapp/resources/js/pages/projects/samples/components/SampleIcons.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react"; -import { LockTwoTone } from "@ant-design/icons"; -import { Popover, Space } from "antd"; -import { red6 } from "../../../../styles/colors"; - -/** - * React component to render any icons onto the sample listing table that - * give extra information about the sample. - * @param {object} sample - * @returns {JSX.Element} - * @constructor - */ -export default function SampleIcons({ sample }) { - return ( - - {!sample.owner && ( - - - - )} - - ); -} diff --git a/src/main/webapp/resources/js/pages/projects/samples/components/SamplesMenu.jsx b/src/main/webapp/resources/js/pages/projects/samples/components/SamplesMenu.jsx deleted file mode 100644 index 6fba0d0adc9..00000000000 --- a/src/main/webapp/resources/js/pages/projects/samples/components/SamplesMenu.jsx +++ /dev/null @@ -1,397 +0,0 @@ -import React, { lazy, Suspense } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { Button, Dropdown, Menu, notification, Row, Space } from "antd"; -import { - addToCart, - clearFilterByFile, - downloadSamples, - exportSamplesToFile, - filterByFile, - reloadTable, -} from "../../redux/samplesSlice"; -import { setBaseUrl } from "../../../../utilities/url-utilities"; -import { - validateSamplesForLinker, - validateSamplesForMergeOrShare, - validateSamplesForRemove, -} from "../services/sample.utilities"; -import { - IconCloseSquare, - IconCloudDownload, - IconCloudUpload, - IconCode, - IconDropDown, - IconFile, - IconFileExcel, - IconPlusSquare, - IconShare, - IconShoppingCart, -} from "../../../../components/icons/Icons"; -import { - CloseCircleOutlined, - FileTextOutlined, - MergeCellsOutlined, -} from "@ant-design/icons"; -import { useGetProjectDetailsQuery } from "../../../../apis/projects/project"; -import { storeSamples } from "../../../../utilities/session-utilities"; -import { - updateCart, - useAddSamplesToCartMutation, -} from "../../../../apis/cart/cart"; - -const MergeModal = lazy(() => import("./MergeModal")); -const RemoveModal = lazy(() => import("./RemoveModal")); -const CreateModal = lazy(() => import("./CreateNewSample")); -const LinkerModal = lazy(() => import("./LinkerModal")); -const FilterByFileModal = lazy(() => import("./FilterByFileModal")); - -/** - * React element to render a row of actions that can be performed on - * samples in the table - * @returns {JSX.Element} - * @constructor - */ -export default function SamplesMenu() { - const dispatch = useDispatch(); - const [addSamplesToCart] = useAddSamplesToCartMutation(); - - const { - projectId, - selected, - selectedCount, - filterByFile: fileFiltered, - } = useSelector((state) => state.samples); - const { data: details = {} } = useGetProjectDetailsQuery(projectId); - - /** - * Flag for if the merge samples modal is visible - */ - const [mergeVisible, setMergeVisible] = React.useState(false); - - /** - * Flag for if the remove samples modal is visible - */ - const [removedVisible, setRemovedVisible] = React.useState(false); - - /** - * Flag for if the create sample modal is visible - */ - const [createSampleVisible, setCreateSampleVisible] = React.useState(false); - - /** - * Flag for if the command line linker modal is visible - */ - const [linkerVisible, setLinkerVisible] = React.useState(false); - - /** - * Flag for if the filter by file modal is visible - */ - const [filterByFileVisible, setFilterByFileVisible] = React.useState(false); - - /** - * Variable to hold sorted samples (locked, unlocked, or associated). This is used to - * send the correct sample to whichever modal is displayed. - */ - const [sorted, setSorted] = React.useState({}); - - /** - * When a merge is completed, hide the modal and ask - * the table to reset - */ - const onMergeComplete = () => { - setMergeVisible(false); - dispatch(reloadTable()); - }; - - const onRemoveComplete = () => { - setRemovedVisible(false); - dispatch(reloadTable()); - }; - - const onCreate = () => { - setCreateSampleVisible(false); - dispatch(reloadTable()); - }; - - const onAddToCart = () => { - const samplesList = Object.values(selected); - const projects = samplesList.reduce((prev, current) => { - if (!prev[current.projectId]) prev[current.projectId] = []; - prev[current.projectId].push(current); - return prev; - }, {}); - for (const projectId in projects) { - addSamplesToCart({ projectId, samples: projects[projectId] }); - } - }; - - const onDownload = () => { - dispatch(downloadSamples()); - }; - - const onNCBI = () => { - if (selected.size === 0) return; - formatAndStoreSamples(`ncbi`, Object.values(selected)); - window.location.href = setBaseUrl(`/projects/${projectId}/ncbi`); - }; - - const onExport = (type) => { - dispatch(exportSamplesToFile(type)); - }; - - const formatAndStoreSamples = (path, samples) => { - storeSamples({ - samples: samples.map(({ id, sampleName: name, owner, projectId }) => ({ - id, - name, - owner, - projectId, - })), - projectId, - path, - }); - }; - - /** - * Format samples to share with other projects, - * store minimal information in localStorage - */ - const shareSamples = () => { - if (selected.size === 0) return; - const { valid, locked } = validateSamplesForMergeOrShare(selected); - if (locked.length && valid.length === 0) { - notification.error({ message: i18n("SampleMenu.share-all-locked") }); - } else { - formatAndStoreSamples(`share`, Object.values(selected)); - // Redirect user to share page - window.location.href = setBaseUrl(`/projects/${projectId}/share`); - } - }; - - /** - * Validate samples for specific modals and open the appropriate modal - * if the right samples are available. - * @param {string} name - which modal to open - */ - const validateAndOpenModalFor = (name) => { - if (name === "merge") { - const validated = validateSamplesForMergeOrShare(selected); - if (validated.valid.length >= 2) { - setSorted(validated); - setMergeVisible(true); - } else { - notification.error({ message: i18n("SamplesMenu.merge.error") }); - } - } else if (name === "remove") { - const validated = validateSamplesForRemove(selected, projectId); - if (validated.valid.length > 0) { - setSorted(validated); - setRemovedVisible(true); - } else notification.error({ message: i18n("SamplesMenu.remove.error") }); - } else if (name === "linker") { - const validated = validateSamplesForLinker(selected, projectId); - if (validated.associated.length > 0) { - notification.error({ message: i18n("SampleMenu.linker.error") }); - } else { - setSorted(validated.valid); - setLinkerVisible(true); - } - } - }; - - const onFilterByFile = ({ samples, filename }) => { - dispatch(filterByFile({ filename, samples })); - setFilterByFileVisible(false); - }; - - const toolsMenu = ( - - {!details.remote && ( - } - onClick={() => validateAndOpenModalFor("merge")} - className="t-merge" - > - {i18n("SamplesMenu.merge")} - - )} - } - onClick={shareSamples} - className="t-share" - > - {i18n("SamplesMenu.share")} - - } - onClick={() => validateAndOpenModalFor("remove")} - className="t-remove" - > - {i18n("SamplesMenu.remove")} - - - }> - - {i18n("SamplesMenu.import")} - - - - } - onClick={() => setCreateSampleVisible(true)} - className="t-create-sample" - > - {i18n("SamplesMenu.createSample")} - - - ); - - const exportMenu = ( - - } - onClick={onDownload} - > - {i18n("SampleMenu.download")} - - } - onClick={() => validateAndOpenModalFor("linker")} - className="t-linker" - > - {i18n("SampleMenu.linker")} - - } - onClick={onNCBI} - className="t-ncbi" - > - {i18n("SampleMenu.ncbi")} - - - } - onClick={() => onExport("excel")} - > - {i18n("SampleMenu.excel")} - - } - onClick={() => onExport("csv")} - > - {i18n("SampleMenu.csv")} - - - ); - - return ( - <> - - - {details.canManage && ( - - - - )} - - - - - - {fileFiltered ? ( - - ) : ( - - )} - - {mergeVisible && ( - }> - setMergeVisible(false)} - samples={sorted} - /> - - )} - {removedVisible && ( - }> - setRemovedVisible(false)} - samples={sorted} - /> - - )} - {createSampleVisible && ( - }> - setCreateSampleVisible(false)} - onCreate={onCreate} - /> - - )} - {linkerVisible && ( - }> - setLinkerVisible(false)} - /> - - )} - {filterByFileVisible && ( - }> - setFilterByFileVisible(false)} - onComplete={onFilterByFile} - /> - - )} - - ); -} diff --git a/src/main/webapp/resources/js/pages/projects/samples/components/SamplesTable.jsx b/src/main/webapp/resources/js/pages/projects/samples/components/SamplesTable.jsx deleted file mode 100644 index 9caaa0e4846..00000000000 --- a/src/main/webapp/resources/js/pages/projects/samples/components/SamplesTable.jsx +++ /dev/null @@ -1,346 +0,0 @@ -import React from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { Button, Checkbox, DatePicker, Select, Space, Table, Tag } from "antd"; -import { useListAssociatedProjectsQuery } from "../../../../apis/projects/associated-projects"; -import { formatInternationalizedDateTime } from "../../../../utilities/date-utilities"; -import { - formatSearch, - formatSort, -} from "../../../../utilities/table-utilities"; -import SampleIcons from "./SampleIcons"; -import { useListSamplesQuery } from "../../../../apis/projects/samples"; -import { - addSelectedSample, - clearSelectedSamples, - removeSelectedSample, - selectAllSamples, - updateTable, -} from "../../redux/samplesSlice"; -import SampleQuality from "../../../../components/sample-quality"; -import { setBaseUrl } from "../../../../utilities/url-utilities"; -import { IconSearch } from "../../../../components/icons/Icons"; -import { blue6 } from "../../../../styles/colors"; -import { generateColourForItem } from "../../../../utilities/colour-utilities"; -import { getPaginationOptions } from "../../../../utilities/antdesign-table-utilities"; -import { SampleDetailViewer } from "../../../../components/samples/SampleDetailViewer"; -import ProjectTag from "../../../search/ProjectTag"; - -const { RangePicker } = DatePicker; - -/** - * React element to render a table display samples belong to a project, - * and the project's associated projects. - * @returns {JSX.Element} - * @constructor - */ -export function SamplesTable() { - const dispatch = useDispatch(); - const { - projectId, - options, - selected, - selectedCount, - loadingLong, - filterByFile, - } = useSelector((state) => state.samples); - - /** - * Fetch the current state of the table. - * Re-fetch whenever one of the - * table options (filter, sort, or pagination) changes. - */ - const { data: { content: samples, total } = {}, isFetching } = - useListSamplesQuery(options, { - refetchOnMountOrArgChange: true, - }); - - /** - * Fetch projects that have been associated with this project. - * Request formats them into a format that can be consumed by the - * project column filter. - */ - const { data: associatedProjects } = - useListAssociatedProjectsQuery(projectId); - - /** - * Handle row selection change event - * @param event - * @param sample - */ - const onRowSelectionChange = (event, sample) => { - if (event.target.checked) { - dispatch(addSelectedSample(sample)); - } else { - dispatch(removeSelectedSample(sample.key)); - } - }; - - /** - * Called by select all/none table header - * @param e - React synthetic event - * @returns {*} - */ - const updateSelectAll = (e) => - e.target.checked - ? dispatch(selectAllSamples()) - : dispatch(clearSelectedSamples()); - - /** - * Handle changes made to the table options. This will trigger an automatic - * reload of the table content. - * @param pagination - * @param tableFilters - * @param sorter - * @returns {*} - */ - const onTableChange = (pagination, tableFilters, sorter) => { - let { associated, ...filters } = tableFilters; - const search = formatSearch(filters); - if (filterByFile) search.push(filterByFile.fileFilter); - - dispatch( - updateTable({ - filters: { associated: associated === undefined ? null : associated }, // Null conversion for comparison with default values in slice - pagination, - order: formatSort(sorter), - search, - }) - ); - }; - - const handleSearch = (selectedKeys, confirm) => { - confirm(); - }; - - const handleClearSearch = (clearFilters, confirm) => { - clearFilters(); - confirm({ closeDropdown: false }); - }; - - const getColumnSearchProps = ( - dataIndex, - filterName = "", - placeholder = "" - ) => ({ - filterDropdown: ({ - setSelectedKeys, - selectedKeys, - confirm, - clearFilters, - }) => ( -
-