Просмотр исходного кода

Merge pull request #6290 from weseek/imprv/create-new-pages

Imprv/Show Editor Mode
Yuki Takei 3 лет назад
Родитель
Сommit
5a73ce0539

+ 7 - 4
packages/app/src/client/services/ContextExtractor.tsx

@@ -9,7 +9,7 @@ import { IUserUISettings } from '~/interfaces/user-ui-settings';
 import {
   useIsDeviceSmallerThanMd, useIsDeviceSmallerThanLg,
   usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
-  useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
+  useSelectedGrant,
 } from '~/stores/ui';
 import { useSetupGlobalSocket, useSetupGlobalAdminSocket } from '~/stores/websocket';
 
@@ -133,6 +133,9 @@ const ContextExtractorOnce: FC = () => {
     plantumlUri: configByContextHydrate.env.PLANTUML_URI,
     blockdiagUri: configByContextHydrate.env.BLOCKDIAG_URI,
   });
+  // useNoCdn(configByContextHydrate.env.NO_CDN);
+  // useUploadableImage(configByContextHydrate.upload.image);
+  // useUploadableFile(configByContextHydrate.upload.file);
 
   // Page
   useDeleteUsername(deleteUsername);
@@ -170,9 +173,9 @@ const ContextExtractorOnce: FC = () => {
   useIsDeviceSmallerThanMd();
 
   // Editor
-  useSelectedGrant(grant);
-  useSelectedGrantGroupId(grantGroupId);
-  useSelectedGrantGroupName(grantGroupName);
+  // useSelectedGrant(grant);
+  // useSelectedGrantGroupId(grantGroupId);
+  // useSelectedGrantGroupName(grantGroupName);
 
   // SearchResult
   useIsDeviceSmallerThanLg();

+ 7 - 2
packages/app/src/components/Layout/BasicLayout.tsx

@@ -11,10 +11,13 @@ import { RawLayout } from './RawLayout';
 type Props = {
   title: string
   className?: string,
+  expandContainer?: boolean,
   children?: ReactNode
 }
 
-export const BasicLayout = ({ children, title, className }: Props): JSX.Element => {
+export const BasicLayout = ({
+  children, title, className, expandContainer,
+}: Props): JSX.Element => {
 
   // const HotkeysManager = dynamic(() => import('../client/js/components/Hotkeys/HotkeysManager'), { ssr: false });
   // const PageCreateModal = dynamic(() => import('../client/js/components/PageCreateModal'), { ssr: false });
@@ -28,8 +31,10 @@ export const BasicLayout = ({ children, title, className }: Props): JSX.Element
   const PageRenameModal = dynamic(() => import('../PageRenameModal'), { ssr: false });
   const PagePresentationModal = dynamic(() => import('../PagePresentationModal'), { ssr: false });
 
+  const myClassName = `${className ?? ''} ${expandContainer ? 'growi-layout-fluid' : ''}`;
+
   return (
-    <RawLayout title={title} className={className}>
+    <RawLayout title={title} className={myClassName}>
       <GrowiNavbar />
 
       <div className="page-wrapper d-flex d-print-block">

+ 2 - 2
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -17,7 +17,6 @@ import CountBadge from '../Common/CountBadge';
 import PageListIcon from '../Icons/PageListIcon';
 import NotFoundPage from '../NotFoundPage';
 import { Page } from '../Page';
-// import PageEditor from '../PageEditor';
 // import PageEditorByHackmd from '../PageEditorByHackmd';
 import TableOfContents from '../TableOfContents';
 import UserInfo from '../User/UserInfo';
@@ -33,6 +32,7 @@ const { isTopPage } = pagePathUtils;
 const DisplaySwitcher = (): JSX.Element => {
   const { t } = useTranslation();
 
+  const PageEditor = dynamic(() => import('../PageEditor'), { ssr: false });
   const EditorNavbarBottom = dynamic(() => import('../PageEditor/EditorNavbarBottom'), { ssr: false });
   const HashChanged = dynamic(() => import('../EventListeneres/HashChanged'), { ssr: false });
   const ContentLinkButtons = dynamic(() => import('../ContentLinkButtons'), { ssr: false });
@@ -125,7 +125,7 @@ const DisplaySwitcher = (): JSX.Element => {
         { isEditable && (
           <TabPane tabId={EditorMode.Editor}>
             <div data-testid="page-editor" id="page-editor">
-              {/* <PageEditor /> */}
+              <PageEditor />
             </div>
           </TabPane>
         ) }

+ 104 - 102
packages/app/src/components/PageEditor.tsx

@@ -8,31 +8,32 @@ import { envUtils } from '@growi/core';
 import detectIndent from 'detect-indent';
 import { throttle, debounce } from 'throttle-debounce';
 
-import AppContainer from '~/client/services/AppContainer';
-import EditorContainer from '~/client/services/EditorContainer';
-import PageContainer from '~/client/services/PageContainer';
+// import AppContainer from '~/client/services/AppContainer';
+// import EditorContainer from '~/client/services/EditorContainer';
+// import PageContainer from '~/client/services/PageContainer';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
 import { getOptionsToSave } from '~/client/util/editor';
 import {
-  useIsEditable, useIsIndentSizeForced, useCurrentPagePath, useCurrentPageId,
+  useIsEditable, useIsIndentSizeForced, useCurrentPagePath, useCurrentPageId, useIsUploadableFile, useIsUploadableImage,
 } from '~/stores/context';
 import {
   useCurrentIndentSize, useSWRxSlackChannels, useIsSlackEnabled, useIsTextlintEnabled, usePageTagsForEditors,
   useIsEnabledUnsavedWarning,
 } from '~/stores/editor';
+import { useSWRxCurrentPage } from '~/stores/page';
 import { usePreviewOptions } from '~/stores/renderer';
 import {
   EditorMode,
-  useEditorMode, useIsMobile, useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
+  useEditorMode, useIsMobile, useSelectedGrant,
 } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 
 
-import { ConflictDiffModal } from './PageEditor/ConflictDiffModal';
+// import { ConflictDiffModal } from './PageEditor/ConflictDiffModal';
 import Editor from './PageEditor/Editor';
 import Preview from './PageEditor/Preview';
 import scrollSyncHelper from './PageEditor/ScrollSyncHelper';
-import { withUnstatedContainers } from './UnstatedUtils';
+// import { withUnstatedContainers } from './UnstatedUtils';
 
 
 // TODO: remove this when omitting unstated is completed
@@ -52,26 +53,26 @@ type EditorRef = {
 }
 
 type Props = {
-  appContainer: AppContainer,
-  pageContainer: PageContainer,
-  editorContainer: EditorContainer,
-
-  isEditable: boolean,
-
-  editorMode: string,
-  isSlackEnabled: boolean,
-  slackChannels: string,
-  isMobile?: boolean,
-
-  grant: number,
-  grantGroupId?: string,
-  grantGroupName?: string,
-  mutateGrant: (grant: number) => void,
-
-  isTextlintEnabled?: boolean,
-  isIndentSizeForced?: boolean,
-  indentSize?: number,
-  mutateCurrentIndentSize: (indent: number) => void,
+  // appContainer: AppContainer,
+  // pageContainer: PageContainer,
+  // editorContainer: EditorContainer,
+
+  // isEditable: boolean,
+
+  // editorMode: string,
+  // isSlackEnabled: boolean,
+  // slackChannels: string,
+  // isMobile?: boolean,
+
+  // grant: number,
+  // grantGroupId?: string,
+  // grantGroupName?: string,
+  // mutateGrant: (grant: number) => void,
+
+  // isTextlintEnabled?: boolean,
+  // isIndentSizeForced?: boolean,
+  // indentSize?: number,
+  // mutateCurrentIndentSize: (indent: number) => void,
 };
 
 // for scrolling
@@ -80,9 +81,9 @@ let isOriginOfScrollSyncEditor = false;
 let isOriginOfScrollSyncPreview = false;
 
 const PageEditor = (props: Props): JSX.Element => {
-  const {
-    appContainer, pageContainer, editorContainer,
-  } = props;
+  // const {
+  //   appContainer, pageContainer, editorContainer,
+  // } = props;
 
   const { data: isEditable } = useIsEditable();
   const { data: editorMode } = useEditorMode();
@@ -92,76 +93,75 @@ const PageEditor = (props: Props): JSX.Element => {
   const { data: pageTags } = usePageTagsForEditors(pageId);
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
-  const { data: grant, mutate: mutateGrant } = useSelectedGrant();
-  const { data: grantGroupId } = useSelectedGrantGroupId();
-  const { data: grantGroupName } = useSelectedGrantGroupName();
+  const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
   const { data: isTextlintEnabled } = useIsTextlintEnabled();
   const { data: isIndentSizeForced } = useIsIndentSizeForced();
   const { data: indentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
+  const { data: isUploadableFile } = useIsUploadableFile();
+  const { data: isUploadableImage } = useIsUploadableImage();
+  const { data: currentPage } = useSWRxCurrentPage();
 
   const { data: rendererOptions } = usePreviewOptions();
 
-  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-  const [markdown, setMarkdown] = useState<string>(pageContainer.state.markdown!);
+  const [markdown, setMarkdown] = useState<string>('');
+
+  useEffect(() => {
+    if (currentPage != null) {
+      setMarkdown(currentPage.revision.body);
+    }
+  }, [currentPage, currentPage?.revision.body]);
 
 
   const editorRef = useRef<EditorRef>(null);
   const previewRef = useRef<HTMLDivElement>(null);
 
   const setMarkdownWithDebounce = useMemo(() => debounce(50, throttle(100, value => setMarkdown(value))), []);
-  const saveDraftWithDebounce = useMemo(() => debounce(800, () => {
-    editorContainer.saveDraft(pageContainer.state.path, markdown);
-  }), [editorContainer, markdown, pageContainer.state.path]);
+  // const saveDraftWithDebounce = useMemo(() => debounce(800, () => {
+  //   editorContainer.saveDraft(pageContainer.state.path, markdown);
+  // }), [editorContainer, markdown, pageContainer.state.path]);
 
   const markdownChangedHandler = useCallback((value: string): void => {
     setMarkdownWithDebounce(value);
     // only when the first time to edit
-    if (!pageContainer.state.revisionId) {
-      saveDraftWithDebounce();
-    }
-  }, [pageContainer.state.revisionId, saveDraftWithDebounce, setMarkdownWithDebounce]);
+    // if (!pageContainer.state.revisionId) {
+    //   saveDraftWithDebounce();
+    // }
+  // }, [pageContainer.state.revisionId, saveDraftWithDebounce, setMarkdownWithDebounce]);
+  }, [setMarkdownWithDebounce]);
 
 
   const saveWithShortcut = useCallback(async() => {
-    if (grant == null) {
+    if (grantData == null) {
       return;
     }
 
     const slackChannels = slackChannelsData ? slackChannelsData.toString() : '';
 
-    const optionsToSave = getOptionsToSave(isSlackEnabled ?? false, slackChannels, grant, grantGroupId, grantGroupName, pageTags || []);
+    const optionsToSave = getOptionsToSave(
+      isSlackEnabled ?? false, slackChannels,
+      grantData.grant, grantData.grantedGroup?.id, grantData.grantedGroup?.name,
+      pageTags || [],
+    );
 
     try {
       // disable unsaved warning
       mutateIsEnabledUnsavedWarning(false);
 
       // eslint-disable-next-line no-unused-vars
-      const { tags } = await pageContainer.save(markdown, editorMode, optionsToSave);
+      // const { tags } = await pageContainer.save(markdown, editorMode, optionsToSave);
       logger.debug('success to save');
 
-      pageContainer.showSuccessToastr();
+      // pageContainer.showSuccessToastr();
 
       // update state of EditorContainer
-      editorContainer.setState({ tags });
+      // editorContainer.setState({ tags });
     }
     catch (error) {
       logger.error('failed to save', error);
-      pageContainer.showErrorToastr(error);
+      // pageContainer.showErrorToastr(error);
     }
-  }, [
-    editorContainer,
-    editorMode,
-    grant,
-    grantGroupId,
-    grantGroupName,
-    isSlackEnabled,
-    slackChannelsData,
-    markdown,
-    pageContainer,
-    pageTags,
-    mutateIsEnabledUnsavedWarning,
-  ]);
+  }, [grantData, isSlackEnabled, slackChannelsData, pageTags, mutateIsEnabledUnsavedWarning]);
 
 
   /**
@@ -184,10 +184,10 @@ const PageEditor = (props: Props): JSX.Element => {
       }
 
       const formData = new FormData();
-      const { pageId, path } = pageContainer.state;
+      // const { pageId, path } = pageContainer.state;
       formData.append('file', file);
-      if (path != null) {
-        formData.append('path', path);
+      if (currentPagePath != null) {
+        formData.append('path', currentPagePath);
       }
       if (pageId != null) {
         formData.append('page_id', pageId);
@@ -208,18 +208,19 @@ const PageEditor = (props: Props): JSX.Element => {
       // when if created newly
       if (res.pageCreated) {
         logger.info('Page is created', res.page._id);
-        pageContainer.updateStateAfterSave(res.page, res.tags, res.revision, editorMode);
+        // pageContainer.updateStateAfterSave(res.page, res.tags, res.revision, editorMode);
         mutateGrant(res.page.grant);
       }
     }
     catch (e) {
       logger.error('failed to upload', e);
-      pageContainer.showErrorToastr(e);
+      // pageContainer.showErrorToastr(e);
     }
     finally {
       editorRef.current.terminateUploadingState();
     }
-  }, [editorMode, mutateGrant, pageContainer]);
+  // }, [editorMode, mutateGrant, pageContainer]);
+  }, [editorMode, mutateGrant]);
 
 
   const scrollPreviewByEditorLine = useCallback((line: number) => {
@@ -274,7 +275,9 @@ const PageEditor = (props: Props): JSX.Element => {
 
     // turn on the flag
     isOriginOfScrollSyncEditor = true;
-    scrollSyncHelper.scrollPreviewToRevealOverflowing(previewRef.current, line);
+    if (previewRef.current != null) {
+      scrollSyncHelper.scrollPreviewToRevealOverflowing(previewRef.current, line);
+    }
   }, []);
   const scrollPreviewByCursorMovingWithThrottle = useMemo(() => throttle(20, scrollPreviewByCursorMoving), [scrollPreviewByCursorMoving]);
 
@@ -314,14 +317,14 @@ const PageEditor = (props: Props): JSX.Element => {
 
 
   // register dummy instance to get markdown
-  useEffect(() => {
-    const pageEditorInstance = {
-      getMarkdown: () => {
-        return markdown;
-      },
-    };
-    appContainer.registerComponentInstance('PageEditor', pageEditorInstance);
-  }, [appContainer, markdown]);
+  // useEffect(() => {
+  //   const pageEditorInstance = {
+  //     getMarkdown: () => {
+  //       return markdown;
+  //     },
+  //   };
+  //   appContainer.registerComponentInstance('PageEditor', pageEditorInstance);
+  // }, [appContainer, markdown]);
 
   // initial caret line
   useEffect(() => {
@@ -369,23 +372,23 @@ const PageEditor = (props: Props): JSX.Element => {
   }, []);
 
   // Displays an alert if there is a difference with pageContainer's markdown
-  useEffect(() => {
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    if (pageContainer.state.markdown! !== markdown) {
-      mutateIsEnabledUnsavedWarning(true);
-    }
-  }, [editorContainer, markdown, mutateIsEnabledUnsavedWarning, pageContainer.state.markdown]);
+  // useEffect(() => {
+  //   // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+  //   if (pageContainer.state.markdown! !== markdown) {
+  //     mutateIsEnabledUnsavedWarning(true);
+  //   }
+  // }, [editorContainer, markdown, mutateIsEnabledUnsavedWarning, pageContainer.state.markdown]);
 
   // Detect indent size from contents (only when users are allowed to change it)
-  useEffect(() => {
-    const currentPageMarkdown = pageContainer.state.markdown;
-    if (!isIndentSizeForced && currentPageMarkdown != null) {
-      const detectedIndent = detectIndent(currentPageMarkdown);
-      if (detectedIndent.type === 'space' && new Set([2, 4]).has(detectedIndent.amount)) {
-        mutateCurrentIndentSize(detectedIndent.amount);
-      }
-    }
-  }, [isIndentSizeForced, mutateCurrentIndentSize, pageContainer.state.markdown]);
+  // useEffect(() => {
+  //   const currentPageMarkdown = pageContainer.state.markdown;
+  //   if (!isIndentSizeForced && currentPageMarkdown != null) {
+  //     const detectedIndent = detectIndent(currentPageMarkdown);
+  //     if (detectedIndent.type === 'space' && new Set([2, 4]).has(detectedIndent.amount)) {
+  //       mutateCurrentIndentSize(detectedIndent.amount);
+  //     }
+  //   }
+  // }, [isIndentSizeForced, mutateCurrentIndentSize, pageContainer.state.markdown]);
 
 
   if (!isEditable) {
@@ -396,12 +399,10 @@ const PageEditor = (props: Props): JSX.Element => {
     return <></>;
   }
 
-  const config = props.appContainer.getConfig();
-  const isUploadable = config.upload.image || config.upload.file;
-  const isUploadableFile = config.upload.file;
-  const isMathJaxEnabled = !!config.env.MATHJAX;
+  // const config = props.appContainer.getConfig();
+  // const isUploadable = config.upload.image || config.upload.file;
+  const isUploadable = isUploadableImage || isUploadableFile;
 
-  const noCdn = envUtils.toBoolean(config.env.NO_CDN);
 
   // TODO: omit no-explicit-any -- 2022.06.02 Yuki Takei
   // It is impossible to avoid the error
@@ -410,13 +411,14 @@ const PageEditor = (props: Props): JSX.Element => {
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   const EditorAny = Editor as any;
 
+  // console.log('EditorAny', markdown);
+
   return (
     <div className="d-flex flex-wrap">
       <div className="page-editor-editor-container flex-grow-1 flex-basis-0 mw-0">
         <EditorAny
           ref={editorRef}
           value={markdown}
-          noCdn={noCdn}
           isMobile={isMobile}
           isUploadable={isUploadable}
           isUploadableFile={isUploadableFile}
@@ -434,17 +436,16 @@ const PageEditor = (props: Props): JSX.Element => {
           markdown={markdown}
           rendererOptions={rendererOptions}
           ref={previewRef}
-          // isMathJaxEnabled={isMathJaxEnabled}
           renderMathJaxOnInit={false}
           onScroll={offset => scrollEditorByPreviewScrollWithThrottle(offset)}
         />
       </div>
-      <ConflictDiffModal
+      {/* <ConflictDiffModal
         isOpen={pageContainer.state.isConflictDiffModalOpen}
         onClose={() => pageContainer.setState({ isConflictDiffModalOpen: false })}
         pageContainer={pageContainer}
         markdownOnEdit={markdown}
-      />
+      /> */}
     </div>
   );
 };
@@ -452,6 +453,7 @@ const PageEditor = (props: Props): JSX.Element => {
 /**
    * Wrapper component for using unstated
    */
-const PageEditorWrapper = withUnstatedContainers(PageEditor, [AppContainer, PageContainer, EditorContainer]);
+// const PageEditorWrapper = withUnstatedContainers(PageEditor, [AppContainer, PageContainer, EditorContainer]);
 
-export default PageEditorWrapper;
+// export default PageEditorWrapper;
+export default PageEditor;

+ 26 - 22
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -16,14 +16,14 @@ import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
 
 import AbstractEditor from './AbstractEditor';
 import CommentMentionHelper from './CommentMentionHelper';
-import DrawioModal from './DrawioModal';
+// import DrawioModal from './DrawioModal';
 import EditorIcon from './EditorIcon';
 import EmojiPicker from './EmojiPicker';
 import EmojiPickerHelper from './EmojiPickerHelper';
-import GridEditModal from './GridEditModal';
+// import GridEditModal from './GridEditModal';
 import geu from './GridEditorUtil';
-import HandsontableModal from './HandsontableModal';
-import LinkEditModal from './LinkEditModal';
+// import HandsontableModal from './HandsontableModal';
+// import LinkEditModal from './LinkEditModal';
 import mdu from './MarkdownDrawioUtil';
 import mlu from './MarkdownLinkUtil';
 import MarkdownTableInterceptor from './MarkdownTableInterceptor';
@@ -32,6 +32,8 @@ import pasteHelper from './PasteHelper';
 import PreventMarkdownListInterceptor from './PreventMarkdownListInterceptor';
 import SimpleCheatsheet from './SimpleCheatsheet';
 
+import styles from './CodeMirrorEditor.module.scss';
+
 // Textlint
 window.JSHINT = JSHINT;
 window.kuromojin = { dicPath: '/static/dict' };
@@ -50,7 +52,6 @@ require('codemirror/addon/edit/matchtags');
 require('codemirror/addon/edit/closetag');
 require('codemirror/addon/edit/continuelist');
 require('codemirror/addon/hint/show-hint');
-require('codemirror/addon/hint/show-hint.css');
 require('codemirror/addon/search/searchcursor');
 require('codemirror/addon/search/match-highlighter');
 require('codemirror/addon/selection/active-line');
@@ -58,12 +59,10 @@ require('codemirror/addon/scroll/annotatescrollbar');
 require('codemirror/addon/scroll/scrollpastend');
 require('codemirror/addon/fold/foldcode');
 require('codemirror/addon/fold/foldgutter');
-require('codemirror/addon/fold/foldgutter.css');
 require('codemirror/addon/fold/markdown-fold');
 require('codemirror/addon/fold/brace-fold');
 require('codemirror/addon/display/placeholder');
 require('codemirror/addon/lint/lint');
-require('codemirror/addon/lint/lint.css');
 require('~/client/util/codemirror/autorefresh.ext');
 require('~/client/util/codemirror/drawio-fold.ext');
 require('~/client/util/codemirror/gfm-growi.mode');
@@ -790,19 +789,19 @@ class CodeMirrorEditor extends AbstractEditor {
   }
 
   showGridEditorHandler() {
-    this.gridEditModal.current.show(geu.getGridHtml(this.getCodeMirror()));
+    // this.gridEditModal.current.show(geu.getGridHtml(this.getCodeMirror()));
   }
 
   showLinkEditHandler() {
-    this.linkEditModal.current.show(mlu.getMarkdownLink(this.getCodeMirror()));
+    // this.linkEditModal.current.show(mlu.getMarkdownLink(this.getCodeMirror()));
   }
 
   showHandsonTableHandler() {
-    this.handsontableModal.current.show(mtu.getMarkdownTable(this.getCodeMirror()));
+    // this.handsontableModal.current.show(mtu.getMarkdownTable(this.getCodeMirror()));
   }
 
   showDrawioHandler() {
-    this.drawioModal.current.show(mdu.getMarkdownDrawioMxfile(this.getCodeMirror()));
+    // this.drawioModal.current.show(mdu.getMarkdownDrawioMxfile(this.getCodeMirror()));
   }
 
 
@@ -988,8 +987,11 @@ class CodeMirrorEditor extends AbstractEditor {
       gutters.push('CodeMirror-lint-markers');
     }
 
+    console.log(' this.state.value', this.state.value);
+    console.log(' this.props.value', this.props.value);
+
     return (
-      <React.Fragment>
+      <div className={`grw-codemirror-editor ${styles['grw-codemirror-editor']}`}>
 
         <UncontrolledCodeMirror
           ref={(c) => { this.cm = c }}
@@ -1000,7 +1002,9 @@ class CodeMirrorEditor extends AbstractEditor {
             editor.on('paste', this.pasteHandler);
             editor.on('scrollCursorIntoView', this.scrollCursorIntoViewHandler);
           }}
-          value={this.state.value}
+          // temporary set props.value
+          // value={this.state.value}
+          value={this.props.value}
           options={{
             indentUnit: this.props.indentSize,
             theme: this.props.editorSettings.theme ?? 'elegant',
@@ -1052,25 +1056,25 @@ class CodeMirrorEditor extends AbstractEditor {
         { this.renderCheatsheetOverlay() }
         { this.renderEmojiPicker() }
 
-        <GridEditModal
+        {/* <GridEditModal
           ref={this.gridEditModal}
           onSave={(grid) => { return geu.replaceGridWithHtmlWithEditor(this.getCodeMirror(), grid) }}
-        />
-        <LinkEditModal
+        /> */}
+        {/* <LinkEditModal
           ref={this.linkEditModal}
           onSave={(linkText) => { return mlu.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText) }}
-        />
-        <HandsontableModal
+        /> */}
+        {/* <HandsontableModal
           ref={this.handsontableModal}
           onSave={(table) => { return mtu.replaceFocusedMarkdownTableWithEditor(this.getCodeMirror(), table) }}
           autoFormatMarkdownTable={this.props.editorSettings.autoFormatMarkdownTable}
-        />
-        <DrawioModal
+        /> */}
+        {/* <DrawioModal
           ref={this.drawioModal}
           onSave={this.onSaveForDrawio}
-        />
+        /> */}
 
-      </React.Fragment>
+      </div>
     );
   }
 

+ 93 - 0
packages/app/src/components/PageEditor/CodeMirrorEditor.module.scss

@@ -0,0 +1,93 @@
+@use '~/styles/variables' as var;
+@use '~/styles/bootstrap/init' as bs;
+
+.grw-codemirror-editor :global {
+  @import '~codemirror/lib/codemirror';
+
+  // addons
+  @import '~codemirror/addon/hint/show-hint';
+  @import '~codemirror/addon/fold/foldgutter';
+  @import '~codemirror/addon/lint/lint';
+
+  // themes
+  @import '~codemirror/theme/elegant';
+  @import '~codemirror/theme/eclipse';
+
+  .CodeMirror {
+    pre.CodeMirror-line.grw-cm-header-line {
+      padding-top: 0.16em;
+      padding-bottom: 0.08em;
+      font-family: bs.$font-family-monospace;
+
+      // '#'
+      .cm-formatting-header {
+        font-style: italic;
+        font-weight: bold;
+        opacity: 0.5;
+      }
+
+      .cm-header-1 {
+        font-size: 1.9em;
+      }
+      .cm-header-2 {
+        font-size: 1.6em;
+      }
+      .cm-header-3 {
+        font-size: 1.4em;
+      }
+      .cm-header-4 {
+        font-size: 1.35em;
+      }
+      .cm-header-5 {
+        font-size: 1.25em;
+      }
+      .cm-header-6 {
+        font-size: 1.2em;
+      }
+    }
+
+    .cm-matchhighlight {
+      color: bs.$gray-900 !important;
+      background-color: cyan;
+    }
+
+    .CodeMirror-selection-highlight-scrollbar {
+      background-color: darkcyan;
+    }
+
+    // overwrite .CodeMirror-placeholder
+    pre.CodeMirror-line-like.CodeMirror-placeholder {
+      color: bs.$text-muted;
+    }
+  }
+
+  // patch to fix https://github.com/codemirror/CodeMirror/issues/4089
+  // see also https://github.com/codemirror/CodeMirror/commit/51a1e7da60a99e019f026a118dc7c98c2b1f9d62
+  .CodeMirror-wrap > div > textarea {
+    font-size: #{bs.$line-height-base}rem;
+  }
+
+  // overwrite .CodeMirror-hints
+  .CodeMirror-hints {
+    max-height: 30em !important;
+
+    .CodeMirror-hint.crowi-emoji-autocomplete {
+      font-family: var.$font-family-monospace-not-strictly;
+      line-height: 1.6em;
+
+      .img-container {
+        display: inline-block;
+        width: 30px;
+      }
+    }
+
+    // active line
+    .CodeMirror-hint-active.crowi-emoji-autocomplete {
+      .img-container {
+        padding-top: 0.3em;
+        padding-bottom: 0.3em;
+        font-size: 15px; // adjust to .wiki
+      }
+    }
+  }
+}

+ 1 - 8
packages/app/src/components/PageEditor/EditorNavbarBottom.tsx

@@ -1,10 +1,8 @@
 import React, { useCallback, useState, useEffect } from 'react';
 
-import PropTypes from 'prop-types';
 import { Collapse, Button } from 'reactstrap';
 
 
-import EditorContainer from '~/client/services/EditorContainer';
 import { useCurrentPagePath, useIsSlackConfigured } from '~/stores/context';
 import { useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
 import {
@@ -14,7 +12,6 @@ import {
 import SavePageControls from '../SavePageControls';
 import SlackLogo from '../SlackLogo';
 import { SlackNotification } from '../SlackNotification';
-import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 import OptionsSelector from './OptionsSelector';
@@ -152,8 +149,4 @@ const EditorNavbarBottom = (props) => {
   );
 };
 
-EditorNavbarBottom.propTypes = {
-  editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
-};
-
-export default withUnstatedContainers(EditorNavbarBottom, [EditorContainer]);
+export default EditorNavbarBottom;

+ 6 - 69
packages/app/src/components/PageEditor/Preview.tsx

@@ -1,17 +1,16 @@
 import React, {
-  useCallback, useEffect, useMemo, useState, SyntheticEvent, RefObject,
+  SyntheticEvent, RefObject,
 } from 'react';
 
-import InterceptorManager from '~/services/interceptor-manager';
+import ReactMarkdown from 'react-markdown';
+
+
 import { RendererOptions } from '~/services/renderer/renderer';
 import { useEditorSettings } from '~/stores/editor';
 
 import RevisionBody from '../Page/RevisionBody';
 
 
-declare const interceptorManager: InterceptorManager;
-
-
 type Props = {
   rendererOptions: RendererOptions,
   markdown?: string,
@@ -27,59 +26,8 @@ const Preview = React.forwardRef((props: Props, ref: RefObject<HTMLDivElement>):
     markdown, pagePath,
   } = props;
 
-  const [html, setHtml] = useState('');
-
   const { data: editorSettings } = useEditorSettings();
 
-  const context = useMemo(() => {
-    return {
-      markdown,
-      pagePath,
-      renderDrawioInRealtime: editorSettings?.renderDrawioInRealtime,
-      currentPathname: decodeURIComponent(window.location.pathname),
-      parsedHTML: null,
-    };
-  }, [markdown, pagePath, editorSettings?.renderDrawioInRealtime]);
-
-  const renderPreview = useCallback(async() => {
-
-    // TODO: use ReactMarkdown
-
-    // if (interceptorManager != null) {
-    //   await interceptorManager.process('preRenderPreview', context);
-    //   await interceptorManager.process('prePreProcess', context);
-    //   context.markdown = rendererOptions.preProcess(context.markdown, context);
-    //   await interceptorManager.process('postPreProcess', context);
-    //   context.parsedHTML = rendererOptions.process(context.markdown, context);
-    //   await interceptorManager.process('prePostProcess', context);
-    //   context.parsedHTML = rendererOptions.postProcess(context.parsedHTML, context);
-    //   await interceptorManager.process('postPostProcess', context);
-    //   await interceptorManager.process('preRenderPreviewHtml', context);
-    // }
-
-    // setHtml(context.parsedHTML ?? '');
-  }, [context, rendererOptions]);
-
-  useEffect(() => {
-    if (markdown == null) {
-      setHtml('');
-    }
-
-    renderPreview();
-  }, [markdown, renderPreview]);
-
-  useEffect(() => {
-    if (html == null) {
-      return;
-    }
-
-    if (interceptorManager != null) {
-      interceptorManager.process('postRenderPreviewHtml', {
-        ...context,
-        parsedHTML: html,
-      });
-    }
-  }, [context, html]);
 
   return (
     <div
@@ -91,11 +39,7 @@ const Preview = React.forwardRef((props: Props, ref: RefObject<HTMLDivElement>):
         }
       }}
     >
-      <RevisionBody
-        {...props}
-        html={html}
-        renderMathJaxInRealtime={editorSettings?.renderMathJaxInRealtime}
-      />
+      <ReactMarkdown {...rendererOptions} >{markdown || ''}</ReactMarkdown>
     </div>
   );
 
@@ -103,11 +47,4 @@ const Preview = React.forwardRef((props: Props, ref: RefObject<HTMLDivElement>):
 
 Preview.displayName = 'Preview';
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-const PreviewWrapper = React.forwardRef((props: Props, ref: RefObject<HTMLDivElement>): JSX.Element => {
-  return <Preview ref={ref} {...props} />;
-});
-
-PreviewWrapper.displayName = 'PreviewWrapper';
-
-export default PreviewWrapper;
+export default Preview;

+ 5 - 7
packages/app/src/components/PageEditorByHackmd.jsx

@@ -13,7 +13,7 @@ import {
   useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
 } from '~/stores/editor';
 import {
-  useEditorMode, useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
+  useEditorMode, useSelectedGrant,
 } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 
@@ -456,9 +456,7 @@ const PageEditorByHackmdWrapper = (props) => {
   const { data: isSlackEnabled } = useIsSlackEnabled();
   const { data: pageId } = useCurrentPageId();
   const { data: pageTags } = usePageTagsForEditors(pageId);
-  const { data: grant } = useSelectedGrant();
-  const { data: grantGroupId } = useSelectedGrantGroupId();
-  const { data: grantGroupName } = useSelectedGrantGroupName();
+  const { data: grantData } = useSelectedGrant();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
 
   if (editorMode == null) {
@@ -473,9 +471,9 @@ const PageEditorByHackmdWrapper = (props) => {
       isSlackEnabled={isSlackEnabled}
       slackChannels={slackChannelsData.toString()}
       pageTags={pageTags}
-      grant={grant}
-      grantGroupId={grantGroupId}
-      grantGroupName={grantGroupName}
+      grant={grantData.grant}
+      grantGroupId={grantData.grantGroup?.id}
+      grantGroupName={grantData.grantedGroup?.name}
       mutateIsEnabledUnsavedWarning={mutateIsEnabledUnsavedWarning}
     />
   );

+ 35 - 32
packages/app/src/components/SavePageControls.jsx

@@ -7,12 +7,12 @@ import {
   DropdownToggle, DropdownMenu, DropdownItem,
 } from 'reactstrap';
 
-import PageContainer from '~/client/services/PageContainer';
+// import PageContainer from '~/client/services/PageContainer';
 import { getOptionsToSave } from '~/client/util/editor';
 import { useIsEditable, useCurrentPageId, useIsAclEnabled } from '~/stores/context';
 import { usePageTagsForEditors, useIsEnabledUnsavedWarning } from '~/stores/editor';
 import {
-  useEditorMode, useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
+  useEditorMode, useSelectedGrant,
 } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 
@@ -43,7 +43,7 @@ class SavePageControls extends React.Component {
 
   async save() {
     const {
-      isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, pageContainer, pageTags, mutateIsEnabledUnsavedWarning,
+      isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, /* pageContainer, */ pageTags, mutateIsEnabledUnsavedWarning,
     } = this.props;
     // disable unsaved warning
     mutateIsEnabledUnsavedWarning(false);
@@ -51,25 +51,25 @@ class SavePageControls extends React.Component {
     try {
       // save
       const optionsToSave = getOptionsToSave(isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, pageTags);
-      await pageContainer.saveAndReload(optionsToSave, this.props.editorMode);
+      // await pageContainer.saveAndReload(optionsToSave, this.props.editorMode);
     }
     catch (error) {
       logger.error('failed to save', error);
-      pageContainer.showErrorToastr(error);
+      // pageContainer.showErrorToastr(error);
       if (error.code === 'conflict') {
-        pageContainer.setState({
-          remoteRevisionId: error.data.revisionId,
-          remoteRevisionBody: error.data.revisionBody,
-          remoteRevisionUpdateAt: error.data.createdAt,
-          lastUpdateUser: error.data.user,
-        });
+        // pageContainer.setState({
+        //   remoteRevisionId: error.data.revisionId,
+        //   remoteRevisionBody: error.data.revisionBody,
+        //   remoteRevisionUpdateAt: error.data.createdAt,
+        //   lastUpdateUser: error.data.user,
+        // });
       }
     }
   }
 
   saveAndOverwriteScopesOfDescendants() {
     const {
-      isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, pageContainer, pageTags, mutateIsEnabledUnsavedWarning,
+      isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, /* pageContainer, */ pageTags, mutateIsEnabledUnsavedWarning,
     } = this.props;
     // disable unsaved warning
     mutateIsEnabledUnsavedWarning(false);
@@ -78,18 +78,18 @@ class SavePageControls extends React.Component {
     const optionsToSave = Object.assign(currentOptionsToSave, {
       overwriteScopesOfDescendants: true,
     });
-    pageContainer.saveAndReload(optionsToSave, this.props.editorMode);
+    // pageContainer.saveAndReload(optionsToSave, this.props.editorMode);
   }
 
   render() {
 
     const {
-      t, pageContainer, isAclEnabled, grant, grantGroupId, grantGroupName,
+      t, /* pageContainer, */ isAclEnabled, grant, grantGroupId, grantGroupName,
     } = this.props;
 
-    const isRootPage = pageContainer.state.path === '/';
-    const labelSubmitButton = pageContainer.state.pageId == null ? t('Create') : t('Update');
-    const labelOverwriteScopes = t('page_edit.overwrite_scopes', { operation: labelSubmitButton });
+    // const isRootPage = pageContainer.state.path === '/';
+    // const labelSubmitButton = pageContainer.state.pageId == null ? t('Create') : t('Update');
+    // const labelOverwriteScopes = t('page_edit.overwrite_scopes', { operation: labelSubmitButton });
 
     return (
       <div className="d-flex align-items-center form-inline flex-nowrap">
@@ -98,7 +98,7 @@ class SavePageControls extends React.Component {
           && (
             <div className="mr-2">
               <GrantSelector
-                disabled={isRootPage}
+                // disabled={isRootPage}
                 grant={grant}
                 grantGroupId={grantGroupId}
                 grantGroupName={grantGroupName}
@@ -109,11 +109,15 @@ class SavePageControls extends React.Component {
         }
 
         <UncontrolledButtonDropdown direction="up">
-          <Button id="caret" color="primary" className="btn-submit" onClick={this.save}>{labelSubmitButton}</Button>
+          <Button id="caret" color="primary" className="btn-submit" onClick={this.save}>
+          labelSubmitButton
+            {/* {labelSubmitButton} */}
+          </Button>
           <DropdownToggle caret color="primary" />
           <DropdownMenu right>
             <DropdownItem onClick={this.saveAndOverwriteScopesOfDescendants}>
-              {labelOverwriteScopes}
+            labelOverwriteScopes
+              {/* {labelOverwriteScopes} */}
             </DropdownItem>
           </DropdownMenu>
         </UncontrolledButtonDropdown>
@@ -127,22 +131,20 @@ class SavePageControls extends React.Component {
 /**
  * Wrapper component for using unstated
  */
-const SavePageControlsHOCWrapper = withUnstatedContainers(SavePageControls, [PageContainer]);
+// const SavePageControlsHOCWrapper = withUnstatedContainers(SavePageControls, [PageContainer]);
 
 const SavePageControlsWrapper = (props) => {
   const { t } = useTranslation();
   const { data: isEditable } = useIsEditable();
   const { data: editorMode } = useEditorMode();
   const { data: isAclEnabled } = useIsAclEnabled();
-  const { data: grant, mutate: mutateGrant } = useSelectedGrant();
-  const { data: grantGroupId, mutate: mutateGrantGroupId } = useSelectedGrantGroupId();
-  const { data: grantGroupName, mutate: mutateGrantGroupName } = useSelectedGrantGroupName();
+  const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
   const { data: pageId } = useCurrentPageId();
   const { data: pageTags } = usePageTagsForEditors(pageId);
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
 
 
-  if (isEditable == null || editorMode == null || isAclEnabled == null) {
+  if (isEditable == null || editorMode == null || isAclEnabled == null || grantData == null) {
     return null;
   }
 
@@ -151,17 +153,18 @@ const SavePageControlsWrapper = (props) => {
   }
 
   return (
-    <SavePageControlsHOCWrapper
+    // <SavePageControlsHOCWrapper
+    <SavePageControls
       t={t}
       {...props}
       editorMode={editorMode}
       isAclEnabled={isAclEnabled}
-      grant={grant}
-      grantGroupId={grantGroupId}
-      grantGroupName={grantGroupName}
+      grant={grantData.grant}
+      grantGroupId={grantData.grantGroup?.id}
+      grantGroupName={grantData.grantedGroup?.name}
       mutateGrant={mutateGrant}
-      mutateGrantGroupId={mutateGrantGroupId}
-      mutateGrantGroupName={mutateGrantGroupName}
+      // mutateGrantGroupId={mutateGrantGroupId}
+      // mutateGrantGroupName={mutateGrantGroupName}
       mutateIsEnabledUnsavedWarning={mutateIsEnabledUnsavedWarning}
       pageTags={pageTags}
     />
@@ -171,7 +174,7 @@ const SavePageControlsWrapper = (props) => {
 SavePageControls.propTypes = {
   t: PropTypes.func.isRequired, // i18next
 
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+  // pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
   // TODO: remove this when omitting unstated is completed
   editorMode: PropTypes.string.isRequired,

+ 1 - 1
packages/app/src/components/UncontrolledCodeMirror.tsx

@@ -19,7 +19,7 @@ interface UncontrolledCodeMirrorCoreProps extends UncontrolledCodeMirrorProps {
   forwardedRef: Ref<UncontrolledCodeMirrorCore>;
 }
 
-class UncontrolledCodeMirrorCore extends AbstractEditor<UncontrolledCodeMirrorCoreProps> {
+export class UncontrolledCodeMirrorCore extends AbstractEditor<UncontrolledCodeMirrorCoreProps> {
 
   override render(): ReactNode {
 

+ 1 - 0
packages/app/src/components/UnstatedUtils.tsx

@@ -4,6 +4,7 @@ import React from 'react';
 
 import { Provider, Subscribe } from 'unstated';
 
+
 /**
  * generate K/V object by specified instances
  *

+ 14 - 12
packages/app/src/pages/[[...path]].page.tsx

@@ -34,9 +34,9 @@ import { PageModel, PageDocument } from '~/server/models/page';
 import { PageRedirectModel } from '~/server/models/page-redirect';
 import UserUISettings from '~/server/models/user-ui-settings';
 import Xss from '~/services/xss';
-import { useSWRxCurrentPage, useSWRxPageInfo } from '~/stores/page';
+import { useSWRxCurrentPage, useSWRxIsGrantNormalized, useSWRxPageInfo } from '~/stores/page';
 import {
-  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
+  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth, useSelectedGrant,
 } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 
@@ -149,7 +149,6 @@ type Props = CommonProps & {
   // noCdn: string,
   // highlightJsStyle: string,
   // isAllReplyShown: boolean,
-  // isContainerFluid: boolean,
   // editorConfig: any,
   isEnabledStaleNotification: boolean,
   // isEnabledLinebreaks: boolean,
@@ -236,16 +235,24 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
     shouldRenderPutbackPageModal = _isTrashPage(pageWithMeta.data.path);
   }
 
-  useCurrentPageId(pageWithMeta?.data._id);
+  const pageId = pageWithMeta?.data._id;
+
+  useCurrentPageId(pageId);
   useSWRxCurrentPage(undefined, pageWithMeta?.data); // store initial data
-  // useSWRxPage(pageWithMeta?.data._id);
-  useSWRxPageInfo(pageWithMeta?.data._id, undefined, pageWithMeta?.meta); // store initial data
+  useSWRxPageInfo(pageId, undefined, pageWithMeta?.meta); // store initial data
   useIsTrashPage(_isTrashPage(pageWithMeta?.data.path ?? ''));
   useIsUserPage(isUserPage(pageWithMeta?.data.path ?? ''));
   useIsNotCreatable(props.isForbidden || !isCreatablePage(pageWithMeta?.data.path ?? '')); // TODO: need to include props.isIdentical
   useCurrentPagePath(pageWithMeta?.data.path);
   useCurrentPathname(props.currentPathname);
   useEditingMarkdown(pageWithMeta?.data.revision.body);
+  const { data: grantData } = useSWRxIsGrantNormalized(pageId);
+  const { mutate: mutateSelectedGrant } = useSelectedGrant();
+
+  // sync grant data
+  useEffect(() => {
+    mutateSelectedGrant(grantData?.grantData.currentPageGrant);
+  }, [grantData?.grantData.currentPageGrant, mutateSelectedGrant]);
 
   // sync pathname by Shallow Routing https://nextjs.org/docs/routing/shallow-routing
   useEffect(() => {
@@ -263,9 +270,6 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   //     classNames.push('on-edit', 'hackmd');
   //     break;
   // }
-  // if (props.isContainerFluid) {
-  //   classNames.push('growi-layout-fluid');
-  // }
   // if (page == null) {
   //   classNames.push('not-found-page');
   // }
@@ -281,7 +285,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
         */}
       </Head>
       {/* <BasicLayout title={useCustomTitle(props, t('GROWI'))} className={classNames.join(' ')}> */}
-      <BasicLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')}>
+      <BasicLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')} expandContainer={props.isContainerFluid}>
         <header className="py-0">
           <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
         </header>
@@ -307,7 +311,6 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
                     {/* <DisplaySwitcher /> */}
                     <div id="page-editor-navbar-bottom-container" className="d-none d-edit-block"></div>
                     {/* <PageStatusAlert /> */}
-                    PageStatusAlert
                   </>
                 ) }
 
@@ -479,7 +482,6 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   // props.noCdn = configManager.getConfig('crowi', 'app:noCdn');
   // props.highlightJsStyle = configManager.getConfig('crowi', 'customize:highlightJsStyle');
   // props.isAllReplyShown = configManager.getConfig('crowi', 'customize:isAllReplyShown');
-  // props.isContainerFluid = configManager.getConfig('crowi', 'customize:isContainerFluid');
   props.isEnabledStaleNotification = configManager.getConfig('crowi', 'customize:isEnabledStaleNotification');
   // props.isEnabledLinebreaks = configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks');
   // props.isEnabledLinebreaksInComments = configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments');

+ 2 - 0
packages/app/src/pages/utils/commons.ts

@@ -17,6 +17,7 @@ export type CommonProps = {
   theme: GrowiThemes,
   customTitleTemplate: string,
   csrfToken: string,
+  isContainerFluid: boolean,
   growiVersion: string,
 } & Partial<SSRConfig>;
 
@@ -41,6 +42,7 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     theme: configManager.getConfig('crowi', 'customize:theme'),
     customTitleTemplate: customizeService.customTitleTemplate,
     csrfToken: req.csrfToken(),
+    isContainerFluid: configManager.getConfig('crowi', 'customize:isContainerFluid') ?? false,
     growiVersion: crowi.version,
   };
 

+ 7 - 0
packages/app/src/stores/context.tsx

@@ -240,6 +240,13 @@ export const useEditingMarkdown = (initialData?: string): SWRResponse<string, Er
   return useStaticSWR('currentMarkdown', initialData);
 };
 
+export const useIsUploadableImage = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useStaticSWR('isUploadableImage', initialData);
+};
+
+export const useIsUploadableFile = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useStaticSWR('isUploadableFile', initialData);
+};
 
 /** **********************************************************
  *                     Computed contexts

+ 3 - 11
packages/app/src/stores/ui.tsx

@@ -14,6 +14,7 @@ import useSWRImmutable from 'swr/immutable';
 import { IFocusable } from '~/client/interfaces/focusable';
 import { useUserUISettings } from '~/client/services/user-ui-settings';
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
+import { IPageGrantData } from '~/interfaces/page';
 import { ISidebarConfig } from '~/interfaces/sidebar-config';
 import { SidebarContentsType } from '~/interfaces/ui';
 import { UpdateDescCountData } from '~/interfaces/websocket';
@@ -365,17 +366,8 @@ export const useSidebarResizeDisabled = (isDisabled?: boolean): SWRResponse<bool
   return useStaticSWR('isSidebarResizeDisabled', isDisabled, { fallbackData: false });
 };
 
-
-export const useSelectedGrant = (initialData?: number): SWRResponse<number, Error> => {
-  return useStaticSWR<number, Error>('grant', initialData);
-};
-
-export const useSelectedGrantGroupId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
-  return useStaticSWR<Nullable<string>, Error>('grantGroupId', initialData);
-};
-
-export const useSelectedGrantGroupName = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
-  return useStaticSWR<Nullable<string>, Error>('grantGroupName', initialData);
+export const useSelectedGrant = (initialData?: Nullable<IPageGrantData>): SWRResponse<Nullable<IPageGrantData>, Error> => {
+  return useStaticSWR<Nullable<IPageGrantData>, Error>('selectedGrant', initialData);
 };
 
 export const useGlobalSearchFormRef = (initialData?: RefObject<IFocusable>): SWRResponse<RefObject<IFocusable>, Error> => {

+ 5 - 5
packages/app/src/styles/_layout.scss

@@ -6,16 +6,16 @@ body {
   overscroll-behavior-y: none;
 }
 
-body:not(.growi-layout-fluid) .grw-container-convertible {
+.wrapper:not(.growi-layout-fluid) .grw-container-convertible {
   @extend .container-lg;
 }
 
-body.not-found-page .grw-container-convertible {
-  @extend .container-lg;
+.wrapper.growi-layout-fluid .grw-container-convertible {
+  @extend .container-fluid;
 }
 
-body.growi-layout-fluid .grw-container-convertible {
-  @extend .container-fluid;
+body.not-found-page .grw-container-convertible {
+  @extend .container-lg;
 }
 
 .grw-modal-head {

+ 6 - 33
packages/app/src/styles/_on-edit.scss

@@ -1,12 +1,9 @@
+@import './bootstrap/init' ;
+@import './variables' ;
+@import './mixins' ;
+@import 'sidebar-wiki';
 @import 'editor-overlay';
 
-body:not(.on-edit) {
-  // hide .page-editor-footer
-  .page-editor-footer {
-    display: none !important;
-    border: none;
-  }
-}
 
 body.on-edit {
   overflow-y: hidden !important;
@@ -296,43 +293,19 @@ body.on-edit {
 }
 
 body.on-edit {
-  &:not(.growi-layout-fluid) .page-editor-preview-body {
+  .wrapper:not(.growi-layout-fluid) .page-editor-preview-body {
     .wiki {
       max-width: 980px;
       margin: 0 auto;
     }
   }
-  &.growi-layout-fluid .page-editor-preview-body {
+  .wrapper.growi-layout-fluid .page-editor-preview-body {
     .wiki {
       margin: 0 auto;
     }
   }
 }
 
-// overwrite .CodeMirror-hints
-.CodeMirror-hints {
-  max-height: 30em !important;
-
-  .CodeMirror-hint.crowi-emoji-autocomplete {
-    font-family: $font-family-monospace-not-strictly;
-    line-height: 1.6em;
-
-    .img-container {
-      display: inline-block;
-      width: 30px;
-    }
-  }
-
-  // active line
-  .CodeMirror-hint-active.crowi-emoji-autocomplete {
-    .img-container {
-      padding-top: 0.3em;
-      padding-bottom: 0.3em;
-      font-size: 15px; // adjust to .wiki
-    }
-  }
-}
-
 #tag-edit-button-tooltip {
   .tooltip-inner {
     color: black;

+ 0 - 53
packages/app/src/styles/_override-codemirror.scss

@@ -1,53 +0,0 @@
-.CodeMirror {
-  pre.CodeMirror-line.grw-cm-header-line {
-    padding-top: 0.16em;
-    padding-bottom: 0.08em;
-    font-family: $font-family-monospace;
-
-    // '#'
-    .cm-formatting-header {
-      font-style: italic;
-      font-weight: bold;
-      opacity: 0.5;
-    }
-
-    .cm-header-1 {
-      font-size: 1.9em;
-    }
-    .cm-header-2 {
-      font-size: 1.6em;
-    }
-    .cm-header-3 {
-      font-size: 1.4em;
-    }
-    .cm-header-4 {
-      font-size: 1.35em;
-    }
-    .cm-header-5 {
-      font-size: 1.25em;
-    }
-    .cm-header-6 {
-      font-size: 1.2em;
-    }
-  }
-
-  .cm-matchhighlight {
-    color: $gray-900 !important;
-    background-color: cyan;
-  }
-
-  .CodeMirror-selection-highlight-scrollbar {
-    background-color: darkcyan;
-  }
-
-  // overwrite .CodeMirror-placeholder
-  pre.CodeMirror-line-like.CodeMirror-placeholder {
-    color: $text-muted;
-  }
-}
-
-// patch to fix https://github.com/codemirror/CodeMirror/issues/4089
-// see also https://github.com/codemirror/CodeMirror/commit/51a1e7da60a99e019f026a118dc7c98c2b1f9d62
-.CodeMirror-wrap > div > textarea {
-  font-size: #{$line-height-base}rem;
-}

+ 1 - 1
packages/app/src/styles/style-next.scss

@@ -52,7 +52,7 @@
 // @import 'modal';
 // @import 'navbar';
 // @import 'old-ios';
-// @import 'on-edit';
+@import 'on-edit';
 // @import 'page-duplicate-modal';
 // @import 'page_list';
 // @import 'page-accessories-control';

+ 0 - 4
packages/app/src/styles/theme/_apply-colors.scss

@@ -524,10 +524,6 @@ body.on-edit {
     .page-editor-preview-container {
       background-color: $bgcolor-global;
     }
-
-    .page-editor-footer {
-      border-top-color: $border-color-theme;
-    }
   }
 }