From a3a5bb200578a8b6063fa874c2f47ef969e868b9 Mon Sep 17 00:00:00 2001 From: karolina-siemieniuk-morawska <55505399+karolina-siemieniuk-morawska@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:38:31 +0200 Subject: [PATCH] ui: connect delete button in new holdingpen * ref: cern-sis/issues-inspire#533 --- ui/src/actions/actionTypes.ts | 3 + ui/src/actions/holdingpen.ts | 49 ++ ui/src/common/components/EmptyOrChildren.tsx | 2 +- .../DeleteWorkflow/DeleteWorkflow.less | 5 + .../DeleteWorkflow/DeleteWorkflow.tsx | 48 ++ .../__tests__/DeleteWorkflow.test.tsx | 56 ++ .../AuthorDetailPageContainer.tsx | 571 +++++++++--------- ui/src/holdingpen-new/notifications.ts | 16 + ui/src/reducers/holdingpen.js | 11 + 9 files changed, 487 insertions(+), 274 deletions(-) create mode 100644 ui/src/holdingpen-new/components/DeleteWorkflow/DeleteWorkflow.less create mode 100644 ui/src/holdingpen-new/components/DeleteWorkflow/DeleteWorkflow.tsx create mode 100644 ui/src/holdingpen-new/components/__tests__/DeleteWorkflow.test.tsx diff --git a/ui/src/actions/actionTypes.ts b/ui/src/actions/actionTypes.ts index 2c7e7be47e..fd99474ee7 100644 --- a/ui/src/actions/actionTypes.ts +++ b/ui/src/actions/actionTypes.ts @@ -151,3 +151,6 @@ export const HOLDINGPEN_RESOLVE_ACTION_SUCCESS = 'HOLDINGPEN_RESOLVE_ACTION_SUCCESS'; export const HOLDINGPEN_RESOLVE_ACTION_ERROR = 'HOLDINGPEN_RESOLVE_ACTION_ERROR'; +export const HOLDINGPEN_DELETE_REQUEST = 'HOLDINGPEN_DELETE_REQUEST'; +export const HOLDINGPEN_DELETE_SUCCESS = 'HOLDINGPEN_DELETE_SUCCESS'; +export const HOLDINGPEN_DELETE_ERROR = 'HOLDINGPEN_DELETE_ERROR'; diff --git a/ui/src/actions/holdingpen.ts b/ui/src/actions/holdingpen.ts index ae5b67ec6b..7bbcfcd446 100644 --- a/ui/src/actions/holdingpen.ts +++ b/ui/src/actions/holdingpen.ts @@ -21,6 +21,9 @@ import { HOLDINGPEN_RESOLVE_ACTION_SUCCESS, HOLDINGPEN_RESOLVE_ACTION_ERROR, HOLDINGPEN_LOGIN_REQUEST, + HOLDINGPEN_DELETE_SUCCESS, + HOLDINGPEN_DELETE_ERROR, + HOLDINGPEN_DELETE_REQUEST, } from './actionTypes'; import { BACKOFFICE_API, @@ -35,6 +38,8 @@ import { notifyLoginError, notifyActionError, notifyActionSuccess, + notifyDeleteSuccess, + notifyDeleteError, } from '../holdingpen-new/notifications'; import { refreshToken } from '../holdingpen-new/utils/utils'; @@ -318,3 +323,47 @@ export function resolveAction( } }; } + +// DELETE ACTIONS + +export const deletingWorkflow = () => { + return { + type: HOLDINGPEN_DELETE_REQUEST, + }; +}; + +export const deleteWorkflowSuccess = () => { + return { + type: HOLDINGPEN_DELETE_SUCCESS, + }; +}; + +export const deleteWorkflowError = (errorPayload: { error: Error }) => { + return { + type: HOLDINGPEN_DELETE_ERROR, + payload: { ...errorPayload }, + }; +}; + +export function deleteWorkflow( + id: string +): (dispatch: ActionCreator) => Promise { + return async (dispatch) => { + dispatch(deletingWorkflow()); + try { + await httpClient.delete(`${BACKOFFICE_API}/${id}/`); + + dispatch(deleteWorkflowSuccess()); + notifyDeleteSuccess(); + } catch (err) { + const { error } = httpErrorToActionPayload(err); + + dispatch(deleteWorkflowError(error)); + notifyDeleteError( + (typeof error?.error === 'string' + ? error?.error + : error?.error?.detail) || 'An error occurred' + ); + } + }; +} diff --git a/ui/src/common/components/EmptyOrChildren.tsx b/ui/src/common/components/EmptyOrChildren.tsx index 0e806db773..25128366a0 100644 --- a/ui/src/common/components/EmptyOrChildren.tsx +++ b/ui/src/common/components/EmptyOrChildren.tsx @@ -18,7 +18,7 @@ const EmptyOrChildren = ({ }: { data: any; children: JSX.Element; - title: string; + title: string | JSX.Element; description?: string; }) => { return isEmptyCollection(data) ? ( diff --git a/ui/src/holdingpen-new/components/DeleteWorkflow/DeleteWorkflow.less b/ui/src/holdingpen-new/components/DeleteWorkflow/DeleteWorkflow.less new file mode 100644 index 0000000000..5eef0efd73 --- /dev/null +++ b/ui/src/holdingpen-new/components/DeleteWorkflow/DeleteWorkflow.less @@ -0,0 +1,5 @@ +.delete-modal { + .ant-modal-footer { + border-top: none; + } +} diff --git a/ui/src/holdingpen-new/components/DeleteWorkflow/DeleteWorkflow.tsx b/ui/src/holdingpen-new/components/DeleteWorkflow/DeleteWorkflow.tsx new file mode 100644 index 0000000000..b89694ab70 --- /dev/null +++ b/ui/src/holdingpen-new/components/DeleteWorkflow/DeleteWorkflow.tsx @@ -0,0 +1,48 @@ +import React, { useState } from 'react'; +import { Button, Modal } from 'antd'; +import { Action, ActionCreator } from 'redux'; + +import './DeleteWorkflow.less'; +import { deleteWorkflow } from '../../../actions/holdingpen'; + +const DeleteWorkflow: React.FC<{ + dispatch: ActionCreator; + id: string; +}> = ({ dispatch, id }) => { + const [open, setOpen] = useState(false); + + const showModal = () => { + setOpen(true); + }; + + const hideModal = () => { + setOpen(false); + }; + + return ( + <> + + { + dispatch(deleteWorkflow(id)); + hideModal(); + }} + onCancel={hideModal} + okText="Confirm" + cancelText="Cancel" + className="delete-modal" + > +

+ Are you sure you want to delete workflow? This operation is + unreversable. +

+
+ + ); +}; + +export default DeleteWorkflow; diff --git a/ui/src/holdingpen-new/components/__tests__/DeleteWorkflow.test.tsx b/ui/src/holdingpen-new/components/__tests__/DeleteWorkflow.test.tsx new file mode 100644 index 0000000000..1da64e9712 --- /dev/null +++ b/ui/src/holdingpen-new/components/__tests__/DeleteWorkflow.test.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import userEvent from '@testing-library/user-event'; + +import DeleteWorkflow from '../DeleteWorkflow/DeleteWorkflow'; + +jest.mock('../../../actions/holdingpen', () => ({ + deleteWorkflow: jest.fn((id) => ({ + type: 'HOLDINGPEN_DELETE_REQUEST', + payload: id, + })), +})); + +describe('DeleteWorkflow component', () => { + const mockDispatch = jest.fn(); + const mockId = 'test-id'; + + beforeEach(() => { + mockDispatch.mockClear(); + }); + + it('should render delete button', () => { + render(); + expect(screen.getByText('Delete')).toBeInTheDocument(); + }); + + it('should open the modal when the delete button is clicked', async () => { + render(); + + await waitFor(() => userEvent.click(screen.getByText('Delete'))); + + await waitFor(() => + expect( + screen.getByText( + 'Are you sure you want to delete workflow? This operation is unreversable.' + ) + ).toBeVisible() + ); + }); + + it('should call dispatch with deleteWorkflow and close the modal on confirm', async () => { + render(); + + await waitFor(() => userEvent.click(screen.getByText('Delete'))); + + await waitFor(() => userEvent.click(screen.getByText('Confirm'))); + + await waitFor(() => + expect(mockDispatch).toHaveBeenCalledWith({ + type: 'HOLDINGPEN_DELETE_REQUEST', + payload: mockId, + }) + ); + }); +}); diff --git a/ui/src/holdingpen-new/containers/DetailPageContainer/AuthorDetailPageContainer.tsx b/ui/src/holdingpen-new/containers/DetailPageContainer/AuthorDetailPageContainer.tsx index 6cb81475e3..aead679c5d 100644 --- a/ui/src/holdingpen-new/containers/DetailPageContainer/AuthorDetailPageContainer.tsx +++ b/ui/src/holdingpen-new/containers/DetailPageContainer/AuthorDetailPageContainer.tsx @@ -11,6 +11,7 @@ import { import { Action, ActionCreator } from 'redux'; import { connect, RootStateOrAny } from 'react-redux'; import { Map } from 'immutable'; +import { push } from 'connected-react-router'; import './DetailPageContainer.less'; import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; @@ -27,6 +28,11 @@ import { } from './columnData'; import { getConfigFor } from '../../../common/config'; import { resolveDecision } from '../../utils/utils'; +import DeleteWorkflow from '../../components/DeleteWorkflow/DeleteWorkflow'; +import EmptyOrChildren from '../../../common/components/EmptyOrChildren'; +import GoBackLinkContainer from '../../../common/containers/GoBackLinkContainer'; +import LinkLikeButton from '../../../common/components/LinkLikeButton/LinkLikeButton'; +import { HOLDINGPEN_SEARCH_NEW } from '../../../common/routes'; interface AuthorDetailPageContainerProps { dispatch: ActionCreator; @@ -74,298 +80,317 @@ const AuthorDetailPageContainer: React.FC = ({ href2={id} /> - - - {author?.get('status') && ( + + Author not found
+ dispatch(push(HOLDINGPEN_SEARCH_NEW))} + > +

Go to search page

+
+ + } + > + + + {author?.get('status') && ( + + +
+

+ {author?.get('status').toUpperCase()} +

+
+ +
+ )} - -
-

- {author?.get('status').toUpperCase()} -

-
- -
- )} - - - -

{data?.getIn(['name', 'value'])}

- {data?.getIn(['name', 'preferred_name']) && ( -

- Preferred name:{' '} - {data?.getIn(['name', 'preferred_name'])} -

- )} - {data?.get('status') && ( -

- Status: {data?.get('status')} -

- )} - {(data?.get('ids') as any[])?.find( - (id: any) => id?.get('schema') === 'ORCID' - ) && ( -

- id?.get('schema') === 'ORCID' - ) as unknown as Map + + +

{data?.getIn(['name', 'value'])}

+ {data?.getIn(['name', 'preferred_name']) && ( +

+ Preferred name:{' '} + {data?.getIn(['name', 'preferred_name'])} +

+ )} + {data?.get('status') && ( +

+ Status: {data?.get('status')} +

+ )} + {(data?.get('ids') as any[])?.find( + (id: any) => id?.get('schema') === 'ORCID' + ) && ( +

+ id?.get('schema') === 'ORCID' + ) as unknown as Map + } + noIcon + /> +

+ )} +
+ + + + `${record?.institution}+${Math.random()}` } - noIcon /> -

- )} - - - -
- `${record?.institution}+${Math.random()}` - } - /> - - -
`${record?.name}+${Math.random()}`} - /> - - {(data?.get('urls') || data?.get('ids')) && ( - - - )} - - - -

Subject areas

-
({ term }))} - pagination={false} - size="small" - rowKey={(record) => - `${record?.term}+${Math.random()}` - } - /> - - -

Advisors

-
- `${record?.name}+${Math.random()}` - } + +
`${record?.name}+${Math.random()}`} + /> + + {(data?.get('urls') || data?.get('ids')) && ( + + - - - - {author?.get('status') === 'error' && ( - -

- See error details here:{' '} - {`${ERRORS_URL}/${id}`} -

+
+ )} + + + +

Subject areas

+
({ term }))} + pagination={false} + size="small" + rowKey={(record) => + `${record?.term}+${Math.random()}` + } + /> + + +

Advisors

+
+ `${record?.name}+${Math.random()}` + } + /> + + - )} - - - - - - - {author?.get('status') && - author?.get('status') !== 'error' && - author?.get('status') !== 'running' && ( + {author?.get('status') === 'error' && ( + +

+ See error details here:{' '} + {`${ERRORS_URL}/${id}`} +

+
+ )} + + + + + + + {author?.get('status') && + author?.get('status') !== 'error' && + author?.get('status') !== 'running' && ( + + {author?.get('status') === 'approval' ? ( +
+ + + +
+ ) : ( +

+ This workflow is{' '} + + {resolveDecision(decision?.get('action')) + ?.decision || 'completed'} + + . +

+ )} +
+ )} + + Submitted by{' '} + {data?.getIn(['acquisition_source', 'email'])} on{' '} + + {new Date( + data?.getIn(['acquisition_source', 'datetime']) as Date + ).toLocaleDateString()} + + . + + {data?.get('_private_notes') && ( - {author?.get('status') === 'approval' ? ( -
- - - -
- ) : ( -

- This workflow is{' '} - - {resolveDecision(decision?.get('action')) - ?.decision || 'completed'} - - . -

- )} + + {data?.get('_private_notes')?.map((note: any) => ( +

+ "{note?.get('value')}" +

+ ))} +
)} - - Submitted by{' '} - {data?.getIn(['acquisition_source', 'email'])} on{' '} - - {new Date( - data?.getIn(['acquisition_source', 'datetime']) as Date - ).toLocaleDateString()} - - . - - {data?.get('_private_notes') && ( - - {data?.get('_private_notes')?.map((note: any) => ( -

- "{note?.get('value')}" -

- ))} -
+ {tickets && ( +

+ See related ticket + + {' '} + #{tickets?.first()?.get('ticket_id')} + +

+ )}
- )} - - {tickets && ( -

- See related ticket - +

+ - - + - {/* TODO2: change to skip step once it's ready */} - -
-
- - - - + + Restart current step + + + {/* TODO2: change to skip step once it's ready */} + + + + + + + + ); diff --git a/ui/src/holdingpen-new/notifications.ts b/ui/src/holdingpen-new/notifications.ts index de5d353f45..25b39d9823 100644 --- a/ui/src/holdingpen-new/notifications.ts +++ b/ui/src/holdingpen-new/notifications.ts @@ -25,3 +25,19 @@ export function notifyActionError(error: string) { duration: 10, }); } + +export function notifyDeleteSuccess() { + notification.success({ + message: 'Success', + description: 'Workflow deleted successfully', + duration: 10, + }); +} + +export function notifyDeleteError(error: string) { + notification.error({ + message: 'Unable to delete workflow', + description: error, + duration: 10, + }); +} diff --git a/ui/src/reducers/holdingpen.js b/ui/src/reducers/holdingpen.js index 4dae972b00..455785b3d9 100644 --- a/ui/src/reducers/holdingpen.js +++ b/ui/src/reducers/holdingpen.js @@ -16,6 +16,9 @@ import { HOLDINGPEN_RESOLVE_ACTION_REQUEST, HOLDINGPEN_RESOLVE_ACTION_SUCCESS, HOLDINGPEN_RESOLVE_ACTION_ERROR, + HOLDINGPEN_DELETE_REQUEST, + HOLDINGPEN_DELETE_SUCCESS, + HOLDINGPEN_DELETE_ERROR, } from '../actions/actionTypes'; export const initialState = fromJS({ @@ -71,6 +74,14 @@ const HoldingpenReducer = (state = initialState, action) => { return state.set('actionInProgress', false); case HOLDINGPEN_RESOLVE_ACTION_ERROR: return state.set('actionInProgress', false); + case HOLDINGPEN_DELETE_REQUEST: + return state.set('loading', true); + case HOLDINGPEN_DELETE_SUCCESS: + return state + .set('author', initialState.get('author')) + .set('loading', false); + case HOLDINGPEN_DELETE_ERROR: + return state.set('loading', false); default: return state; }