From 664fecc3fdc7ed30139f5be2d93041c6e337da43 Mon Sep 17 00:00:00 2001 From: Ahtesham Quraish Date: Mon, 27 Jul 2026 17:12:45 +0500 Subject: [PATCH 1/3] feat: soft delete feature for news and articles content --- .../WebsiteContentDraftListingPage.test.tsx | 54 ++++++++++++++ .../WebsiteContentDraftListingPage.tsx | 23 +++++- .../news/NewsEditor.happydom.test.tsx | 74 +++++++++++++++++++ .../core/WebsiteContentEditor.tsx | 19 +++++ .../DeleteWebsiteContentDialog.tsx | 57 ++++++++++++++ .../0006_add_deleted_on_to_websitecontent.py | 15 ++++ website_content/models.py | 4 + website_content/views.py | 16 +++- website_content/views_test.py | 59 +++++++++++++-- 9 files changed, 312 insertions(+), 9 deletions(-) create mode 100644 frontends/main/src/app-pages/WebsiteContent/WebsiteContentDraftListingPage.test.tsx create mode 100644 frontends/main/src/page-components/WebsiteContentDialogs/DeleteWebsiteContentDialog.tsx create mode 100644 website_content/migrations/0006_add_deleted_on_to_websitecontent.py diff --git a/frontends/main/src/app-pages/WebsiteContent/WebsiteContentDraftListingPage.test.tsx b/frontends/main/src/app-pages/WebsiteContent/WebsiteContentDraftListingPage.test.tsx new file mode 100644 index 0000000000..bed6d12415 --- /dev/null +++ b/frontends/main/src/app-pages/WebsiteContent/WebsiteContentDraftListingPage.test.tsx @@ -0,0 +1,54 @@ +import React from "react" +import { renderWithProviders, screen, user, waitFor } from "@/test-utils" +import { setMockResponse, factories, urls, makeRequest } from "api/test-utils" +import { WebsiteContentDraftListingPage } from "./WebsiteContentDraftListingPage" + +const mockPush = jest.fn() +jest.mock("next/navigation", () => ({ + useRouter: () => ({ push: mockPush }), +})) + +const setupEditor = () => { + setMockResponse.get( + urls.userMe.get(), + factories.user.user({ is_authenticated: true, is_article_editor: true }), + ) +} + +describe("WebsiteContentDraftListingPage delete", () => { + test("editor can delete a draft after confirming", async () => { + setupEditor() + const draft = factories.websiteContent.websiteContent({ + title: "My Draft", + content_type: "news", + is_published: false, + }) + setMockResponse.get(expect.stringContaining("/api/v1/website_content/"), { + count: 1, + next: null, + previous: null, + results: [draft], + }) + setMockResponse.delete(urls.websiteContent.details(draft.id), null) + + renderWithProviders() + + const deleteButton = await screen.findByRole("button", { + name: `Delete ${draft.title}`, + }) + await user.click(deleteButton) + + // Confirmation dialog appears; clicking "Yes, delete" fires the request. + const confirm = await screen.findByRole("button", { name: "Yes, delete" }) + await user.click(confirm) + + await waitFor(() => { + expect(makeRequest).toHaveBeenCalledWith( + expect.objectContaining({ + method: "delete", + url: urls.websiteContent.details(draft.id), + }), + ) + }) + }) +}) diff --git a/frontends/main/src/app-pages/WebsiteContent/WebsiteContentDraftListingPage.tsx b/frontends/main/src/app-pages/WebsiteContent/WebsiteContentDraftListingPage.tsx index 5a08eb4841..85c0dd2009 100644 --- a/frontends/main/src/app-pages/WebsiteContent/WebsiteContentDraftListingPage.tsx +++ b/frontends/main/src/app-pages/WebsiteContent/WebsiteContentDraftListingPage.tsx @@ -19,11 +19,16 @@ import type { WebsiteContentApiWebsiteContentListRequest as WebsiteContentListRequest, } from "api/v1" import { LocalDate } from "ol-utilities" -import { RiArrowLeftLine, RiArrowRightLine } from "@remixicon/react" +import { + RiArrowLeftLine, + RiArrowRightLine, + RiDeleteBinLine, +} from "@remixicon/react" import { extractFirstImage } from "@/common/websiteContentUtils" import { websiteContentEditView, websiteContentCreateView } from "@/common/urls" import RestrictedRoute from "@/components/RestrictedRoute/RestrictedRoute" -import { ButtonLink } from "@mitodl/smoot-design" +import { Button, ButtonLink } from "@mitodl/smoot-design" +import { showDeleteWebsiteContentDialog } from "@/page-components/WebsiteContentDialogs/DeleteWebsiteContentDialog" const PAGE_SIZE = 20 @@ -45,6 +50,9 @@ const PageHeader = styled.div` align-items: center; margin-bottom: 40px; ` +const StyledDeleteButton = styled(Button)` + padding-bottom: 0; +` const DraftContentCard = styled(Card)` display: flex; @@ -112,6 +120,17 @@ const DraftItem: React.FC<{ contentItem: WebsiteContent; type: string }> = ({ )} + + } + onClick={() => showDeleteWebsiteContentDialog(contentItem)} + > + Delete + + ) } diff --git a/frontends/main/src/page-components/TiptapEditor/contentTypes/news/NewsEditor.happydom.test.tsx b/frontends/main/src/page-components/TiptapEditor/contentTypes/news/NewsEditor.happydom.test.tsx index 3d2493ab96..1f72049a56 100644 --- a/frontends/main/src/page-components/TiptapEditor/contentTypes/news/NewsEditor.happydom.test.tsx +++ b/frontends/main/src/page-components/TiptapEditor/contentTypes/news/NewsEditor.happydom.test.tsx @@ -1598,3 +1598,77 @@ describe("NewsEditor - Byline publish date", () => { expect(screen.queryByText("Jan 1, 2020")).not.toBeInTheDocument() }) }) + +describe("NewsEditor - Delete draft", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test("editor can delete a draft from the edit toolbar", async () => { + const user = factories.user.user({ + is_authenticated: true, + is_article_editor: true, + }) + setMockResponse.get(urls.userMe.get(), user) + + const newsItem = factories.websiteContent.websiteContent({ + id: 321, + title: "Draft to delete", + content_type: "news", + is_published: false, + }) + setMockResponse.get(urls.websiteContent.details(newsItem.id), newsItem) + setMockResponse.delete(urls.websiteContent.details(newsItem.id), null) + + renderWithProviders( + , + { + user, + }, + ) + + await screen.findByTestId("editor") + + await userEvent.click(await screen.findByRole("button", { name: "Delete" })) + await userEvent.click( + await screen.findByRole("button", { name: "Yes, delete" }), + ) + + await waitFor(() => { + expect(makeRequest).toHaveBeenCalledWith( + expect.objectContaining({ + method: "delete", + url: urls.websiteContent.details(newsItem.id), + }), + ) + }) + }) + + test("delete button is not shown for published content", async () => { + const user = factories.user.user({ + is_authenticated: true, + is_article_editor: true, + }) + setMockResponse.get(urls.userMe.get(), user) + + const newsItem = factories.websiteContent.websiteContent({ + id: 322, + title: "Published item", + content_type: "news", + is_published: true, + }) + setMockResponse.get(urls.websiteContent.details(newsItem.id), newsItem) + + renderWithProviders( + , + { + user, + }, + ) + + await screen.findByTestId("editor") + expect( + screen.queryByRole("button", { name: "Delete" }), + ).not.toBeInTheDocument() + }) +}) diff --git a/frontends/main/src/page-components/TiptapEditor/core/WebsiteContentEditor.tsx b/frontends/main/src/page-components/TiptapEditor/core/WebsiteContentEditor.tsx index c92fa885d7..620e045538 100644 --- a/frontends/main/src/page-components/TiptapEditor/core/WebsiteContentEditor.tsx +++ b/frontends/main/src/page-components/TiptapEditor/core/WebsiteContentEditor.tsx @@ -16,6 +16,9 @@ import { Alert, Button, ButtonLink } from "@mitodl/smoot-design" import { useUserHasPermission, Permission } from "api/hooks/user" import { useQueryClient, type QueryClient } from "@tanstack/react-query" import dynamic from "next/dynamic" +import { useRouter } from "next-nprogress-bar" +import { RiDeleteBinLine } from "@remixicon/react" +import { showDeleteWebsiteContentDialog } from "@/page-components/WebsiteContentDialogs/DeleteWebsiteContentDialog" import { Toolbar } from "../vendor/components/tiptap-ui-primitive/toolbar" import { TiptapEditor, MainToolbarContent, TipTapViewer } from "../TiptapEditor" @@ -210,6 +213,7 @@ const WebsiteContentEditor = ({ uploadImageRef.current = uploadImage const queryClient = useQueryClient() + const router = useRouter() const isArticleEditor = useUserHasPermission(Permission.ArticleEditor) const uploadHandler = useCallback( @@ -408,6 +412,21 @@ const WebsiteContentEditor = ({ ) : ( + {contentItem && !contentItem.is_published ? ( + + ) : null} {!contentItem?.is_published ? (