From e1e9d016a1c8fc59f9c69bfdd0d3f7dd1c7a00f3 Mon Sep 17 00:00:00 2001 From: Diana Nanyanzi <diana@dhis2.org> Date: Wed, 11 Dec 2024 08:58:17 +0300 Subject: [PATCH] feat: add deletion functionality - use context for Side Panel components - add delete modal to handle deletion of namespaces and keys - add context menu button on every rendered link ` --- i18n/en.pot | 47 +++++++----- src/components/Panel.module.css | 9 +++ src/components/modals/CreateModal.tsx | 18 +++-- src/components/modals/DeleteModal.tsx | 84 +++++++++++++++++++++ src/components/panels/EditPanel.tsx | 1 + src/components/sidepanel/ContextButton.tsx | 62 +++++++++++++++ src/components/sidepanel/CreateButton.tsx | 7 +- src/components/sidepanel/PanelLink.tsx | 31 ++++---- src/components/sidepanel/PanelLinksList.tsx | 70 +++++++++++------ src/components/sidepanel/SidePanel.tsx | 51 ++++++------- src/context/SidePanelContext.tsx | 35 +++++++++ src/hooks/useCustomAlert.tsx | 2 +- src/hooks/useDeleteMutation.tsx | 31 ++++++++ 13 files changed, 350 insertions(+), 98 deletions(-) create mode 100644 src/components/modals/DeleteModal.tsx create mode 100644 src/components/sidepanel/ContextButton.tsx create mode 100644 src/context/SidePanelContext.tsx create mode 100644 src/hooks/useDeleteMutation.tsx diff --git a/i18n/en.pot b/i18n/en.pot index a46ec1f..6786514 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-12-11T00:48:22.223Z\n" -"PO-Revision-Date: 2024-12-11T00:48:22.223Z\n" +"POT-Creation-Date: 2024-12-11T01:36:42.741Z\n" +"PO-Revision-Date: 2024-12-11T01:36:42.741Z\n" msgid "View keys" msgstr "View keys" @@ -23,6 +23,24 @@ msgstr "An error occurred" msgid "Error" msgstr "Error" +msgid "Add New Namespace" +msgstr "Add New Namespace" + +msgid "Add New Key" +msgstr "Add New Key" + +msgid "Add Namespace" +msgstr "Add Namespace" + +msgid "Add Key" +msgstr "Add Key" + +msgid "New namespace" +msgstr "New namespace" + +msgid "New key" +msgstr "New key" + msgid "Namespace" msgstr "Namespace" @@ -32,6 +50,12 @@ msgstr "Key" msgid "Cancel" msgstr "Cancel" +msgid "This will delete all the keys in this namespace" +msgstr "This will delete all the keys in this namespace" + +msgid "Delete" +msgstr "Delete" + msgid "Key successfully updated" msgstr "Key successfully updated" @@ -59,20 +83,5 @@ msgstr "Search" msgid "Key created successfully" msgstr "Key created successfully" -msgid "Add New Namespace" -msgstr "Add New Namespace" - -msgid "Add New Key" -msgstr "Add New Key" - -msgid "Add Namespace" -msgstr "Add Namespace" - -msgid "Add Key" -msgstr "Add Key" - -msgid "New namespace" -msgstr "New namespace" - -msgid "New key" -msgstr "New key" +msgid "There was an error creating the key" +msgstr "There was an error creating the key" diff --git a/src/components/Panel.module.css b/src/components/Panel.module.css index 05d03f1..fa496b7 100644 --- a/src/components/Panel.module.css +++ b/src/components/Panel.module.css @@ -34,6 +34,7 @@ margin: 0.5em 0 0.3em 0; width: 100%; border: none; + justify-content: start; } .sidebarList ul { @@ -97,3 +98,11 @@ .navLink.active:hover { background: var(--colors-teal050); } + +/* context menu */ +.contextMenu button { + background-color: transparent; + border: none; + width: 100%; + justify-content: start; +} diff --git a/src/components/modals/CreateModal.tsx b/src/components/modals/CreateModal.tsx index acff421..dc872b5 100644 --- a/src/components/modals/CreateModal.tsx +++ b/src/components/modals/CreateModal.tsx @@ -8,6 +8,7 @@ import { InputField, } from '@dhis2/ui' import React from 'react' +import { useSidePanelContext } from '../../context/SidePanelContext' import i18n from '../../locales' import { CreateFieldValues } from '../sidepanel/SidePanel' @@ -16,9 +17,6 @@ type CreateModalProps = { values: CreateFieldValues setValues: (values) => void closeModal: () => void - title: string - type: string - buttonLabel: string } const CreateModal = ({ @@ -26,12 +24,18 @@ const CreateModal = ({ values, setValues, closeModal, - title, - type, - buttonLabel, }: CreateModalProps) => { + const { panelType: type } = useSidePanelContext() + + const title = + type === 'namespace' + ? i18n.t('Add New Namespace') + : i18n.t('Add New Key') + const buttonLabel = + type === 'namespace' ? i18n.t('Add Namespace') : i18n.t('Add Key') + return ( - <Modal> + <Modal position="middle"> <ModalTitle>{title}</ModalTitle> <ModalContent> {type === 'namespace' && ( diff --git a/src/components/modals/DeleteModal.tsx b/src/components/modals/DeleteModal.tsx new file mode 100644 index 0000000..5d8b7b0 --- /dev/null +++ b/src/components/modals/DeleteModal.tsx @@ -0,0 +1,84 @@ +import { + Modal, + ModalContent, + ModalActions, + ModalTitle, + Button, + ButtonStrip, +} from '@dhis2/ui' +import React from 'react' +import { useParams } from 'react-router-dom' +import { useSidePanelContext } from '../../context/SidePanelContext' +import i18n from '../../locales' + +const DeleteModal = ({ + handleDeleteAction, +}: { + handleDeleteAction: () => void +}) => { + const { + panelType: type, + totalItems, + selectedLinkItem: value, + setOpenDeleteModal, + } = useSidePanelContext() + const { namespace: currentNamespace } = useParams() + + const title = + type === 'namespace' ? i18n.t('Delete Namespace') : i18n.t('Delete Key') + + return ( + <Modal position="middle"> + <ModalTitle>{title}</ModalTitle> + <ModalContent> + {type === 'namespace' && ( + <> + <p> + {i18n.t( + `Are you sure you want to delete '${value}'?` + )} + </p> + <p> + {i18n.t( + `This will delete all the keys in this namespace` + )} + </p> + </> + )} + {type === 'keys' && ( + <> + <p> + {i18n.t( + `Are you sure you want to delete '${value}' in ${currentNamespace}?` + )} + </p> + {totalItems < 2 && ( + <p> + {i18n.t( + `This will also delete the namespace '${currentNamespace}'` + )} + </p> + )} + </> + )} + </ModalContent> + <ModalActions> + <ButtonStrip end> + <Button secondary onClick={() => setOpenDeleteModal(false)}> + {i18n.t('Cancel')} + </Button> + <Button + destructive + onClick={() => { + handleDeleteAction() + setOpenDeleteModal(false) + }} + > + {i18n.t('Delete')} + </Button> + </ButtonStrip> + </ModalActions> + </Modal> + ) +} +export default DeleteModal diff --git a/src/components/panels/EditPanel.tsx b/src/components/panels/EditPanel.tsx index 741ee0d..4156b58 100644 --- a/src/components/panels/EditPanel.tsx +++ b/src/components/panels/EditPanel.tsx @@ -75,6 +75,7 @@ const EditPanel = () => { }) } catch (error) { // setUpdateError(error.message) + console.error(error.message) const message = i18n.t('There was an error updating the key') showError(message) } diff --git a/src/components/sidepanel/ContextButton.tsx b/src/components/sidepanel/ContextButton.tsx new file mode 100644 index 0000000..4d9603a --- /dev/null +++ b/src/components/sidepanel/ContextButton.tsx @@ -0,0 +1,62 @@ +import { Button, IconMore16, Popover, IconDelete16 } from '@dhis2/ui' +import React, { useRef } from 'react' +import { useSidePanelContext } from '../../context/SidePanelContext' +import i18n from '../../locales' +import classes from '../Panel.module.css' + +type ContextMenuButtonProps = { + handleContextMenu: () => void + openContextMenu: boolean + setOpenContextMenu: (boolean) => void +} + +const ContextMenuButton = ({ + handleContextMenu, + openContextMenu, + setOpenContextMenu, +}: ContextMenuButtonProps) => { + const ref = useRef(null) + const { setOpenDeleteModal } = useSidePanelContext() + + return ( + <div ref={ref}> + <Button + aria-label="More" + icon={<IconMore16 />} + name="more" + onClick={handleContextMenu} + title="More" + /> + {openContextMenu && ( + <Popover + reference={ref} + placement="right-start" + onClickOutside={() => setOpenContextMenu(false)} + > + <div + className={classes.contextMenu} + style={{ + width: '150px', + padding: 6, + }} + > + <Button + aria-label={i18n.t('delete')} + icon={<IconDelete16 />} + name={i18n.t('delete')} + onClick={() => { + setOpenContextMenu(false) + setOpenDeleteModal(true) + }} + title={i18n.t('delete')} + > + {i18n.t('Delete')} + </Button> + </div> + </Popover> + )} + </div> + ) +} + +export default ContextMenuButton diff --git a/src/components/sidepanel/CreateButton.tsx b/src/components/sidepanel/CreateButton.tsx index 071bfd3..6736dbd 100644 --- a/src/components/sidepanel/CreateButton.tsx +++ b/src/components/sidepanel/CreateButton.tsx @@ -1,15 +1,18 @@ import { Button } from '@dhis2/ui' import { IconAdd16 } from '@dhis2/ui-icons' import React from 'react' +import { useSidePanelContext } from '../../context/SidePanelContext' import i18n from '../../locales' import classes from '../Panel.module.css' type CreateButtonProps = { - label: string handleClick: () => void } -const CreateButton = ({ label, handleClick }: CreateButtonProps) => { +const CreateButton = ({ handleClick }: CreateButtonProps) => { + const { panelType: type } = useSidePanelContext() + const label = + type === 'namespace' ? i18n.t('New namespace') : i18n.t('New key') return ( <div className={classes.createButton}> <Button diff --git a/src/components/sidepanel/PanelLink.tsx b/src/components/sidepanel/PanelLink.tsx index d808858..e4e9500 100644 --- a/src/components/sidepanel/PanelLink.tsx +++ b/src/components/sidepanel/PanelLink.tsx @@ -1,13 +1,9 @@ -import { Button } from '@dhis2/ui' -import { - IconFile16, - IconMore16, - IconFolder16, - IconFolderOpen16, -} from '@dhis2/ui-icons' -import React from 'react' +import { IconFile16, IconFolder16, IconFolderOpen16 } from '@dhis2/ui-icons' +import React, { useState } from 'react' import { NavLink } from 'react-router-dom' +import { useSidePanelContext } from '../../context/SidePanelContext' import classes from '../Panel.module.css' +import ContextMenuButton from './ContextButton' type SidePanelLinkProps = { label: string @@ -16,6 +12,13 @@ type SidePanelLinkProps = { } const SidePanelLink = ({ to, label, type }: SidePanelLinkProps) => { + const { setSelectedLinkItem } = useSidePanelContext() + const [openContextMenu, setOpenContextMenu] = useState(false) + + const handleContextMenu = () => { + setSelectedLinkItem(label) + setOpenContextMenu((prev) => !prev) + } const renderIcon = ({ isActive }) => type === 'namespace' ? ( isActive ? ( @@ -38,16 +41,14 @@ const SidePanelLink = ({ to, label, type }: SidePanelLinkProps) => { <> {renderIcon({ isActive })} <span>{label}</span> - <Button - aria-label="More" - icon={<IconMore16 />} - name="more" - // onClick={} - title="More" - /> </> )} </NavLink> + <ContextMenuButton + handleContextMenu={handleContextMenu} + openContextMenu={openContextMenu} + setOpenContextMenu={setOpenContextMenu} + /> </li> ) } diff --git a/src/components/sidepanel/PanelLinksList.tsx b/src/components/sidepanel/PanelLinksList.tsx index df612af..e35a869 100644 --- a/src/components/sidepanel/PanelLinksList.tsx +++ b/src/components/sidepanel/PanelLinksList.tsx @@ -1,49 +1,68 @@ import React, { useEffect } from 'react' -import { useParams } from 'react-router-dom' -import { ErrorResponse } from '../error/ErrorComponent' +import { useNavigate, useParams } from 'react-router-dom' +import { useSidePanelContext } from '../../context/SidePanelContext' +import { + useDeleteKeyMutation, + useDeleteNamespaceMutation, +} from '../../hooks/useDeleteMutation' +import DeleteModal from '../modals/DeleteModal' import classes from '../Panel.module.css' -import CenteredLoader from './Loader' import SidePanelLink from './PanelLink' type PanelLinksListProps = { data: { results: [] } - error: { details: ErrorResponse } - loading: boolean refetchList: () => void - type: string } -function PanelLinksList({ - data, - error, - loading, - refetchList, - type, -}: PanelLinksListProps) { - const { store, namespace, key } = useParams() - // const [selectedLink, setSelectedLink] = useState('') +function PanelLinksList({ data, refetchList }: PanelLinksListProps) { + const { store, namespace: currentNamespace, key } = useParams() + const navigate = useNavigate() + const { + panelType: type, + openDeleteModal, + selectedLinkItem, + } = useSidePanelContext() useEffect(() => { refetchList() - }, [store, namespace, key, refetchList]) + }, [store, currentNamespace, key, refetchList]) - if (error) { - throw new Response('', { - status: error?.details.httpStatusCode, - statusText: error?.details.status || error.details.message, - }) - } + const handleDeleteAction = + type === 'namespace' + ? useDeleteNamespaceMutation({ + namespace: selectedLinkItem, + store, + refetch: () => { + refetchList() + navigate(`${store}`) + }, + }) + : type === 'keys' + ? data?.results?.length < 2 + ? useDeleteNamespaceMutation({ + namespace: currentNamespace, + store, + refetch: () => { + navigate(`/${store}`) + }, + }) + : useDeleteKeyMutation({ + namespace: currentNamespace, + key: selectedLinkItem, + store, + refetch: refetchList, + }) + : null return ( <div className={classes.sidebarList}> - {loading && <CenteredLoader />} {data && ( <ul> {data.results.map((value: string, index) => { const path = type === 'namespace' ? `/${store}/edit/${value}` - : `/${store}/edit/${namespace}/${value}` + : `/${store}/edit/${currentNamespace}/${value}` return ( <SidePanelLink key={`${index}-${value}`} @@ -55,6 +74,9 @@ function PanelLinksList({ })} </ul> )} + {openDeleteModal && ( + <DeleteModal handleDeleteAction={handleDeleteAction} /> + )} </div> ) } diff --git a/src/components/sidepanel/SidePanel.tsx b/src/components/sidepanel/SidePanel.tsx index 7104ab2..7ad2b3e 100644 --- a/src/components/sidepanel/SidePanel.tsx +++ b/src/components/sidepanel/SidePanel.tsx @@ -1,14 +1,16 @@ -import { useDataEngine } from '@dhis2/app-service-data' +import { useDataEngine } from '@dhis2/app-runtime' import React, { useState } from 'react' import { useNavigate, useParams } from 'react-router' +import { SidePanelContextProvider } from '../../context/SidePanelContext' +import useCustomAlert from '../../hooks/useCustomAlert' import i18n from '../../locales' import { ErrorResponse } from '../error/ErrorComponent' import CreateModal from '../modals/CreateModal' import classes from '../Panel.module.css' import CreateButton from './CreateButton' +import CenteredLoader from './Loader' import PanelLinksList from './PanelLinksList' import PanelSearchField from './SearchField' -import useCustomAlert from '../../hooks/useCustomAlert' type SidePanelProps = { data: { results: [] } @@ -33,7 +35,7 @@ const SidePanel = ({ const engine = useDataEngine() const navigate = useNavigate() const { store, namespace: currentNamespace } = useParams() - const [openModal, setOpenModal] = useState(false) + const [openCreateModal, setOpenCreateModal] = useState(false) const [values, setValues] = useState({}) const { showSuccess, showError } = useCustomAlert() @@ -73,47 +75,36 @@ const SidePanel = ({ }, } ) - setOpenModal(false) + setOpenCreateModal(false) } - const derivedModalProps = { - title: - type === 'namespace' - ? i18n.t('Add New Namespace') - : i18n.t('Add New Key'), - buttonLabel: - type === 'namespace' ? i18n.t('Add Namespace') : i18n.t('Add Key'), + if (error) { + throw new Response('', { + status: error?.details.httpStatusCode, + statusText: error?.details.status || error.details.message, + }) } - const createButtonLabel = - type === 'namespace' ? i18n.t('New namespace') : i18n.t('New key') return ( - <> + <SidePanelContextProvider + panelType={type} + totalItems={data?.results?.length} + > <div className={classes.sidebarContent}> <PanelSearchField /> - <CreateButton - label={createButtonLabel} - handleClick={() => setOpenModal(true)} - /> - <PanelLinksList - data={data} - error={error} - loading={loading} - refetchList={refetchList} - type={type} - /> + <CreateButton handleClick={() => setOpenCreateModal(true)} /> + {loading && <CenteredLoader />} + <PanelLinksList data={data} refetchList={refetchList} /> </div> - {openModal && ( + {openCreateModal && ( <CreateModal createFn={handleCreate} values={values} setValues={setValues} - closeModal={() => setOpenModal(false)} - type={type} - {...derivedModalProps} + closeModal={() => setOpenCreateModal(false)} /> )} - </> + </SidePanelContextProvider> ) } diff --git a/src/context/SidePanelContext.tsx b/src/context/SidePanelContext.tsx new file mode 100644 index 0000000..8ab122e --- /dev/null +++ b/src/context/SidePanelContext.tsx @@ -0,0 +1,35 @@ +import React, { createContext, useContext, useState } from 'react' + +type SidePanelContextProps = { + panelType: string + children: React.ReactElement[] + totalItems: number +} + +export const SidePanelContext = createContext(null) + +export const SidePanelContextProvider = ({ + panelType, + children, + totalItems, +}: SidePanelContextProps) => { + const [selectedLinkItem, setSelectedLinkItem] = useState(null) + const [openDeleteModal, setOpenDeleteModal] = useState(false) + + return ( + <SidePanelContext.Provider + value={{ + panelType, + selectedLinkItem, + setSelectedLinkItem, + totalItems, + openDeleteModal, + setOpenDeleteModal, + }} + > + {children} + </SidePanelContext.Provider> + ) +} + +export const useSidePanelContext = () => useContext(SidePanelContext) diff --git a/src/hooks/useCustomAlert.tsx b/src/hooks/useCustomAlert.tsx index 2d40f0b..687728b 100644 --- a/src/hooks/useCustomAlert.tsx +++ b/src/hooks/useCustomAlert.tsx @@ -1,4 +1,4 @@ -import { useAlert } from "@dhis2/app-runtime" +import { useAlert } from '@dhis2/app-runtime' const useCustomAlert = () => { const { show } = useAlert( diff --git a/src/hooks/useDeleteMutation.tsx b/src/hooks/useDeleteMutation.tsx new file mode 100644 index 0000000..4c82089 --- /dev/null +++ b/src/hooks/useDeleteMutation.tsx @@ -0,0 +1,31 @@ +import { useDataEngine } from '@dhis2/app-runtime' + +export const useDeleteKeyMutation = ({ key, namespace, store, refetch }) => { + const engine = useDataEngine() + + const handleDeleteAction = async () => { + await engine.mutate({ + type: 'delete' as const, + resource: `${store}/${namespace}`, + id: key, + }) + refetch() + } + + return handleDeleteAction +} + +export const useDeleteNamespaceMutation = ({ namespace, store, refetch }) => { + const engine = useDataEngine() + + const handleDeleteAction = async () => { + await engine.mutate({ + type: 'delete' as const, + resource: `${store}`, + id: namespace, + }) + refetch() + } + + return handleDeleteAction +}