import type { ReactNode, JSX } from 'react'; import React, { useCallback, useState, useEffect, useLayoutEffect, useMemo, } from 'react'; import { GlobalCodeMirrorEditorKey } from '@growi/editor'; import { CodeMirrorEditorComment } from '@growi/editor/dist/client/components/CodeMirrorEditorComment'; import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor'; import { useResolvedThemeForEditor } from '@growi/editor/dist/client/stores/use-resolved-theme'; import { UserPicture } from '@growi/ui/dist/components'; import { useAtomValue } from 'jotai'; import { useTranslation } from 'next-i18next'; import dynamic from 'next/dynamic'; import { TabContent, TabPane, } from 'reactstrap'; import { uploadAttachments } from '~/client/services/upload-attachments'; import { toastError } from '~/client/util/toastr'; import { useCurrentUser } from '~/states/global'; import { useCurrentPagePath } from '~/states/page'; import { isSlackConfiguredAtom } from '~/states/server-configurations'; import { useAcceptedUploadFileType } from '~/stores-universal/context'; import { useNextThemes } from '~/stores-universal/use-next-themes'; import { useSWRxPageComment } from '~/stores/comment'; import { useSWRxSlackChannels, useIsSlackEnabled, useIsEnabledUnsavedWarning, useEditorSettings, } from '~/stores/editor'; import { useCommentEditorDirtyMap } from '~/stores/ui'; import loggerFactory from '~/utils/logger'; import { NotAvailableForGuest } from '../NotAvailableForGuest'; import { NotAvailableIfReadOnlyUserNotAllowedToComment } from '../NotAvailableForReadOnlyUser'; import { CommentPreview } from './CommentPreview'; import { SwitchingButtonGroup } from './SwitchingButtonGroup'; import '@growi/editor/dist/style.css'; import styles from './CommentEditor.module.scss'; const logger = loggerFactory('growi:components:CommentEditor'); const SlackNotification = dynamic(() => import('../SlackNotification').then(mod => mod.SlackNotification), { ssr: false }); const CommentEditorLayout = ({ children }: { children: ReactNode }): JSX.Element => { return (
{children}
); }; type CommentEditorProps = { pageId: string, replyTo?: string, revisionId: string, currentCommentId?: string, commentBody?: string, onCanceled?: () => void, onCommented?: () => void, } export const CommentEditor = (props: CommentEditorProps): JSX.Element => { const { pageId, replyTo, revisionId, currentCommentId, commentBody, onCanceled, onCommented, } = props; const currentUser = useCurrentUser(); const currentPagePath = useCurrentPagePath(); const { update: updateComment, post: postComment } = useSWRxPageComment(pageId); const { data: isSlackEnabled, mutate: mutateIsSlackEnabled } = useIsSlackEnabled(); const { data: acceptedUploadFileType } = useAcceptedUploadFileType(); const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath); const isSlackConfigured = useAtomValue(isSlackConfiguredAtom); const { data: editorSettings } = useEditorSettings(); const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning(); const { evaluate: evaluateEditorDirtyMap, clean: cleanEditorDirtyMap, } = useCommentEditorDirtyMap(); const { mutate: mutateResolvedTheme } = useResolvedThemeForEditor(); const { resolvedTheme } = useNextThemes(); mutateResolvedTheme({ themeData: resolvedTheme }); const editorKey = useMemo(() => { if (replyTo != null) { return `comment_replyTo_${replyTo}`; } if (currentCommentId != null) { return `comment_edit_${currentCommentId}`; } return GlobalCodeMirrorEditorKey.COMMENT_NEW; }, [currentCommentId, replyTo]); const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey); const [showPreview, setShowPreview] = useState(false); const [error, setError] = useState(); const [slackChannels, setSlackChannels] = useState(''); const { t } = useTranslation(''); const handleSelect = useCallback((showPreview: boolean) => { setShowPreview(showPreview); }, []); // DO NOT dependent on slackChannelsData directly: https://github.com/weseek/growi/pull/7332 const slackChannelsDataString = slackChannelsData?.toString(); const initializeSlackEnabled = useCallback(() => { setSlackChannels(slackChannelsDataString ?? ''); mutateIsSlackEnabled(false); }, [mutateIsSlackEnabled, slackChannelsDataString]); useEffect(() => { initializeSlackEnabled(); }, [initializeSlackEnabled]); const isSlackEnabledToggleHandler = (isSlackEnabled: boolean) => { mutateIsSlackEnabled(isSlackEnabled, false); }; const slackChannelsChangedHandler = useCallback((slackChannels: string) => { setSlackChannels(slackChannels); }, []); const initializeEditor = useCallback(async() => { const dirtyNum = await cleanEditorDirtyMap(editorKey); mutateIsEnabledUnsavedWarning(dirtyNum > 0); setShowPreview(false); setError(undefined); initializeSlackEnabled(); }, [editorKey, cleanEditorDirtyMap, mutateIsEnabledUnsavedWarning, initializeSlackEnabled]); const cancelButtonClickedHandler = useCallback(() => { initializeEditor(); onCanceled?.(); }, [onCanceled, initializeEditor]); const postCommentHandler = useCallback(async() => { const commentBodyToPost = codeMirrorEditor?.getDocString() ?? ''; try { if (currentCommentId != null) { // update current comment await updateComment(commentBodyToPost, revisionId, currentCommentId); } else { // post new comment const postCommentArgs = { commentForm: { comment: commentBodyToPost, revisionId, replyTo, }, slackNotificationForm: { isSlackEnabled, slackChannels, }, }; await postComment(postCommentArgs); } initializeEditor(); onCommented?.(); // Insert empty string as new comment editor is opened after comment codeMirrorEditor?.initDoc(''); } catch (err) { const errorMessage = err.message || 'An unknown error occured when posting comment'; setError(errorMessage); } // eslint-disable-next-line max-len }, [currentCommentId, initializeEditor, onCommented, codeMirrorEditor, updateComment, revisionId, replyTo, isSlackEnabled, slackChannels, postComment]); // the upload event handler const uploadHandler = useCallback((files: File[]) => { uploadAttachments(pageId, files, { onUploaded: (attachment) => { const fileName = attachment.originalName; const prefix = attachment.fileFormat.startsWith('image/') ? '!' // use "![fileName](url)" syntax when image : ''; const insertText = `${prefix}[${fileName}](${attachment.filePathProxied})\n`; codeMirrorEditor?.insertText(insertText); }, onError: (error) => { toastError(error); }, }); }, [codeMirrorEditor, pageId]); const cmProps = useMemo(() => ({ onChange: async(value: string) => { const dirtyNum = await evaluateEditorDirtyMap(editorKey, value); mutateIsEnabledUnsavedWarning(dirtyNum > 0); }, }), [editorKey, evaluateEditorDirtyMap, mutateIsEnabledUnsavedWarning]); // initialize CodeMirrorEditor useEffect(() => { if (commentBody == null) { return; } codeMirrorEditor?.initDoc(commentBody); }, [codeMirrorEditor, commentBody]); // set handler to focus useLayoutEffect(() => { if (showPreview) return; codeMirrorEditor?.focus(); }, [codeMirrorEditor, showPreview]); const errorMessage = useMemo(() => {error}, [error]); const cancelButton = useMemo(() => ( ), [cancelButtonClickedHandler, t]); const submitButton = useMemo(() => { return ( ); }, [postCommentHandler, t]); return (

{t('page_comment.add_a_comment')}

{errorMessage && errorMessage} {isSlackConfigured && isSlackEnabled != null && (
) }
{cancelButton}{submitButton}
{error && errorMessage} {cancelButton}{submitButton}
); }; export const CommentEditorPre = (props: CommentEditorProps): JSX.Element => { const { onCommented, onCanceled, ...rest } = props; const currentUser = useCurrentUser(); const { mutate: mutateResolvedTheme } = useResolvedThemeForEditor(); const { resolvedTheme } = useNextThemes(); mutateResolvedTheme({ themeData: resolvedTheme }); const [isReadyToUse, setIsReadyToUse] = useState(false); const { t } = useTranslation(''); const render = useCallback((): JSX.Element => { return ( ); }, [currentUser, t]); return isReadyToUse ? ( { onCommented?.(); setIsReadyToUse(false); }} onCanceled={() => { onCanceled?.(); setIsReadyToUse(false); }} {...rest} /> ) : render(); };