Skip to content

Conversation

@ahtesham-quraish
Copy link
Contributor

What are the relevant tickets?

In reference to #91

Description (What does it do?)

  • Fetch the article from existing API endpoint (e.g., /api/articles/1)
  • Display each article with:
    • Title
    • "Edit" button / link to navigate to article edit page
  • Show loader while fetching data
  • Handle error state (API failure, network error, etc.)

Screenshots (if appropriate):

Screen.Recording.2025-11-06.at.3.50.27.PM.mp4

How can this be tested?

  • run yarn to install the dependencies
  • after installing the dependencies you need to up the frontend next app
  • then hit the following url /articles/new for accessing the page

Additional Context

@github-actions
Copy link

github-actions bot commented Nov 6, 2025

OpenAPI Changes

Show/hide 3 changes: 0 error, 0 warning, 3 info
3 changes: 0 error, 0 warning, 3 info
info	[request-parameter-property-type-generalized] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/contentfiles/
		for the 'query' request parameter 'resource_id', the type/format of property '/items/' was generalized from 'integer'/'' to 'number'/''

info	[request-parameter-property-type-generalized] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/courses/{learning_resource_id}/contentfiles/
		for the 'query' request parameter 'resource_id', the type/format of property '/items/' was generalized from 'integer'/'' to 'number'/''

info	[request-parameter-property-type-generalized] at head/openapi/specs/v1.yaml	
	in API GET /api/v1/learning_resources/{learning_resource_id}/contentfiles/
		for the 'query' request parameter 'resource_id', the type/format of property '/items/' was generalized from 'integer'/'' to 'number'/''


Unexpected changes? Ensure your branch is up-to-date with main (consider rebasing).

@ahtesham-quraish ahtesham-quraish force-pushed the ahtesham/#9109-article-detail branch 4 times, most recently from 84043af to dc89ff1 Compare November 6, 2025 11:34
@ahtesham-quraish ahtesham-quraish changed the title Ahtesham/#9109 article detail feat: add detail and editing page for article feature Nov 6, 2025
@ahtesham-quraish ahtesham-quraish force-pushed the ahtesham/#9109-article-detail branch 3 times, most recently from b5b2859 to 6a71023 Compare November 6, 2025 11:51
@ahtesham-quraish ahtesham-quraish force-pushed the ahtesham/#9109-article-detail branch from 6a71023 to a8f9f22 Compare November 6, 2025 12:02
Copy link
Contributor

@ChristopherChudzicki ChristopherChudzicki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit: Please see https://github.com/mitodl/hq/issues/9198 before working more on this.

Original post

The new page + editing + viewing functionality is working well 👍

I left some feedback about more standard ways to do things within learn, using the component library, and testing stuff.

Overall Request:

  • Don't spend any more time on CKEditor changes... As mentioned in slack, we're planning to switch to tiptap. We can leave the CKEditor code in this PR as-is.
  • The changes I've mentioned here are not CKEditor related, so are still worth making.
  • IMO, simplest to close #2671 and focus on this PR. (This one is based off that, anyway, right?)

return notFound()
}
return (
<Container>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Here and on the other pages, I'd do something like

const Page = styled(Container)({
  marginTop: "40px",
  marginBottom: "40px",
})

(note: don't want to override the horizontal margins on container) and use that as the root element for some top/bottom margins.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the way you suggested

const ArticleTitle = styled.h1({
fontSize: "24px",
marginBottom: "12px",
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, avoid specifying any font size, etc, directly. Here we'd do

const ArticleTitle = styled.h1(({theme})=>({
  ...theme.custom.typography.h3, // I'm making up h3 since we don't have designs.
  marginBottom: "12px",
}))

though I'd probably do <Typography variant="h3" component="h1" sx={{marginBottom: "12px"}}> see https://mui.com/system/getting-started/the-sx-prop/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have used Typography for our use

Comment on lines +54 to +62
<EditButton>
<EditButtonLink
href={`/articles/${data.id}/edit`}
className="btn btn-edit"
color="red"
>
Edit
</EditButtonLink>
</EditButton>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of custom styles, this should be replaceable by <ButtonLink href={/articles/${data.id}/edit} variant="primary">. See https://mitodl.github.io/smoot-design/?path=/docs/smoot-design-button--docs#links for more.

Re className="btn btn-edit": Here and elsewhere, let's not add unneessary classes. Styling in mit-learn is generally done through styled(...).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have used the ButtonLink


const [title, setTitle] = React.useState<string>("")
const [editorContent, setEditorContent] = React.useState<string>("")
const [editorKey] = React.useState(0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have removed this

Comment on lines +36 to +37
const [alertText, setAlertText] = React.useState("")
const [severity, setSeverity] = React.useState<"success" | "error">("success")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not use state for this... check articleUpdate.isError. (I don't think clearing the error upon title change, body change, is necessary. It's an edge case anyway and will be cleared on a successful submission. If you want to clear it, you can do articleUpdate.reset().

const articleUpdate = useArticlePartialUpdate()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have removed the severity in our case and error remains there if user does not remove it and will be gone upon successful submission

Comment on lines +68 to +70
<TestErrorBoundary>
<NewArticlePage />
</TestErrorBoundary>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a custom error boundary, there is a TestingErrorBoundary in @/test-utils (aka frontends/main/src/test-utils/index.tsx) that you could use like

const onError = jest.fn()
renderWithProviders(<TestingErrorBoundary onError={onError}>...<TestingErrorBoundary/>)
await waitFor(() => expect(onError).toHaveBeenCalled() )

This should definitely be easier to test, though :/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +18 to +30
useArticleDetail: (id: number) => ({
data: {
id,
title: "Existing Title",
html: "<p>Existing content</p>",
},
isLoading: false,
}),
useArticlePartialUpdate: () => ({
mutate: mockUpdateMutate,
isPending: false,
}),
}))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Here and elsewhere in the pr)

Request: Don't mock these hooks—it can be fragile. For example:

  • useArticleDetail removing the custom hooks and using queries directly would artificially break this (code would work, just tests fail)
  • For the mutation, switching the code to something along the lines of below would break the updating tests.
    onSubmit = {() => {
    await articleUpdate.mutateAsync(newData)
        redirect(article.id)
    })

Instead, mock the API call:

const makeArticle = factories.articles.article

const article = makeArticle()
setMockResponse.get(urls.articles.details(article.id), article)
setMockResponse.post(urls.articles.details(article.id), WHATEVER_RESPONSE_BODY)

// if you need an error response:
setMockResponse.post(urls.articles.details(article.id), WHATEVER_RESPONSE_BODY, { code: 500 } )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

})

// Click save
fireEvent.click(screen.getByText(/save article/i))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Here and elsewhere in the PR)

Request: Instead of fireEvent.click, use

import userEvent from "@testing-library/user-event"

// then
await userEvent.click(screen.getByText(/save article/i))

fireEvent basically just fires a click event, period. If you corresponding HTML button happens to be disabled, it will still fire the event (even though a user couldn't actually trigger that in a real browser).

userEvent.click is a higher-level API. It performs a number of other things:

  • fires pointer down + pointer up events
  • performs some sanity checks, like "is the button disabled? if yes, throw an error".

See https://testing-library.com/docs/user-event/intro/#differences-from-fireevent for more.

import { ArticleDetailPage } from "@/app-pages/Articles/ArticleDetailPage"

export const metadata: Metadata = standardizeMetadata({
title: "Article Detail",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add noindex, nofollow here, too.

color: "#FFFFFF",
})

export const ArticleDetailPage = ({ articleId }: { articleId: number }) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request: This should be a restrictedroute, too. We're at an early stage so none of these pages should be public yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants