From 12ed1a0cfffe79245747b3b8638fa0564192c8ff Mon Sep 17 00:00:00 2001 From: Hatim Dinia Date: Thu, 10 Aug 2023 11:26:41 +0200 Subject: [PATCH] feat(ui-study): enhance study upgrade dialog (#1687) Co-authored-by: Samir Kamal <1954121+skamril@users.noreply.github.com> --- webapp/public/locales/en/main.json | 2 +- webapp/public/locales/fr/main.json | 2 +- .../components/App/Singlestudy/NavHeader.tsx | 658 ------------------ .../App/Singlestudy/NavHeader/Actions.tsx | 131 ++++ .../App/Singlestudy/NavHeader/ActionsMenu.tsx | 54 ++ .../App/Singlestudy/NavHeader/Details.tsx | 112 +++ .../App/Singlestudy/NavHeader/index.tsx | 320 +++++++++ .../App/Singlestudy/UpgradeDialog.tsx | 75 ++ .../src/components/App/Studies/StudyCard.tsx | 2 +- webapp/src/services/api/study.ts | 11 +- webapp/src/theme.ts | 2 +- 11 files changed, 705 insertions(+), 664 deletions(-) delete mode 100644 webapp/src/components/App/Singlestudy/NavHeader.tsx create mode 100644 webapp/src/components/App/Singlestudy/NavHeader/Actions.tsx create mode 100644 webapp/src/components/App/Singlestudy/NavHeader/ActionsMenu.tsx create mode 100644 webapp/src/components/App/Singlestudy/NavHeader/Details.tsx create mode 100644 webapp/src/components/App/Singlestudy/NavHeader/index.tsx create mode 100644 webapp/src/components/App/Singlestudy/UpgradeDialog.tsx diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index 60b228e010..8ea07b6405 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -574,8 +574,8 @@ "studies.enterTag": "Enter a tag", "studies.favorites": "Favorites", "studies.tagsLabel": "Tags", - "studies.bookmark": "Bookmark", "studies.moreActions": "More actions", + "studies.addFavorite": "Add to favorites", "studies.removeFavorite": "Remove from favorites", "studies.exportOptions": "Export options", "studies.exportOutput": "Export particular output", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index 0bd13a3227..afecd951bb 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -574,8 +574,8 @@ "studies.enterTag": "Entrer un tag", "studies.favorites": "Favoris", "studies.tagsLabel": "Tags", - "studies.bookmark": "Bookmark", "studies.moreActions": "Plus d'actions", + "studies.addFavorite": "Ajouter aux favoris", "studies.removeFavorite": "Retirer des favoris", "studies.exportOptions": "Options d'export", "studies.exportOutput": "Exporter une sortie", diff --git a/webapp/src/components/App/Singlestudy/NavHeader.tsx b/webapp/src/components/App/Singlestudy/NavHeader.tsx deleted file mode 100644 index eba5b34b97..0000000000 --- a/webapp/src/components/App/Singlestudy/NavHeader.tsx +++ /dev/null @@ -1,658 +0,0 @@ -import { useState } from "react"; -import * as React from "react"; -import debug from "debug"; -import { useSnackbar } from "notistack"; -import { Link, useNavigate } from "react-router-dom"; -import { AxiosError } from "axios"; -import { - Box, - Button, - Chip, - Divider, - ListItemIcon, - ListItemText, - Menu, - MenuItem, - styled, - Tooltip, - Typography, -} from "@mui/material"; -import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import MoreVertIcon from "@mui/icons-material/MoreVert"; -import ErrorIcon from "@mui/icons-material/Error"; -import HistoryOutlinedIcon from "@mui/icons-material/HistoryOutlined"; -import UnarchiveOutlinedIcon from "@mui/icons-material/UnarchiveOutlined"; -import DownloadOutlinedIcon from "@mui/icons-material/DownloadOutlined"; -import ArchiveOutlinedIcon from "@mui/icons-material/ArchiveOutlined"; -import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined"; -import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; -import ScheduleOutlinedIcon from "@mui/icons-material/ScheduleOutlined"; -import UpdateOutlinedIcon from "@mui/icons-material/UpdateOutlined"; -import AltRouteOutlinedIcon from "@mui/icons-material/AltRouteOutlined"; -import SecurityOutlinedIcon from "@mui/icons-material/SecurityOutlined"; -import AccountTreeOutlinedIcon from "@mui/icons-material/AccountTreeOutlined"; -import PersonOutlineOutlinedIcon from "@mui/icons-material/PersonOutlineOutlined"; -import ContentCopyIcon from "@mui/icons-material/ContentCopy"; -import { useTranslation } from "react-i18next"; -import { indigo } from "@mui/material/colors"; -import { StudyMetadata, VariantTree } from "../../../common/types"; -import { STUDIES_HEIGHT_HEADER } from "../../../theme"; -import { - archiveStudy as callArchiveStudy, - upgradeStudy as callUpgradeStudy, - unarchiveStudy as callUnarchiveStudy, -} from "../../../services/api/study"; -import { deleteStudy, toggleFavorite } from "../../../redux/ducks/studies"; -import LauncherDialog from "../Studies/LauncherDialog"; -import PropertiesDialog from "./PropertiesDialog"; -import { - buildModificationDate, - convertUTCToLocalTime, - countAllChildrens, - displayVersionName, -} from "../../../services/utils"; -import useEnqueueErrorSnackbar from "../../../hooks/useEnqueueErrorSnackbar"; -import { - getLatestStudyVersion, - isCurrentStudyFavorite, -} from "../../../redux/selectors"; -import ExportDialog from "../Studies/ExportModal"; -import StarToggle from "../../common/StarToggle"; -import ConfirmationDialog from "../../common/dialogs/ConfirmationDialog"; -import useAppSelector from "../../../redux/hooks/useAppSelector"; -import useAppDispatch from "../../../redux/hooks/useAppDispatch"; -import CheckBoxFE from "../../common/fieldEditors/CheckBoxFE"; -import { PUBLIC_MODE_LIST } from "../../common/utils/constants"; - -const logError = debug("antares:singlestudy:navheader:error"); - -const TinyText = styled(Typography)(({ theme }) => ({ - fontSize: "14px", - color: theme.palette.text.secondary, -})); - -const LinkText = styled(Link)(({ theme }) => ({ - fontSize: "14px", - color: theme.palette.secondary.main, -})); - -const StyledDivider = styled(Divider)(({ theme }) => ({ - margin: theme.spacing(0, 1), - width: "1px", - height: "20px", - backgroundColor: theme.palette.divider, -})); - -const MAX_STUDY_TITLE_LENGTH = 45; - -interface Props { - study: StudyMetadata | undefined; - parent: StudyMetadata | undefined; - childrenTree: VariantTree | undefined; - isExplorer?: boolean; - openCommands?: VoidFunction; - updateStudyData: VoidFunction; -} - -function NavHeader(props: Props) { - const { - study, - parent, - childrenTree, - isExplorer, - openCommands, - updateStudyData, - } = props; - const [t, i18n] = useTranslation(); - const navigate = useNavigate(); - const dispatch = useAppDispatch(); - const isStudyFavorite = useAppSelector(isCurrentStudyFavorite); - const latestVersion = useAppSelector(getLatestStudyVersion); - const [anchorEl, setAnchorEl] = useState(null); - const [openMenu, setOpenMenu] = useState(""); - const [openLauncherDialog, setOpenLauncherDialog] = useState(false); - const [openPropertiesDialog, setOpenPropertiesDialog] = useState(false); - const [openUpgradeDialog, setOpenUpgradeDialog] = useState(false); - const [deleteChildren, setDeleteChildren] = useState(false); - const [openDeleteDialog, setOpenDeleteDialog] = useState(false); - const [openExportDialog, setOpenExportDialog] = useState(false); - const { enqueueSnackbar } = useSnackbar(); - const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); - const isLatestVersion = study?.version === latestVersion; - const publicModeLabel = - PUBLIC_MODE_LIST.find((mode) => mode.id === study?.publicMode)?.name || ""; - - //////////////////////////////////////////////////////////////// - // Event Handlers - //////////////////////////////////////////////////////////////// - - const handleClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - setOpenMenu(event.currentTarget.id); - }; - - const handleClose = () => { - setAnchorEl(null); - setOpenMenu(""); - }; - - const onBackClick = () => { - if (isExplorer) { - navigate(`/studies/${study?.id}`); - } else { - navigate("/studies"); - } - }; - - const onLaunchClick = (): void => { - setOpenLauncherDialog(true); - }; - - const archiveStudy = async (study: StudyMetadata) => { - try { - await callArchiveStudy(study.id); - } catch (e) { - enqueueErrorSnackbar( - t("studies.error.archive", { studyname: study.name }), - e as AxiosError - ); - } - }; - - const unarchiveStudy = async (study: StudyMetadata) => { - try { - await callUnarchiveStudy(study.id); - } catch (e) { - enqueueErrorSnackbar( - t("studies.error.unarchive", { studyname: study.name }), - e as AxiosError - ); - } - }; - - const upgradeStudy = async () => { - if (study) { - try { - await callUpgradeStudy(study.id); - } catch (e) { - enqueueErrorSnackbar( - t("study.error.upgrade", { studyname: study.name }), - e as AxiosError - ); - } finally { - setOpenUpgradeDialog(false); - } - } - }; - - const onDeleteStudy = async () => { - if (study) { - try { - await dispatch(deleteStudy({ id: study.id, deleteChildren })).unwrap(); - navigate(parent ? `/studies/${parent?.id}` : "/studies"); - } catch (err) { - enqueueErrorSnackbar(t("studies.error.deleteStudy"), err as AxiosError); - logError("Failed to delete study", study, err); - } finally { - setDeleteChildren(false); - setOpenDeleteDialog(false); - } - } - }; - - const copyId = async (): Promise => { - if (study) { - try { - await navigator.clipboard.writeText(study.id); - enqueueSnackbar(t("study.success.studyIdCopy"), { - variant: "success", - }); - } catch (e) { - enqueueErrorSnackbar(t("study.error.studyIdCopy"), e as AxiosError); - } - } - }; - - //////////////////////////////////////////////////////////////// - // JSX - //////////////////////////////////////////////////////////////// - - return ( - - - - - - - - - - {study?.name} - - - { - if (study) { - dispatch(toggleFavorite(study.id)); - } - }} - /> - - - - {study?.managed && ( - - )} - {!study?.managed && study?.workspace && ( - - )} - {study?.tags && - study.tags.map((elm) => ( - - ))} - {isExplorer && ( - - )} - {study && study.type === "variantstudy" && ( - - )} - - - - - {study?.archived ? ( - { - unarchiveStudy(study); - handleClose(); - }} - > - - - - {t("global.unarchive")} - - ) : ( -
- { - setOpenPropertiesDialog(true); - handleClose(); - }} - > - - - - {t("study.properties")} - - {!isLatestVersion && ( - { - setOpenUpgradeDialog(true); - handleClose(); - }} - > - - - - {t("study.upgrade")} - - )} - { - setOpenExportDialog(true); - handleClose(); - }} - > - - - - {t("global.export")} - - {study?.managed && ( - { - archiveStudy(study); - handleClose(); - }} - > - - - - {t("global.archive")} - - )} -
- )} - {study?.managed && ( - { - setOpenDeleteDialog(true); - handleClose(); - }} - > - - - - - {t("global.delete")} - - - )} -
-
-
- {study && ( - - - - {convertUTCToLocalTime(study.creationDate)} - - - - - {buildModificationDate(study.modificationDate, t, i18n.language)} - - - - - {`v${displayVersionName(study.version)}`} - - {parent && ( - - - - - {`${parent.name.substring(0, MAX_STUDY_TITLE_LENGTH)}...`} - - - - )} - {childrenTree && ( - - - {countAllChildrens(childrenTree)} - - )} - - - - {study?.owner.name} - - - - {t(publicModeLabel)} - - - )} - {study && openLauncherDialog && ( - setOpenLauncherDialog(false)} - /> - )} - {openPropertiesDialog && study && ( - setOpenPropertiesDialog(false)} - study={study as StudyMetadata} - updateStudyData={updateStudyData} - /> - )} - {openDeleteDialog && ( - setOpenDeleteDialog(false)} - onConfirm={onDeleteStudy} - alert="warning" - open - > - - {t("studies.question.delete")} - setDeleteChildren(checked)} - /> - - - )} - {openUpgradeDialog && ( - setOpenUpgradeDialog(false)} - onConfirm={upgradeStudy} - alert="warning" - open - > - {t("study.question.upgrade")} - - )} - {study && openExportDialog && ( - setOpenExportDialog(false)} - study={study} - /> - )} -
- ); -} - -export default NavHeader; diff --git a/webapp/src/components/App/Singlestudy/NavHeader/Actions.tsx b/webapp/src/components/App/Singlestudy/NavHeader/Actions.tsx new file mode 100644 index 0000000000..e811e17cf7 --- /dev/null +++ b/webapp/src/components/App/Singlestudy/NavHeader/Actions.tsx @@ -0,0 +1,131 @@ +import { Box, Tooltip, Typography, Chip, Button, Divider } from "@mui/material"; +import HistoryOutlinedIcon from "@mui/icons-material/HistoryOutlined"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import { useTranslation } from "react-i18next"; +import MoreVertIcon from "@mui/icons-material/MoreVert"; +import { StudyMetadata, StudyType } from "../../../../common/types"; +import { toggleFavorite } from "../../../../redux/ducks/studies"; +import StarToggle from "../../../common/StarToggle"; +import useAppDispatch from "../../../../redux/hooks/useAppDispatch"; +import useAppSelector from "../../../../redux/hooks/useAppSelector"; +import { isCurrentStudyFavorite } from "../../../../redux/selectors"; + +interface Props { + study: StudyMetadata | undefined; + isExplorer: boolean | undefined; + onCopyId: () => Promise; + onUnarchive: () => Promise; + onLaunch: VoidFunction; + onOpenCommands: VoidFunction; + onOpenMenu: React.MouseEventHandler; +} + +function Actions({ + study, + isExplorer, + onCopyId, + onUnarchive, + onLaunch, + onOpenCommands, + onOpenMenu, +}: Props) { + const [t] = useTranslation(); + const dispatch = useAppDispatch(); + const isStudyFavorite = useAppSelector(isCurrentStudyFavorite); + const isManaged = study?.managed; + const isArchived = study?.archived; + + if (!study) { + return null; + } + + return ( + + + + {study.name} + + + dispatch(toggleFavorite(study.id))} + /> + + + + {isManaged ? ( + + ) : ( + + )} + {study.tags?.map((tag) => ( + + ))} + {isExplorer && ( + + )} + + {study.type === StudyType.VARIANT && ( + + )} + + + ); +} + +export default Actions; diff --git a/webapp/src/components/App/Singlestudy/NavHeader/ActionsMenu.tsx b/webapp/src/components/App/Singlestudy/NavHeader/ActionsMenu.tsx new file mode 100644 index 0000000000..427937ea6b --- /dev/null +++ b/webapp/src/components/App/Singlestudy/NavHeader/ActionsMenu.tsx @@ -0,0 +1,54 @@ +import { Menu, MenuItem, ListItemIcon, ListItemText } from "@mui/material"; +import { SvgIconComponent } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; + +export interface ActionsMenuItem { + key: string; + icon: SvgIconComponent; + action: VoidFunction | (() => Promise); + condition?: boolean; + color?: string; +} + +interface Props { + anchorEl: HTMLElement | null; + open: boolean; + onClose: VoidFunction; + items: ActionsMenuItem[]; +} + +function ActionsMenu({ anchorEl, open, onClose, items }: Props) { + const [t] = useTranslation(); + + return ( + + {items.map( + (item) => + item.condition && ( + { + item.action(); + onClose(); + }} + key={item.key} + > + + + + + {t(item.key)} + + + ) + )} + + ); +} + +export default ActionsMenu; diff --git a/webapp/src/components/App/Singlestudy/NavHeader/Details.tsx b/webapp/src/components/App/Singlestudy/NavHeader/Details.tsx new file mode 100644 index 0000000000..3da34c1f23 --- /dev/null +++ b/webapp/src/components/App/Singlestudy/NavHeader/Details.tsx @@ -0,0 +1,112 @@ +import ScheduleOutlinedIcon from "@mui/icons-material/ScheduleOutlined"; +import UpdateOutlinedIcon from "@mui/icons-material/UpdateOutlined"; +import AltRouteOutlinedIcon from "@mui/icons-material/AltRouteOutlined"; +import SecurityOutlinedIcon from "@mui/icons-material/SecurityOutlined"; +import AccountTreeOutlinedIcon from "@mui/icons-material/AccountTreeOutlined"; +import PersonOutlineOutlinedIcon from "@mui/icons-material/PersonOutlineOutlined"; +import { Box, Divider, Tooltip, Typography, styled } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; +import { + buildModificationDate, + convertUTCToLocalTime, + countAllChildrens, + displayVersionName, +} from "../../../../services/utils"; +import { StudyMetadata, VariantTree } from "../../../../common/types"; +import { PUBLIC_MODE_LIST } from "../../../common/utils/constants"; + +const MAX_STUDY_TITLE_LENGTH = 45; + +const TinyText = styled(Typography)(({ theme }) => ({ + fontSize: "14px", + color: theme.palette.text.secondary, +})); + +const LinkText = styled(Link)(({ theme }) => ({ + fontSize: "14px", + color: theme.palette.secondary.main, +})); + +const StyledDivider = styled(Divider)(({ theme }) => ({ + margin: theme.spacing(0, 1), + width: "1px", + height: "20px", + backgroundColor: theme.palette.divider, +})); + +const BoxContainer = styled(Box)(({ theme }) => ({ + display: "flex", + flexDirection: "row", + justifyContent: "flex-start", + alignItems: "center", + margin: theme.spacing(0, 3), +})); + +interface Props { + study: StudyMetadata | undefined; + parent: StudyMetadata | undefined; + childrenTree: VariantTree | undefined; +} + +function Details({ study, parent, childrenTree }: Props) { + const [t, i18n] = useTranslation(); + const publicModeLabel = + PUBLIC_MODE_LIST.find((mode) => mode.id === study?.publicMode)?.name || ""; + + if (!study) { + return null; + } + + return ( + + + + {convertUTCToLocalTime(study.creationDate)} + + + + + {buildModificationDate(study.modificationDate, t, i18n.language)} + + + + + {`v${displayVersionName(study.version)}`} + + {parent && ( + + + + + {`${parent.name.substring(0, MAX_STUDY_TITLE_LENGTH)}...`} + + + + )} + {childrenTree && ( + + + {countAllChildrens(childrenTree)} + + )} + + + + {study.owner.name} + + + + {t(publicModeLabel)} + + + ); +} + +export default Details; diff --git a/webapp/src/components/App/Singlestudy/NavHeader/index.tsx b/webapp/src/components/App/Singlestudy/NavHeader/index.tsx new file mode 100644 index 0000000000..2583038c4b --- /dev/null +++ b/webapp/src/components/App/Singlestudy/NavHeader/index.tsx @@ -0,0 +1,320 @@ +import { useState, MouseEvent } from "react"; +import debug from "debug"; +import { useSnackbar } from "notistack"; +import { useNavigate } from "react-router-dom"; +import { AxiosError } from "axios"; +import { Box, Button, Tooltip, Typography } from "@mui/material"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import UpgradeIcon from "@mui/icons-material/Upgrade"; +import UnarchiveOutlinedIcon from "@mui/icons-material/UnarchiveOutlined"; +import DownloadOutlinedIcon from "@mui/icons-material/DownloadOutlined"; +import ArchiveOutlinedIcon from "@mui/icons-material/ArchiveOutlined"; +import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined"; +import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; +import { useTranslation } from "react-i18next"; +import { StudyMetadata, VariantTree } from "../../../../common/types"; +import { STUDIES_HEIGHT_HEADER } from "../../../../theme"; +import { archiveStudy, unarchiveStudy } from "../../../../services/api/study"; +import { deleteStudy } from "../../../../redux/ducks/studies"; +import LauncherDialog from "../../Studies/LauncherDialog"; +import PropertiesDialog from "../PropertiesDialog"; +import useEnqueueErrorSnackbar from "../../../../hooks/useEnqueueErrorSnackbar"; +import { getLatestStudyVersion } from "../../../../redux/selectors"; +import ExportDialog from "../../Studies/ExportModal"; +import ConfirmationDialog from "../../../common/dialogs/ConfirmationDialog"; +import useAppSelector from "../../../../redux/hooks/useAppSelector"; +import useAppDispatch from "../../../../redux/hooks/useAppDispatch"; +import CheckBoxFE from "../../../common/fieldEditors/CheckBoxFE"; +import Details from "./Details"; +import Actions from "./Actions"; +import UpgradeDialog from "../UpgradeDialog"; +import ActionsMenu, { ActionsMenuItem } from "./ActionsMenu"; + +const logError = debug("antares:singlestudy:navheader:error"); + +interface Props { + study: StudyMetadata | undefined; + parent: StudyMetadata | undefined; + childrenTree: VariantTree | undefined; + isExplorer?: boolean; + openCommands: VoidFunction; + updateStudyData: VoidFunction; +} + +function NavHeader({ + study, + parent, + childrenTree, + isExplorer, + openCommands, + updateStudyData, +}: Props) { + const [t] = useTranslation(); + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const latestVersion = useAppSelector(getLatestStudyVersion); + const [anchorEl, setAnchorEl] = useState(null); + const [openLauncherDialog, setOpenLauncherDialog] = useState(false); + const [openPropertiesDialog, setOpenPropertiesDialog] = useState(false); + const [openUpgradeDialog, setOpenUpgradeDialog] = useState(false); + const [deleteChildren, setDeleteChildren] = useState(false); + const [openDeleteDialog, setOpenDeleteDialog] = useState(false); + const [openExportDialog, setOpenExportDialog] = useState(false); + const { enqueueSnackbar } = useSnackbar(); + const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); + const isLatestVersion = study?.version === latestVersion; + const isManaged = !!study?.managed; + const isArchived = !!study?.archived; + + //////////////////////////////////////////////////////////////// + // Event Handlers + //////////////////////////////////////////////////////////////// + + const handleOpenMenu = (event: MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleClickBack = () => { + if (isExplorer) { + navigate(`/studies/${study?.id}`); + } else { + navigate("/studies"); + } + }; + + const handleLaunch = () => { + setOpenLauncherDialog(true); + }; + + const handleArchive = async () => { + if (study) { + try { + await archiveStudy(study.id); + } catch (e) { + enqueueErrorSnackbar( + t("studies.error.archive", { studyname: study.name }), + e as AxiosError + ); + } finally { + handleClose(); + } + } + }; + + const handleUnarchive = async () => { + if (study) { + try { + await unarchiveStudy(study.id); + } catch (e) { + enqueueErrorSnackbar( + t("studies.error.unarchive", { studyname: study.name }), + e as AxiosError + ); + } finally { + handleClose(); + } + } + }; + + const handleDelete = async () => { + if (study) { + try { + await dispatch(deleteStudy({ id: study.id, deleteChildren })).unwrap(); + navigate(parent ? `/studies/${parent?.id}` : "/studies"); + } catch (err) { + enqueueErrorSnackbar(t("studies.error.deleteStudy"), err as AxiosError); + logError("Failed to delete study", study, err); + } finally { + setDeleteChildren(false); + setOpenDeleteDialog(false); + } + } + }; + + const handleCopyId = async () => { + if (study) { + try { + await navigator.clipboard.writeText(study.id); + enqueueSnackbar(t("study.success.studyIdCopy"), { + variant: "success", + }); + } catch (e) { + enqueueErrorSnackbar(t("study.error.studyIdCopy"), e as AxiosError); + } + } + }; + + //////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////// + + const menuItems: ActionsMenuItem[] = [ + { + key: "study.properties", + icon: EditOutlinedIcon, + action: () => setOpenPropertiesDialog(true), + condition: !isArchived, + }, + { + key: "study.upgrade", + icon: UpgradeIcon, + action: () => setOpenUpgradeDialog(true), + condition: !isArchived && !isLatestVersion, + }, + { + key: "global.export", + icon: DownloadOutlinedIcon, + action: () => setOpenExportDialog(true), + condition: !isArchived, + }, + { + key: "global.archive", + icon: ArchiveOutlinedIcon, + action: handleArchive, + condition: !isArchived && isManaged, + }, + { + key: "global.unarchive", + icon: UnarchiveOutlinedIcon, + action: handleUnarchive, + condition: isArchived, + }, + { + key: "global.delete", + icon: DeleteOutlinedIcon, + action: () => setOpenDeleteDialog(true), + color: "error.light", + condition: isManaged, + }, + ]; + + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + + return ( + + + + + + + + + +
+ {study && openLauncherDialog && ( + setOpenLauncherDialog(false)} + /> + )} + {study && openPropertiesDialog && ( + setOpenPropertiesDialog(false)} + study={study} + updateStudyData={updateStudyData} + /> + )} + {openDeleteDialog && ( + setOpenDeleteDialog(false)} + onConfirm={handleDelete} + alert="warning" + open + > + + {t("studies.question.delete")} + setDeleteChildren(checked)} + /> + + + )} + {study && openUpgradeDialog && ( + setOpenUpgradeDialog(false)} + /> + )} + {study && openExportDialog && ( + setOpenExportDialog(false)} + study={study} + /> + )} + + ); +} + +export default NavHeader; diff --git a/webapp/src/components/App/Singlestudy/UpgradeDialog.tsx b/webapp/src/components/App/Singlestudy/UpgradeDialog.tsx new file mode 100644 index 0000000000..f5d7f56cea --- /dev/null +++ b/webapp/src/components/App/Singlestudy/UpgradeDialog.tsx @@ -0,0 +1,75 @@ +import UpgradeIcon from "@mui/icons-material/Upgrade"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { StudyMetadata } from "../../../common/types"; +import { SubmitHandlerPlus } from "../../common/Form/types"; +import Fieldset from "../../common/Fieldset"; +import SelectFE from "../../common/fieldEditors/SelectFE"; +import FormDialog from "../../common/dialogs/FormDialog"; +import useAppSelector from "../../../redux/hooks/useAppSelector"; +import { getStudyVersionsFormatted } from "../../../redux/selectors"; +import { upgradeStudy } from "../../../services/api/study"; + +interface Props { + study: StudyMetadata; + onClose: () => void; + open: boolean; +} + +const defaultValues = { + version: "", +}; + +function UpgradeDialog({ study, onClose, open }: Props) { + const [t] = useTranslation(); + const versions = useAppSelector(getStudyVersionsFormatted); + const versionOptions = useMemo(() => { + return versions + .filter((version) => version.id > study.version) + .sort((a, b) => b.name.localeCompare(a.name)) + .map(({ id, name }) => ({ + value: id, + label: name, + })); + }, [versions, study.version]); + + //////////////////////////////////////////////////////////////// + // Event handlers + //////////////////////////////////////////////////////////////// + + const handleSubmit = async ( + data: SubmitHandlerPlus + ) => { + return upgradeStudy(study.id, data.values.version).then(onClose); + }; + + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + + return ( + + {({ control }) => ( +
+ +
+ )} +
+ ); +} + +export default UpgradeDialog; diff --git a/webapp/src/components/App/Studies/StudyCard.tsx b/webapp/src/components/App/Studies/StudyCard.tsx index 414a734f97..20362b7577 100644 --- a/webapp/src/components/App/Studies/StudyCard.tsx +++ b/webapp/src/components/App/Studies/StudyCard.tsx @@ -261,7 +261,7 @@ const StudyCard = memo((props: Props) => { diff --git a/webapp/src/services/api/study.ts b/webapp/src/services/api/study.ts index ca8e9f24fd..1795f64121 100644 --- a/webapp/src/services/api/study.ts +++ b/webapp/src/services/api/study.ts @@ -166,8 +166,15 @@ export const unarchiveStudy = async (sid: string): Promise => { await client.put(`/v1/studies/${sid}/unarchive`); }; -export const upgradeStudy = async (sid: string): Promise => { - await client.put(`/v1/studies/${sid}/upgrade`); +export const upgradeStudy = async ( + studyId: string, + targetVersion: string +): Promise => { + await client.put( + `/v1/studies/${studyId}/upgrade?target_version=${encodeURIComponent( + targetVersion + )}` + ); }; export const deleteStudy = async ( diff --git a/webapp/src/theme.ts b/webapp/src/theme.ts index 04da51e72c..155f60c88f 100644 --- a/webapp/src/theme.ts +++ b/webapp/src/theme.ts @@ -3,7 +3,7 @@ import { createTheme } from "@mui/material/styles"; export const DRAWER_WIDTH = 60; export const DRAWER_WIDTH_EXTENDED = 240; -export const STUDIES_HEIGHT_HEADER = 166; +export const STUDIES_HEIGHT_HEADER = 130; export const STUDIES_SIDE_NAV_WIDTH = 300; export const STUDIES_LIST_HEADER_HEIGHT = 100; export const STUDIES_FILTER_WIDTH = 300;