Skip to content

Commit 8563fe5

Browse files
jonkaftonpre-commit-ci[bot]Ahtesham QuraishAhtesham QuraishAhtesham Quraish
authored
Article editor refactor for reuse and layout updates (#2699)
* Initial Tiptap Editor * Update lockfile * Ignore stylelint errors in vendor sheets * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Run fmt-fix * Eslint ignore vendor code * Revert main package.json * Remove aliases from Tiptap template code imports * Prettier fixes * CodeQL fix * React in scope fixes * Format fix * Transform react-hotkeys-hook to resolve esm error * feat: adding tiptap integration with article crud operation * feat: add upload image control in toolbar * made wrapper component for editor * Refactor ArticleEditor. Layour changed (wip) * Refactor and layout fixes * Remove initial editor route * Reinstate edit page redirect. Update tests * Remove unused value prop * Lint * Invalidate article detail cache * Detail page layout. Fix race condition on content display after save * Remove EditorContainer * Update test --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Ahtesham Quraish <ahtesham.quraish@A006-01455.local> Co-authored-by: Ahtesham Quraish <ahtesham.quraish@192.168.10.30> Co-authored-by: Ahtesham Quraish <ahtesham.quraish@192.168.10.8>
1 parent 4169b0c commit 8563fe5

File tree

15 files changed

+385
-493
lines changed

15 files changed

+385
-493
lines changed

frontends/api/src/hooks/articles/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ describe("Article CRUD", () => {
9090
const { id, ...patchData } = article
9191
expect(makeRequest).toHaveBeenCalledWith("patch", url, patchData)
9292
expect(queryClient.invalidateQueries).toHaveBeenCalledWith({
93-
queryKey: articleKeys.root,
93+
queryKey: articleKeys.detail(article.id),
9494
})
9595
})
9696

frontends/api/src/hooks/articles/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const useArticleList = (
1818
}
1919

2020
/**
21-
* Query is diabled if id is undefined.
21+
* Query is disabled if id is undefined.
2222
*/
2323
const useArticleDetail = (id: number | undefined) => {
2424
return useQuery({
@@ -58,8 +58,8 @@ const useArticlePartialUpdate = () => {
5858
PatchedRichTextArticleRequest: data,
5959
})
6060
.then((response) => response.data),
61-
onSuccess: (_data) => {
62-
client.invalidateQueries({ queryKey: articleKeys.root })
61+
onSuccess: (article: Article) => {
62+
client.invalidateQueries({ queryKey: articleKeys.detail(article.id) })
6363
},
6464
})
6565
}

frontends/main/src/app-pages/ArticlePage/NewArticlePage.tsx

Lines changed: 0 additions & 36 deletions
This file was deleted.

frontends/main/src/app-pages/Articles/ArticleDetailPage.tsx

Lines changed: 10 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,66 +2,27 @@
22

33
import React from "react"
44
import { useArticleDetail } from "api/hooks/articles"
5-
import {
6-
Container,
7-
LoadingSpinner,
8-
styled,
9-
Typography,
10-
TiptapEditorContainer,
11-
} from "ol-components"
12-
import { ButtonLink } from "@mitodl/smoot-design"
5+
import { LoadingSpinner, ArticleEditor } from "ol-components"
136
import { notFound } from "next/navigation"
147
import { Permission } from "api/hooks/user"
158
import RestrictedRoute from "@/components/RestrictedRoute/RestrictedRoute"
16-
import { articlesEditView } from "@/common/urls"
17-
18-
const Page = styled(Container)({
19-
marginTop: "40px",
20-
marginBottom: "40px",
21-
})
22-
23-
const ControlsContainer = styled.div({
24-
display: "flex",
25-
justifyContent: "flex-end",
26-
margin: "10px",
27-
})
28-
const WrapperContainer = styled.div({
29-
borderBottom: "1px solid rgb(222, 208, 208)",
30-
paddingBottom: "10px",
31-
})
329

3310
export const ArticleDetailPage = ({ articleId }: { articleId: number }) => {
34-
const id = Number(articleId)
35-
const { data, isLoading } = useArticleDetail(id)
11+
const {
12+
data: article,
13+
isLoading,
14+
isFetching,
15+
} = useArticleDetail(Number(articleId))
3616

37-
const editUrl = articlesEditView(id)
38-
39-
if (isLoading) {
40-
return <LoadingSpinner color="inherit" loading={isLoading} size={32} />
17+
if (isLoading || isFetching) {
18+
return <LoadingSpinner color="inherit" loading size={32} />
4119
}
42-
if (!data) {
20+
if (!article) {
4321
return notFound()
4422
}
4523
return (
4624
<RestrictedRoute requires={Permission.ArticleEditor}>
47-
<Page>
48-
<WrapperContainer>
49-
<Typography variant="h3" component="h1">
50-
{data?.title}
51-
</Typography>
52-
53-
<ControlsContainer>
54-
<ButtonLink href={editUrl} variant="primary">
55-
Edit
56-
</ButtonLink>
57-
</ControlsContainer>
58-
</WrapperContainer>
59-
<TiptapEditorContainer
60-
data-testid="editor"
61-
value={data.content}
62-
readOnly
63-
/>
64-
</Page>
25+
<ArticleEditor article={article} readOnly />
6526
</RestrictedRoute>
6627
)
6728
}

frontends/main/src/app-pages/Articles/ArticleEditPage.test.tsx

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import React from "react"
22
import { screen, renderWithProviders, setMockResponse } from "@/test-utils"
33
import { waitFor, fireEvent } from "@testing-library/react"
44
import userEvent from "@testing-library/user-event"
5-
import { factories, urls } from "api/test-utils"
5+
import { factories, urls, makeRequest } from "api/test-utils"
66
import { ArticleEditPage } from "./ArticleEditPage"
77

8-
const pushMock = jest.fn()
8+
const mockPush = jest.fn()
99

1010
jest.mock("next-nprogress-bar", () => ({
1111
useRouter: () => ({
12-
push: pushMock,
12+
push: mockPush,
1313
}),
1414
}))
1515

@@ -38,12 +38,12 @@ describe("ArticleEditPage", () => {
3838

3939
renderWithProviders(<ArticleEditPage articleId={"42"} />)
4040

41-
expect(await screen.findByText("Edit Article")).toBeInTheDocument()
42-
expect(screen.getByTestId("editor")).toBeInTheDocument()
41+
await screen.findByTestId("editor")
42+
4343
expect(screen.getByDisplayValue("Existing Title")).toBeInTheDocument()
4444
})
4545

46-
test("submits article successfully and redirects", async () => {
46+
test("submits article successfully", async () => {
4747
const user = factories.user.user({
4848
is_authenticated: true,
4949
is_article_editor: true,
@@ -65,22 +65,30 @@ describe("ArticleEditPage", () => {
6565
})
6666
setMockResponse.get(urls.articles.details(article.id), article)
6767

68-
// ✅ Mock successful update response
6968
const updated = { ...article, title: "Updated Title" }
7069
setMockResponse.patch(urls.articles.details(article.id), updated)
7170

7271
renderWithProviders(<ArticleEditPage articleId={"123"} />)
7372

74-
const titleInput = await screen.findByPlaceholderText("Enter article title")
73+
await screen.findByTestId("editor")
74+
75+
const titleInput = await screen.findByPlaceholderText("Article title")
7576

7677
fireEvent.change(titleInput, { target: { value: "Updated Title" } })
7778

7879
await waitFor(() => expect(titleInput).toHaveValue("Updated Title"))
7980

80-
await userEvent.click(screen.getByText(/save article/i))
81+
await userEvent.click(screen.getByRole("button", { name: "Save" }))
8182

82-
// ✅ Wait for redirect after update success
83-
await waitFor(() => expect(pushMock).toHaveBeenCalledWith("/articles/123"))
83+
await waitFor(() =>
84+
expect(makeRequest).toHaveBeenCalledWith(
85+
"patch",
86+
urls.articles.details(article.id),
87+
expect.objectContaining({ title: "Updated Title" }),
88+
),
89+
)
90+
91+
await waitFor(() => expect(mockPush).toHaveBeenCalledWith("/articles/123"))
8492
})
8593

8694
test("shows error alert on failure", async () => {
@@ -105,7 +113,6 @@ describe("ArticleEditPage", () => {
105113
})
106114
setMockResponse.get(urls.articles.details(article.id), article)
107115

108-
// ✅ Mock failed update (500)
109116
setMockResponse.patch(
110117
urls.articles.details(article.id),
111118
{ detail: "Server Error" },
@@ -114,10 +121,12 @@ describe("ArticleEditPage", () => {
114121

115122
renderWithProviders(<ArticleEditPage articleId={"7"} />)
116123

117-
const titleInput = await screen.findByPlaceholderText("Enter article title")
124+
await screen.findByTestId("editor")
125+
126+
const titleInput = await screen.findByPlaceholderText("Article title")
118127
fireEvent.change(titleInput, { target: { value: "Bad Article" } })
119128

120-
await userEvent.click(screen.getByText(/save article/i))
129+
await userEvent.click(screen.getByRole("button", { name: "Save" }))
121130

122131
expect(await screen.findByText(/Mock Error/i)).toBeInTheDocument()
123132
})

frontends/main/src/app-pages/Articles/ArticleEditPage.tsx

Lines changed: 26 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,125 +1,50 @@
11
"use client"
2-
import React, { useEffect, useState } from "react"
3-
import { Permission } from "api/hooks/user"
2+
3+
import React from "react"
44
import { useRouter } from "next-nprogress-bar"
5-
import { useArticleDetail, useArticlePartialUpdate } from "api/hooks/articles"
6-
import { Button, Alert } from "@mitodl/smoot-design"
5+
import { notFound } from "next/navigation"
6+
import { Permission } from "api/hooks/user"
7+
import { useArticleDetail } from "api/hooks/articles"
78
import RestrictedRoute from "@/components/RestrictedRoute/RestrictedRoute"
89
import {
9-
Container,
10-
Typography,
1110
styled,
1211
LoadingSpinner,
13-
TiptapEditorContainer,
14-
JSONContent,
12+
ArticleEditor,
13+
HEADER_HEIGHT,
1514
} from "ol-components"
16-
17-
import { notFound } from "next/navigation"
1815
import { articlesView } from "@/common/urls"
1916

20-
const SaveButton = styled.div({
21-
textAlign: "right",
22-
margin: "10px",
23-
})
24-
25-
const ClientContainer = styled.div({
26-
width: "100%",
27-
margin: "10px 0",
28-
})
17+
const PageContainer = styled.div(({ theme }) => ({
18+
color: theme.custom.colors.darkGray2,
19+
display: "flex",
20+
height: `calc(100vh - ${HEADER_HEIGHT}px - 132px)`,
21+
}))
2922

3023
const ArticleEditPage = ({ articleId }: { articleId: string }) => {
24+
const {
25+
data: article,
26+
isLoading,
27+
isFetching,
28+
} = useArticleDetail(Number(articleId))
3129
const router = useRouter()
3230

33-
const id = Number(articleId)
34-
const { data: article, isLoading } = useArticleDetail(id)
35-
36-
const [title, setTitle] = useState<string>("")
37-
const [json, setJson] = useState<JSONContent>({
38-
type: "doc",
39-
content: [{ type: "paragraph", content: [] }],
40-
})
41-
const [alertText, setAlertText] = useState("")
42-
43-
const { mutate: updateArticle, isPending } = useArticlePartialUpdate()
44-
45-
const handleSave = () => {
46-
const payload = {
47-
id: id,
48-
title: title.trim(),
49-
content: json,
50-
}
51-
52-
updateArticle(payload, {
53-
onSuccess: (article) => {
54-
router.push(articlesView(article.id))
55-
},
56-
onError: (error) => {
57-
setAlertText(`❌ ${error.message}`)
58-
},
59-
})
60-
}
61-
62-
useEffect(() => {
63-
if (article && !title) {
64-
setTitle(article.title)
65-
setJson(article.content)
66-
}
67-
// eslint-disable-next-line react-hooks/exhaustive-deps
68-
}, [article])
69-
70-
if (isLoading) {
31+
if (isLoading || isFetching) {
7132
return <LoadingSpinner color="inherit" loading={isLoading} size={32} />
7233
}
7334
if (!article) {
7435
return notFound()
7536
}
7637

77-
const handleChange = (json: object) => {
78-
setJson(json)
79-
}
80-
8138
return (
8239
<RestrictedRoute requires={Permission.ArticleEditor}>
83-
<Container>
84-
<Typography variant="h3" component="h1">
85-
Edit Article
86-
</Typography>
87-
{alertText && (
88-
<Alert
89-
key={alertText}
90-
severity="error"
91-
className="info-alert"
92-
closable
93-
>
94-
<Typography variant="body2" color="textPrimary">
95-
{alertText}
96-
</Typography>
97-
</Alert>
98-
)}
99-
100-
<ClientContainer>
101-
<TiptapEditorContainer
102-
data-testid="editor"
103-
value={json}
104-
onChange={handleChange}
105-
title={title}
106-
setTitle={(e) => {
107-
setTitle(e.target.value)
108-
setAlertText("")
109-
}}
110-
/>
111-
</ClientContainer>
112-
113-
<SaveButton>
114-
<Button
115-
variant="primary"
116-
disabled={isPending || !title.trim()}
117-
onClick={handleSave}
118-
>
119-
{isPending ? "Saving..." : "Save Article"}
120-
</Button>
121-
</SaveButton>
122-
</Container>
40+
<PageContainer>
41+
<ArticleEditor
42+
article={article}
43+
onSave={(article) => {
44+
router.push(articlesView(article.id))
45+
}}
46+
/>
47+
</PageContainer>
12348
</RestrictedRoute>
12449
)
12550
}

0 commit comments

Comments
 (0)