diff --git a/src/containers/StructurePage/resourceComponents/RemoveResource.tsx b/src/containers/StructurePage/resourceComponents/RemoveResource.tsx new file mode 100644 index 0000000000..6003c40084 --- /dev/null +++ b/src/containers/StructurePage/resourceComponents/RemoveResource.tsx @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2023-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import styled from '@emotion/styled'; +import { ButtonV2, CloseButton } from '@ndla/button'; +import { colors, spacing } from '@ndla/core'; +import { ModalBody, ModalHeaderV2 } from '@ndla/modal'; +import { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import Spinner from '../../../components/Spinner'; +import { PUBLISHED, UNPUBLISHED } from '../../../constants'; +import { updateStatusDraft } from '../../../modules/draft/draftApi'; +import { fetchLearningpathsWithArticle } from '../../../modules/learningpath/learningpathApi'; +import { ResourceWithNodeConnection } from '../../../modules/nodes/nodeApiTypes'; +import { useDeleteResourceForNodeMutation } from '../../../modules/nodes/nodeMutations'; +import { resourcesWithNodeConnectionQueryKey } from '../../../modules/nodes/nodeQueries'; +import { getIdFromUrn } from '../../../util/taxonomyHelpers'; +import { useTaxonomyVersion } from '../../StructureVersion/TaxonomyVersionProvider'; +import { ResourceWithNodeConnectionAndMeta } from './StructureResources'; + +interface Props { + onClose: () => void; + nodeId: string; + deleteResource: ResourceWithNodeConnectionAndMeta; +} + +const ButtonWrapper = styled.div` + width: 100%; + display: flex; + gap: ${spacing.small}; + justify-content: flex-end; +`; + +const RightAlign = styled.div` + display: flex; + flex-direction: column; + align-items: flex-end; +`; + +const ErrorText = styled.p` + color: ${colors.support.red}; + margin-bottom: 0; +`; + +const RemoveResource = ({ deleteResource, nodeId, onClose }: Props) => { + const { t, i18n } = useTranslation(); + const [error, setError] = useState(false); + const { taxonomyVersion } = useTaxonomyVersion(); + const qc = useQueryClient(); + const articleId = useMemo( + () => + deleteResource.contentUri?.includes('article') + ? getIdFromUrn(deleteResource.contentUri) + : undefined, + [deleteResource.contentUri], + ); + + const compKey = useMemo( + () => + resourcesWithNodeConnectionQueryKey({ + id: nodeId, + language: i18n.language, + taxonomyVersion, + }), + [i18n.language, nodeId, taxonomyVersion], + ); + + const { data, isInitialLoading } = useQuery( + ['contains-article', articleId], + () => fetchLearningpathsWithArticle(articleId!), + { + enabled: !!articleId, + }, + ); + + const { mutateAsync, isLoading: isDeleting } = useDeleteResourceForNodeMutation({ + onMutate: async ({ id }) => { + await qc.cancelQueries(compKey); + const prevData = qc.getQueryData(compKey) ?? []; + const withoutDeleted = prevData.filter((res) => res.connectionId !== id); + qc.setQueryData(compKey, withoutDeleted); + return prevData; + }, + onSuccess: () => qc.invalidateQueries(compKey), + }); + + const deleteText = useMemo( + () => + data?.length || deleteResource.paths.length > 1 + ? t('taxonomy.resource.confirmDelete') + : t('taxonomy.resource.confirmDeleteAndUnpublish'), + [data?.length, t, deleteResource.paths], + ); + + const onDelete = async () => { + try { + setError(false); + if (!data?.length && articleId && deleteResource.contentMeta?.status?.current === PUBLISHED) { + await updateStatusDraft(articleId, UNPUBLISHED); + } + await mutateAsync({ id: deleteResource.connectionId, taxonomyVersion }); + onClose(); + } catch (e) { + setError(true); + } + }; + + return ( + <> + +

{t('taxonomy.removeResource')}

+ +
+ + {isInitialLoading ? : deleteText} + + + + {t('form.abort')} + + + {isDeleting ? : t('form.remove')} + + + {error && {t('taxonomy.errorMessage')}} + + + + ); +}; + +export default RemoveResource; diff --git a/src/containers/StructurePage/resourceComponents/Resource.tsx b/src/containers/StructurePage/resourceComponents/Resource.tsx index 4ad41a0d57..151cf44eb5 100644 --- a/src/containers/StructurePage/resourceComponents/Resource.tsx +++ b/src/containers/StructurePage/resourceComponents/Resource.tsx @@ -124,7 +124,7 @@ interface Props { id?: string; // required for MakeDndList, otherwise ignored responsible?: string; resource: ResourceWithNodeConnectionAndMeta; - onDelete?: (connectionId: string) => void; + onDelete?: () => void; updateResource?: (resource: ResourceWithNodeConnection) => void; dragHandleProps?: DraggableProvidedDragHandleProps; contentMetaLoading: boolean; @@ -250,12 +250,7 @@ const Resource = ({ currentNodeId={currentNodeId} /> - (onDelete ? onDelete(resource.connectionId) : null)} - size="xsmall" - colorTheme="danger" - disabled={!onDelete} - > + {t('form.remove')} diff --git a/src/containers/StructurePage/resourceComponents/ResourceItems.tsx b/src/containers/StructurePage/resourceComponents/ResourceItems.tsx index b6746e31f8..7bff698af0 100644 --- a/src/containers/StructurePage/resourceComponents/ResourceItems.tsx +++ b/src/containers/StructurePage/resourceComponents/ResourceItems.tsx @@ -12,14 +12,11 @@ import { useQueryClient } from '@tanstack/react-query'; import { DropResult } from 'react-beautiful-dnd'; import sortBy from 'lodash/sortBy'; import styled from '@emotion/styled'; +import { ModalV2 } from '@ndla/modal'; import Resource from './Resource'; import handleError from '../../../util/handleError'; -import { - useDeleteResourceForNodeMutation, - usePutResourceForNodeMutation, -} from '../../../modules/nodes/nodeMutations'; +import { usePutResourceForNodeMutation } from '../../../modules/nodes/nodeMutations'; import { ResourceWithNodeConnection } from '../../../modules/nodes/nodeApiTypes'; -import AlertModal from '../../../components/AlertModal'; import MakeDndList from '../../../components/MakeDndList'; import { useTaxonomyVersion } from '../../StructureVersion/TaxonomyVersionProvider'; import { @@ -28,6 +25,7 @@ import { } from '../../../modules/nodes/nodeQueries'; import { ResourceWithNodeConnectionAndMeta } from './StructureResources'; import { Auth0UserData, Dictionary } from '../../../interfaces'; +import RemoveResource from './RemoveResource'; const StyledResourceItems = styled.ul` list-style: none; @@ -35,11 +33,6 @@ const StyledResourceItems = styled.ul` padding: 0; `; -const StyledErrorMessage = styled.div` - text-align: center; - color: #fe5f55; -`; - interface Props { resources: ResourceWithNodeConnectionAndMeta[]; currentNodeId: string; @@ -48,8 +41,6 @@ interface Props { users?: Dictionary; } -const isError = (error: unknown): error is Error => (error as Error).message !== undefined; - const ResourceItems = ({ resources, currentNodeId, @@ -57,9 +48,11 @@ const ResourceItems = ({ contentMetaLoading, users, }: Props) => { - const { t, i18n } = useTranslation(); - const [deleteId, setDeleteId] = useState(''); + const { i18n } = useTranslation(); const { taxonomyVersion } = useTaxonomyVersion(); + const [deleteResource, setDeleteResource] = useState< + ResourceWithNodeConnectionAndMeta | undefined + >(undefined); const qc = useQueryClient(); const compKey = resourcesWithNodeConnectionQueryKey({ @@ -67,15 +60,6 @@ const ResourceItems = ({ language: i18n.language, taxonomyVersion, }); - const deleteNodeResource = useDeleteResourceForNodeMutation({ - onMutate: async ({ id }) => { - await qc.cancelQueries(compKey); - const prevData = qc.getQueryData(compKey) ?? []; - const withoutDeleted = prevData.filter((res) => res.connectionId !== id); - qc.setQueryData(compKey, withoutDeleted); - return prevData; - }, - }); const onUpdateRank = async (id: string, newRank: number) => { await qc.cancelQueries(compKey); @@ -97,14 +81,6 @@ const ResourceItems = ({ onSuccess: () => qc.invalidateQueries(compKey), }); - const onDelete = async (deleteId: string) => { - setDeleteId(''); - await deleteNodeResource.mutateAsync( - { id: deleteId, taxonomyVersion }, - { onSuccess: () => qc.invalidateQueries(compKey) }, - ); - }; - const onDragEnd = async ({ destination, source }: DropResult) => { if (!destination) return; const { connectionId, primary, relevanceId, rank: currentRank } = resources[source.index]; @@ -123,8 +99,8 @@ const ResourceItems = ({ }); }; - const toggleDelete = (newDeleteId: string) => { - setDeleteId(newDeleteId); + const toggleDelete = (resource: ResourceWithNodeConnectionAndMeta) => { + setDeleteResource(resource); }; return ( @@ -144,33 +120,23 @@ const ResourceItems = ({ contentMeta: resource.contentUri ? contentMeta[resource.contentUri] : undefined, }} key={resource.id} - onDelete={toggleDelete} + onDelete={() => toggleDelete(resource)} contentMetaLoading={contentMetaLoading} /> ))} - {deleteNodeResource.error && isError(deleteNodeResource.error) && ( - - {`${t('taxonomy.errorMessage')}: ${deleteNodeResource.error.message}`} - - )} - toggleDelete(''), - }, - { - text: t('alertModal.delete'), - onClick: () => onDelete(deleteId!), - }, - ]} - onCancel={() => toggleDelete('')} - /> + setDeleteResource(undefined)}> + {(close) => + deleteResource && ( + + ) + } + ); }; diff --git a/src/phrases/phrases-en.ts b/src/phrases/phrases-en.ts index ee615a5bb5..69975f5df2 100644 --- a/src/phrases/phrases-en.ts +++ b/src/phrases/phrases-en.ts @@ -1262,7 +1262,7 @@ const phrases = { }, errorMessage: { title: 'Oops, something went wrong', - description: 'Sorry, an error occurd.', + description: 'Sorry, an error occurred.', back: 'Back', goToFrontPage: 'Go to frontpage', invalidUrl: 'Invalid url', @@ -1395,6 +1395,8 @@ const phrases = { resource: { confirmDelete: 'Do you want to delete the resource from this folder? This will not affect the placement other places', + confirmDeleteAndUnpublish: + 'Do you want to delete the resource from this folder. This is the last place this resource is used. It will be unpublished if you delete it.', copyError: 'An error occurred while copying resources. Double check the copied resources and try to fix deficiencies manually, or delete the copied resources and try to copy again', addResourceConflict: 'The resource you attempted to add already exists on the topic.', diff --git a/src/phrases/phrases-nb.ts b/src/phrases/phrases-nb.ts index f989193fbe..321e7e3fd6 100644 --- a/src/phrases/phrases-nb.ts +++ b/src/phrases/phrases-nb.ts @@ -1397,6 +1397,8 @@ const phrases = { resource: { confirmDelete: 'Vil du fjerne ressursen fra denne mappen? Dette vil ikke påvirke plasseringen andre steder', + confirmDeleteAndUnpublish: + 'Vil du fjerne ressursen fra denne mappen? Dette er det siste stedet ressursen brukes. Den vil bli avpublisert dersom du fjerner den.', copyError: 'Det oppsto en feil ved kopiering av ressurser. Dobbeltsjekk de kopierte ressursene og prøv å fikse mangler manuelt, eller slett de kopierte ressursene og prøv å kopiere på nytt', addResourceConflict: 'Ressursen du forsøkte å legge til finnes allerede på emnet.', diff --git a/src/phrases/phrases-nn.ts b/src/phrases/phrases-nn.ts index 49359e1e03..d4bf481d4a 100644 --- a/src/phrases/phrases-nn.ts +++ b/src/phrases/phrases-nn.ts @@ -1397,6 +1397,8 @@ const phrases = { resource: { confirmDelete: 'Vil du fjerne ressursen frå denne mappa? Dette vil ikkje påverke plasseringa andre steder', + confirmDeleteAndUnpublish: + 'Vil du fjerne ressursen frå denne mappa? Dette er den siste staden ressursen brukast. Den vil bli avpublisert dersom du fjernar den.', copyError: 'Det oppstod ein feil ved kopiering av ressursar. Dobbeltsjekk dei kopierte ressursane og prøv å fikse manglar manuelt, eller slett dei kopierte ressursane og prøv å kopiere på nytt', addResourceConflict: 'Ressursen du forsøkte å legge til finnes allerede på emnet.',