Skip to content
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
1 change: 1 addition & 0 deletions frontends/api/src/hooks/articles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,5 @@ export {
useArticleCreate,
useArticleDestroy,
useArticlePartialUpdate,
articleQueries,
}
18 changes: 11 additions & 7 deletions frontends/main/src/app-pages/Articles/ArticleDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,23 @@ const PageContainer = styled.div({
height: "100%",
})

const Spinner = styled(LoadingSpinner)({
margin: "auto",
position: "absolute",
top: "40%",
left: "50%",
transform: "translate(-50%, -50%)",
})

export const ArticleDetailPage = ({ articleId }: { articleId: number }) => {
const {
data: article,
isLoading,
isFetching,
} = useArticleDetail(Number(articleId))
const { data: article, isLoading } = useArticleDetail(Number(articleId))

const showArticleDetail = useFeatureFlagEnabled(
FeatureFlags.ArticleEditorView,
)

if (isLoading || isFetching) {
return <LoadingSpinner color="inherit" loading size={32} />
if (isLoading) {
return <Spinner color="inherit" loading size={32} />
}
if (!article || !showArticleDetail) {
return notFound()
Expand Down
10 changes: 9 additions & 1 deletion frontends/main/src/app-pages/Articles/ArticleEditPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ const PageContainer = styled.div(({ theme }) => ({
height: "100%",
}))

const Spinner = styled(LoadingSpinner)({
margin: "auto",
position: "absolute",
top: "40%",
left: "50%",
transform: "translate(-50%, -50%)",
})

const ArticleEditPage = ({ articleId }: { articleId: string }) => {
const {
data: article,
Expand All @@ -24,7 +32,7 @@ const ArticleEditPage = ({ articleId }: { articleId: string }) => {
const router = useRouter()

if (isLoading || isFetching) {
return <LoadingSpinner color="inherit" loading={isLoading} size={32} />
return <Spinner color="inherit" loading={isLoading} size={32} />
}
if (!article) {
return notFound()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type LoadingSpinnerProps = {
size?: number | string
"aria-label"?: string
color?: "primary" | "inherit"
className?: string
}

const noDelay = { transitionDelay: "0ms" }
Expand All @@ -27,9 +28,10 @@ const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
size,
"aria-label": label = "Loading",
color,
className,
}) => {
return (
<Container>
<Container className={className}>
<Fade in={loading} style={!loading ? noDelay : undefined} unmountOnExit>
<CircularProgress color={color} aria-label={label} size={size} />
</Fade>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createContext, useContext } from "react"
import type { RichTextArticle } from "api/v1"

interface ArticleContextValue {
article?: RichTextArticle
}

const ArticleContext = createContext<ArticleContextValue>({})

export const ArticleProvider = ArticleContext.Provider

export function useArticle() {
return useContext(ArticleContext).article
}
176 changes: 108 additions & 68 deletions frontends/ol-components/src/components/TiptapEditor/ArticleEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import React, { ChangeEventHandler, useState, useEffect } from "react"
import styled from "@emotion/styled"
import { EditorContext, JSONContent, useEditor } from "@tiptap/react"
import type { RichTextArticle } from "api/v1"
import { LoadingSpinner } from "../LoadingSpinner/LoadingSpinner"
import Document from "@tiptap/extension-document"
import { Placeholder, Selection } from "@tiptap/extensions"

import type { Node as ProseMirrorNode } from "@tiptap/pm/model"
import { StarterKit } from "@tiptap/starter-kit"
import { TaskItem, TaskList } from "@tiptap/extension-list"
import { Heading } from "@tiptap/extension-heading"
Expand All @@ -20,6 +22,7 @@ import { Toolbar } from "./vendor/components/tiptap-ui-primitive/toolbar"
import { Spacer } from "./vendor/components/tiptap-ui-primitive/spacer"

import TiptapEditor, { MainToolbarContent } from "./TiptapEditor"
import { ArticleProvider } from "./ArticleContext"

import { DividerNode } from "./extensions/node/Divider/DividerNode"
import { ArticleByLineInfoBarNode } from "./extensions/node/ArticleByLineInfoBar/ArticleByLineInfoBarNode"
Expand All @@ -38,6 +41,7 @@ import "./vendor/components/tiptap-node/image-node/image-node.scss"
import "./vendor/components/tiptap-node/heading-node/heading-node.scss"
import "./vendor/components/tiptap-node/paragraph-node/paragraph-node.scss"

import type { ExtendedNodeConfig } from "./extensions/node/types"
import { handleImageUpload, MAX_FILE_SIZE } from "./vendor/lib/tiptap-utils"

import "./vendor/styles/_keyframe-animations.scss"
Expand All @@ -49,7 +53,6 @@ import {
useArticlePartialUpdate,
useMediaUpload,
} from "api/hooks/articles"
import type { RichTextArticle } from "api/v1"
import { Alert, Button, ButtonLink } from "@mitodl/smoot-design"
import Typography from "@mui/material/Typography"
import { useUserHasPermission, Permission } from "api/hooks/user"
Expand Down Expand Up @@ -82,6 +85,12 @@ const StyledToolbar = styled(Toolbar)(({ theme }) => ({
const StyledAlert = styled(Alert)({
margin: "20px auto",
maxWidth: "1000px",
position: "fixed",
top: "108px",
left: "50%",
width: "690px",
transform: "translateX(-50%)",
zIndex: 1,
})

const ArticleDocument = Document.extend({
Expand All @@ -104,13 +113,11 @@ const ArticleEditor = ({ onSave, readOnly, article }: ArticleEditorProps) => {
const {
mutate: createArticle,
isPending: isCreating,
isError: isCreateError,
error: createError,
} = useArticleCreate()
const {
mutate: updateArticle,
isPending: isUpdating,
isError: isUpdateError,
error: updateError,
} = useArticlePartialUpdate()

Expand Down Expand Up @@ -215,7 +222,6 @@ const ArticleEditor = ({ onSave, readOnly, article }: ArticleEditorProps) => {

onUpdate: ({ editor }) => {
const json = editor.getJSON()

setContent(json)
setTouched(true)
},
Expand Down Expand Up @@ -256,11 +262,46 @@ const ArticleEditor = ({ onSave, readOnly, article }: ArticleEditorProps) => {
Placeholder.configure({
showOnlyCurrent: false,
includeChildren: true,
placeholder: ({ node }) => {
placeholder: ({ node, editor }): string => {
let parentNode: typeof node | null = null

editor.state.doc.descendants((n: ProseMirrorNode) => {
n.forEach((childNode: ProseMirrorNode) => {
if (childNode === node) {
parentNode = n
}
})
if (parentNode) {
return false
}
return undefined
})

if (parentNode) {
const parentExtension = editor.extensionManager.extensions.find(
(ext) => ext.name === parentNode!.type.name,
)

if (
parentExtension &&
"config" in parentExtension &&
parentExtension.config &&
typeof (parentExtension.config as ExtendedNodeConfig)
.getPlaceholders === "function"
) {
const placeholder = (
parentExtension.config as ExtendedNodeConfig
).getPlaceholders(node)
if (placeholder) {
return placeholder
}
}
}

if (node.type.name === "heading") {
return "Add heading..."
return "Add a heading"
}
return "Add text..."
return "Add some text"
},
}),
HorizontalRule,
Expand All @@ -283,7 +324,7 @@ const ArticleEditor = ({ onSave, readOnly, article }: ArticleEditorProps) => {
maxSize: MAX_FILE_SIZE,
limit: 3,
upload: uploadHandler,
onError: (error) => console.error("Upload failed:", error),
onError: (error) => setUploadError(error.message),
}),
BannerNode,
],
Expand Down Expand Up @@ -320,79 +361,78 @@ const ArticleEditor = ({ onSave, readOnly, article }: ArticleEditorProps) => {
if (!editor) return null

const isPending = isCreating || isUpdating
const isError = isCreateError || isUpdateError
const error = createError || updateError

const publishButtonLabel = (() => {
if (isPending && article?.is_published) return "Updating..."

if (isPending && isPublishing && !article?.is_published)
return "Publishing..."

if (!isPending && article?.is_published) return "Update"

return "Publish"
})()
const error = createError || updateError || uploadError

return (
<ViewContainer toolbarVisible={isArticleEditor}>
<EditorContext.Provider value={{ editor }}>
{isArticleEditor ? (
readOnly ? (
<StyledToolbar>
<Spacer />
<ButtonLink
variant="primary"
href={`/articles/${article?.id}/edit`}
size="small"
>
Edit
</ButtonLink>
</StyledToolbar>
) : (
<StyledToolbar>
<MainToolbarContent editor={editor} />
{(!article || !article?.is_published) && (
<ArticleProvider value={{ article }}>
<EditorContext.Provider value={{ editor }}>
{isArticleEditor ? (
readOnly ? (
<StyledToolbar>
<Spacer />
<ButtonLink
variant="primary"
href={`/articles/${article?.id}/edit`}
size="small"
>
Edit
</ButtonLink>
</StyledToolbar>
) : (
<StyledToolbar>
<MainToolbarContent editor={editor} />
{!article?.is_published ? (
<Button
variant="secondary"
disabled={isPending || !touched || !title}
onClick={() => {
setIsPublishing(false)
handleSave(false)
}}
size="small"
endIcon={
isPending && !isPublishing ? (
<LoadingSpinner size={14} color="inherit" loading />
) : null
}
>
Save As Draft
</Button>
) : null}
Copy link
Contributor

Choose a reason for hiding this comment

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

i think let have more readable format of this condition

{!article?.is_published && (
  <Button
    variant="secondary"
    disabled={isPending || !touched || !title}
    onClick={() => {
      setIsPublishing(false)
      handleSave(false)
    }}
    size="small"
    endIcon={
      isPending && !isPublishing ? (
        <LoadingSpinner size={14} color="inherit" loading />
      ) : null
    }
  >
    Save As Draft
  </Button>
)}


<Button
variant="secondary"
disabled={isPending || !touched || !title}
variant="primary"
disabled={
isPending || !title || (!touched && article?.is_published)
}
onClick={() => {
handleSave(false)
setIsPublishing(false)
setIsPublishing(true)
handleSave(true)
}}
size="small"
endIcon={
isPending && isPublishing ? (
<LoadingSpinner size={14} color="inherit" loading />
) : null
}
>
{isPending && !isPublishing ? "Saving..." : "Save As Draft"}
Publish
</Button>
)}

<Button
variant="primary"
disabled={isPending || !touched || !title}
onClick={() => {
handleSave(true)
setIsPublishing(true)
}}
size="small"
>
{publishButtonLabel}
</Button>
</StyledToolbar>
)
) : null}
{isError ||
(uploadError && (
</StyledToolbar>
)
) : null}
{error ? (
<StyledAlert severity="error" closable>
<Typography variant="body2" color="textPrimary">
{error?.message ??
uploadError ??
"An error occurred while saving"}
{error instanceof Error ? error.message : error}
</Typography>
</StyledAlert>
))}
) : null}

<TiptapEditor editor={editor} readOnly={readOnly} fullWidth />
</EditorContext.Provider>
<TiptapEditor editor={editor} readOnly={readOnly} fullWidth />
</EditorContext.Provider>
</ArticleProvider>
</ViewContainer>
)
}
Expand Down
Loading
Loading