Skip to content

Commit

Permalink
feat: [DHIS2-18017] Ability to unlink event from edit/view event page (
Browse files Browse the repository at this point in the history
…#3846)

* feat: add menu items for unlik and delete event

* feat: temp

* feat: delete and unlink function

* fix: review changes

* fix: remove update data

* fix: use invalidatequeries

* fix: remove noticebox and add alerterror

* feat: add validation

* fix: indexeddb write access

* fix: review comments

* fix: user message improvements

* Merge remote-tracking branch 'origin/master' into hv/feat/DHIS2-18017_AbilityToUnlinkEvent

* Revert "Merge remote-tracking branch 'origin/master' into hv/feat/DHIS2-18017_AbilityToUnlinkEvent"

This reverts commit e3284e9.

* fix: merge conflict

* fix: dublicate code

* feat: update dhis ui

* fix: merge error in package json

* Revert "feat: update dhis ui"

This reverts commit a0c848f.

* fix: revert changes in version after dhis2 ui update
  • Loading branch information
henrikmv authored Dec 17, 2024
1 parent 0e16f71 commit ad352f5
Show file tree
Hide file tree
Showing 18 changed files with 419 additions and 64 deletions.
39 changes: 37 additions & 2 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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-03T10:58:18.077Z\n"
"PO-Revision-Date: 2024-12-03T10:58:18.077Z\n"
"POT-Creation-Date: 2024-12-05T11:39:04.447Z\n"
"PO-Revision-Date: 2024-12-05T11:39:04.447Z\n"

msgid "Choose one or more dates..."
msgstr "Choose one or more dates..."
Expand Down Expand Up @@ -1466,9 +1466,44 @@ msgstr "{{ scheduledEvents }} scheduled"
msgid "Stages and Events"
msgstr "Stages and Events"

msgid "An error occurred while unlinking and deleting the event."
msgstr "An error occurred while unlinking and deleting the event."

msgid "Unlink and delete linked event"
msgstr "Unlink and delete linked event"

msgid ""
"Are you sure you want to remove the link and delete the linked event? This "
"action permanently removes the link, linked event, and all related data."
msgstr ""
"Are you sure you want to remove the link and delete the linked event? This "
"action permanently removes the link, linked event, and all related data."

msgid "Yes, unlink and delete linked event"
msgstr "Yes, unlink and delete linked event"

msgid "Unlink event"
msgstr "Unlink event"

msgid ""
"Are you sure you want to remove the link between these events? This action "
"removes the link itself, but the linked event will remain."
msgstr ""
"Are you sure you want to remove the link between these events? This action "
"removes the link itself, but the linked event will remain."

msgid "Yes, unlink event"
msgstr "Yes, unlink event"

msgid "View linked event"
msgstr "View linked event"

msgid "You do not have access to remove the link between these events"
msgstr "You do not have access to remove the link between these events"

msgid "You do not have access to remove the link and delete the linked event"
msgstr "You do not have access to remove the link and delete the linked event"

msgid "An error occurred while loading the widget."
msgstr "An error occurred while loading the widget."

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type Props = {
...CssClasses,
}

export const ScheduleInOrgUnitPlain = ({
const ScheduleInOrgUnitPlain = ({
relatedStagesDataValues,
setRelatedStagesDataValues,
saveAttempted,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// @flow
import React from 'react';
import i18n from '@dhis2/d2-i18n';
import {
Button,
ButtonStrip,
Modal,
ModalActions,
ModalContent,
ModalTitle,
} from '@dhis2/ui';
import log from 'loglevel';
import { useDataEngine, useAlert } from '@dhis2/app-runtime';
import { useMutation, useQueryClient } from 'react-query';
import { ReactQueryAppNamespace } from 'capture-core/utils/reactQueryHelpers';
import type { Props } from './UnlinkAndDeleteModal.types';

export const UnlinkAndDeleteModal = ({
setOpenModal,
eventId,
originEventId,
}: Props) => {
const dataEngine = useDataEngine();
const queryClient = useQueryClient();
const { show: showErrorAlert } = useAlert(
i18n.t('An error occurred while unlinking and deleting the event.'),
{ critical: true },
);

const deleteEvent = async () => {
const mutation = {
resource: 'tracker?async=false&importStrategy=DELETE',
type: 'create',
data: { events: [{ event: eventId }] },
};

return dataEngine.mutate(mutation);
};

const mutation = useMutation(deleteEvent, {
onSuccess: () => {
queryClient.invalidateQueries([
ReactQueryAppNamespace,
'linkedEventByOriginEvent',
originEventId,
]);
setOpenModal(false);
},
onError: (error) => {
showErrorAlert();
log.error(
`Failed to unlink and delete event with ID: ${eventId}`,
error,
);
},
});

return (
<Modal dataTest="event-unlink-and-delete-modal">
<ModalTitle>{i18n.t('Unlink and delete linked event')}</ModalTitle>
<ModalContent>
<p>
{i18n.t(
'Are you sure you want to remove the link and delete the linked event? This action permanently removes the link, linked event, and all related data.',
)}
</p>
</ModalContent>
<ModalActions>
<ButtonStrip end>
<Button
onClick={() => setOpenModal(false)}
secondary
>
{i18n.t('No, cancel')}
</Button>
<Button
destructive
onClick={() => mutation.mutate()}
disabled={mutation.isLoading}
>
{i18n.t('Yes, unlink and delete linked event')}
</Button>
</ButtonStrip>
</ModalActions>
</Modal>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// @flow
export type Props = {|
setOpenModal: (open: boolean) => void,
eventId: string,
originEventId: string,
|};
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// @flow
import React from 'react';
import i18n from '@dhis2/d2-i18n';
import {
Modal,
ModalContent,
ModalTitle,
ModalActions,
ButtonStrip,
Button,
} from '@dhis2/ui';
import log from 'loglevel';
import { useDataEngine, useAlert } from '@dhis2/app-runtime';
import { useMutation, useQueryClient } from 'react-query';
import { ReactQueryAppNamespace } from 'capture-core/utils/reactQueryHelpers';
import type { Props } from './UnlinkModal.types';

export const UnlinkModal = ({
setOpenModal,
relationshipId,
originEventId,
}: Props) => {
const dataEngine = useDataEngine();
const queryClient = useQueryClient();
const { show: showErrorAlert } = useAlert(
i18n.t('An error occurred while unlinking and deleting the event.'),
{ critical: true },
);

const deleteRelationship = async () => {
const mutation = {
resource: 'tracker?importStrategy=DELETE&async=false',
type: 'create',
data: { relationships: [{ relationship: relationshipId }] },
};

return dataEngine.mutate(mutation);
};

const mutation = useMutation(deleteRelationship, {
onSuccess: () => {
queryClient.invalidateQueries([
ReactQueryAppNamespace,
'linkedEventByOriginEvent',
originEventId,
]);
setOpenModal(false);
},
onError: (error) => {
showErrorAlert();
log.error(
`Failed to remove relationship with id ${relationshipId}`,
error,
);
},
});

return (
<Modal dataTest="event-unlink-modal">
<ModalTitle>
{i18n.t('Unlink event')}
</ModalTitle>
<ModalContent>
<p>{i18n.t('Are you sure you want to remove the link between these events? This action removes the link itself, but the linked event will remain.')}</p>
</ModalContent>
<ModalActions>
<ButtonStrip end>
<Button onClick={() => setOpenModal(false)} secondary>
{i18n.t('No, cancel')}
</Button>
<Button
destructive
onClick={() => mutation.mutate()}
disabled={mutation.isLoading}
>
{i18n.t('Yes, unlink event')}
</Button>
</ButtonStrip>
</ModalActions>
</Modal>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// @flow
export type Props = {|
setOpenModal: (open: boolean) => void,
relationshipId: string,
originEventId: string,
|};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @flow
export { UnlinkModal } from './UnlinkModal';
export { UnlinkAndDeleteModal } from './UnlinkAndDeleteModal';
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// @flow
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import {
Divider,
FlyoutMenu,
IconDelete16,
IconLink16,
IconMore16,
IconView16,
MenuItem,
} from '@dhis2/ui';
import i18n from '@dhis2/d2-i18n';
import { ConditionalTooltip } from '../../Tooltips/ConditionalTooltip';
import { OverflowButton } from '../../Buttons';
import { UnlinkModal, UnlinkAndDeleteModal } from './Modal';
import { buildUrlQueryString } from '../../../utils/routing';
import type { Props } from './OverflowMenu.types';
import { useRelationshipTypeAccess } from '../hooks';

export const OverflowMenuComponent = ({
linkedEvent,
relationshipId,
orgUnitId,
originEventId,
stageWriteAccess,
relationshipType,
}: Props) => {
const { push } = useHistory();
const [isActionsOpen, setIsActionsOpen] = useState(false);
const [isUnlinkModalOpen, setIsUnlinkModalOpen] = useState(false);
const [isUnlinkAndDeleteModalOpen, setIsUnlinkAndDeleteModalOpen] = useState(false);
const { relationshipTypeWriteAccess } = useRelationshipTypeAccess(relationshipType);

const handleViewLinkedEvent = () => {
push(`/enrollmentEventEdit?${buildUrlQueryString({ eventId: linkedEvent.event, orgUnitId })}`);
setIsActionsOpen(false);
};

const handleUnlinkEvent = () => {
setIsUnlinkModalOpen(true);
setIsActionsOpen(false);
};

const handleUnlinkAndDeleteEvent = () => {
setIsUnlinkAndDeleteModalOpen(true);
setIsActionsOpen(false);
};

return (
<>
<OverflowButton
open={isActionsOpen}
onClick={() => setIsActionsOpen(prev => !prev)}
icon={<IconMore16 />}
small
secondary
dataTest="widget-linked-event-overflow-menu"
component={
<FlyoutMenu dense maxWidth="250px">
<MenuItem
label={i18n.t('View linked event')}
icon={<IconView16 />}
dataTest="event-overflow-view-linked-event"
onClick={handleViewLinkedEvent}
/>
<Divider />
<ConditionalTooltip
content={i18n.t('You do not have access to remove the link between these events')}
enabled={!relationshipTypeWriteAccess}
>
<MenuItem
label={i18n.t('Unlink event')}
icon={<IconLink16 />}
disabled={!relationshipTypeWriteAccess}
dense
dataTest="event-overflow-unlink-event"
onClick={handleUnlinkEvent}
/>
</ConditionalTooltip>
<ConditionalTooltip
content={i18n.t('You do not have access to remove the link and delete the linked event')}
enabled={!stageWriteAccess || !relationshipTypeWriteAccess}
>
<MenuItem
label={i18n.t('Unlink and delete linked event')}
icon={<IconDelete16 />}
disabled={!stageWriteAccess || !relationshipTypeWriteAccess}
dense
destructive
dataTest="event-overflow-unlink-and-delete-event"
onClick={handleUnlinkAndDeleteEvent}
/>
</ConditionalTooltip>
</FlyoutMenu>
}
/>
{isUnlinkModalOpen && (
<UnlinkModal
setOpenModal={setIsUnlinkModalOpen}
relationshipId={relationshipId}
originEventId={originEventId}
/>
)}
{isUnlinkAndDeleteModalOpen && (
<UnlinkAndDeleteModal
setOpenModal={setIsUnlinkAndDeleteModalOpen}
eventId={linkedEvent.event}
originEventId={originEventId}
/>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @flow
export type Props = {
linkedEvent: any,
relationshipId: string,
orgUnitId: string,
originEventId: string,
stageWriteAccess: boolean,
relationshipType: string,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// @flow
export { OverflowMenuComponent } from './OverflowMenu.component';
Loading

0 comments on commit ad352f5

Please sign in to comment.