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

Merge branch 'dev/5.0.x' into support/node-16

Yuki Takei 4 лет назад
Родитель
Сommit
9f5141bb51
59 измененных файлов с 399 добавлено и 323 удалено
  1. 28 1
      CHANGELOG.md
  2. 1 1
      packages/app/bin/download-cdn-resources.ts
  3. 0 2
      packages/app/config/webpack.common.js
  4. 0 1
      packages/app/config/webpack.dev.dll.js
  5. 2 2
      packages/app/docker/README.md
  6. 1 2
      packages/app/package.json
  7. 2 0
      packages/app/resource/locales/en_US/admin/admin.json
  8. 2 0
      packages/app/resource/locales/ja_JP/admin/admin.json
  9. 2 0
      packages/app/resource/locales/zh_CN/admin/admin.json
  10. 1 1
      packages/app/src/client/services/ContextExtractor.tsx
  11. 0 1
      packages/app/src/client/services/PageContainer.js
  12. 1 1
      packages/app/src/client/util/interceptor/detach-code-blocks.js
  13. 1 1
      packages/app/src/client/util/interceptor/drawio-interceptor.js
  14. 1 1
      packages/app/src/components/PageEditor.jsx
  15. 1 1
      packages/app/src/components/PageEditor/MarkdownTableInterceptor.js
  16. 1 1
      packages/app/src/components/PageEditor/PreventMarkdownListInterceptor.js
  17. 1 1
      packages/app/src/components/SearchForm.jsx
  18. 6 1
      packages/app/src/components/SearchPage/SortControl.tsx
  19. 29 1
      packages/app/src/components/Sidebar/PageTree.tsx
  20. 2 5
      packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx
  21. 1 0
      packages/app/src/interfaces/page-listing-results.ts
  22. 1 1
      packages/app/src/server/models/GlobalNotificationSetting/index.js
  23. 39 21
      packages/app/src/server/models/obsolete-page.js
  24. 22 15
      packages/app/src/server/models/page.ts
  25. 1 1
      packages/app/src/server/routes/apiv3/app-settings.js
  26. 3 2
      packages/app/src/server/routes/apiv3/pages.js
  27. 9 7
      packages/app/src/server/routes/page.js
  28. 1 1
      packages/app/src/server/service/config-loader.ts
  29. 1 1
      packages/app/src/server/service/page.js
  30. 1 1
      packages/app/src/server/service/slack-command-handler/create-page-service.js
  31. 1 1
      packages/app/src/server/service/slack-command-handler/search.js
  32. 1 1
      packages/app/src/server/util/middlewares.js
  33. 1 1
      packages/app/src/server/util/swigFunctions.js
  34. 5 0
      packages/app/src/server/views/layout/layout.html
  35. 0 1
      packages/app/src/server/views/widget/page_content.html
  36. 1 1
      packages/app/src/services/cdn-resources-service.js
  37. 2 2
      packages/app/src/stores/page-listing.tsx
  38. 0 129
      packages/app/src/test/integration/service/page.test.js
  39. 194 0
      packages/app/src/test/integration/service/v5-migration.test.js
  40. 0 27
      packages/core/README.md
  41. 11 14
      packages/core/src/index.js
  42. 2 4
      packages/core/src/plugin/service/tag-cache-manager.js
  43. 1 3
      packages/core/src/service/localstorage-manager.js
  44. 1 3
      packages/core/src/utils/basic-interceptor.js
  45. 1 1
      packages/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPostRenderInterceptor.js
  46. 1 1
      packages/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPreRenderInterceptor.js
  47. 1 1
      packages/plugin-attachment-refs/src/client/js/util/RefsContext.js
  48. 1 1
      packages/plugin-attachment-refs/src/client/js/util/TagCacheManagerFactory.js
  49. 1 1
      packages/plugin-attachment-refs/src/server/routes/refs.js
  50. 1 1
      packages/plugin-lsx/src/client/js/components/Lsx.jsx
  51. 1 1
      packages/plugin-lsx/src/client/js/components/LsxPageList/LsxPage.jsx
  52. 1 1
      packages/plugin-lsx/src/client/js/util/Interceptor/LsxLogoutInterceptor.js
  53. 1 1
      packages/plugin-lsx/src/client/js/util/Interceptor/LsxPostRenderInterceptor.js
  54. 1 1
      packages/plugin-lsx/src/client/js/util/Interceptor/LsxPreRenderInterceptor.js
  55. 1 1
      packages/plugin-lsx/src/client/js/util/LsxContext.js
  56. 1 18
      packages/plugin-lsx/src/client/js/util/TagCacheManagerFactory.js
  57. 1 1
      packages/plugin-lsx/src/server/routes/lsx.js
  58. 0 27
      packages/ui/README.md
  59. 5 5
      yarn.lock

+ 28 - 1
CHANGELOG.md

@@ -1,9 +1,36 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v4.5.2...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v4.5.3...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v4.5.3](https://github.com/weseek/growi/compare/v4.5.2...v4.5.3) - 2021-12-17
+
+### 💎 Features
+
+- feat: user activation by email (#4862) @kaoritokashiki
+
+### 🚀 Improvement
+
+- imprv: Use SWR for isSlackEnabled (#4827) @stevenfukase
+- imprv: Disable rubber band scroll for Mac & iOS users (#4834) @hakumizuki
+- imprv: Omit atlaskit and implement sidebar only with original codes (#4598) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: GROWI Bot search command after transplanting search service from dev/5.0.x (#4916) @hakumizuki
+- fix: Set min-height to sidebar scroll target (#4884) @yuki-takei
+
+### 🧰 Maintenance
+
+- support: fix dependabot alert for kind-of (#4891) @LuqmanHakim-Grune
+- support: fix dependabot alert for ini (#4892) @LuqmanHakim-Grune
+- support: fix and debug mixin-deep dependabot alert (#4867) @LuqmanHakim-Grune
+- support: dependabot alert xmlhttprequest-ssl (#4878) @mudana-grune
+- support: Transplant search service from dev/5.0.x (#4869) @hakumizuki
+- support: dependabot alert set-value (#4864) @LuqmanHakim-Grune
+- ci(deps): bump aws-sdk from 2.179.0 to 2.1044.0 (#4821) @dependabot
+
 ## [v4.5.2](https://github.com/weseek/growi/compare/v4.5.1...v4.5.2) - 2021-12-06
 
 ### 🐛 Bug Fixes

+ 1 - 1
packages/app/bin/download-cdn-resources.ts

@@ -3,7 +3,7 @@
  *
  * @author Yuki Takei <yuki@weseek.co.jp>
  */
-import { envUtils } from 'growi-commons';
+import { envUtils } from '@growi/core';
 
 import CdnResourcesDownloader from './cdn/cdn-resources-downloader';
 import loggerFactory from '../src/utils/logger';

+ 0 - 2
packages/app/config/webpack.common.js

@@ -83,8 +83,6 @@ module.exports = (options) => {
           exclude: {
             test: /node_modules/,
             exclude: [ // include as a result
-              { test: /node_modules\/growi-plugin-/ },
-              /node_modules\/growi-commons/,
               /node_modules\/codemirror/,
             ],
           },

+ 0 - 1
packages/app/config/webpack.dev.dll.js

@@ -17,7 +17,6 @@ module.exports = {
       'diff2html',
       'debug',
       'entities',
-      'growi-commons',
       'i18next', 'i18next-browser-languagedetector',
       'jquery-slimscroll',
       'lodash', 'pako',

+ 2 - 2
packages/app/docker/README.md

@@ -12,8 +12,8 @@ Supported tags and respective Dockerfile links
 
 * [`5.0.0`, `5.0`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.0/docker/Dockerfile)
 * [`5.0.0-nocdn`, `5.0-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.0/docker/Dockerfile)
-* [`4.5.2`, `4.5`, `4` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.2/docker/Dockerfile)
-* [`4.5.2-nocdn`, `4.5-nocdn`, `4-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.2/docker/Dockerfile)
+* [`4.5.3`, `4.5`, `4` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.3/docker/Dockerfile)
+* [`4.5.3-nocdn`, `4.5-nocdn`, `4-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.3/docker/Dockerfile)
 * [`4.4.13`, `4.4` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
 * [`4.4.13-nocdn`, `4.4-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
 

+ 1 - 2
packages/app/package.json

@@ -99,7 +99,6 @@
     "express-validator": "^6.1.1",
     "express-webpack-assets": "^0.1.0",
     "graceful-fs": "^4.1.11",
-    "growi-commons": "^5.0.4",
     "helmet": "^4.6.0",
     "http-errors": "~1.8.0",
     "i18next": "^20.3.2",
@@ -148,7 +147,7 @@
     "unzipper": "^0.10.5",
     "url-join": "^4.0.0",
     "validator": "^13.6.0",
-    "ws": "^7.4.6",
+    "ws": "^8.3.0",
     "xss": "^1.0.6"
   },
   "// comments for defDependencies": {

+ 2 - 0
packages/app/resource/locales/en_US/admin/admin.json

@@ -20,6 +20,8 @@
     "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>then submit your issue to GitHub.</a>"
   },
   "v5_page_migration": {
+    "page_tree_not_avaliable" : "Page tree feature is not available yet.",
+    "go_to_settings": "Go to settings to enable the feature",
     "migration_desc": "Some of the public pages have the old schema. To take advantage of new features such as page trees and easy renaming, please upgrade the schema of all your pages.",
     "migration_note": "Note: You will lose unique constraints from the page paths.",
     "upgrade_to_v5": "Upgrade to V5",

+ 2 - 0
packages/app/resource/locales/ja_JP/admin/admin.json

@@ -20,6 +20,8 @@
     "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>次に GitHub で Issue を投稿してください。</a>"
   },
   "v5_page_migration": {
+    "page_tree_not_avaliable" : "Page Tree 機能は現在使用できません。",
+    "go_to_settings": "設定する",
     "migration_desc": "公開されているページに古いスキーマのものが存在します。ページツリーや簡単なリネームなどの新機能を利用するには、全てのページのスキーマをアップグレードしてください。",
     "migration_note": "注意: ページパスからユニーク制約が失われます。",
     "upgrade_to_v5": "V5 にアップグレード",

+ 2 - 0
packages/app/resource/locales/zh_CN/admin/admin.json

@@ -20,6 +20,8 @@
     "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>然后提交你的问题到GitHub。</a>"
   },
   "v5_page_migration": {
+    "page_tree_not_avaliable": "Page Tree 功能不可用",
+    "go_to_settings": "进入设置,启用该功能",
     "migration_desc": "Some of the public pages have the old schema. To take advantage of new features such as page trees and easy renaming, please upgrade the schema of all your pages. ",
     "migration_note": "Note: You will lose unique constraints from the page paths.",
     "upgrade_to_v5": "Upgrade to V5",

+ 1 - 1
packages/app/src/client/services/ContextExtractor.tsx

@@ -60,7 +60,7 @@ const ContextExtractorOnce: FC = () => {
   const hasDraftOnHackmd = !!mainContent?.getAttribute('data-page-has-draft-on-hackmd');
   const creator = JSON.parse(mainContent?.getAttribute('data-page-creator') || jsonNull);
   const revisionAuthor = JSON.parse(mainContent?.getAttribute('data-page-revision-author') || jsonNull);
-  const targetAndAncestors = JSON.parse(mainContent?.getAttribute('data-target-and-ancestors') || jsonNull);
+  const targetAndAncestors = JSON.parse(document.getElementById('growi-pagetree-target-and-ancestors')?.textContent || jsonNull);
   const slackChannels = mainContent?.getAttribute('data-slack-channels') || '';
 
   /*

+ 0 - 1
packages/app/src/client/services/PageContainer.js

@@ -82,7 +82,6 @@ export default class PageContainer extends Container {
       templateTagData: mainContent.getAttribute('data-template-tags') || null,
       shareLinksNumber: mainContent.getAttribute('data-share-links-number'),
       shareLinkId: JSON.parse(mainContent.getAttribute('data-share-link-id') || null),
-      targetAndAncestors: JSON.parse(mainContent.getAttribute('data-target-and-ancestors') || null),
 
       // latest(on remote) information
       remoteRevisionId: revisionId,

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

@@ -1,4 +1,4 @@
-import { BasicInterceptor } from 'growi-commons';
+import { BasicInterceptor } from '@growi/core';
 
 import loggerFactory from '~/utils/logger';
 

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

@@ -2,7 +2,7 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
 import { Provider } from 'unstated';
-import { BasicInterceptor } from 'growi-commons';
+import { BasicInterceptor } from '@growi/core';
 
 import Drawio from '~/components/Drawio';
 

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

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import detectIndent from 'detect-indent';
 
 import { throttle, debounce } from 'throttle-debounce';
-import { envUtils } from 'growi-commons';
+import { envUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 
 import AppContainer from '~/client/services/AppContainer';

+ 1 - 1
packages/app/src/components/PageEditor/MarkdownTableInterceptor.js

@@ -1,4 +1,4 @@
-import { BasicInterceptor } from 'growi-commons';
+import { BasicInterceptor } from '@growi/core';
 
 import mtu from './MarkdownTableUtil';
 import MarkdownTable from '~/client/models/MarkdownTable';

+ 1 - 1
packages/app/src/components/PageEditor/PreventMarkdownListInterceptor.js

@@ -1,4 +1,4 @@
-import { BasicInterceptor } from 'growi-commons';
+import { BasicInterceptor } from '@growi/core';
 
 import mlu from './MarkdownListUtil';
 

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

@@ -40,7 +40,7 @@ class SearchForm extends React.Component {
 
     // navigate to page
     if (page != null) {
-      window.location = page.path;
+      window.location = page.pageData._id;
     }
   }
 

+ 6 - 1
packages/app/src/components/SearchPage/SortControl.tsx

@@ -26,7 +26,12 @@ const SortControl: FC <Props> = (props: Props) => {
   };
 
   const renderSortItem = (sort, order) => {
-    return <div className="d-flex align-items-center"><span className="mr-3">{t(`search_result.sort_axis.${sort}`)}</span>{renderOrderIcon(order)}</div>;
+    return (
+      <div className="d-flex align-items-center justify-content-between w-100">
+        <span className="mr-3">{t(`search_result.sort_axis.${sort}`)}</span>
+        {renderOrderIcon(order)}
+      </div>
+    );
   };
 
   return (

+ 29 - 1
packages/app/src/components/Sidebar/PageTree.tsx

@@ -19,12 +19,40 @@ const PageTree: FC = memo(() => {
   const { data: targetId } = useCurrentPageId();
   const { data: targetAndAncestorsData } = useTargetAndAncestors();
 
-  const { data: migrationStatus } = useSWRxV5MigrationStatus(!isGuestUser);
+  const { data: migrationStatus } = useSWRxV5MigrationStatus();
 
   // for delete modal
   const [isDeleteModalOpen, setDeleteModalOpen] = useState(false);
   const [pagesToDelete, setPagesToDelete] = useState<IPageForPageDeleteModal[]>([]);
 
+  if (migrationStatus == null) {
+    return (
+      <>
+        <div className="grw-sidebar-content-header p-3">
+          <h3 className="mb-0">{t('Page Tree')}</h3>
+        </div>
+        <div className="mt-5 mx-2 text-center">
+          <h3 className="text-gray">Page Tree now loading...</h3>
+        </div>
+      </>
+    );
+  }
+
+  if (!migrationStatus?.isV5Compatible) {
+    // TODO : improve design
+    // Story : https://redmine.weseek.co.jp/issues/83755
+    return (
+      <>
+        <div className="grw-sidebar-content-header p-3">
+          <h3 className="mb-0">{t('Page Tree')}</h3>
+        </div>
+        <div className="mt-5 mx-2 text-center">
+          <h3 className="text-gray">{t('admin:v5_page_migration.page_tree_not_avaliable')}</h3>
+          <a href="/admin">{t('admin:v5_page_migration.go_to_settings')}</a>
+        </div>
+      </>
+    );
+  }
   /*
    * dependencies
    */

+ 2 - 5
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -1,4 +1,4 @@
-import React, { FC, useState } from 'react';
+import React, { FC } from 'react';
 
 import { IPageHasId } from '../../../interfaces/page';
 import { ItemNode } from './ItemNode';
@@ -93,8 +93,6 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
   const { data: ancestorsChildrenData, error: error1 } = useSWRxPageAncestorsChildren(targetPath);
   const { data: rootPageData, error: error2 } = useSWRxRootPage();
 
-  const [isRenderedCompletely, setRenderedCompletely] = useState(false);
-
   const DeleteModal = (
     <PageDeleteModal
       isOpen={isDeleteModalOpen}
@@ -114,9 +112,8 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
   /*
    * Render completely
    */
-  if (!isRenderedCompletely && ancestorsChildrenData != null && rootPageData != null) {
+  if (ancestorsChildrenData != null && rootPageData != null) {
     const initialNode = generateInitialNodeAfterResponse(ancestorsChildrenData.ancestorsChildren, new ItemNode(rootPageData.rootPage));
-    setRenderedCompletely(true); // render once
     return renderByInitialNode(initialNode, DeleteModal, isEnableActions, targetId, onClickDeleteByPage);
   }
 

+ 1 - 0
packages/app/src/interfaces/page-listing-results.ts

@@ -24,5 +24,6 @@ export interface TargetAndAncestors {
 
 
 export interface V5MigrationStatus {
+  isV5Compatible : boolean,
   migratablePagesCount: number
 }

+ 1 - 1
packages/app/src/server/models/GlobalNotificationSetting/index.js

@@ -1,6 +1,6 @@
 const mongoose = require('mongoose');
 const nodePath = require('path');
-const { pathUtils } = require('growi-commons');
+const { pathUtils } = require('@growi/core');
 
 /**
  * parent schema for GlobalNotificationSetting model

+ 39 - 21
packages/app/src/server/models/obsolete-page.js

@@ -12,7 +12,7 @@ const urljoin = require('url-join');
 const mongoose = require('mongoose');
 const differenceInYears = require('date-fns/differenceInYears');
 
-const { pathUtils } = require('growi-commons');
+const { pathUtils } = require('@growi/core');
 const escapeStringRegexp = require('escape-string-regexp');
 
 const { isTopPage, isTrashPage } = pagePathUtils;
@@ -79,8 +79,17 @@ const populateDataToShowRevision = (page, userPublicFields) => {
 
 export class PageQueryBuilder {
 
-  constructor(query) {
+  constructor(query, includeEmpty = false) {
     this.query = query;
+    if (!includeEmpty) {
+      this.query = this.query
+        .and({
+          $or: [
+            { isEmpty: false },
+            { isEmpty: null }, // for v4 compatibility
+          ],
+        });
+    }
   }
 
   addConditionToExcludeTrashed() {
@@ -249,12 +258,18 @@ export class PageQueryBuilder {
   /*
    * Add this condition when get any ancestor pages including the target's parent
    */
-  addConditionToSortAncestorPages() {
+  addConditionToSortPagesByDescPath() {
     this.query = this.query.sort('-path');
 
     return this;
   }
 
+  addConditionToSortPagesByAscPath() {
+    this.query = this.query.sort('path');
+
+    return this;
+  }
+
   addConditionToMinimizeDataForRendering() {
     this.query = this.query.select('_id path isEmpty grant revision');
 
@@ -586,7 +601,7 @@ export const getPageSchema = (crowi) => {
    * @param {User} user User instance
    * @param {UserGroup[]} userGroups List of UserGroup instances
    */
-  pageSchema.statics.findByIdAndViewer = async function(id, user, userGroups) {
+  pageSchema.statics.findByIdAndViewer = async function(id, user, userGroups, includeEmpty = false) {
     const baseQuery = this.findOne({ _id: id });
 
     let relatedUserGroups = userGroups;
@@ -596,18 +611,21 @@ export const getPageSchema = (crowi) => {
       relatedUserGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
     }
 
-    const queryBuilder = new PageQueryBuilder(baseQuery);
+    const queryBuilder = new PageQueryBuilder(baseQuery, includeEmpty);
     queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups, true);
 
-    return await queryBuilder.query.exec();
+    return queryBuilder.query.exec();
   };
 
   // find page by path
-  pageSchema.statics.findByPath = function(path) {
+  pageSchema.statics.findByPath = function(path, includeEmpty = false) {
     if (path == null) {
       return null;
     }
-    return this.findOne({ path });
+
+    const builder = new PageQueryBuilder(this.findOne({ path }), includeEmpty);
+
+    return builder.query.exec();
   };
 
   /**
@@ -615,7 +633,7 @@ export const getPageSchema = (crowi) => {
    * @param {User} user User instance
    * @param {UserGroup[]} userGroups List of UserGroup instances
    */
-  pageSchema.statics.findAncestorByPathAndViewer = async function(path, user, userGroups) {
+  pageSchema.statics.findAncestorByPathAndViewer = async function(path, user, userGroups, includeEmpty = false) {
     if (path == null) {
       throw new Error('path is required.');
     }
@@ -636,10 +654,10 @@ export const getPageSchema = (crowi) => {
       relatedUserGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
     }
 
-    const queryBuilder = new PageQueryBuilder(baseQuery);
+    const queryBuilder = new PageQueryBuilder(baseQuery, includeEmpty);
     queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups);
 
-    return await queryBuilder.query.exec();
+    return queryBuilder.query.exec();
   };
 
   pageSchema.statics.findByRedirectTo = function(path) {
@@ -649,22 +667,22 @@ export const getPageSchema = (crowi) => {
   /**
    * find pages that is match with `path` and its descendants
    */
-  pageSchema.statics.findListWithDescendants = async function(path, user, option = {}) {
-    const builder = new PageQueryBuilder(this.find());
+  pageSchema.statics.findListWithDescendants = async function(path, user, option = {}, includeEmpty = false) {
+    const builder = new PageQueryBuilder(this.find(), includeEmpty);
     builder.addConditionToListWithDescendants(path, option);
 
-    return await findListFromBuilderAndViewer(builder, user, false, option);
+    return findListFromBuilderAndViewer(builder, user, false, option);
   };
 
   /**
    * find pages that is match with `path` and its descendants whitch user is able to manage
    */
-  pageSchema.statics.findManageableListWithDescendants = async function(page, user, option = {}) {
+  pageSchema.statics.findManageableListWithDescendants = async function(page, user, option = {}, includeEmpty = false) {
     if (user == null) {
       return null;
     }
 
-    const builder = new PageQueryBuilder(this.find());
+    const builder = new PageQueryBuilder(this.find(), includeEmpty);
     builder.addConditionToListWithDescendants(page.path, option);
     builder.addConditionToExcludeRedirect();
 
@@ -685,11 +703,11 @@ export const getPageSchema = (crowi) => {
   /**
    * find pages that start with `path`
    */
-  pageSchema.statics.findListByStartWith = async function(path, user, option) {
-    const builder = new PageQueryBuilder(this.find());
+  pageSchema.statics.findListByStartWith = async function(path, user, option, includeEmpty = false) {
+    const builder = new PageQueryBuilder(this.find(), includeEmpty);
     builder.addConditionToListByStartWith(path, option);
 
-    return await findListFromBuilderAndViewer(builder, user, false, option);
+    return findListFromBuilderAndViewer(builder, user, false, option);
   };
 
   /**
@@ -1096,8 +1114,8 @@ export const getPageSchema = (crowi) => {
     await this.removeRedirectOriginPageByPath(redirectPage.path);
   };
 
-  pageSchema.statics.findListByPathsArray = async function(paths) {
-    const queryBuilder = new PageQueryBuilder(this.find());
+  pageSchema.statics.findListByPathsArray = async function(paths, includeEmpty = false) {
+    const queryBuilder = new PageQueryBuilder(this.find(), includeEmpty);
     queryBuilder.addConditionToListByPathsArray(paths);
 
     return await queryBuilder.query.exec();

+ 22 - 15
packages/app/src/server/models/page.ts

@@ -41,7 +41,7 @@ export interface PageModel extends Model<PageDocument> {
   [x: string]: any; // for obsolete methods
   createEmptyPagesByPaths(paths: string[], publicOnly?: boolean): Promise<void>
   getParentIdAndFillAncestors(path: string): Promise<string | null>
-  findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: boolean): Promise<PageDocument[]>
+  findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: boolean, includeEmpty?: boolean): Promise<PageDocument[]>
   findTargetAndAncestorsByPathOrId(pathOrId: string): Promise<TargetAndAncestorsResult>
   findChildrenByParentPathOrIdAndViewer(parentPathOrId: string, user, userGroups?): Promise<PageDocument[]>
   findAncestorsChildrenByPathAndViewer(path: string, user, userGroups?): Promise<Record<string, PageDocument[]>>
@@ -143,7 +143,7 @@ const generateChildrenRegExp = (path: string): RegExp => {
  */
 schema.statics.createEmptyPagesByPaths = async function(paths: string[], publicOnly = false): Promise<void> {
   // find existing parents
-  const builder = new PageQueryBuilder(this.find(publicOnly ? { grant: GRANT_PUBLIC } : {}, { _id: 0, path: 1 }));
+  const builder = new PageQueryBuilder(this.find(publicOnly ? { grant: GRANT_PUBLIC } : {}, { _id: 0, path: 1 }), true);
   const existingPages = await builder
     .addConditionToListByPathsArray(paths)
     .query
@@ -165,7 +165,7 @@ schema.statics.createEmptyPagesByPaths = async function(paths: string[], publicO
 };
 
 /*
- * Find the pages parent and update if the parent exists.
+ * Find the parent and update if the parent exists.
  * If not,
  *   - first   run createEmptyPagesByPaths with ancestor's paths to ensure all the ancestors exist
  *   - second  update ancestor pages' parent
@@ -175,25 +175,27 @@ schema.statics.getParentIdAndFillAncestors = async function(path: string): Promi
   const parentPath = nodePath.dirname(path);
 
   const parent = await this.findOne({ path: parentPath }); // find the oldest parent which must always be the true parent
-  if (parent != null) { // fill parents if parent is null
+  if (parent != null) {
     return parent._id;
   }
 
+  /*
+   * Fill parents if parent is null
+   */
   const ancestorPaths = collectAncestorPaths(path); // paths of parents need to be created
 
   // just create ancestors with empty pages
   await this.createEmptyPagesByPaths(ancestorPaths);
 
   // find ancestors
-  const builder = new PageQueryBuilder(this.find({}, { _id: 1, path: 1 }));
+  const builder = new PageQueryBuilder(this.find({}, { _id: 1, path: 1 }), true);
   const ancestors = await builder
     .addConditionToListByPathsArray(ancestorPaths)
-    .addConditionToSortAncestorPages()
+    .addConditionToSortPagesByDescPath()
     .query
     .lean()
     .exec();
 
-
   const ancestorsMap = new Map(); // Map<path, _id>
   ancestors.forEach(page => ancestorsMap.set(page.path, page._id));
 
@@ -234,14 +236,14 @@ const addViewerCondition = async(queryBuilder: PageQueryBuilder, user, userGroup
  * Find a page by path and viewer. Pass false to useFindOne to use findOne method.
  */
 schema.statics.findByPathAndViewer = async function(
-    path: string | null, user, userGroups = null, useFindOne = true,
+    path: string | null, user, userGroups = null, useFindOne = true, includeEmpty = false,
 ): Promise<PageDocument | PageDocument[] | null> {
   if (path == null) {
     throw new Error('path is required.');
   }
 
   const baseQuery = useFindOne ? this.findOne({ path }) : this.find({ path });
-  const queryBuilder = new PageQueryBuilder(baseQuery);
+  const queryBuilder = new PageQueryBuilder(baseQuery, includeEmpty);
   await addViewerCondition(queryBuilder, user, userGroups);
 
   return queryBuilder.query.exec();
@@ -269,14 +271,14 @@ schema.statics.findTargetAndAncestorsByPathOrId = async function(pathOrId: strin
   ancestorPaths.push(path); // include target
 
   // Do not populate
-  const queryBuilder = new PageQueryBuilder(this.find());
+  const queryBuilder = new PageQueryBuilder(this.find(), true);
   await addViewerCondition(queryBuilder, user, userGroups);
 
   const _targetAndAncestors: PageDocument[] = await queryBuilder
     .addConditionAsMigrated()
     .addConditionToListByPathsArray(ancestorPaths)
     .addConditionToMinimizeDataForRendering()
-    .addConditionToSortAncestorPages()
+    .addConditionToSortPagesByDescPath()
     .query
     .lean()
     .exec();
@@ -298,15 +300,19 @@ schema.statics.findChildrenByParentPathOrIdAndViewer = async function(parentPath
   if (hasSlash(parentPathOrId)) {
     const path = parentPathOrId;
     const regexp = generateChildrenRE2(path);
-    queryBuilder = new PageQueryBuilder(this.find({ path: { $regex: regexp.source } }));
+    queryBuilder = new PageQueryBuilder(this.find({ path: { $regex: regexp.source } }), true);
   }
   else {
     const parentId = parentPathOrId;
-    queryBuilder = new PageQueryBuilder(this.find({ parent: parentId }));
+    queryBuilder = new PageQueryBuilder(this.find({ parent: parentId }), true);
   }
   await addViewerCondition(queryBuilder, user, userGroups);
 
-  return queryBuilder.query.lean().exec();
+  return queryBuilder
+    .addConditionToSortPagesByAscPath()
+    .query
+    .lean()
+    .exec();
 };
 
 schema.statics.findAncestorsChildrenByPathAndViewer = async function(path: string, user, userGroups = null): Promise<Record<string, PageDocument[]>> {
@@ -314,11 +320,12 @@ schema.statics.findAncestorsChildrenByPathAndViewer = async function(path: strin
   const regexps = ancestorPaths.map(path => new RegExp(generateChildrenRegExp(path))); // cannot use re2
 
   // get pages at once
-  const queryBuilder = new PageQueryBuilder(this.find({ path: { $in: regexps } }));
+  const queryBuilder = new PageQueryBuilder(this.find({ path: { $in: regexps } }), true);
   await addViewerCondition(queryBuilder, user, userGroups);
   const _pages = await queryBuilder
     .addConditionAsMigrated()
     .addConditionToMinimizeDataForRendering()
+    .addConditionToSortPagesByAscPath()
     .query
     .lean()
     .exec();

+ 1 - 1
packages/app/src/server/routes/apiv3/app-settings.js

@@ -7,7 +7,7 @@ const debug = require('debug')('growi:routes:admin');
 
 const express = require('express');
 
-const { pathUtils } = require('growi-commons');
+const { pathUtils } = require('@growi/core');
 const { listLocaleIds } = require('~/utils/locale-utils');
 
 const router = express.Router();

+ 3 - 2
packages/app/src/server/routes/apiv3/pages.js

@@ -3,7 +3,7 @@ import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:routes:apiv3:pages'); // eslint-disable-line no-unused-vars
 const express = require('express');
-const pathUtils = require('growi-commons').pathUtils;
+const { pathUtils } = require('@growi/core');
 const mongoose = require('mongoose');
 
 const { body } = require('express-validator');
@@ -715,8 +715,9 @@ module.exports = (crowi) => {
 
   router.get('/v5-migration-status', accessTokenParser, loginRequired, async(req, res) => {
     try {
+      const isV5Compatible = crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
       const migratablePagesCount = await crowi.pageService.v5MigratablePrivatePagesCount(req.user);
-      return res.apiv3({ migratablePagesCount });
+      return res.apiv3({ isV5Compatible, migratablePagesCount });
     }
     catch (err) {
       return res.apiv3Err(new ErrorV3('Failed to obtain migration status'));

+ 9 - 7
packages/app/src/server/routes/page.js

@@ -138,7 +138,7 @@ module.exports = function(crowi, app) {
   const logger = loggerFactory('growi:routes:page');
   const swig = require('swig-templates');
 
-  const pathUtils = require('growi-commons').pathUtils;
+  const { pathUtils } = require('@growi/core');
 
   const Page = crowi.model('Page');
   const User = crowi.model('User');
@@ -264,10 +264,10 @@ module.exports = function(crowi, app) {
     renderVars.pages = result.pages;
   }
 
-  async function addRenderVarsForPageTree(renderVars, path, user) {
-    const { targetAndAncestors, rootPage } = await Page.findTargetAndAncestorsByPathOrId(path, user);
+  async function addRenderVarsForPageTree(renderVars, pathOrId, user) {
+    const { targetAndAncestors, rootPage } = await Page.findTargetAndAncestorsByPathOrId(pathOrId, user);
 
-    if (targetAndAncestors.length === 0 && !isTopPage(path)) {
+    if (targetAndAncestors.length === 0 && pathOrId.includes('/') && !isTopPage(pathOrId)) {
       throw new Error('Ancestors must have at least one page.');
     }
 
@@ -291,6 +291,7 @@ module.exports = function(crowi, app) {
 
   async function _notFound(req, res) {
     const path = getPathFromRequest(req);
+    const pathOrId = req.params.id || path;
 
     let view;
     const renderVars = { path };
@@ -326,6 +327,7 @@ module.exports = function(crowi, app) {
     const limit = 50;
     const offset = parseInt(req.query.offset) || 0;
     await addRenderVarsForDescendants(renderVars, path, req.user, offset, limit, true);
+    await addRenderVarsForPageTree(renderVars, pathOrId, req.user);
 
     return res.render(view, renderVars);
   }
@@ -334,7 +336,7 @@ module.exports = function(crowi, app) {
     const id = req.params.id;
     const { revisionId } = req.query;
 
-    let page = await Page.findByIdAndViewer(id, req.user);
+    let page = await Page.findByIdAndViewer(id, req.user, null, true, true);
 
     if (page == null) {
       next();
@@ -395,7 +397,7 @@ module.exports = function(crowi, app) {
     const id = req.params.id;
     const revisionId = req.query.revision;
 
-    let page = await Page.findByIdAndViewer(id, req.user);
+    let page = await Page.findByIdAndViewer(id, req.user, null, true, true);
 
     if (page == null) {
       // check the page is forbidden or just does not exist.
@@ -592,7 +594,7 @@ module.exports = function(crowi, app) {
    * redirector
    */
   async function redirector(req, res, next, path) {
-    const pages = await Page.findByPathAndViewer(path, req.user, null, false);
+    const pages = await Page.findByPathAndViewer(path, req.user, null, false, true);
     const { redirectFrom } = req.query;
 
     if (pages.length >= 2) {

+ 1 - 1
packages/app/src/server/service/config-loader.ts

@@ -1,4 +1,4 @@
-import { envUtils } from 'growi-commons';
+import { envUtils } from '@growi/core';
 
 import loggerFactory from '~/utils/logger';
 

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

@@ -1001,7 +1001,7 @@ class PageService {
         await Page.createEmptyPagesByPaths(parentPaths, publicOnly);
 
         // find parents again
-        const builder = new PageQueryBuilder(Page.find({}, { _id: 1, path: 1 }));
+        const builder = new PageQueryBuilder(Page.find({}, { _id: 1, path: 1 }), true);
         const parents = await builder
           .addConditionToListByPathsArray(parentPaths)
           .query

+ 1 - 1
packages/app/src/server/service/slack-command-handler/create-page-service.js

@@ -3,7 +3,7 @@ import loggerFactory from '~/utils/logger';
 const logger = loggerFactory('growi:service:CreatePageService');
 const { reshapeContentsBody, respond, markdownSectionBlock } = require('@growi/slack');
 const mongoose = require('mongoose');
-const pathUtils = require('growi-commons').pathUtils;
+const { pathUtils } = require('@growi/core');
 
 class CreatePageService {
 

+ 1 - 1
packages/app/src/server/service/slack-command-handler/search.js

@@ -41,7 +41,7 @@ module.exports = (crowi) => {
 
     const { searchService } = crowi;
     const options = { limit: PAGINGLIMIT, offset };
-    const results = await searchService.searchKeyword(keywords, null, {}, options);
+    const [results] = await searchService.searchKeyword(keywords, null, {}, options);
     const resultsTotal = results.meta.total;
 
     const pages = results.data.map((data) => {

+ 1 - 1
packages/app/src/server/util/middlewares.js

@@ -5,7 +5,7 @@ import loggerFactory from '~/utils/logger';
 // eslint-disable-next-line no-unused-vars
 
 const { formatDistanceStrict } = require('date-fns');
-const pathUtils = require('growi-commons').pathUtils;
+const { pathUtils } = require('@growi/core');
 const md5 = require('md5');
 const entities = require('entities');
 

+ 1 - 1
packages/app/src/server/util/swigFunctions.js

@@ -2,7 +2,7 @@ module.exports = function(crowi, req, locals) {
   const debug = require('debug')('growi:lib:swigFunctions');
   const stringWidth = require('string-width');
 
-  const { pathUtils } = require('growi-commons');
+  const { pathUtils } = require('@growi/core');
 
   const Page = crowi.model('Page');
   const User = crowi.model('User');

+ 5 - 0
packages/app/src/server/views/layout/layout.html

@@ -125,6 +125,11 @@
   {{ userUISettings|json|safe }}
   </script>
 {% endif %}
+{% if targetAndAncestors != null %}
+  <script type="application/json" id="growi-pagetree-target-and-ancestors">
+  {{ targetAndAncestors|json|safe }}
+  </script>
+{% endif %}
 
 
 {% block custom_script %}

+ 0 - 1
packages/app/src/server/views/widget/page_content.html

@@ -27,7 +27,6 @@
   data-page-user="{% if pageUser %}{{ pageUser|json }}{% else %}null{% endif %}"
   data-share-links-number="{% if page %}{{ sharelinksNumber }}{% endif %}"
   data-share-link-id="{% if sharelink %}{{ sharelink._id|json }}{% endif %}"
-  data-target-and-ancestors="{% if targetAndAncestors %}{{ targetAndAncestors|json }}{% endif %}"
   >
 {% else %}
 <div id="content-main" class="content-main d-flex"

+ 1 - 1
packages/app/src/services/cdn-resources-service.js

@@ -4,7 +4,7 @@ import { resolveFromRoot } from '~/utils/project-dir-utils';
 const { URL } = require('url');
 const urljoin = require('url-join');
 
-const { envUtils } = require('growi-commons');
+const { envUtils } = require('@growi/core');
 
 const cdnLocalScriptRoot = 'public/static/js/cdn';
 const cdnLocalScriptWebRoot = '/static/js/cdn';

+ 2 - 2
packages/app/src/stores/page-listing.tsx

@@ -46,12 +46,12 @@ export const useSWRxPageChildren = (
 };
 
 export const useSWRxV5MigrationStatus = (
-    shouldFetch = true,
 ): SWRResponse<V5MigrationStatus, Error> => {
   return useSWR(
-    shouldFetch ? '/pages/v5-migration-status' : null,
+    '/pages/v5-migration-status',
     endpoint => apiv3Get(endpoint).then((response) => {
       return {
+        isV5Compatible: response.data.isV5Compatible,
         migratablePagesCount: response.data.migratablePagesCount,
       };
     }),

+ 0 - 129
packages/app/src/test/integration/service/page.test.js

@@ -821,133 +821,4 @@ describe('PageService', () => {
     });
   });
 
-  describe('v5MigrationByPageIds()', () => {
-    test('should migrate all pages specified by pageIds', async() => {
-      jest.restoreAllMocks();
-
-      // initialize pages for test
-      const pages = await Page.insertMany([
-        {
-          path: '/private1',
-          grant: Page.GRANT_OWNER,
-          creator: testUser1,
-          lastUpdateUser: testUser1,
-        },
-        {
-          path: '/dummyParent/private1',
-          grant: Page.GRANT_OWNER,
-          creator: testUser1,
-          lastUpdateUser: testUser1,
-        },
-        {
-          path: '/dummyParent/private1/private2',
-          grant: Page.GRANT_OWNER,
-          creator: testUser1,
-          lastUpdateUser: testUser1,
-        },
-        {
-          path: '/dummyParent/private1/private3',
-          grant: Page.GRANT_OWNER,
-          creator: testUser1,
-          lastUpdateUser: testUser1,
-        },
-      ]);
-
-      const pageIds = pages.map(page => page._id);
-      // migrate
-      await crowi.pageService.v5MigrationByPageIds(pageIds);
-
-      const migratedPages = await Page.find({
-        path: {
-          $in: ['/private1', '/dummyParent', '/dummyParent/private1', '/dummyParent/private1/private2', '/dummyParent/private1/private3'],
-        },
-      });
-      const migratedPagePaths = migratedPages.filter(doc => doc.parent != null).map(doc => doc.path);
-
-      const expected = ['/private1', '/dummyParent', '/dummyParent/private1', '/dummyParent/private1/private2', '/dummyParent/private1/private3'];
-
-      expect(migratedPagePaths.sort()).toStrictEqual(expected.sort());
-    });
-
-  });
-
-  describe('v5InitialMigration()', () => {
-    test('should migrate all public pages & replace private parents with empty pages', async() => {
-      jest.restoreAllMocks();
-
-      // initialize pages for test
-      const pages = await Page.insertMany([
-        {
-          path: '/publicA',
-          grant: Page.GRANT_PUBLIC,
-          creator: testUser1,
-          lastUpdateUser: testUser1,
-        },
-        {
-          path: '/publicA/privateB',
-          grant: Page.GRANT_OWNER,
-          creator: testUser1,
-          lastUpdateUser: testUser1,
-        },
-        {
-          path: '/publicA/privateB/publicC',
-          grant: Page.GRANT_PUBLIC,
-          creator: testUser1,
-          lastUpdateUser: testUser1,
-        },
-        {
-          path: '/parenthesis/(a)[b]{c}d',
-          grant: Page.GRANT_PUBLIC,
-          creator: testUser1,
-          lastUpdateUser: testUser1,
-        },
-        {
-          path: '/parenthesis/(a)[b]{c}d/public',
-          grant: Page.GRANT_PUBLIC,
-          creator: testUser1,
-          lastUpdateUser: testUser1,
-        },
-      ]);
-
-      const parent = await Page.find({ path: '/' });
-      await Page.insertMany([
-        {
-          path: '/migratedD',
-          grant: Page.GRANT_PUBLIC,
-          creator: testUser1,
-          lastUpdateUser: testUser1,
-          parent: parent._id,
-        },
-      ]);
-
-      // migrate
-      await crowi.pageService.v5InitialMigration(Page.GRANT_PUBLIC);
-
-      const nMigratedPages = await Page.count({
-        path: {
-          $in: ['/publicA', '/publicA/privateB/publicC', '/parenthesis/(a)[b]{c}d', '/parenthesis/(a)[b]{c}d/public', '/migratedD'],
-        },
-        isEmpty: false,
-        parent: { $ne: null },
-      });
-      const nMigratedEmptyPages = await Page.count({
-        path: {
-          $in: ['/publicA/privateB', '/parenthesis'],
-        },
-        isEmpty: true,
-        parent: { $ne: null },
-      });
-      const nNonMigratedPages = await Page.count({
-        path: {
-          $in: ['/publicA/privateB'],
-        },
-        parent: null,
-      });
-
-      expect(nMigratedPages).toBe(5);
-      expect(nMigratedEmptyPages).toBe(2);
-      expect(nNonMigratedPages).toBe(1);
-    });
-  });
-
 });

+ 194 - 0
packages/app/src/test/integration/service/v5-migration.test.js

@@ -0,0 +1,194 @@
+const mongoose = require('mongoose');
+
+const { getInstance } = require('../setup-crowi');
+
+describe('V5 page migration', () => {
+  let crowi;
+  let Page;
+
+  let testUser1;
+
+  beforeAll(async() => {
+    jest.restoreAllMocks();
+
+    crowi = await getInstance();
+    Page = mongoose.model('Page');
+  });
+
+
+  describe('v5MigrationByPageIds()', () => {
+    test('should migrate all pages specified by pageIds', async() => {
+      jest.restoreAllMocks();
+
+      // initialize pages for test
+      const pages = await Page.insertMany([
+        {
+          path: '/private1',
+          grant: Page.GRANT_OWNER,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+        },
+        {
+          path: '/dummyParent/private1',
+          grant: Page.GRANT_OWNER,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+        },
+        {
+          path: '/dummyParent/private1/private2',
+          grant: Page.GRANT_OWNER,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+        },
+        {
+          path: '/dummyParent/private1/private3',
+          grant: Page.GRANT_OWNER,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+        },
+      ]);
+
+      const pageIds = pages.map(page => page._id);
+      // migrate
+      await crowi.pageService.v5MigrationByPageIds(pageIds);
+
+      const migratedPages = await Page.find({
+        path: {
+          $in: ['/private1', '/dummyParent', '/dummyParent/private1', '/dummyParent/private1/private2', '/dummyParent/private1/private3'],
+        },
+      });
+      const migratedPagePaths = migratedPages.filter(doc => doc.parent != null).map(doc => doc.path);
+
+      const expected = ['/private1', '/dummyParent', '/dummyParent/private1', '/dummyParent/private1/private2', '/dummyParent/private1/private3'];
+
+      expect(migratedPagePaths.sort()).toStrictEqual(expected.sort());
+    });
+
+  });
+
+  describe('v5InitialMigration()', () => {
+    let createPagePaths;
+    let allPossiblePagePaths;
+    beforeAll(async() => {
+      createPagePaths = [
+        '/publicA', '/publicA/privateB', '/publicA/privateB/publicC', '/parenthesis/(a)[b]{c}d', '/parenthesis/(a)[b]{c}d/public', '/migratedD',
+      ];
+      allPossiblePagePaths = [...createPagePaths, '/parenthesis', '/'];
+
+      // initialize pages for test
+      await Page.insertMany([
+        {
+          path: '/publicA',
+          grant: Page.GRANT_PUBLIC,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+        },
+        {
+          path: '/publicA/privateB',
+          grant: Page.GRANT_OWNER,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+        },
+        {
+          path: '/publicA/privateB/publicC',
+          grant: Page.GRANT_PUBLIC,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+        },
+        {
+          path: '/parenthesis/(a)[b]{c}d',
+          grant: Page.GRANT_PUBLIC,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+        },
+        {
+          path: '/parenthesis/(a)[b]{c}d/public',
+          grant: Page.GRANT_PUBLIC,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+        },
+      ]);
+
+      const parent = await Page.find({ path: '/' });
+      await Page.insertMany([
+        {
+          path: '/migratedD',
+          grant: Page.GRANT_PUBLIC,
+          creator: testUser1,
+          lastUpdateUser: testUser1,
+          parent: parent._id,
+        },
+      ]);
+
+      // migrate
+      await crowi.pageService.v5InitialMigration(Page.GRANT_PUBLIC);
+    });
+
+    test('should migrate all public pages', async() => {
+      const migratedPages = await Page.find({
+        path: {
+          $in: allPossiblePagePaths,
+        },
+        parent: { $ne: null },
+      });
+      const migratedEmptyPages = await Page.find({
+        path: {
+          $in: allPossiblePagePaths,
+        },
+        isEmpty: true,
+        parent: { $ne: null },
+      });
+      const nonMigratedPages = await Page.find({
+        path: {
+          $in: allPossiblePagePaths,
+        },
+        parent: null,
+      });
+
+      const migratedPaths = migratedPages.map(page => page.path).sort();
+      const migratedEmptyPaths = migratedEmptyPages.map(page => page.path).sort();
+      const nonMigratedPaths = nonMigratedPages.map(page => page.path).sort();
+
+      const expectedMigratedPaths = allPossiblePagePaths.filter(path => path !== '/').sort();
+      const expectedMigratedEmptyPaths = ['/publicA/privateB', '/parenthesis'].sort();
+      const expectedNonMigratedPaths = ['/publicA/privateB', '/'].sort();
+
+      expect(migratedPaths).toStrictEqual(expectedMigratedPaths);
+      expect(migratedEmptyPaths).toStrictEqual(expectedMigratedEmptyPaths);
+      expect(nonMigratedPaths).toStrictEqual(expectedNonMigratedPaths);
+    });
+  });
+
+  test('replace private parents with empty pages', async() => {
+    const replacedPathPages = await Page.find({ path: '/publicA/privateB' }); // ex-private page
+
+    const _newEmptyPage = replacedPathPages.filter(page => page.parent != null)[0];
+    const newEmptyPage = {
+      path: _newEmptyPage.path,
+      grant: _newEmptyPage.grant,
+      isEmpty: _newEmptyPage.isEmpty,
+    };
+    const expectedNewEmptyPage = {
+      path: '/publicA/privateB',
+      grant: Page.GRANT_PUBLIC,
+      isEmpty: true,
+    };
+
+    const _privatePage = replacedPathPages.filter(page => page.parent == null)[0];
+    const privatePage = {
+      path: _privatePage.path,
+      grant: _privatePage.grant,
+      isEmpty: _privatePage.isEmpty,
+    };
+    const expectedPrivatePage = {
+      path: '/publicA/privateB',
+      grant: Page.GRANT_OWNER,
+      isEmpty: false,
+    };
+
+    expect(replacedPathPages.length).toBe(2);
+    expect(newEmptyPage).toStrictEqual(expectedNewEmptyPage);
+    expect(privatePage).toStrictEqual(expectedPrivatePage);
+  });
+
+});

+ 0 - 27
packages/core/README.md

@@ -1,27 +0,0 @@
-# growi-commons
-
-[![dependencies status](https://david-dm.org/weseek/growi-commons.svg)](https://david-dm.org/weseek/growi-commons)
-[![devDependencies Status](https://david-dm.org/weseek/growi-commons/dev-status.svg)](https://david-dm.org/weseek/growi-commons?type=dev)
-[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
-
-[GROWI](https://growi.org) Commons Libraries to develop GROWI and plugins
-
-
-Overview
---------
-
-growi-commons package is includes some functions, classes and modules to develop GROWI substance and GROWI plugins.
-
-Install
---------
-
-1. install plugin
-
-    ```
-    $ npm install --save growi-commons
-    ```
-
-Documentation
-------------
-
-See https://docs.growi.org/api/commons/

+ 11 - 14
packages/core/src/index.js

@@ -1,22 +1,19 @@
 import * as _pathUtils from './utils/path-utils';
+import * as _envUtils from './utils/env-utils';
 import * as _pagePathUtils from './utils/page-path-utils';
 import * as _templateChecker from './utils/template-checker';
-
-// module.exports = {
-//   BasicInterceptor: require('./utils/basic-interceptor'),
-//   envUtils: require('./utils/env-utils'),
-//   // plugin
-//   customTagUtils: require('./plugin/util/custom-tag-utils'),
-//   TagCacheManager: require('./plugin/service/tag-cache-manager'),
-//   // service
-//   LocalStorageManager: require('./service/localstorage-manager'),
-// };
-
-export * from './plugin/interfaces/plugin-definition-v4';
-export * from './models/devided-page-path';
-export * from './utils/mongoose-utils';
+import * as _customTagUtils from './plugin/util/custom-tag-utils';
 
 // export utils
 export const pathUtils = _pathUtils;
+export const envUtils = _envUtils;
 export const pagePathUtils = _pagePathUtils;
 export const templateChecker = _templateChecker;
+export const customTagUtils = _customTagUtils;
+
+export * from './plugin/interfaces/plugin-definition-v4';
+export * from './plugin/service/tag-cache-manager';
+export * from './models/devided-page-path';
+export * from './service/localstorage-manager';
+export * from './utils/basic-interceptor';
+export * from './utils/mongoose-utils';

+ 2 - 4
packages/core/src/plugin/service/tag-cache-manager.js

@@ -1,9 +1,9 @@
-const LocalStorageManager = require('../../service/localstorage-manager');
+import { LocalStorageManager } from '../../service/localstorage-manager';
 
 /**
  * Service Class for caching React state and TagContext
  */
-class TagCacheManager {
+export class TagCacheManager {
 
   /**
    * @callback generateCacheKey
@@ -67,5 +67,3 @@ class TagCacheManager {
   }
 
 }
-
-module.exports = TagCacheManager;

+ 1 - 3
packages/core/src/service/localstorage-manager.js

@@ -1,5 +1,5 @@
 let _instance = null;
-class LocalStorageManager {
+export class LocalStorageManager {
 
   static getInstance() {
     if (_instance == null) {
@@ -52,5 +52,3 @@ class LocalStorageManager {
   }
 
 }
-
-module.exports = LocalStorageManager;

+ 1 - 3
packages/core/src/utils/basic-interceptor.js

@@ -1,7 +1,7 @@
 /**
  * Basic Interceptor class
  */
-class BasicInterceptor {
+export class BasicInterceptor {
 
   /**
    * getter for id
@@ -43,5 +43,3 @@ class BasicInterceptor {
   }
 
 }
-
-module.exports = BasicInterceptor;

+ 1 - 1
packages/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPostRenderInterceptor.js

@@ -1,7 +1,7 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
 
-import { BasicInterceptor } from 'growi-commons';
+import { BasicInterceptor } from '@growi/core';
 
 import RefsContext from '../RefsContext';
 import GalleryContext from '../GalleryContext';

+ 1 - 1
packages/plugin-attachment-refs/src/client/js/util/Interceptor/RefsPreRenderInterceptor.js

@@ -1,4 +1,4 @@
-import { customTagUtils, BasicInterceptor } from 'growi-commons';
+import { customTagUtils, BasicInterceptor } from '@growi/core';
 
 import TagCacheManagerFactory from '../TagCacheManagerFactory';
 

+ 1 - 1
packages/plugin-attachment-refs/src/client/js/util/RefsContext.js

@@ -1,6 +1,6 @@
 import * as url from 'url';
 
-import { customTagUtils, pathUtils } from 'growi-commons';
+import { customTagUtils, pathUtils } from '@growi/core';
 
 const { TagContext, ArgsParser, OptionParser } = customTagUtils;
 

+ 1 - 1
packages/plugin-attachment-refs/src/client/js/util/TagCacheManagerFactory.js

@@ -1,4 +1,4 @@
-import { TagCacheManager } from 'growi-commons';
+import { TagCacheManager } from '@growi/core';
 
 const STATE_CACHE_NS = 'refs-state-cache';
 

+ 1 - 1
packages/plugin-attachment-refs/src/server/routes/refs.js

@@ -1,6 +1,6 @@
 import loggerFactory from '../../utils/logger';
 
-const { customTagUtils } = require('growi-commons');
+const { customTagUtils } = require('@growi/core');
 
 const { OptionParser } = customTagUtils;
 

+ 1 - 1
packages/plugin-lsx/src/client/js/components/Lsx.jsx

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 
 import * as url from 'url';
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 
 // eslint-disable-next-line no-unused-vars
 import styles from '../../css/index.css';

+ 1 - 1
packages/plugin-lsx/src/client/js/components/LsxPageList/LsxPage.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 
 import { PageListMeta } from '@growi/ui';
 

+ 1 - 1
packages/plugin-lsx/src/client/js/util/Interceptor/LsxLogoutInterceptor.js

@@ -1,4 +1,4 @@
-import { BasicInterceptor } from 'growi-commons';
+import { BasicInterceptor } from '@growi/core';
 
 import { TagCacheManagerFactory } from '../TagCacheManagerFactory';
 

+ 1 - 1
packages/plugin-lsx/src/client/js/util/Interceptor/LsxPostRenderInterceptor.js

@@ -1,7 +1,7 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
 
-import { BasicInterceptor } from 'growi-commons';
+import { BasicInterceptor } from '@growi/core';
 
 import { LsxContext } from '../LsxContext';
 import { Lsx } from '../../components/Lsx';

+ 1 - 1
packages/plugin-lsx/src/client/js/util/Interceptor/LsxPreRenderInterceptor.js

@@ -1,5 +1,5 @@
 import ReactDOM from 'react-dom';
-import { customTagUtils, BasicInterceptor } from 'growi-commons';
+import { customTagUtils, BasicInterceptor } from '@growi/core';
 
 /**
  * The interceptor for lsx

+ 1 - 1
packages/plugin-lsx/src/client/js/util/LsxContext.js

@@ -1,6 +1,6 @@
 import * as url from 'url';
 
-import { customTagUtils, pathUtils } from 'growi-commons';
+import { customTagUtils, pathUtils } from '@growi/core';
 
 const { TagContext, ArgsParser, OptionParser } = customTagUtils;
 

+ 1 - 18
packages/plugin-lsx/src/client/js/util/TagCacheManagerFactory.js

@@ -1,29 +1,12 @@
-import { TagCacheManager } from 'growi-commons';
+import { TagCacheManager } from '@growi/core';
 
 const LSX_STATE_CACHE_NS = 'lsx-state-cache';
 
 
-// validate growi-commons version
-function validateGrowiCommonsVersion() {
-  // TagCacheManager was created on growi-commons@4.0.7
-  if (TagCacheManager == null) {
-    throw new Error(
-      'This version of \'growi-plugin-lsx\' requires \'growi-commons >= 4.0.7\'.\n'
-      + 'To resolve this, please process  either a) or b).\n'
-      + '\n'
-      + 'a) Use \'growi-plugin-lsx@3.0.0\'\n'
-      + 'b) Edit \'package.json\' of growi and upgrade \'growi-commons\' to v4.0.7 or above.',
-    );
-  }
-}
-
-
 let _instance;
 export class TagCacheManagerFactory {
 
   static getInstance() {
-    validateGrowiCommonsVersion();
-
     if (_instance == null) {
       // create generateCacheKey implementation
       const generateCacheKey = (lsxContext) => {

+ 1 - 1
packages/plugin-lsx/src/server/routes/lsx.js

@@ -1,4 +1,4 @@
-const { customTagUtils } = require('growi-commons');
+const { customTagUtils } = require('@growi/core');
 
 const { OptionParser } = customTagUtils;
 

+ 0 - 27
packages/ui/README.md

@@ -1,27 +0,0 @@
-# growi-commons
-
-[![dependencies status](https://david-dm.org/weseek/growi-commons.svg)](https://david-dm.org/weseek/growi-commons)
-[![devDependencies Status](https://david-dm.org/weseek/growi-commons/dev-status.svg)](https://david-dm.org/weseek/growi-commons?type=dev)
-[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
-
-[GROWI](https://growi.org) Commons Libraries to develop GROWI and plugins
-
-
-Overview
---------
-
-growi-commons package is includes some functions, classes and modules to develop GROWI substance and GROWI plugins.
-
-Install
---------
-
-1. install plugin
-
-    ```
-    $ npm install --save growi-commons
-    ```
-
-Documentation
-------------
-
-See https://docs.growi.org/api/commons/

+ 5 - 5
yarn.lock

@@ -9440,11 +9440,6 @@ grapheme-splitter@^1.0.4:
   resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
   integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
 
-growi-commons@^5.0.4:
-  version "5.0.4"
-  resolved "https://registry.yarnpkg.com/growi-commons/-/growi-commons-5.0.4.tgz#1235b7955a3f492803e8c714fef5c4a797e442b7"
-  integrity sha512-K282Pe97SnJgbZWAuMz9pNDTmvmw4JYPf/oYQaPmBsUjaxG4FDwd7+p5UFc5GqZUWcLwXvtJZQZMZEH/xpg+nA==
-
 growly@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
@@ -21295,6 +21290,11 @@ ws@^7.4.6:
   resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.1.tgz#44fc000d87edb1d9c53e51fbc69a0ac1f6871d66"
   integrity sha512-2c6faOUH/nhoQN6abwMloF7Iyl0ZS2E9HGtsiLrWn0zOOMWlhtDmdf/uihDt6jnuCxgtwGBNy6Onsoy2s2O2Ow==
 
+ws@^8.3.0:
+  version "8.3.0"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-8.3.0.tgz#7185e252c8973a60d57170175ff55fdbd116070d"
+  integrity sha512-Gs5EZtpqZzLvmIM59w4igITU57lrtYVFneaa434VROv4thzJyV6UjIL3D42lslWlI+D4KzLYnxSwtfuiO79sNw==
+
 ws@~7.4.2:
   version "7.4.6"
   resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"