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

Merge branch 'master' into imprv/check-grant-normalized-when-create-by-system

Haku Mizuki 3 лет назад
Родитель
Сommit
62b0aa6dfd
25 измененных файлов с 307 добавлено и 236 удалено
  1. 1 0
      packages/app/resource/Contributor.js
  2. 4 0
      packages/app/src/client/base.jsx
  3. 0 27
      packages/app/src/client/services/AppContainer.js
  4. 3 11
      packages/app/src/client/services/PageContainer.js
  5. 1 1
      packages/app/src/client/util/GrowiRenderer.js
  6. 2 8
      packages/app/src/client/util/interceptor/detach-code-blocks.js
  7. 6 10
      packages/app/src/client/util/interceptor/drawio-interceptor.js
  8. 1 1
      packages/app/src/client/util/markdown-it/table-with-handsontable-button.js
  9. 5 12
      packages/app/src/client/util/markdown-it/toc-and-anchor.js
  10. 5 5
      packages/app/src/components/CreateTemplateModal.jsx
  11. 0 108
      packages/app/src/components/Drawio.jsx
  12. 97 0
      packages/app/src/components/Drawio.tsx
  13. 25 25
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  14. 7 9
      packages/app/src/components/NotAvailableForGuest.jsx
  15. 32 5
      packages/app/src/components/Page.jsx
  16. 1 0
      packages/app/src/components/PageAccessoriesModal.tsx
  17. 7 3
      packages/app/src/components/PageAttachment.jsx
  18. 1 1
      packages/app/src/components/PageHistory.jsx
  19. 8 1
      packages/app/src/components/PagePresentationModal.jsx
  20. 13 4
      packages/app/src/components/TableOfContents.jsx
  21. 1 1
      packages/app/src/styles/theme/_apply-colors-dark.scss
  22. 7 0
      packages/app/src/styles/theme/_apply-colors-light.scss
  23. 0 4
      packages/app/src/styles/theme/_apply-colors.scss
  24. 44 0
      packages/app/test/cypress/integration/20-basic-features/access-to-page.spec.ts
  25. 36 0
      packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts

+ 1 - 0
packages/app/resource/Contributor.js

@@ -41,6 +41,7 @@ const contributors = [
           { name: 'N1koge' },
           { name: 'Ertai87' },
           { name: 'takayuki-t' },
+          { name: 'ayaka0417' },
           { name: 'zahmis' },
           { name: 'takeru0001' },
           { name: 'Shu Katabe' },

+ 4 - 0
packages/app/src/client/base.jsx

@@ -1,5 +1,7 @@
 import React from 'react';
 
+import EventEmitter from 'events';
+
 import AppContainer from '~/client/services/AppContainer';
 import SocketIoContainer from '~/client/services/SocketIoContainer';
 import { DescendantsPageListModal } from '~/components/DescendantsPageListModal';
@@ -28,6 +30,8 @@ if (!window) {
 const xss = new Xss();
 window.xss = xss;
 
+window.globalEmitter = new EventEmitter();
+
 // create unstated container instance
 const appContainer = new AppContainer();
 // eslint-disable-next-line no-unused-vars

+ 0 - 27
packages/app/src/client/services/AppContainer.js

@@ -26,12 +26,6 @@ export default class AppContainer extends Container {
       this.currentUser = JSON.parse(currentUserElem.textContent);
     }
 
-    const isSharedPageElem = document.getElementById('is-shared-page');
-
-    // check what kind of user
-    this.isGuestUser = this.currentUser == null;
-    this.isSharedUser = isSharedPageElem != null && this.currentUser == null;
-
     const userLocaleId = this.currentUser?.lang;
     this.i18n = i18nFactory(userLocaleId);
 
@@ -186,25 +180,4 @@ export default class AppContainer extends Container {
     return renderer;
   }
 
-
-  launchHandsontableModal(componentKind, beginLineNumber, endLineNumber) {
-    let targetComponent;
-    switch (componentKind) {
-      case 'page':
-        targetComponent = this.getComponentInstance('Page');
-        break;
-    }
-    targetComponent.launchHandsontableModal(beginLineNumber, endLineNumber);
-  }
-
-  launchDrawioModal(componentKind, beginLineNumber, endLineNumber) {
-    let targetComponent;
-    switch (componentKind) {
-      case 'page':
-        targetComponent = this.getComponentInstance('Page');
-        break;
-    }
-    targetComponent.launchDrawioModal(beginLineNumber, endLineNumber);
-  }
-
 }

+ 3 - 11
packages/app/src/client/services/PageContainer.js

@@ -52,7 +52,6 @@ export default class PageContainer extends Container {
       revisionId,
       revisionCreatedAt: +mainContent.getAttribute('data-page-revision-created'),
       path,
-      tocHtml: '',
 
       createdAt: mainContent.getAttribute('data-page-created-at'),
       // please use useCurrentUpdatedAt instead
@@ -101,13 +100,12 @@ export default class PageContainer extends Container {
     }
 
     const { interceptorManager } = this.appContainer;
-    interceptorManager.addInterceptor(new DetachCodeBlockInterceptor(appContainer), 10); // process as soon as possible
-    interceptorManager.addInterceptor(new DrawioInterceptor(appContainer), 20);
-    interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(appContainer), 900); // process as late as possible
+    interceptorManager.addInterceptor(new DetachCodeBlockInterceptor(), 10); // process as soon as possible
+    interceptorManager.addInterceptor(new DrawioInterceptor(), 20);
+    interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(), 900); // process as late as possible
 
     this.initStateMarkdown();
 
-    this.setTocHtml = this.setTocHtml.bind(this);
     this.save = this.save.bind(this);
 
     this.emitJoinPageRoomRequest = this.emitJoinPageRoomRequest.bind(this);
@@ -194,12 +192,6 @@ export default class PageContainer extends Container {
     this.setState(newState);
   }
 
-  async setTocHtml(tocHtml) {
-    if (this.state.tocHtml !== tocHtml) {
-      this.setState({ tocHtml });
-    }
-  }
-
   /**
    * save success handler
    * @param {object} page Page instance

+ 1 - 1
packages/app/src/client/util/GrowiRenderer.js

@@ -85,7 +85,7 @@ export default class GrowiRenderer {
 
         this.markdownItConfigurers = this.markdownItConfigurers.concat([
           new FooternoteConfigurer(appContainer),
-          new TocAndAnchorConfigurer(appContainer, pageContainer.setTocHtml),
+          new TocAndAnchorConfigurer(),
           new HeaderLineNumberConfigurer(appContainer),
           new HeaderWithEditLinkConfigurer(appContainer),
           new TableWithHandsontableButtonConfigurer(appContainer),

+ 2 - 8
packages/app/src/client/util/interceptor/detach-code-blocks.js

@@ -15,12 +15,9 @@ class DetachCodeBlockUtil {
  */
 export class DetachCodeBlockInterceptor extends BasicInterceptor {
 
-  constructor(crowi) {
+  constructor() {
     super();
     this.logger = loggerFactory('growi:interceptor:DetachCodeBlockInterceptor');
-
-    this.crowi = crowi;
-    this.crowiForJquery = crowi.getCrowiForJquery();
   }
 
   /**
@@ -94,12 +91,9 @@ export class DetachCodeBlockInterceptor extends BasicInterceptor {
  */
 export class RestoreCodeBlockInterceptor extends BasicInterceptor {
 
-  constructor(crowi) {
+  constructor() {
     super();
     this.logger = loggerFactory('growi:interceptor:DetachCodeBlockInterceptor');
-
-    this.crowi = crowi;
-    this.crowiForJquery = crowi.getCrowiForJquery();
   }
 
   /**

+ 6 - 10
packages/app/src/client/util/interceptor/drawio-interceptor.js

@@ -3,7 +3,6 @@ import React from 'react';
 
 import { BasicInterceptor } from '@growi/core';
 import ReactDOM from 'react-dom';
-import { Provider } from 'unstated';
 
 import Drawio from '~/components/Drawio';
 
@@ -14,11 +13,10 @@ import Drawio from '~/components/Drawio';
  */
 export class DrawioInterceptor extends BasicInterceptor {
 
-  constructor(appContainer) {
+  constructor() {
     super();
 
     this.previousPreviewContext = null;
-    this.appContainer = appContainer;
   }
 
   /**
@@ -125,13 +123,11 @@ export class DrawioInterceptor extends BasicInterceptor {
   renderReactDOM(drawioMapEntry, elem, isPreview) {
     ReactDOM.render(
       // eslint-disable-next-line react/jsx-filename-extension
-      <Provider inject={[this.appContainer]}>
-        <Drawio
-          drawioContent={drawioMapEntry.contentHtml}
-          isPreview={isPreview}
-          rangeLineNumberOfMarkdown={drawioMapEntry.rangeLineNumberOfMarkdown}
-        />
-      </Provider>,
+      <Drawio
+        drawioContent={drawioMapEntry.contentHtml}
+        isPreview={isPreview}
+        rangeLineNumberOfMarkdown={drawioMapEntry.rangeLineNumberOfMarkdown}
+      />,
       elem,
     );
   }

+ 1 - 1
packages/app/src/client/util/markdown-it/table-with-handsontable-button.js

@@ -9,7 +9,7 @@ export default class TableWithHandsontableButtonConfigurer {
       const beginLine = tokens[idx].map[0] + 1;
       const endLine = tokens[idx].map[1];
       // eslint-disable-next-line max-len
-      return `<div class="editable-with-handsontable"><button class="handsontable-modal-trigger" onClick="crowi.launchHandsontableModal('page', ${beginLine}, ${endLine})"><i class="icon-note"></i></button><table class="table table-bordered">`;
+      return `<div class="editable-with-handsontable"><button class="handsontable-modal-trigger" onClick="globalEmitter.emit('launchHandsontableModal', ${beginLine}, ${endLine})"><i class="icon-note"></i></button><table class="table table-bordered">`;
     };
 
     md.renderer.rules.table_close = (tokens, idx) => {

+ 5 - 12
packages/app/src/client/util/markdown-it/toc-and-anchor.js

@@ -5,11 +5,6 @@ import { emojiMartData } from './emoji-mart-data';
 
 export default class TocAndAnchorConfigurer {
 
-  constructor(crowi, setHtml) {
-    this.crowi = crowi;
-    this.setHtml = setHtml;
-  }
-
   configure(md) {
     md.use(markdownItEmojiMart, { defs: emojiMartData })
       .use(markdownItToc, {
@@ -21,13 +16,11 @@ export default class TocAndAnchorConfigurer {
       });
 
     // set toc render function
-    if (this.setHtml != null) {
-      md.set({
-        tocCallback: (tocMarkdown, tocArray, tocHtml) => {
-          this.setHtml(tocHtml);
-        },
-      });
-    }
+    md.set({
+      tocCallback: (tocMarkdown, tocArray, tocHtml) => {
+        window.globalEmitter.emit('renderTocHtml', tocHtml);
+      },
+    });
   }
 
 }

+ 5 - 5
packages/app/src/components/CreateTemplateModal.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
-import PropTypes from 'prop-types';
-
-import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 
-import { withTranslation } from 'react-i18next';
 import { pathUtils } from '@growi/core';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 import urljoin from 'url-join';
 
 
@@ -30,6 +29,7 @@ const CreateTemplateModal = (props) => {
         </div>
         <div className="card-footer text-center">
           <a
+            data-testid={`template-button-${target}`}
             href={generateUrl(label)}
             className="btn btn-sm btn-primary"
             id={`template-button-${target}`}
@@ -42,7 +42,7 @@ const CreateTemplateModal = (props) => {
   }
 
   return (
-    <Modal isOpen={props.isOpen} toggle={props.onClose} className="grw-create-page">
+    <Modal isOpen={props.isOpen} toggle={props.onClose} data-testid="page-template-modal" className="grw-create-page">
       <ModalHeader tag="h4" toggle={props.onClose} className="bg-primary text-light">
         {t('template.modal_label.Create/Edit Template Page')}
       </ModalHeader>

+ 0 - 108
packages/app/src/components/Drawio.jsx

@@ -1,108 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { debounce } from 'throttle-debounce';
-
-import { withTranslation } from 'react-i18next';
-
-import AppContainer from '~/client/services/AppContainer';
-
-import { withUnstatedContainers } from './UnstatedUtils';
-
-import NotAvailableForGuest from './NotAvailableForGuest';
-
-class Drawio extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.drawioContainer = React.createRef();
-
-    this.style = {
-      borderRadius: 3,
-      border: '1px solid #d7d7d7',
-      margin: '20px 0',
-    };
-
-    this.isPreview = this.props.isPreview;
-    this.drawioContent = this.props.drawioContent;
-
-    this.onEdit = this.onEdit.bind(this);
-
-    // create debounced method for rendering Drawio
-    this.renderDrawioWithDebounce = debounce(200, this.renderDrawio);
-  }
-
-  onEdit() {
-    const { appContainer, rangeLineNumberOfMarkdown } = this.props;
-    const { beginLineNumber, endLineNumber } = rangeLineNumberOfMarkdown;
-    appContainer.launchDrawioModal('page', beginLineNumber, endLineNumber);
-  }
-
-  componentDidMount() {
-    const DrawioViewer = window.GraphViewer;
-    if (DrawioViewer != null) {
-      this.renderDrawio();
-    }
-    else {
-      this.renderDrawioWithDebounce();
-    }
-  }
-
-  renderDrawio() {
-    const DrawioViewer = window.GraphViewer;
-    if (DrawioViewer != null) {
-      const mxgraphs = this.drawioContainer.getElementsByClassName('mxgraph');
-      if (mxgraphs.length > 0) {
-        // GROWI では、mxgraph element は最初のものをレンダリングする前提とする
-        const div = mxgraphs[0];
-
-        if (div != null) {
-          div.innerHTML = '';
-          DrawioViewer.createViewerForElement(div);
-        }
-      }
-    }
-    else {
-      this.renderDrawioWithDebounce();
-    }
-  }
-
-  renderContents() {
-    return this.drawioContent;
-  }
-
-  render() {
-    return (
-      <div className="editable-with-drawio position-relative">
-        { !this.isPreview && (
-          <NotAvailableForGuest>
-            <button type="button" className="drawio-iframe-trigger position-absolute btn btn-outline-secondary" onClick={this.onEdit}>
-              <i className="icon-note mr-1"></i>{this.props.t('Edit')}
-            </button>
-          </NotAvailableForGuest>
-        ) }
-        <div
-          className="drawio"
-          style={this.style}
-          ref={(c) => { this.drawioContainer = c }}
-          // eslint-disable-next-line react/no-danger
-          dangerouslySetInnerHTML={{ __html: this.renderContents() }}
-        >
-        </div>
-      </div>
-    );
-  }
-
-}
-
-Drawio.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.object.isRequired,
-
-  drawioContent: PropTypes.any.isRequired,
-  isPreview: PropTypes.bool,
-  rangeLineNumberOfMarkdown: PropTypes.object.isRequired,
-};
-
-export default withTranslation()(withUnstatedContainers(Drawio, [AppContainer]));

+ 97 - 0
packages/app/src/components/Drawio.tsx

@@ -0,0 +1,97 @@
+import React, {
+  useCallback, useEffect, useMemo, useRef,
+} from 'react';
+
+import EventEmitter from 'events';
+
+import { useTranslation } from 'react-i18next';
+import { debounce } from 'throttle-debounce';
+
+import NotAvailableForGuest from './NotAvailableForGuest';
+
+
+declare let window: {
+  globalEmitter: EventEmitter,
+  GraphViewer: {
+    createViewerForElement: (Element) => void,
+  };
+};
+
+type Props = {
+  drawioContent: string,
+  rangeLineNumberOfMarkdown: { beginLineNumber: number, endLineNumber: number },
+  isPreview?: boolean,
+}
+
+const Drawio = (props: Props): JSX.Element => {
+
+  const { t } = useTranslation();
+
+  const { drawioContent, rangeLineNumberOfMarkdown, isPreview } = props;
+
+  // const { open: openDrawioModal } = useDrawioModalForPage();
+
+  const drawioContainerRef = useRef<HTMLDivElement>(null);
+
+  const editButtonClickHandler = useCallback(() => {
+    const { beginLineNumber, endLineNumber } = rangeLineNumberOfMarkdown;
+    window.globalEmitter.emit('launchDrawioModal', beginLineNumber, endLineNumber);
+  }, [rangeLineNumberOfMarkdown]);
+
+  const renderDrawio = useCallback(() => {
+    if (drawioContainerRef.current == null) {
+      return;
+    }
+
+    const mxgraphs = drawioContainerRef.current.getElementsByClassName('mxgraph');
+    if (mxgraphs.length > 0) {
+      // GROWI では、mxgraph element は最初のものをレンダリングする前提とする
+      const div = mxgraphs[0];
+
+      if (div != null) {
+        div.innerHTML = '';
+        window.GraphViewer.createViewerForElement(div);
+      }
+    }
+  }, []);
+
+  const renderDrawioWithDebounce = useMemo(() => debounce(200, renderDrawio), [renderDrawio]);
+
+  const { GraphViewer } = window;
+  useEffect(() => {
+    if (GraphViewer == null) {
+      return;
+    }
+
+    renderDrawioWithDebounce();
+  }, [GraphViewer, renderDrawioWithDebounce]);
+
+  return (
+    <div className="editable-with-drawio position-relative">
+      { !isPreview && (
+        <NotAvailableForGuest>
+          <button type="button" className="drawio-iframe-trigger position-absolute btn btn-outline-secondary" onClick={editButtonClickHandler}>
+            <i className="icon-note mr-1"></i>{t('Edit')}
+          </button>
+        </NotAvailableForGuest>
+      ) }
+      <div
+        className="drawio"
+        style={
+          {
+            borderRadius: 3,
+            border: '1px solid #d7d7d7',
+            margin: '20px 0',
+          }
+        }
+        ref={drawioContainerRef}
+        // eslint-disable-next-line react/no-danger
+        dangerouslySetInnerHTML={{ __html: drawioContent }}
+      >
+      </div>
+    </div>
+  );
+
+};
+
+export default Drawio;

+ 25 - 25
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -1,45 +1,42 @@
 import React, { useState, useCallback } from 'react';
-import { useTranslation } from 'react-i18next';
-import PropTypes from 'prop-types';
-
 
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
 import { DropdownItem } from 'reactstrap';
 
-import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
-import { IPageHasId, IPageToRenameWithMeta, IPageWithMeta } from '~/interfaces/page';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
 import EditorContainer from '~/client/services/EditorContainer';
+import { exportAsMarkdown } from '~/client/services/page-operation';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiPost } from '~/client/util/apiv1-client';
+import { IPageHasId, IPageToRenameWithMeta, IPageWithMeta } from '~/interfaces/page';
+import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import {
-  EditorMode, useDrawerMode, useEditorMode, useIsDeviceSmallerThanMd, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel,
-  useIsAbleToShowPageEditorModeManager, useIsAbleToShowPageAuthors,
-} from '~/stores/ui';
+  useCurrentCreatedAt, useCurrentUpdatedAt, useCurrentPageId, useRevisionId, useCurrentPagePath,
+  useCreator, useRevisionAuthor, useCurrentUser, useIsGuestUser, useIsSharedUser, useShareLinkId,
+} from '~/stores/context';
 import {
   usePageAccessoriesModal, PageAccessoriesModalContents, IPageForPageDuplicateModal,
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal,
 } from '~/stores/modal';
-
-
-import {
-  useCurrentCreatedAt, useCurrentUpdatedAt, useCurrentPageId, useRevisionId, useCurrentPagePath,
-  useCreator, useRevisionAuthor, useCurrentUser, useIsGuestUser, useIsSharedUser, useShareLinkId,
-} from '~/stores/context';
 import { useSWRTagsInfo } from '~/stores/page';
+import {
+  EditorMode, useDrawerMode, useEditorMode, useIsDeviceSmallerThanMd, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel,
+  useIsAbleToShowPageEditorModeManager, useIsAbleToShowPageAuthors,
+} from '~/stores/ui';
 
-
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { apiPost } from '~/client/util/apiv1-client';
-
-import HistoryIcon from '../Icons/HistoryIcon';
+import { AdditionalMenuItemsRendererProps } from '../Common/Dropdown/PageItemControl';
 import AttachmentIcon from '../Icons/AttachmentIcon';
+import HistoryIcon from '../Icons/HistoryIcon';
+import { withUnstatedContainers } from '../UnstatedUtils';
+
 import ShareLinkIcon from '../Icons/ShareLinkIcon';
-import { AdditionalMenuItemsRendererProps } from '../Common/Dropdown/PageItemControl';
-import { SubNavButtons } from './SubNavButtons';
-import PageEditorModeManager from './PageEditorModeManager';
+
 import { GrowiSubNavigation } from './GrowiSubNavigation';
+import PageEditorModeManager from './PageEditorModeManager';
+import { SubNavButtons } from './SubNavButtons';
+
 import PresentationIcon from '../Icons/PresentationIcon';
 import CreateTemplateModal from '../CreateTemplateModal';
-import { exportAsMarkdown } from '~/client/services/page-operation';
 
 
 type AdditionalMenuItemsProps = AdditionalMenuItemsRendererProps & {
@@ -101,6 +98,7 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
       <DropdownItem
         onClick={() => openAccessoriesModal(PageAccessoriesModalContents.PageHistory)}
         disabled={isGuestUser || isSharedUser}
+        data-testid="open-page-accessories-modal-btn-with-history-tab"
         className="grw-page-control-dropdown-item"
       >
         <span className="grw-page-control-dropdown-icon">
@@ -111,6 +109,7 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
 
       <DropdownItem
         onClick={() => openAccessoriesModal(PageAccessoriesModalContents.Attachment)}
+        data-testid="open-page-accessories-modal-btn-with-attachment-data-tab"
         className="grw-page-control-dropdown-item"
       >
         <span className="grw-page-control-dropdown-icon">
@@ -136,6 +135,7 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
       <DropdownItem
         onClick={openPageTemplateModalHandler}
         className="grw-page-control-dropdown-item"
+        data-testid="open-page-template-modal-btn"
       >
         <i className="icon-fw icon-magic-wand grw-page-control-dropdown-icon"></i>
         { t('template.option_label.create/edit') }

+ 7 - 9
packages/app/src/components/NotAvailableForGuest.jsx

@@ -1,17 +1,16 @@
 import React from 'react';
-import PropTypes from 'prop-types';
 
+import PropTypes from 'prop-types';
 import { UncontrolledTooltip } from 'reactstrap';
 
-import AppContainer from '~/client/services/AppContainer';
-
-import { withUnstatedContainers } from './UnstatedUtils';
+import { useIsGuestUser } from '~/stores/context';
 
 const NotAvailableForGuest = (props) => {
-  const { appContainer, children } = props;
-  const isLoggedin = appContainer.currentUser != null;
+  const { children } = props;
+
+  const { data: isGuestUser } = useIsGuestUser();
 
-  if (isLoggedin) {
+  if (!isGuestUser) {
     return props.children;
   }
 
@@ -34,8 +33,7 @@ const NotAvailableForGuest = (props) => {
 };
 
 NotAvailableForGuest.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   children: PropTypes.node.isRequired,
 };
 
-export default withUnstatedContainers(NotAvailableForGuest, [AppContainer]);
+export default NotAvailableForGuest;

+ 32 - 5
packages/app/src/components/Page.jsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect, useRef } from 'react';
 
 import PropTypes from 'prop-types';
 
@@ -49,10 +49,6 @@ class Page extends React.Component {
     this.saveHandlerForDrawioModal = this.saveHandlerForDrawioModal.bind(this);
   }
 
-  componentWillMount() {
-    this.props.appContainer.registerComponentInstance('Page', this);
-  }
-
   /**
    * launch HandsontableModal with data specified by arguments
    * @param beginLineNumber
@@ -197,6 +193,36 @@ const PageWrapper = (props) => {
   const { data: grantGroupId } = useSelectedGrantGroupId();
   const { data: grantGroupName } = useSelectedGrantGroupName();
 
+  const pageRef = useRef(null);
+
+  // set handler to open DrawioModal
+  useEffect(() => {
+    const handler = (beginLineNumber, endLineNumber) => {
+      if (pageRef?.current != null) {
+        pageRef.current.launchDrawioModal(beginLineNumber, endLineNumber);
+      }
+    };
+    window.globalEmitter.on('launchDrawioModal', handler);
+
+    return function cleanup() {
+      window.globalEmitter.removeListener('launchDrawioModal', handler);
+    };
+  }, []);
+
+  // set handler to open HandsontableModal
+  useEffect(() => {
+    const handler = (beginLineNumber, endLineNumber) => {
+      if (pageRef?.current != null) {
+        pageRef.current.launchHandsontableModal(beginLineNumber, endLineNumber);
+      }
+    };
+    window.globalEmitter.on('launchHandsontableModal', handler);
+
+    return function cleanup() {
+      window.globalEmitter.removeListener('launchHandsontableModal', handler);
+    };
+  }, []);
+
   if (currentPagePath == null || editorMode == null || isGuestUser == null) {
     return null;
   }
@@ -204,6 +230,7 @@ const PageWrapper = (props) => {
   return (
     <Page
       {...props}
+      ref={pageRef}
       pagePath={currentPagePath}
       editorMode={editorMode}
       isGuestUser={isGuestUser}

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

@@ -106,6 +106,7 @@ const PageAccessoriesModal = (props: Props): JSX.Element => {
       size="xl"
       isOpen={isOpened}
       toggle={close}
+      data-testid="page-accessories-modal"
       className={`grw-page-accessories-modal ${isWindowExpanded ? 'grw-modal-expanded' : ''} `}
     >
       <ModalHeader className="p-0" toggle={close} close={buttons}>

+ 7 - 3
packages/app/src/components/PageAttachment.jsx

@@ -118,7 +118,11 @@ class PageAttachment extends React.Component {
   render() {
     const { t } = this.props;
     if (this.state.attachments.length === 0) {
-      return t('No_attachments_yet');
+      return (
+        <div data-testid="page-attachment">
+          {t('No_attachments_yet')}
+        </div>
+      );
     }
 
     let deleteAttachmentModal = '';
@@ -149,7 +153,7 @@ class PageAttachment extends React.Component {
     }
 
     return (
-      <>
+      <div data-testid="page-attachment">
         <PageAttachmentList
           attachments={this.state.attachments}
           inUse={this.state.inUse}
@@ -166,7 +170,7 @@ class PageAttachment extends React.Component {
           pagingLimit={this.state.limit}
           align="center"
         />
-      </>
+      </div>
     );
   }
 

+ 1 - 1
packages/app/src/components/PageHistory.jsx

@@ -68,7 +68,7 @@ function PageHistory(props) {
   }
 
   return (
-    <div className="revision-history">
+    <div className="revision-history" data-testid="page-history">
       <PageRevisionTable
         pageHistoryContainer={pageHistoryContainer}
         revisionComparerContainer={revisionComparerContainer}

+ 8 - 1
packages/app/src/components/PagePresentationModal.jsx

@@ -1,4 +1,5 @@
 import React from 'react';
+
 import {
   Modal, ModalBody,
 } from 'reactstrap';
@@ -10,7 +11,13 @@ const PagePresentationModal = () => {
   const { data: presentationData, close: closePresentationModal } = usePagePresentationModal();
 
   return (
-    <Modal isOpen={presentationData.isOpened} toggle={closePresentationModal} className="grw-presentation-modal" unmountOnClose={false}>
+    <Modal
+      isOpen={presentationData.isOpened}
+      toggle={closePresentationModal}
+      data-testid="page-presentation-modal"
+      className="grw-presentation-modal"
+      unmountOnClose={false}
+    >
       <ModalBody className="modal-body">
         <iframe src={presentationData.href} />
       </ModalBody>

+ 13 - 4
packages/app/src/components/TableOfContents.jsx

@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
 
 import PropTypes from 'prop-types';
 
@@ -25,6 +25,8 @@ const TableOfContents = (props) => {
   const { pageUser } = pageContainer.state;
   const isUserPage = pageUser != null;
 
+  const [tocHtml, setTocHtml] = useState('');
+
   const calcViewHeight = useCallback(() => {
     // calculate absolute top of '#revision-toc' element
     const parentElem = document.querySelector('.grw-side-contents-container');
@@ -45,15 +47,22 @@ const TableOfContents = (props) => {
     return bottom - (containerTop + containerPaddingTop);
   }, [isUserPage]);
 
-  const { tocHtml } = pageContainer.state;
-
-  // execute after generation toc html
   useEffect(() => {
     const tocDom = document.getElementById('revision-toc-content');
     const anchorsInToc = Array.from(tocDom.getElementsByTagName('a'));
     addSmoothScrollEvent(anchorsInToc, blinkElem);
   }, [tocHtml]);
 
+  // set handler to render ToC
+  useEffect(() => {
+    const handler = html => setTocHtml(html);
+    window.globalEmitter.on('renderTocHtml', handler);
+
+    return function cleanup() {
+      window.globalEmitter.removeListener('renderTocHtml', handler);
+    };
+  }, []);
+
   return (
     <StickyStretchableScroller
       stickyElemSelector=".grw-side-contents-sticky-container"

+ 1 - 1
packages/app/src/styles/theme/_apply-colors-dark.scss

@@ -234,7 +234,7 @@ ul.pagination {
       background-color: $bgcolor-list-hover;
     }
     .page-list-snippet {
-      color: theme-color('light');
+      color: darken($body-color, 10%);
     }
   }
 }

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

@@ -224,6 +224,13 @@ $dropdown-link-active-bg: $bgcolor-dropdown-link-active;
       }
     }
   }
+  // List group
+  .list-group-item {
+    // TODO: fix color light theme
+    .page-list-snippet {
+      color: $gray-600;
+    }
+  }
 }
 
 /*

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

@@ -500,10 +500,6 @@ ul.pagination {
         }
       }
 
-      .page-list-snippet {
-        color: $gray-600;
-      }
-
       &.list-group-item-action {
         &.active {
           background-color: $bgcolor-page-list-group-item-active;

+ 44 - 0
packages/app/test/cypress/integration/20-basic-features/access-to-page.spec.ts

@@ -102,3 +102,47 @@ context('Access to special pages', () => {
   });
 
 });
+
+context('Access to Template Editing Mode', () => {
+  const ssPrefix = 'access-to-modal-';
+
+  beforeEach(() => {
+    // login
+    cy.fixture("user-admin.json").then(user => {
+      cy.login(user.username, user.password);
+    });
+    // collapse sidebar
+    cy.collapseSidebar(true);
+  });
+
+
+  it('Access to Template Editor mode for only child pages successfully', () => {
+     cy.visit('/Sandbox/Bootstrap4', {  });
+     cy.get('#grw-subnav-container').within(() => {
+       cy.getByTestid('open-page-item-control-btn').click();
+       cy.getByTestid('open-page-template-modal-btn').click();
+    });
+
+     cy.getByTestid('page-template-modal').should('be.visible')
+     cy.screenshot(`${ssPrefix}-open-page-template-bootstrap4`);
+     cy.getByTestid('template-button-children').click();
+     cy.url().should('include', '/_template#edit');
+     cy.screenshot();
+  });
+
+  it('Access to Template Editor mode including decendants successfully', () => {
+    cy.visit('/Sandbox/Bootstrap4', {  });
+    cy.get('#grw-subnav-container').within(() => {
+      cy.getByTestid('open-page-item-control-btn').click();
+      cy.getByTestid('open-page-template-modal-btn').click();
+   });
+
+    cy.getByTestid('page-template-modal').should('be.visible')
+    // cy.screenshot(`${ssPrefix}-open-page-template-bootstrap4`);
+    cy.getByTestid('template-button-decendants').click();
+    cy.url().should('include', '/__template#edit');
+    cy.screenshot();
+ });
+
+});
+

+ 36 - 0
packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts

@@ -105,3 +105,39 @@ context('Open presentation modal', () => {
   });
 
 });
+
+context('Page Accessories Modal', () => {
+
+  const ssPrefix = 'access-to-page-accessories-modal';
+
+  beforeEach(() => {
+    // login
+    cy.fixture("user-admin.json").then(user => {
+      cy.login(user.username, user.password);
+    });
+  });
+
+  it('Page History is shown successfully', () => {
+     cy.visit('/Sandbox/Bootstrap4', {  });
+     cy.get('#grw-subnav-container').within(() => {
+       cy.getByTestid('open-page-item-control-btn').click();
+       cy.getByTestid('open-page-accessories-modal-btn-with-history-tab').click();
+    });
+
+     cy.getByTestid('page-accessories-modal').should('be.visible')
+     cy.getByTestid('page-history').should('be.visible')
+     cy.screenshot(`${ssPrefix}-open-page-history-bootstrap4`);
+  });
+  it('Page Attachment Data is shown successfully', () => {
+     cy.visit('/Sandbox/Bootstrap4', {  });
+     cy.get('#grw-subnav-container').within(() => {
+       cy.getByTestid('open-page-item-control-btn').click();
+       cy.getByTestid('open-page-accessories-modal-btn-with-attachment-data-tab').click();
+    });
+
+     cy.getByTestid('page-accessories-modal').should('be.visible')
+     cy.getByTestid('page-attachment').should('be.visible')
+     cy.screenshot(`${ssPrefix}-open-page-attachment-data-bootstrap4`);
+  });
+
+});