From 9d6b0f35044cc8a7713f3e8d4b7412ae2a6114cd Mon Sep 17 00:00:00 2001 From: Jonas Ege Carlsen Date: Wed, 15 Feb 2023 14:01:09 +0100 Subject: [PATCH 1/3] Unpublish articles when they are not used in taxonomy or learningpaths --- .../resourceComponents/RemoveResource.tsx | 138 ++++++++++++++++++ .../resourceComponents/Resource.tsx | 8 +- .../resourceComponents/ResourceItems.tsx | 79 ++++------ src/phrases/phrases-en.ts | 4 +- src/phrases/phrases-nb.ts | 2 + src/phrases/phrases-nn.ts | 2 + 6 files changed, 172 insertions(+), 61 deletions(-) create mode 100644 src/containers/StructurePage/resourceComponents/RemoveResource.tsx diff --git a/src/containers/StructurePage/resourceComponents/RemoveResource.tsx b/src/containers/StructurePage/resourceComponents/RemoveResource.tsx new file mode 100644 index 0000000000..b171040708 --- /dev/null +++ b/src/containers/StructurePage/resourceComponents/RemoveResource.tsx @@ -0,0 +1,138 @@ +/** + * 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 '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, isLoading } = 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')}

+ +
+ + {isLoading ? : 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 06ada0ddc9..52b900aa66 100644 --- a/src/containers/StructurePage/resourceComponents/Resource.tsx +++ b/src/containers/StructurePage/resourceComponents/Resource.tsx @@ -173,7 +173,7 @@ interface Props { connectionId?: string; // required for MakeDndList, otherwise ignored id?: string; // required for MakeDndList, otherwise ignored resource: ResourceWithNodeConnectionAndMeta; - onDelete?: (connectionId: string) => void; + onDelete?: () => void; updateResource?: (resource: ResourceWithNodeConnection) => void; dragHandleProps?: DraggableProvidedDragHandleProps; } @@ -353,11 +353,7 @@ const Resource = ({ resource, onDelete, dragHandleProps, currentNodeId }: Props) {t(`form.status.${resource.contentMeta.status.current.toLowerCase()}`)} )} - (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 ae1ce66ee6..f4937f70d9 100644 --- a/src/containers/StructurePage/resourceComponents/ResourceItems.tsx +++ b/src/containers/StructurePage/resourceComponents/ResourceItems.tsx @@ -12,14 +12,11 @@ import { useQueryClient } from '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 { Dictionary } from '../../../interfaces'; +import RemoveResource from './RemoveResource'; const StyledResourceItems = styled.ul` list-style: none; @@ -35,22 +33,17 @@ const StyledResourceItems = styled.ul` padding: 0; `; -const StyledErrorMessage = styled.div` - text-align: center; - color: #fe5f55; -`; - interface Props { resources: ResourceWithNodeConnectionAndMeta[]; currentNodeId: string; contentMeta: Dictionary; } -const isError = (error: unknown): error is Error => (error as Error).message !== undefined; - const ResourceItems = ({ resources, currentNodeId, contentMeta }: Props) => { - const { t, i18n } = useTranslation(); - const [deleteId, setDeleteId] = useState(''); + const { i18n } = useTranslation(); + const [deleteResource, setDeleteResource] = useState< + ResourceWithNodeConnectionAndMeta | undefined + >(undefined); const { taxonomyVersion } = useTaxonomyVersion(); const qc = useQueryClient(); @@ -59,15 +52,6 @@ const ResourceItems = ({ resources, currentNodeId, contentMeta }: Props) => { 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); @@ -89,14 +73,6 @@ const ResourceItems = ({ resources, currentNodeId, contentMeta }: Props) => { 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]; @@ -115,8 +91,11 @@ const ResourceItems = ({ resources, currentNodeId, contentMeta }: Props) => { }); }; - const toggleDelete = (newDeleteId: string) => { - setDeleteId(newDeleteId); + const toggleDelete = (resource: ResourceWithNodeConnection) => { + setDeleteResource({ + ...resource, + contentMeta: resource.contentUri ? contentMeta[resource.contentUri] : undefined, + }); }; return ( @@ -132,30 +111,22 @@ const ResourceItems = ({ resources, currentNodeId, contentMeta }: Props) => { contentMeta: resource.contentUri ? contentMeta[resource.contentUri] : undefined, }} key={resource.id} - onDelete={toggleDelete} + onDelete={() => toggleDelete(resource)} /> ))} - {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 a9845af341..4b2dfe73c6 100644 --- a/src/phrases/phrases-en.ts +++ b/src/phrases/phrases-en.ts @@ -1254,7 +1254,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', @@ -1384,6 +1384,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 1f18742871..c66a5af594 100644 --- a/src/phrases/phrases-nb.ts +++ b/src/phrases/phrases-nb.ts @@ -1386,6 +1386,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 9749f1aade..40cf349daf 100644 --- a/src/phrases/phrases-nn.ts +++ b/src/phrases/phrases-nn.ts @@ -1386,6 +1386,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 det siste stadet ressursen brukes. 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.', From 091f55d5d2c023f9296b704e0214fc978a49b806 Mon Sep 17 00:00:00 2001 From: Jonas Carlsen Date: Wed, 15 Feb 2023 15:56:08 +0100 Subject: [PATCH 2/3] Update src/phrases/phrases-nn.ts Co-authored-by: Gunnar Velle --- src/phrases/phrases-nn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/phrases/phrases-nn.ts b/src/phrases/phrases-nn.ts index 40cf349daf..6b80a1a86a 100644 --- a/src/phrases/phrases-nn.ts +++ b/src/phrases/phrases-nn.ts @@ -1387,7 +1387,7 @@ const phrases = { 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 det siste stadet ressursen brukes. Den vil bli avpublisert dersom du fjernar den.', + '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.', From ea307da6c70b77de4268812f9077ed932a59fa30 Mon Sep 17 00:00:00 2001 From: Jonas Ege Carlsen Date: Wed, 22 Mar 2023 15:43:43 +0100 Subject: [PATCH 3/3] isLoading -> isInitialLoading to avoid inifite load --- .../resourceComponents/RemoveResource.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/containers/StructurePage/resourceComponents/RemoveResource.tsx b/src/containers/StructurePage/resourceComponents/RemoveResource.tsx index f1965873b9..6003c40084 100644 --- a/src/containers/StructurePage/resourceComponents/RemoveResource.tsx +++ b/src/containers/StructurePage/resourceComponents/RemoveResource.tsx @@ -71,7 +71,7 @@ const RemoveResource = ({ deleteResource, nodeId, onClose }: Props) => { [i18n.language, nodeId, taxonomyVersion], ); - const { data, isLoading } = useQuery( + const { data, isInitialLoading } = useQuery( ['contains-article', articleId], () => fetchLearningpathsWithArticle(articleId!), { @@ -118,13 +118,17 @@ const RemoveResource = ({ deleteResource, nodeId, onClose }: Props) => { - {isLoading ? : deleteText} + {isInitialLoading ? : deleteText} {t('form.abort')} - + {isDeleting ? : t('form.remove')}