-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add detail and editing page for article feature #2679
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
base: main
Are you sure you want to change the base?
Conversation
OpenAPI ChangesShow/hide 3 changes: 0 error, 0 warning, 3 infoUnexpected changes? Ensure your branch is up-to-date with |
84043af to
dc89ff1
Compare
b5b2859 to
6a71023
Compare
6a71023 to
a8f9f22
Compare
There was a problem hiding this 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> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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", | ||
| }) |
There was a problem hiding this comment.
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/
There was a problem hiding this comment.
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
| <EditButton> | ||
| <EditButtonLink | ||
| href={`/articles/${data.id}/edit`} | ||
| className="btn btn-edit" | ||
| color="red" | ||
| > | ||
| Edit | ||
| </EditButtonLink> | ||
| </EditButton> |
There was a problem hiding this comment.
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(...).
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have removed this
| const [alertText, setAlertText] = React.useState("") | ||
| const [severity, setSeverity] = React.useState<"success" | "error">("success") |
There was a problem hiding this comment.
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()There was a problem hiding this comment.
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
| <TestErrorBoundary> | ||
| <NewArticlePage /> | ||
| </TestErrorBoundary>, |
There was a problem hiding this comment.
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 :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
| useArticleDetail: (id: number) => ({ | ||
| data: { | ||
| id, | ||
| title: "Existing Title", | ||
| html: "<p>Existing content</p>", | ||
| }, | ||
| isLoading: false, | ||
| }), | ||
| useArticlePartialUpdate: () => ({ | ||
| mutate: mockUpdateMutate, | ||
| isPending: false, | ||
| }), | ||
| })) |
There was a problem hiding this comment.
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:
useArticleDetailremoving 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 } )There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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 }) => { |
There was a problem hiding this comment.
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.
What are the relevant tickets?
In reference to #91
Description (What does it do?)
/api/articles/1)Screenshots (if appropriate):
Screen.Recording.2025-11-06.at.3.50.27.PM.mp4
How can this be tested?
/articles/newfor accessing the pageAdditional Context