Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unpublish articles when they are not used in taxonomy or learningpaths #1691

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions src/containers/StructurePage/resourceComponents/RemoveResource.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean>(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<ResourceWithNodeConnection[]>(compKey) ?? [];
const withoutDeleted = prevData.filter((res) => res.connectionId !== id);
qc.setQueryData<ResourceWithNodeConnection[]>(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 (
<>
<ModalHeaderV2>
<h1>{t('taxonomy.removeResource')}</h1>
<CloseButton onClick={onClose} />
</ModalHeaderV2>
<ModalBody>
{isInitialLoading ? <Spinner /> : deleteText}
<RightAlign>
<ButtonWrapper>
<ButtonV2 variant="outline" onClick={onClose}>
{t('form.abort')}
</ButtonV2>
<ButtonV2
colorTheme="danger"
disabled={isInitialLoading || isDeleting}
onClick={onDelete}
>
{isDeleting ? <Spinner /> : t('form.remove')}
</ButtonV2>
</ButtonWrapper>
{error && <ErrorText aria-live="assertive">{t('taxonomy.errorMessage')}</ErrorText>}
</RightAlign>
</ModalBody>
</>
);
};

export default RemoveResource;
9 changes: 2 additions & 7 deletions src/containers/StructurePage/resourceComponents/Resource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -250,12 +250,7 @@ const Resource = ({
currentNodeId={currentNodeId}
/>
<VersionHistory resource={resource} contentType={contentType} />
<RemoveButton
onClick={() => (onDelete ? onDelete(resource.connectionId) : null)}
size="xsmall"
colorTheme="danger"
disabled={!onDelete}
>
<RemoveButton onClick={onDelete} size="xsmall" colorTheme="danger" disabled={!onDelete}>
{t('form.remove')}
</RemoveButton>
</ButtonRow>
Expand Down
78 changes: 22 additions & 56 deletions src/containers/StructurePage/resourceComponents/ResourceItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -28,18 +25,14 @@ 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;
margin: 0;
padding: 0;
`;

const StyledErrorMessage = styled.div`
text-align: center;
color: #fe5f55;
`;

interface Props {
resources: ResourceWithNodeConnectionAndMeta[];
currentNodeId: string;
Expand All @@ -48,34 +41,25 @@ interface Props {
users?: Dictionary<Auth0UserData>;
}

const isError = (error: unknown): error is Error => (error as Error).message !== undefined;

const ResourceItems = ({
resources,
currentNodeId,
contentMeta,
contentMetaLoading,
users,
}: Props) => {
const { t, i18n } = useTranslation();
const [deleteId, setDeleteId] = useState<string>('');
const { i18n } = useTranslation();
const { taxonomyVersion } = useTaxonomyVersion();
const [deleteResource, setDeleteResource] = useState<
ResourceWithNodeConnectionAndMeta | undefined
>(undefined);

const qc = useQueryClient();
const compKey = resourcesWithNodeConnectionQueryKey({
id: currentNodeId,
language: i18n.language,
taxonomyVersion,
});
const deleteNodeResource = useDeleteResourceForNodeMutation({
onMutate: async ({ id }) => {
await qc.cancelQueries(compKey);
const prevData = qc.getQueryData<ResourceWithNodeConnection[]>(compKey) ?? [];
const withoutDeleted = prevData.filter((res) => res.connectionId !== id);
qc.setQueryData<ResourceWithNodeConnection[]>(compKey, withoutDeleted);
return prevData;
},
});

const onUpdateRank = async (id: string, newRank: number) => {
await qc.cancelQueries(compKey);
Expand All @@ -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];
Expand All @@ -123,8 +99,8 @@ const ResourceItems = ({
});
};

const toggleDelete = (newDeleteId: string) => {
setDeleteId(newDeleteId);
const toggleDelete = (resource: ResourceWithNodeConnectionAndMeta) => {
setDeleteResource(resource);
};

return (
Expand All @@ -144,33 +120,23 @@ const ResourceItems = ({
contentMeta: resource.contentUri ? contentMeta[resource.contentUri] : undefined,
}}
key={resource.id}
onDelete={toggleDelete}
onDelete={() => toggleDelete(resource)}
contentMetaLoading={contentMetaLoading}
/>
))}
</MakeDndList>
{deleteNodeResource.error && isError(deleteNodeResource.error) && (
<StyledErrorMessage data-testid="inlineEditErrorMessage">
{`${t('taxonomy.errorMessage')}: ${deleteNodeResource.error.message}`}
</StyledErrorMessage>
)}
<AlertModal
title={t('taxonomy.deleteResource')}
label={t('taxonomy.deleteResource')}
show={!!deleteId}
text={t('taxonomy.resource.confirmDelete')}
actions={[
{
text: t('form.abort'),
onClick: () => toggleDelete(''),
},
{
text: t('alertModal.delete'),
onClick: () => onDelete(deleteId!),
},
]}
onCancel={() => toggleDelete('')}
/>
<ModalV2 controlled isOpen={!!deleteResource} onClose={() => setDeleteResource(undefined)}>
{(close) =>
deleteResource && (
<RemoveResource
key={deleteResource.id}
deleteResource={deleteResource}
nodeId={currentNodeId}
onClose={close}
/>
)
}
</ModalV2>
</StyledResourceItems>
);
};
Expand Down
4 changes: 3 additions & 1 deletion src/phrases/phrases-en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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.',
Expand Down
2 changes: 2 additions & 0 deletions src/phrases/phrases-nb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
2 changes: 2 additions & 0 deletions src/phrases/phrases-nn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down