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
+}