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

Merge branch 'master' into feat/88261-add-header-to-the-body-of-the-page

Shun Miyazawa 4 лет назад
Родитель
Сommit
1a9986ca3d
33 измененных файлов с 190 добавлено и 95 удалено
  1. 3 1
      .devcontainer/docker-compose.yml
  2. 2 1
      .github/workflows/ci-app.yml
  3. 2 0
      packages/app/.env.development
  4. 20 1
      packages/app/jest.config.js
  5. 1 0
      packages/app/package.json
  6. 4 4
      packages/app/resource/locales/en_US/sandbox.md
  7. 5 1
      packages/app/resource/locales/en_US/translation.json
  8. 3 3
      packages/app/resource/locales/ja_JP/sandbox.md
  9. 5 1
      packages/app/resource/locales/ja_JP/translation.json
  10. 4 4
      packages/app/resource/locales/zh_CN/sandbox.md
  11. 4 2
      packages/app/resource/locales/zh_CN/translation.json
  12. 29 7
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  13. 3 3
      packages/app/src/components/Navbar/SubNavButtons.tsx
  14. 24 4
      packages/app/src/components/Page/TrashPageAlert.jsx
  15. 7 2
      packages/app/src/components/PageDeleteModal.tsx
  16. 15 7
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  17. 2 1
      packages/app/src/components/Sidebar.tsx
  18. 3 5
      packages/app/src/components/Sidebar/PageTree/Item.tsx
  19. 4 15
      packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx
  20. 5 1
      packages/app/src/migrations/20210913153942-migrate-slack-app-integration-schema.js
  21. 1 1
      packages/app/src/server/routes/page.js
  22. 6 6
      packages/app/src/server/service/installer.ts
  23. 1 1
      packages/app/src/stores/ui.tsx
  24. 1 2
      packages/app/test/cypress/integration/2-basic-features/access-to-admin-page.spec.ts
  25. 2 3
      packages/app/test/cypress/integration/2-basic-features/access-to-me-page.spec.ts
  26. 6 0
      packages/app/test/cypress/integration/2-basic-features/access-to-page.spec.ts
  27. 2 2
      packages/app/test/cypress/integration/3-search/access-to-result-page-directly.spec.ts
  28. 10 0
      packages/app/test/cypress/support/commands.ts
  29. 1 0
      packages/app/test/cypress/support/index.ts
  30. 12 8
      packages/app/test/integration/global-setup.js
  31. 1 3
      packages/app/test/integration/migrations/20210913153942-migrate-slack-app-integration-schema.test.ts
  32. 0 6
      packages/app/test/integration/service/page-grant.test.js
  33. 2 0
      packages/app/test/integration/service/v5.migration.test.js

+ 3 - 1
.devcontainer/docker-compose.yml

@@ -40,6 +40,8 @@ services:
     build:
       context: ../../growi-docker-compose/elasticsearch
       dockerfile: ./Dockerfile
+      args:
+        - version=6.8.22
     container_name: elasticsearch
     restart: unless-stopped
     ports:
@@ -57,7 +59,7 @@ services:
 
   #need to adjust kibana version based on elasticsearch version
   kibana:
-    image: docker.elastic.co/kibana/kibana:6.8.0
+    image: docker.elastic.co/kibana/kibana:6.8.22
     restart: unless-stopped
     environment:
       ELASTICSEARCH_HOSTS: 'http://elasticsearch:9200'

+ 2 - 1
.github/workflows/ci-app.yml

@@ -98,7 +98,8 @@ jobs:
     - name: yarn test
       working-directory: ./packages/app
       run: |
-        yarn test
+        yarn test:ci --selectProjects unit server
+        yarn test:ci --selectProjects server-v5
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_test
 

+ 2 - 0
packages/app/.env.development

@@ -14,6 +14,8 @@ MONGO_URI="mongodb://mongo:27017/growi"
 # NCHAN_URI="http://nchan"
 ELASTICSEARCH_URI="http://elasticsearch:9200/growi"
 ELASTICSEARCH_REQUEST_TIMEOUT=15000
+#ELASTICSEARCH_REJECT_UNAUTHORIZED=false
+#USE_ELASTICSEARCH_V6=true
 HACKMD_URI="http://localhost:3010"
 HACKMD_URI_FOR_SERVER="http://hackmd:3000"
 # DRAWIO_URI="http://localhost:8080/?offline=1&https=0"

+ 20 - 1
packages/app/jest.config.js

@@ -37,7 +37,26 @@ module.exports = {
 
       rootDir: '.',
       roots: ['<rootDir>'],
-      testMatch: ['<rootDir>/test/integration/**/*.test.ts', '<rootDir>/test/integration/**/*.test.js'],
+      testMatch: ['<rootDir>/test/integration/**/*.test.ts', '<rootDir>/test/integration/**/*.test.js',
+                  '?!<rootDir>/test/integration/**/v5.*.test.ts', '?!<rootDir>/test/integration/**/v5.*.test.js'],
+
+      testEnvironment: 'node',
+      globalSetup: '<rootDir>/test/integration/global-setup.js',
+      globalTeardown: '<rootDir>/test/integration/global-teardown.js',
+      setupFilesAfterEnv: ['<rootDir>/test/integration/setup.js'],
+
+      // Automatically clear mock calls and instances between every test
+      clearMocks: true,
+      moduleNameMapper: MODULE_NAME_MAPPING,
+    },
+    {
+      displayName: 'server-v5',
+
+      preset: 'ts-jest/presets/js-with-ts',
+
+      rootDir: '.',
+      roots: ['<rootDir>'],
+      testMatch: ['<rootDir>/test/integration/**/v5.*.test.ts', '<rootDir>/test/integration/**/v5.*.test.js'],
 
       testEnvironment: 'node',
       globalSetup: '<rootDir>/test/integration/global-setup.js',

+ 1 - 0
packages/app/package.json

@@ -38,6 +38,7 @@
     "lint:swagger2openapi": "node node_modules/.bin/oas-validate tmp/swagger.json",
     "lint": "run-p lint:*",
     "test": "cross-env NODE_ENV=test jest --passWithNoTests -- ",
+    "test:ci": "cross-env NODE_ENV=test jest",
     "prelint:eslint": "yarn resources:plugin",
     "prelint:swagger2openapi": "yarn openapi:v3",
     "reg:run": "reg-suit run",

+ 4 - 4
packages/app/resource/locales/en_US/sandbox.md

@@ -237,10 +237,10 @@ You can create links using `[Display text](URL)`.
 
 ```
 [/Sandbox]
-&lt;/user/admin1>
+</user/admin1>
 ```
 
-[/Sandbox]
+[/Sandbox]  
 </user/admin1>
 
 ## Pukiwiki like linker
@@ -253,10 +253,10 @@ Both the page description and link address can be displayed on the page.
 
 ```
 [[./Bootstrap4]]
-Example of Bootstrap4 is[[here>./Bootstrap4]]
+Example of Bootstrap4 is [[here>./Bootstrap4]]
 ```
 
-[[../user]]
+[[../Bootstrap4]]  
 Example of Bootstrap4 is[[here>./Bootstrap4]]
 
 # :pencil: Lists

+ 5 - 1
packages/app/resource/locales/en_US/translation.json

@@ -441,6 +441,8 @@
     "recursively": "Delete pages under this path recursively.",
     "completely": "Delete completely instead of putting it into trash."
   },
+  "deleted_pages": "Page(s) has been deleted",
+  "deleted_pages_completely": "Page(s) has been deleted completely",
   "modal_empty":{
     "empty_the_trash": "Empty The Trash",
     "notice": "The pages deleted completely are unrecoverable."
@@ -979,7 +981,9 @@
   },
   "pagetree": {
     "private_legacy_pages": "Private Legacy Pages",
-    "cannot_rename_a_title_that_contains_slash": "Cannot rename a title that contains '/'"
+    "cannot_rename_a_title_that_contains_slash": "Cannot rename a title that contains '/'",
+    "you_cannot_move_this_page_now": "You cannot move this page now",
+    "something_went_wrong_with_moving_page": "Something went wrong with moving page"
   },
   "duplicated_page_alert" : {
     "same_page_name_exists": "Same page name exits as「{{pageName}}」",

+ 3 - 3
packages/app/resource/locales/ja_JP/sandbox.md

@@ -236,10 +236,10 @@ ___
 
 ```
 [/Sandbox]
-&lt;/user/admin1>
+</user/admin1>
 ```
 
-[/Sandbox]
+[/Sandbox]  
 </user/admin1>
 
 ## Pukiwiki like linker
@@ -255,7 +255,7 @@ ___
 Bootstrap4のExampleは[[こちら>./Bootstrap4]]
 ```
 
-[[../user]]
+[[../Bootstrap4]]  
 Bootstrap4のExampleは[[こちら>./Bootstrap4]]
 
 # :pencil: Lists

+ 5 - 1
packages/app/resource/locales/ja_JP/translation.json

@@ -440,6 +440,8 @@
     "recursively": "配下のページも削除します",
     "completely": "ゴミ箱を経由せず、完全に削除します"
   },
+  "deleted_pages": "ページをゴミ箱に入れました",
+  "deleted_pages_completely": "ページを完全に削除しました",
   "modal_empty":{
     "empty_the_trash": "ゴミ箱を空にする",
     "notice": "完全削除したページは元に戻すことができません"
@@ -971,7 +973,9 @@
   },
   "pagetree": {
     "private_legacy_pages": "待避所",
-    "cannot_rename_a_title_that_contains_slash": "`/` が含まれているタイトルにリネームできません"
+    "cannot_rename_a_title_that_contains_slash": "`/` が含まれているタイトルにリネームできません",
+    "you_cannot_move_this_page_now": "現在、このページを移動することはできません",
+    "something_went_wrong_with_moving_page": "ページの移動に問題が発生しました"
   },
   "duplicated_page_alert" : {
     "same_page_name_exists": "ページ名 「{{pageName}}」が重複しています",

+ 4 - 4
packages/app/resource/locales/zh_CN/sandbox.md

@@ -237,10 +237,10 @@ You can create links using `[Display text](URL)`.
 
 ```
 [/Sandbox]
-&lt;/user/admin1>
+</user/admin1>
 ```
 
-[/Sandbox]
+[/Sandbox]  
 </user/admin1>
 
 ## Pukiwiki like linker
@@ -256,8 +256,8 @@ Both the page description and link address can be displayed on the page.
 Example of Bootstrap4 is[[here>./Bootstrap4]]
 ```
 
-[[../user]]
-Example of Bootstrap4 is[[here>./Bootstrap4]]
+[[../Bootstrap4]]  
+Example of Bootstrap4 is [[here>./Bootstrap4]]
 
 # :pencil: Lists
 

+ 4 - 2
packages/app/resource/locales/zh_CN/translation.json

@@ -418,7 +418,7 @@
 		"delete_completely_restriction": "You don't have the authority to delete pages completely.",
 		"recursively": "Delete children of <code>%s</code> recursively.",
 		"completely": "Delete completely instead of putting it into trash."
-	},
+  },
 	"modal_empty": {
 		"empty_the_trash": "Empty The Trash",
 		"notice": "完全删除的页面是不可恢复的。"
@@ -981,7 +981,9 @@
   },
   "pagetree": {
     "private_legacy_pages": "私人遗留页面",
-    "cannot_rename_a_title_that_contains_slash": "不能重命名包含 ’/' 的标题"
+    "cannot_rename_a_title_that_contains_slash": "不能重命名包含 ’/' 的标题",
+    "you_cannot_move_this_page_now": "你现在不能移动这个页面",
+    "something_went_wrong_with_moving_page": "移动页面时出了问题"
   },
   "duplicated_page_alert" : {
     "same_page_name_exists": "页面名称「{{pageName}}」是重复的",

+ 29 - 7
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -1,7 +1,7 @@
 import React, { useState, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
 import PropTypes from 'prop-types';
 
-import { useTranslation } from 'react-i18next';
 
 import { DropdownItem } from 'reactstrap';
 
@@ -13,8 +13,9 @@ import {
 } from '~/stores/ui';
 import {
   usePageAccessoriesModal, PageAccessoriesModalContents,
-  usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal,
+  usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, OnDeletedFunction, usePagePresentationModal,
 } from '~/stores/modal';
+import { useSWRxPageChildren } from '~/stores/page-listing';
 
 
 import {
@@ -142,6 +143,7 @@ const GrowiContextualSubNavigation = (props) => {
   const { data: isAbleToShowPageEditorModeManager } = useIsAbleToShowPageEditorModeManager();
   const { data: isAbleToShowPageAuthors } = useIsAbleToShowPageAuthors();
 
+  const { mutate: mutateChildren } = useSWRxPageChildren(path);
   const { mutate: mutateSWRTagsInfo, data: tagsInfoData } = useSWRTagsInfo(pageId);
 
   const { open: openDuplicateModal } = usePageDuplicateModal();
@@ -186,9 +188,27 @@ const GrowiContextualSubNavigation = (props) => {
     openRenameModal(pageId, revisionId, path);
   }, [openRenameModal]);
 
-  const deleteItemClickedHandler = useCallback(async(pageToDelete) => {
-    openDeleteModal([pageToDelete]);
-  }, [openDeleteModal]);
+  const onDeletedHandler: OnDeletedFunction = useCallback((pathOrPathsToDelete, isRecursively, isCompletely) => {
+    if (typeof pathOrPathsToDelete !== 'string') {
+      return;
+    }
+
+    mutateChildren();
+
+    const path = pathOrPathsToDelete;
+
+    if (isCompletely) {
+      // redirect to NotFound Page
+      window.location.href = path;
+    }
+    else {
+      window.location.reload();
+    }
+  }, [mutateChildren]);
+
+  const deleteItemClickedHandler = useCallback(async(pageToDelete, isAbleToDeleteCompletely) => {
+    openDeleteModal([pageToDelete], onDeletedHandler, isAbleToDeleteCompletely);
+  }, [onDeletedHandler, openDeleteModal]);
 
   const templateMenuItemClickHandler = useCallback(() => {
     setIsPageTempleteModalShown(true);
@@ -200,9 +220,11 @@ const GrowiContextualSubNavigation = (props) => {
       mutateEditorMode(viewType);
     }
 
+    const className = `d-flex flex-column align-items-end justify-content-center ${isViewMode ? ' h-50' : ''}`;
+
     return (
       <>
-        <div className="h-50 d-flex flex-column align-items-end justify-content-center">
+        <div className={className}>
           { pageId != null && isViewMode && (
             <SubNavButtons
               isCompactMode={isCompactMode}
@@ -227,7 +249,7 @@ const GrowiContextualSubNavigation = (props) => {
             />
           ) }
         </div>
-        <div className="h-50 d-flex flex-column align-items-end justify-content-center">
+        <div className={className}>
           {isAbleToShowPageEditorModeManager && (
             <PageEditorModeManager
               onPageEditorModeButtonClicked={onPageEditorModeButtonClicked}

+ 3 - 3
packages/app/src/components/Navbar/SubNavButtons.tsx

@@ -23,7 +23,7 @@ type CommonProps = {
   additionalMenuItemRenderer?: React.FunctionComponent<AdditionalMenuItemsRendererProps>,
   onClickDuplicateMenuItem?: (pageId: string, path: string) => void,
   onClickRenameMenuItem?: (pageId: string, revisionId: string, path: string) => void,
-  onClickDeleteMenuItem?: (pageToDelete: IPageForPageDeleteModal | null) => void,
+  onClickDeleteMenuItem?: (pageToDelete: IPageForPageDeleteModal | null, isAbleToDeleteCompletely: boolean) => void,
 }
 
 type SubNavButtonsSubstanceProps= CommonProps & {
@@ -120,8 +120,8 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
       path,
     };
 
-    onClickDeleteMenuItem(pageToDelete);
-  }, [onClickDeleteMenuItem, pageId, path, revisionId]);
+    onClickDeleteMenuItem(pageToDelete, pageInfo.isAbleToDeleteCompletely);
+  }, [onClickDeleteMenuItem, pageId, pageInfo.isAbleToDeleteCompletely, path, revisionId]);
 
   if (!isIPageInfoForOperation(pageInfo)) {
     return <></>;

+ 24 - 4
packages/app/src/components/Page/TrashPageAlert.jsx

@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
 import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
@@ -10,17 +10,27 @@ import PageContainer from '~/client/services/PageContainer';
 import PutbackPageModal from '../PutbackPageModal';
 import EmptyTrashModal from '../EmptyTrashModal';
 
-import { useCurrentUpdatedAt } from '~/stores/context';
+import { useCurrentUpdatedAt, useShareLinkId } from '~/stores/context';
 import { usePageDeleteModal } from '~/stores/modal';
+import { useSWRxPageInfo } from '~/stores/page';
 
 const TrashPageAlert = (props) => {
   const { t, pageContainer } = props;
   const {
-    pageId, revisionId, path, isDeleted, lastUpdateUsername, deletedUserName, deletedAt, isAbleToDeleteCompletely,
+    pageId, revisionId, path, isDeleted, lastUpdateUsername, deletedUserName, deletedAt,
   } = pageContainer.state;
+  const { data: shareLinkId } = useShareLinkId();
+  const { data: pageInfo } = useSWRxPageInfo(pageId ?? null, shareLinkId);
   const { data: updatedAt } = useCurrentUpdatedAt();
   const [isEmptyTrashModalShown, setIsEmptyTrashModalShown] = useState(false);
   const [isPutbackPageModalShown, setIsPutbackPageModalShown] = useState(false);
+  const [isAbleToDeleteCompletely, setIsAbleToDeleteCompletely] = useState(false);
+
+  useEffect(() => {
+    if (pageInfo != null) {
+      setIsAbleToDeleteCompletely(pageInfo.isAbleToDeleteCompletely);
+    }
+  }, [pageInfo]);
 
   const { open: openDeleteModal } = usePageDeleteModal();
 
@@ -40,13 +50,23 @@ const TrashPageAlert = (props) => {
     setIsPutbackPageModalShown(false);
   }
 
+  const onDeletedHandler = useCallback((pathOrPathsToDelete, isRecursively, isCompletely) => {
+    if (typeof pathOrPathsToDelete !== 'string') {
+      return;
+    }
+
+    const path = pathOrPathsToDelete;
+    window.location.href = path;
+  }, []);
+
   function openPageDeleteModalHandler() {
     const pageToDelete = {
       pageId,
       revisionId,
       path,
     };
-    openDeleteModal([pageToDelete]);
+    const isDeleteCompletelyModal = true;
+    openDeleteModal([pageToDelete], onDeletedHandler, isAbleToDeleteCompletely, isDeleteCompletelyModal);
   }
 
   function renderEmptyButton() {

+ 7 - 2
packages/app/src/components/PageDeleteModal.tsx

@@ -1,4 +1,4 @@
-import React, { useState, FC } from 'react';
+import React, { useState, useEffect, FC } from 'react';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
@@ -36,7 +36,7 @@ const PageDeleteModal: FC = () => {
   const isDeleteCompletelyModal = deleteModalData?.isDeleteCompletelyModal ?? false;
 
   const [isDeleteRecursively, setIsDeleteRecursively] = useState(true);
-  const [isDeleteCompletely, setIsDeleteCompletely] = useState(isDeleteCompletelyModal && isAbleToDeleteCompletely);
+  const [isDeleteCompletely, setIsDeleteCompletely] = useState(false);
   const deleteMode = isDeleteCompletely ? 'completely' : 'temporary';
 
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -46,6 +46,10 @@ const PageDeleteModal: FC = () => {
     setIsDeleteRecursively(!isDeleteRecursively);
   }
 
+  useEffect(() => {
+    setIsDeleteCompletely(isDeleteCompletelyModal && isAbleToDeleteCompletely);
+  }, [isAbleToDeleteCompletely, isDeleteCompletelyModal]);
+
   function changeIsDeleteCompletelyHandler() {
     if (!isAbleToDeleteCompletely) {
       return;
@@ -128,6 +132,7 @@ const PageDeleteModal: FC = () => {
         />
         <label className="custom-control-label" htmlFor="deleteRecursively">
           { t('modal_delete.delete_recursively') }
+          <p className="form-text text-muted mt-0"> { t('modal_delete.recursively') }</p>
         </label>
       </div>
     );

+ 15 - 7
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -1,9 +1,9 @@
 import React, {
   FC, useCallback, useEffect, useRef,
 } from 'react';
+import { useTranslation } from 'react-i18next';
 
 import { DropdownItem } from 'reactstrap';
-import { useTranslation } from 'react-i18next';
 
 import { IPageWithMeta } from '~/interfaces/page';
 import { IPageSearchMeta } from '~/interfaces/search';
@@ -17,7 +17,9 @@ import { GrowiSubNavigation } from '../Navbar/GrowiSubNavigation';
 import { SubNavButtons } from '../Navbar/SubNavButtons';
 import { AdditionalMenuItemsRendererProps } from '../Common/Dropdown/PageItemControl';
 
-import { usePageDuplicateModal, usePageRenameModal, usePageDeleteModal } from '~/stores/modal';
+import {
+  usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, OnDeletedFunction,
+} from '~/stores/modal';
 
 
 type AdditionalMenuItemsProps = AdditionalMenuItemsRendererProps & {
@@ -99,12 +101,11 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
     showPageControlDropdown,
   } = props;
 
+  const page = pageWithMeta?.pageData;
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openRenameModal } = usePageRenameModal();
   const { open: openDeleteModal } = usePageDeleteModal();
 
-  const page = pageWithMeta?.pageData;
-
   const growiRenderer = appContainer.getRenderer('searchresult');
 
 
@@ -116,9 +117,16 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
     openRenameModal(pageId, revisionId, path);
   }, [openRenameModal]);
 
-  const deleteItemClickedHandler = useCallback(async(pageToDelete) => {
-    openDeleteModal([pageToDelete]);
-  }, [openDeleteModal]);
+  const onDeletedHandler: OnDeletedFunction = useCallback((pathOrPathsToDelete, isRecursively, isCompletely) => {
+    if (typeof pathOrPathsToDelete !== 'string') {
+      return;
+    }
+    window.location.reload();
+  }, []);
+
+  const deleteItemClickedHandler = useCallback(async(pageToDelete, isAbleToDeleteCompletely) => {
+    openDeleteModal([pageToDelete], onDeletedHandler, isAbleToDeleteCompletely);
+  }, [onDeletedHandler, openDeleteModal]);
 
   const ControlComponents = useCallback(() => {
     if (page == null) {

+ 2 - 1
packages/app/src/components/Sidebar.tsx

@@ -312,7 +312,7 @@ const Sidebar: FC<Props> = (props: Props) => {
                 style={{ width: isCollapsed ? sidebarMinimizeWidth : currentProductNavWidth }}
               >
                 <div className="grw-contextual-navigation-child">
-                  <div role="group" className={`grw-contextual-navigation-sub ${showContents ? '' : 'd-none'}`}>
+                  <div role="group" data-testid="grw-contextual-navigation-sub" className={`grw-contextual-navigation-sub ${showContents ? '' : 'd-none'}`}>
                     <SidebarContentsWrapper></SidebarContentsWrapper>
                   </div>
                 </div>
@@ -328,6 +328,7 @@ const Sidebar: FC<Props> = (props: Props) => {
                 </div>
               ) }
               <button
+                data-testid="grw-navigation-resize-button"
                 className={`grw-navigation-resize-button ${!isDrawerMode ? 'resizable' : ''} ${isCollapsed ? 'collapsed' : ''} `}
                 type="button"
                 aria-expanded="true"

+ 3 - 5
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -145,18 +145,16 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
 
       // force open
       setIsOpen(true);
-
-      toastSuccess('TODO: i18n Successfully moved pages.');
     }
     catch (err) {
       // display the dropped item
       displayDroppedItemByPageId(droppedPage._id);
 
       if (err.code === 'operation__blocked') {
-        toastWarning('TODO: i18n You cannot move this page now.');
+        toastWarning(t('pagetree.you_cannot_move_this_page_now'));
       }
       else {
-        toastError('TODO: i18n Something went wrong with moving page.');
+        toastError(t('pagetree.something_went_wrong_with_moving_page'));
       }
     }
   };
@@ -373,7 +371,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
           />
         )}
         { !isRenameInputShown && ( */}
-        <a href={page._id} className="grw-pagetree-title-anchor flex-grow-1">
+        <a href={`/${page._id}`} className="grw-pagetree-title-anchor flex-grow-1">
           <p className={`text-truncate m-auto ${page.isEmpty && 'text-muted'}`}>{nodePath.basename(pageTitle as string) || '/'}</p>
         </a>
         {/* )} */}

+ 4 - 15
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
 import { IPageHasId } from '../../../interfaces/page';
 import { ItemNode } from './ItemNode';
 import Item from './Item';
-import { useSWRxPageAncestorsChildren, useSWRxPageChildren, useSWRxRootPage } from '../../../stores/page-listing';
+import { useSWRxPageAncestorsChildren, useSWRxPageChildren, useSWRxRootPage } from '~/stores/page-listing';
 import { TargetAndAncestors } from '~/interfaces/page-listing-results';
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import {
@@ -127,22 +127,11 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
 
     const path = pathOrPathsToDelete;
 
-    if (isRecursively) {
-      if (isCompletely) {
-        toastSuccess(t('deleted_single_page_recursively_completely', { path }));
-      }
-      else {
-        toastSuccess(t('deleted_single_page_recursively', { path }));
-      }
+    if (isCompletely) {
+      toastSuccess(t('deleted_pages_completely', { path }));
     }
     else {
-      // eslint-disable-next-line no-lonely-if
-      if (isCompletely) {
-        toastSuccess(t('deleted_single_page_completely', { path }));
-      }
-      else {
-        toastSuccess(t('deleted_single_page', { path }));
-      }
+      toastSuccess(t('deleted_pages', { path }));
     }
   };
 

+ 5 - 1
packages/app/src/migrations/20210913153942-migrate-slack-app-integration-schema.js

@@ -20,7 +20,11 @@ defaultSupportedCommandsNameForSingleUse.forEach((commandName) => {
 module.exports = {
   async up(db) {
     logger.info('Apply migration');
-    await mongoose.connect(getMongoUri(), mongoOptions);
+    // connect only if disconnected
+    // see: https://mongoosejs.com/docs/api/connection.html#connection_Connection-readyState
+    if (mongoose.connection.readyState === 0) {
+      await mongoose.connect(getMongoUri(), mongoOptions);
+    }
 
     const SlackAppIntegration = getModelSafely('SlackAppIntegration') || require('~/server/models/slack-app-integration')();
 

+ 1 - 1
packages/app/src/server/routes/page.js

@@ -890,7 +890,7 @@ module.exports = function(crowi, app) {
    * - If revision_id is not specified => force update by the new contents.
    */
   api.update = async function(req, res) {
-    const pageBody = body ?? null;
+    const pageBody = req.body.body ?? null;
     const pageId = req.body.page_id || null;
     const revisionId = req.body.revision_id || null;
     const grant = req.body.grant || null;

+ 6 - 6
packages/app/src/server/service/installer.ts

@@ -62,14 +62,14 @@ export class InstallerService {
     const { localeDir } = this.crowi;
     // create /Sandbox/*
     /*
-     * Keep in this order to avoid creating the same pages
+     * Keep in this order to
+     *   1. avoid creating the same pages
+     *   2. avoid difference for order in VRT
      */
     await this.createPage(path.join(localeDir, lang, 'sandbox.md'), '/Sandbox', owner);
-    await Promise.all([
-      this.createPage(path.join(localeDir, lang, 'sandbox-diagrams.md'), '/Sandbox/Diagrams', owner),
-      this.createPage(path.join(localeDir, lang, 'sandbox-bootstrap4.md'), '/Sandbox/Bootstrap4', owner),
-      this.createPage(path.join(localeDir, lang, 'sandbox-math.md'), '/Sandbox/Math', owner),
-    ]);
+    await this.createPage(path.join(localeDir, lang, 'sandbox-bootstrap4.md'), '/Sandbox/Bootstrap4', owner);
+    await this.createPage(path.join(localeDir, lang, 'sandbox-diagrams.md'), '/Sandbox/Diagrams', owner);
+    await this.createPage(path.join(localeDir, lang, 'sandbox-math.md'), '/Sandbox/Math', owner);
 
     // update createdAt and updatedAt fields of all pages
     if (initialPagesCreatedAt != null) {

+ 1 - 1
packages/app/src/stores/ui.tsx

@@ -299,7 +299,7 @@ export const useIsAbleToShowTagLabel = (): SWRResponse<boolean, Error> => {
   const isNotFoundPage = notFoundTargetPathOrId != null;
 
   return useSWRImmutable(
-    includesUndefined ? null : key,
+    includesUndefined ? null : [key, editorMode],
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     () => !isUserPage && !isSharedPage(currentPagePath!) && !isIdenticalPath && !(isViewMode && isNotFoundPage),
   );

+ 1 - 2
packages/app/test/cypress/integration/2-basic-features/access-to-admin-page.spec.ts

@@ -1,5 +1,3 @@
-const ssPrefix = 'access-to-admin-page-';
-
 const adminMenues = [
   'app', // App
   'security', // Security
@@ -15,6 +13,7 @@ const adminMenues = [
 ];
 
 context('Access to Admin page', () => {
+  const ssPrefix = 'access-to-admin-page-';
 
   let connectSid: string | undefined;
 

+ 2 - 3
packages/app/test/cypress/integration/2-basic-features/access-to-me-page.spec.ts

@@ -1,6 +1,5 @@
-const ssPrefix = 'access-to-page-';
-
-context('Access to page', () => {
+context('Access to /me page', () => {
+  const ssPrefix = 'access-to-me-page-';
 
   let connectSid: string | undefined;
 

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

@@ -22,6 +22,12 @@ context('Access to page', () => {
 
   it('/Sandbox is successfully loaded', () => {
     cy.visit('/Sandbox', {  });
+
+    // collapse sidebar and wait saving
+    cy.collapseSidebar(true);
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(1500);
+
     cy.screenshot(`${ssPrefix}-sandbox`, { capture: 'viewport' });
   });
 

+ 2 - 2
packages/app/test/cypress/integration/3-search/access-to-result-page-directly.spec.ts

@@ -20,7 +20,7 @@ context('Access to search result page directly', () => {
   });
 
   it('/_search with "q" param is successfully loaded', () => {
-    cy.visit('/_search', { qs: { q: 'bootstrap4' } });
+    cy.visit('/_search', { qs: { q: 'bootstrap4 labels alerts' } });
 
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
@@ -29,7 +29,7 @@ context('Access to search result page directly', () => {
   });
 
   it('checkboxes behaviors', () => {
-    cy.visit('/_search', { qs: { q: 'bootstrap4' } });
+    cy.visit('/_search', { qs: { q: 'bootstrap4 labels alerts' } });
 
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');

+ 10 - 0
packages/app/test/cypress/support/commands.ts

@@ -37,3 +37,13 @@ Cypress.Commands.add('login', (username, password) => {
     cy.getByTestid('btnSubmitForLogin').click();
   });
 });
+
+Cypress.Commands.add('collapseSidebar', (isCollapsed) => {
+  cy.getByTestid('grw-contextual-navigation-sub').then(($contents) => {
+    const isCurrentCollapsed = $contents.hasClass('d-none');
+    // toggle when the current state and isCoolapsed is not match
+    if (isCurrentCollapsed !== isCollapsed) {
+      cy.getByTestid("grw-navigation-resize-button").click();
+    }
+  });
+});

+ 1 - 0
packages/app/test/cypress/support/index.ts

@@ -25,6 +25,7 @@ declare global {
     interface Chainable {
        getByTestid(selector: string, options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<Element>>,
        login(username: string, password: string): Chainable<void>,
+       collapseSidebar(isCollapsed: boolean): Chainable<void>,
     }
   }
 }

+ 12 - 8
packages/app/test/integration/global-setup.js

@@ -16,22 +16,26 @@ if (process.env.NODE_ENV !== 'test') {
   throw new Error('\'process.env.NODE_ENV\' must be \'test\'');
 }
 
-
-// eslint-disable-next-line @typescript-eslint/no-var-requires
-// const { getInstance } = require('./setup-crowi');
-
 module.exports = async() => {
   initMongooseGlobalSettings();
 
-  await mongoose.connect(getMongoUri(), mongoOptions);
+  mongoose.connect(getMongoUri(), mongoOptions);
 
   // drop database
   await mongoose.connection.dropDatabase();
 
   // init DB
-  // const crowi = await getInstance();
-  // const appService = crowi.appService;
-  // await appService.initDB();
+  const pageCollection = mongoose.connection.collection('pages');
+  const userCollection = mongoose.connection.collection('users');
+
+  // create global user & rootPage
+  const globalUser = (await userCollection.insertMany([{ name: 'globalUser', username: 'globalUser', email: 'globalUser@example.com' }]))[0];
+  await pageCollection.insertMany([{
+    path: '/',
+    grant: 1,
+    creator: globalUser,
+    lastUpdateUser: globalUser,
+  }]);
 
   await mongoose.disconnect();
 };

+ 1 - 3
packages/app/test/integration/migrations/20210913153942-migrate-slack-app-integration-schema.test.ts

@@ -1,6 +1,5 @@
 import mongoose from 'mongoose';
 import { Collection } from 'mongodb';
-import { getMongoUri, mongoOptions } from '@growi/core';
 
 const migrate = require('~/migrations/20210913153942-migrate-slack-app-integration-schema');
 
@@ -9,8 +8,7 @@ describe('migrate-slack-app-integration-schema', () => {
   let collection: Collection;
 
   beforeAll(async() => {
-    await mongoose.connect(getMongoUri(), mongoOptions);
-    collection = mongoose.connection.db.collection('slackappintegrations');
+    collection = mongoose.connection.collection('slackappintegrations');
 
     await collection.insertMany([
       {

+ 0 - 6
packages/app/test/integration/service/page-grant.test.js

@@ -109,12 +109,6 @@ describe('PageGrantService', () => {
     ]);
 
     // Root page (Depth: 0)
-    await Page.insertMany([
-      {
-        path: '/',
-        grant: Page.GRANT_PUBLIC,
-      },
-    ]);
     rootPage = await Page.findOne({ path: '/' });
 
     // Empty pages (Depth: 1)

+ 2 - 0
packages/app/test/integration/service/v5-migration.test.js → packages/app/test/integration/service/v5.migration.test.js

@@ -16,6 +16,8 @@ describe('V5 page migration', () => {
     Page = mongoose.model('Page');
     User = mongoose.model('User');
 
+    await crowi.configManager.updateConfigsInTheSameNamespace('crowi', { 'app:isV5Compatible': true });
+
     await User.insertMany([{ name: 'testUser1', username: 'testUser1', email: 'testUser1@example.com' }]);
     testUser1 = await User.findOne({ username: 'testUser1' });
   });