From 766f4f7ee90ed018a9bad969c3bdc7df126ea3bd Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Thu, 29 Feb 2024 18:27:02 +0800 Subject: [PATCH] feat(article): revise republication logic --- src/common/enums/index.ts | 1 - .../Editor/Article/Summary/index.tsx | 12 +- src/components/Editor/Article/Title/index.tsx | 12 +- src/components/Editor/Article/index.tsx | 67 +-------- src/views/ArticleDetail/Edit/Header/gql.ts | 4 + src/views/ArticleDetail/Edit/Header/index.tsx | 134 ++++++++---------- .../Edit/PublishState/PublishedState.tsx | 5 +- src/views/ArticleDetail/Edit/gql.ts | 1 + src/views/ArticleDetail/Edit/index.tsx | 67 +++++---- 9 files changed, 107 insertions(+), 196 deletions(-) diff --git a/src/common/enums/index.ts b/src/common/enums/index.ts index 5366c26377..50e2bde69b 100644 --- a/src/common/enums/index.ts +++ b/src/common/enums/index.ts @@ -56,7 +56,6 @@ export const MAX_ARTICLE_SUPPORT_LENGTH = 140 export const MAX_ARTICLE_COMMENT_LENGTH = 1200 export const MAX_ARTICLE_REVISION_COUNT = 4 -export const MAX_ARTICLE_REVISION_DIFF = 50 export const MAX_ARTICLE_TAG_LENGTH = 3 export const MAX_ARTICLE_COLLECT_LENGTH = 3 diff --git a/src/components/Editor/Article/Summary/index.tsx b/src/components/Editor/Article/Summary/index.tsx index d660c9a62d..32df7e1f1a 100644 --- a/src/components/Editor/Article/Summary/index.tsx +++ b/src/components/Editor/Article/Summary/index.tsx @@ -13,7 +13,6 @@ import { LanguageContext } from '~/components' * func({ summary: '' })} * /> @@ -22,14 +21,12 @@ import { LanguageContext } from '~/components' interface Props { defaultValue?: string enable?: boolean - readOnly?: boolean update: (params: { summary: any }) => void } const EditorSummary: React.FC = ({ defaultValue = '', enable, - readOnly, update, }) => { const { lang } = useContext(LanguageContext) @@ -65,7 +62,6 @@ const EditorSummary: React.FC = ({ const classes = classNames({ 'editor-summary': true, - 'u-area-disable': readOnly, }) const counterClasses = classNames({ counter: true, @@ -94,11 +90,9 @@ const EditorSummary: React.FC = ({ onChange={handleChange} onKeyDown={handleKeyDown} /> - {!readOnly && ( -
- ({length}/{MAX_ARTICE_SUMMARY_LENGTH}) -
- )} +
+ ({length}/{MAX_ARTICE_SUMMARY_LENGTH}) +
) } diff --git a/src/components/Editor/Article/Title/index.tsx b/src/components/Editor/Article/Title/index.tsx index 10fb67b348..d7fe35b801 100644 --- a/src/components/Editor/Article/Title/index.tsx +++ b/src/components/Editor/Article/Title/index.tsx @@ -9,20 +9,12 @@ import styles from './styles.module.css' interface Props { defaultValue?: string - readOnly?: boolean update: (params: { title: any }) => void } -const EditorTitle: React.FC = ({ - defaultValue = '', - readOnly, - update, -}) => { +const EditorTitle: React.FC = ({ defaultValue = '', update }) => { const { lang } = useContext(LanguageContext) - const classes = classNames( - [styles.editorTitle], - readOnly ? 'u-area-disable' : '' - ) + const classes = classNames([styles.editorTitle]) const [value, setValue] = React.useState(defaultValue) diff --git a/src/components/Editor/Article/index.tsx b/src/components/Editor/Article/index.tsx index f764fc9ee6..308b1e9a17 100644 --- a/src/components/Editor/Article/index.tsx +++ b/src/components/Editor/Article/index.tsx @@ -1,9 +1,5 @@ import { useApolloClient } from '@apollo/react-hooks' -import { - EditorContent, - useArticleEdtor, - useEditArticleEdtor, -} from '@matters/matters-editor' +import { EditorContent, useArticleEdtor } from '@matters/matters-editor' import classNames from 'classnames' import { useContext } from 'react' import { useDebouncedCallback } from 'use-debounce' @@ -104,64 +100,3 @@ export const ArticleEditor: React.FC = ({ ) } - -export const EditArticleEditor: React.FC = ({ - draft, - - update, - upload, -}) => { - const { lang } = useContext(LanguageContext) - const client = useApolloClient() - - const { content, publishState, summary, summaryCustomized, title } = draft - const isPending = publishState === 'pending' - const isReadOnly = isPending - - const editor = useEditArticleEdtor({ - editable: !isReadOnly, - placeholder: translate({ - zh_hant: '請輸入正文…', - zh_hans: '请输入正文…', - en: 'Enter content…', - lang, - }), - content: content || '', - onUpdate: async ({ editor, transaction }) => { - const content = editor.getHTML() - update({ content }) - }, - mentionSuggestion: makeMentionSuggestion({ client }), - extensions: [ - FigureEmbedLinkInput, - FigurePlaceholder.configure({ - placeholder: translate({ - zh_hant: '添加說明文字…', - zh_hans: '添加说明文字…', - en: 'Add caption…', - lang, - }), - }), - ], - }) - - return ( -
- - - - - -
- ) -} diff --git a/src/views/ArticleDetail/Edit/Header/gql.ts b/src/views/ArticleDetail/Edit/Header/gql.ts index 5849954a7b..456c36a0a3 100644 --- a/src/views/ArticleDetail/Edit/Header/gql.ts +++ b/src/views/ArticleDetail/Edit/Header/gql.ts @@ -6,6 +6,8 @@ import articleFragments from '~/components/GQL/fragments/article' export const EDIT_ARTICLE = gql` mutation EditArticle( $id: ID! + $title: String + $summary: String $content: String $cover: ID $tags: [String!] @@ -24,6 +26,8 @@ export const EDIT_ARTICLE = gql` editArticle( input: { id: $id + # title: $title + # summary: $summary content: $content cover: $cover tags: $tags diff --git a/src/views/ArticleDetail/Edit/Header/index.tsx b/src/views/ArticleDetail/Edit/Header/index.tsx index aec9d8cbbc..d0b7c3dc12 100644 --- a/src/views/ArticleDetail/Edit/Header/index.tsx +++ b/src/views/ArticleDetail/Edit/Header/index.tsx @@ -1,47 +1,40 @@ -import { normalizeArticleHTML } from '@matters/matters-editor' -import { useContext, useEffect, useRef } from 'react' +import _isEqual from 'lodash/isEqual' import { FormattedMessage } from 'react-intl' -import { - MAX_ARTICLE_CONTENT_LENGTH, - MAX_ARTICLE_REVISION_DIFF, -} from '~/common/enums' -import { measureDiffs, stripHtml } from '~/common/utils' -import { - Button, - TextIcon, - toast, - Translate, - useMutation, - ViewerContext, -} from '~/components' +import { MAX_ARTICLE_CONTENT_LENGTH } from '~/common/enums' +import { toPath } from '~/common/utils' +import { Button, TextIcon, toast, Translate, useMutation } from '~/components' import { ConfirmStepContentProps, EditorSettingsDialog, EditorSettingsDialogProps, } from '~/components/Editor/SettingsDialog' -import { EditArticleMutation } from '~/gql/graphql' -import { VIEWER_ARTICLES } from '~/views/User/Articles/gql' +import { + AssetFragment, + EditArticleMutation, + QueryEditArticleQuery, +} from '~/gql/graphql' import ConfirmRevisedPublishDialogContent from './ConfirmRevisedPublishDialogContent' import { EDIT_ARTICLE } from './gql' import styles from './styles.module.css' type EditModeHeaderProps = { - article: { - id: string - replyToDonator?: string | null - requestForDonation?: string | null - } - lastContent: string - editContent?: string - coverId?: string + article: NonNullable< + QueryEditArticleQuery['article'] & { + __typename: 'Article' + } + > + + revisedTitle?: string + revisedSummary?: string + revisedContent?: string + revisedCover?: AssetFragment revisionCountLeft: number isOverRevisionLimit: boolean isEditDisabled: boolean - onSaved: () => any onPublish: () => any } & Omit< EditorSettingsDialogProps, @@ -57,44 +50,50 @@ type EditModeHeaderProps = { const EditModeHeader = ({ article, - lastContent, - editContent, - coverId, + revisedTitle, + revisedSummary, + revisedContent, + revisedCover, revisionCountLeft, isOverRevisionLimit, isEditDisabled, - onSaved, onPublish, ...restProps }: EditModeHeaderProps) => { - const viewer = useContext(ViewerContext) - - const initContent = useRef() - useEffect(() => { - initContent.current = lastContent || '' - }, []) - - const currContent = editContent || '' - const diff = - measureDiffs( - stripHtml(normalizeArticleHTML(initContent.current || '')), - stripHtml(normalizeArticleHTML(currContent || '')) - ) || 0 - const diffCount = `${diff}`.padStart(2, '0') - const isOverDiffLimit = diff > MAX_ARTICLE_REVISION_DIFF - const isContentRevised = diff > 0 - - // save or republish const { tags, collection, circle, accessType, license } = restProps const [editArticle, { loading }] = useMutation(EDIT_ARTICLE) + + const isTitleRevised = revisedTitle !== article.title + const isSummaryRevised = revisedSummary !== article.summary + const isContentRevised = revisedContent !== article.contents.html + const isTagRevised = !_isEqual( + tags.map((tag) => tag.id).sort(), + article.tags?.map((tag) => tag.id).sort() + ) + const isCollectionRevised = !_isEqual( + collection.map((collection) => collection.id).sort(), + article.collection.edges?.map(({ node }) => node.id).sort() + ) + const isCoverRevised = article.cover + ? revisedCover?.path !== article.cover + : !!revisedCover?.path + const needRepublish = + isTitleRevised || + isSummaryRevised || + isContentRevised || + isTagRevised || + isCollectionRevised || + isCoverRevised + const onSave = async () => { // check content length - const contentCount = editContent?.length || 0 - if (isContentRevised && contentCount > MAX_ARTICLE_CONTENT_LENGTH) { + const contentCount = revisedContent?.length || 0 + + if (needRepublish && contentCount > MAX_ARTICLE_CONTENT_LENGTH) { toast.error({ message: ( tag.content), - collection: collection.map(({ id: articleId }) => articleId), + ...(isTitleRevised ? { title: revisedTitle } : {}), + ...(isSummaryRevised ? { summary: revisedSummary } : {}), + ...(isContentRevised ? { content: revisedContent } : {}), + ...(isTagRevised ? { tags: tags.map((tag) => tag.content) } : {}), + ...(isCollectionRevised + ? { collection: collection.map(({ id }) => id) } + : {}), + ...(isCoverRevised ? { cover: revisedCover?.id || null } : {}), circle: circle ? circle.id : null, accessType, license, - ...(isContentRevised ? { content: editContent } : {}), - first: null, iscnPublish: restProps.iscnPublish, canComment: restProps.canComment, sensitive: restProps.contentSensitive, }, - refetchQueries: [ - { - query: VIEWER_ARTICLES, - variables: { userName: viewer.userName }, - }, - ], }) - if (isContentRevised) { + if (needRepublish) { onPublish() - } - - if (!isContentRevised) { - onSaved() + } else { + const path = toPath({ page: 'articleDetail', article }) + window.location.href = path.href } } catch (e) { toast.error({ - message: isContentRevised ? ( + message: needRepublish ? ( - -  {diffCount}/50    - ) @@ -224,7 +216,7 @@ const EditModeHeader = ({ bgColor="green" onClick={openEditorSettingsDialog} aria-haspopup="dialog" - disabled={isEditDisabled || isOverDiffLimit} + disabled={isEditDisabled} > diff --git a/src/views/ArticleDetail/Edit/PublishState/PublishedState.tsx b/src/views/ArticleDetail/Edit/PublishState/PublishedState.tsx index 2b37dec20a..74ad40304c 100644 --- a/src/views/ArticleDetail/Edit/PublishState/PublishedState.tsx +++ b/src/views/ArticleDetail/Edit/PublishState/PublishedState.tsx @@ -22,10 +22,7 @@ const BasePublishedState = ({ } const PublishedState = ({ article }: PublishedStateProps) => { - const path = toPath({ - page: 'articleDetail', - article: { ...article }, - }) + const path = toPath({ page: 'articleDetail', article }) return ( const Editor = dynamic( - () => - import('~/components/Editor/Article').then((mod) => mod.EditArticleEditor), + () => import('~/components/Editor/Article').then((mod) => mod.ArticleEditor), { ssr: false, loading: () => } ) const BaseEdit = ({ article }: { article: Article }) => { - const [editContent, setEditContent] = useState('') const [showPublishState, setShowPublishState] = useState(false) + const [editTitle, setEditTitle] = useState('') + const [editSummary, setEditSummary] = useState('') + const [editContent, setEditContent] = useState('') + // cover const assets = article.assets || [] - const [cover, editCover] = useState() + const [cover, editCover] = useState( + assets.find((asset) => asset.path === article.cover) + ) const refetchAssets = useImperativeQuery( GET_EDIT_ARTICLE_ASSETS, { @@ -73,7 +77,7 @@ const BaseEdit = ({ article }: { article: Article }) => { const [tags, editTags] = useState(article.tags || []) const [collection, editCollection] = useState< ArticleDigestDropdownArticleFragment[] - >([]) + >(article.collection.edges?.map(({ node }) => node) || []) // access const [circle, editCircle] = useState< @@ -84,11 +88,11 @@ const BaseEdit = ({ article }: { article: Article }) => { ) // cc2.0 is replace by cc4.0 when editting article - const initialLicense = + const [license, editLicense] = useState( article.license === ArticleLicenseType.CcByNcNd_2 ? ArticleLicenseType.CcByNcNd_4 : article.license - const [license, editLicense] = useState(initialLicense) + ) const ownCircles = article.author.ownCircles const hasOwnCircle = ownCircles && ownCircles.length >= 1 @@ -108,22 +112,6 @@ const BaseEdit = ({ article }: { article: Article }) => { editLicense(newLicense) } - // update cover & collection from retrieved data - useEffect(() => { - if (!article) { - return - } - - // cover, find from `article.assets` since `article.cover` isn't a `Asset` - const currCover = assets.find((asset) => asset.path === article.cover) - if (currCover) { - editCover(currCover) - } - - // collection - editCollection(article.collection.edges?.map(({ node }) => node) || []) - }, [article.id]) - const { edit: editSupport, saving: supportSaving } = useEditArticleDetailSupportSetting(article.id) @@ -193,8 +181,6 @@ const BaseEdit = ({ article }: { article: Article }) => { iscnPublishSaving: false, } - const onSaved = () => {} // TODO - return ( <> { {...accessProps} {...setCommentProps} article={article} - lastContent={article.contents.html} - editContent={editContent || article.contents.html || ''} - coverId={cover?.id} + revisedTitle={editTitle || article.title} + revisedSummary={editSummary || article.summary} + revisedContent={editContent || article.contents.html || ''} + revisedCover={cover} revisionCountLeft={revisionCountLeft} isOverRevisionLimit={isOverRevisionLimit} - isEditDisabled={false} - onSaved={() => { - onSaved() - }} + isEditDisabled={showPublishState} onPublish={() => { setShowPublishState(true) }} @@ -261,16 +245,29 @@ const BaseEdit = ({ article }: { article: Article }) => { { - setEditContent(update.content || '') + if (update.title !== undefined) { + setEditTitle(update.title || '') + } + + if (update.summary !== undefined) { + setEditSummary(update.summary || '') + } + + if (update.content !== undefined) { + setEditContent(update.content || '') + } }} upload={async () => ({ id: '', path: '' })} />