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

Merge pull request #6326 from weseek/master

Release v5.1.1
Yuki Takei 3 лет назад
Родитель
Сommit
483b66f3c5
51 измененных файлов с 457 добавлено и 138 удалено
  1. 3 0
      .github/workflows/reusable-app-prod.yml
  2. 4 1
      .vscode/settings.json
  3. 1 1
      lerna.json
  4. 3 3
      package.json
  5. 11 9
      packages/app/package.json
  6. 9 3
      packages/app/public/static/locales/en_US/admin/admin.json
  7. 1 0
      packages/app/public/static/locales/en_US/translation.json
  8. 8 2
      packages/app/public/static/locales/ja_JP/admin/admin.json
  9. 1 0
      packages/app/public/static/locales/ja_JP/translation.json
  10. 9 2
      packages/app/public/static/locales/zh_CN/admin/admin.json
  11. 1 0
      packages/app/public/static/locales/zh_CN/translation.json
  12. 3 0
      packages/app/src/client/interfaces/clearable.ts
  13. 11 2
      packages/app/src/components/Admin/AuditLog/ActivityTable.tsx
  14. 18 2
      packages/app/src/components/Admin/AuditLog/AuditLogSettings.tsx
  15. 18 3
      packages/app/src/components/Admin/AuditLog/SearchUsernameTypeahead.tsx
  16. 32 8
      packages/app/src/components/Admin/AuditLogManagement.tsx
  17. 2 1
      packages/app/src/components/Common/Dropdown/PageItemControl.tsx
  18. 1 0
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  19. 2 6
      packages/app/src/components/Navbar/SubNavButtons.tsx
  20. 5 1
      packages/app/src/components/Page/FixPageGrantAlert.tsx
  21. 1 1
      packages/app/src/components/ShareLink/ShareLink.jsx
  22. 2 2
      packages/app/src/components/Sidebar/PageTree/Item.tsx
  23. 2 0
      packages/app/src/interfaces/activity.ts
  24. 15 4
      packages/app/src/interfaces/page-operation.ts
  25. 7 6
      packages/app/src/migrations/20211129125654-initialize-private-legacy-pages-named-query.js
  26. 2 1
      packages/app/src/server/crowi/index.js
  27. 0 12
      packages/app/src/server/models/activity.ts
  28. 1 16
      packages/app/src/server/models/page-operation.ts
  29. 6 5
      packages/app/src/server/models/password-reset-order.ts
  30. 1 1
      packages/app/src/server/models/user-registration-order.ts
  31. 26 3
      packages/app/src/server/routes/apiv3/activity.ts
  32. 7 1
      packages/app/src/server/routes/apiv3/pages.js
  33. 15 5
      packages/app/src/server/service/page-operation.ts
  34. 88 9
      packages/app/src/server/service/page.ts
  35. 3 0
      packages/app/src/server/util/rate-limiter.ts
  36. 4 2
      packages/app/src/services/renderer/markdown-it/link-by-relative-path.ts
  37. 1 1
      packages/app/src/stores/activity.ts
  38. 5 0
      packages/app/src/utils/page-operation.ts
  39. 11 0
      packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts
  40. 1 1
      packages/app/test/integration/service/v5.page.test.ts
  41. 1 1
      packages/app/test/integration/service/v5.public-page.test.ts
  42. 1 1
      packages/codemirror-textlint/package.json
  43. 1 1
      packages/core/package.json
  44. 1 1
      packages/plugin-attachment-refs/package.json
  45. 1 1
      packages/plugin-lsx/package.json
  46. 1 1
      packages/plugin-pukiwiki-like-linker/package.json
  47. 1 1
      packages/slack/package.json
  48. 3 3
      packages/slackbot-proxy/package.json
  49. 1 0
      packages/ui/.gitignore
  50. 1 1
      packages/ui/package.json
  51. 104 13
      yarn.lock

+ 3 - 0
.github/workflows/reusable-app-prod.yml

@@ -206,6 +206,9 @@ jobs:
     steps:
     - uses: actions/checkout@v3
 
+    - name: Install fonts
+      run: sudo apt install fonts-noto
+
     - uses: actions/setup-node@v3
       with:
         node-version: ${{ matrix.node-version }}

+ 4 - 1
.vscode/settings.json

@@ -21,5 +21,8 @@
   "editor.codeActionsOnSave": {
     "source.fixAll.eslint": true,
     "source.fixAll.markdownlint": true
-  }
+  },
+  "githubPullRequests.ignoredPullRequestBranches": [
+    "master"
+  ]
 }

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
   "npmClient": "yarn",
   "useWorkspaces": true,
-  "version": "5.1.0",
+  "version": "5.1.1-RC.0",
   "packages": [
     "packages/*"
   ]

+ 3 - 3
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "5.1.0",
+  "version": "5.1.1-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -81,9 +81,9 @@
     "stylelint": "^14.2.0",
     "stylelint-config-recess-order": "^3.0.0",
     "ts-jest": "^27.0.4",
-    "ts-node": "^9.1.1",
+    "ts-node": "^10.9.1",
     "tsconfig-paths": "^3.9.0",
-    "typescript": "~4.6",
+    "typescript": "~4.7",
     "yargs": "^17.3.1"
   },
   "engines": {

+ 11 - 9
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "5.1.0",
+  "version": "5.1.1-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -64,11 +64,11 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.1.0",
-    "@growi/plugin-attachment-refs": "^5.1.0",
-    "@growi/plugin-lsx": "^5.1.0",
-    "@growi/plugin-pukiwiki-like-linker": "^5.1.0",
-    "@growi/slack": "^5.1.0",
+    "@growi/codemirror-textlint": "^5.1.1-RC.0",
+    "@growi/plugin-attachment-refs": "^5.1.1-RC.0",
+    "@growi/plugin-lsx": "^5.1.1-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^5.1.1-RC.0",
+    "@growi/slack": "^5.1.1-RC.0",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
@@ -166,11 +166,12 @@
   "// comments for defDependencies": {
     "@handsontable/react": "v3 requires handsontable >= 7.0.0.",
     "handsontable": "v7.0.0 or above is no loger MIT lisence.",
-    "ts-loader": "v9 is not compatible with webpack@5"
+    "ts-loader": "v9 is not compatible with webpack@5",
+    "ts-node": "v10 occurs 'SyntaxError: Cannot use import statement outside a module' when using migrate-mongo"
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^5.1.0",
+    "@growi/ui": "^5.1.1-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
@@ -254,7 +255,8 @@
     "throttle-debounce": "^3.0.1",
     "toastr": "^2.1.2",
     "ts-loader": "^8.3.0",
-    "ts-node-dev": "^1.1.6",
+    "ts-node": "^9.1.1",
+    "ts-node-dev": "^2.0.0",
     "tsc-alias": "^1.2.9",
     "tsconfig-paths-webpack-plugin": "^3.5.1",
     "unstated": "^2.1.1",

+ 9 - 3
packages/app/public/static/locales/en_US/admin/admin.json

@@ -422,7 +422,7 @@
     }
   },
   "slack_integration_legacy": {
-    "alert_disabled": "This 'Legacy Slack Integration' is currently disabled because <a href='/admin/slack-integration'>the new settings</a> is enabled.",
+    "alert_Pd": "This 'Legacy Slack Integration' is currently disabled because <a href='/admin/slack-integration'>the new settings</a> is enabled.",
     "alert_deplicated": "This 'Legacy Slack Integration' is outdated and will be discontinued in the future. Use <a href='/admin/slack-integration'>the new settings</a> instead. "
   },
   "user_management": {
@@ -529,6 +529,7 @@
     }
   },
   "audit_log_management": {
+    "user": "User",
     "username": "Username",
     "date": "Date",
     "action": "Action",
@@ -536,13 +537,18 @@
     "url": "URL",
     "settings": "Settings",
     "return": "Return",
+    "clear": "Clear search criteria",
+    "reload": "Reload",
     "activity_expiration_date": "Audit Log expiration date",
     "activity_expiration_date_explain": "Created Audit Log are automatically deleted after the number of seconds set in the environment variable from the creation time",
     "fixed_by_env_var": "This is fixed by the env var <code>{{key}}={{value}}</code>.",
     "available_action_list": "Search / View All Available Actions",
-    "available_action_list_explain": "List of actions that can be search / view in the Audit Log",
+    "available_action_list_explain": "List of actions that can be searched/viewed in the current settings",
     "action_list": "Action List",
-    "disable_mode_explain": "Audit log is currently disabled. To enable it, set the environment variable <code>AUDIT_LOG_ENABLED</code> to true."
+    "disable_mode_explain": "Audit log is currently disabled. To enable it, set the environment variable <code>AUDIT_LOG_ENABLED</code> to true.",
+    "docs_url": {
+      "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
+    }
   },
   "audit_log_action_category": {
     "Page": "Page",

+ 1 - 0
packages/app/public/static/locales/en_US/translation.json

@@ -1093,6 +1093,7 @@
       "no_grant_available": "The list of selectable permissions could not be found. Please modify the permissions on the parent page first and try again.",
       "need_to_fix_grant": "The permissions associated with this page must be modified in order to use the functionality correctly. <br> Please select from the options below to make the change.",
       "grant_label": {
+        "public": "Public",
         "isForbidden": "Authority not allowed to view",
         "currentPageGrantLabel": "Authorization for this page: ",
         "parentPageGrantLabel": "Authority of parent page: ",

+ 8 - 2
packages/app/public/static/locales/ja_JP/admin/admin.json

@@ -528,6 +528,7 @@
     }
   },
   "audit_log_management": {
+    "user": "ユーザー",
     "username": "ユーザー名",
     "date": "日付",
     "action": "アクション",
@@ -535,13 +536,18 @@
     "url": "URL",
     "settings": "設定",
     "return": "戻る",
+    "clear": "検索条件のクリア",
+    "reload": "再読み込み",
     "activity_expiration_date": "監査ログの有効期限",
     "activity_expiration_date_explain": "作成された監査ログは、作成時間から環境変数に設定した秒数後に自動的に削除されます",
     "fixed_by_env_var": "環境変数により固定されています <code>{{key}}={{value}}</code>.",
     "available_action_list": "検索 / 表示 可能なアクション一覧",
-    "available_action_list_explain": "監査ログで 検索 / 表示 可能なアクション一覧です",
+    "available_action_list_explain": "現在の設定で検索 / 表示 可能なアクション一覧です",
     "action_list": "アクション一覧",
-    "disable_mode_explain": "現在、監査ログは無効になっています。有効にする場合は環境変数 <code>AUDIT_LOG_ENABLED</code> を true に設定してください。"
+    "disable_mode_explain": "現在、監査ログは無効になっています。有効にする場合は環境変数 <code>AUDIT_LOG_ENABLED</code> を true に設定してください。",
+    "docs_url": {
+      "log_type": "https://docs.growi.org/ja/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
+    }
   },
   "audit_log_action_category": {
     "Page": "ページ",

+ 1 - 0
packages/app/public/static/locales/ja_JP/translation.json

@@ -1086,6 +1086,7 @@
       "no_grant_available": "選択可能な権限のリストが見つかりませんでした。まず親ページの権限を修正したのちに再試行してください。",
       "need_to_fix_grant": "正しく機能を使用するためにはこのページに紐づく権限を修正する必要があります。 <br> 下記の選択肢から選んで変更してください。",
       "grant_label": {
+        "public": "公開",
         "isForbidden": "権限の閲覧が許可されていません",
         "currentPageGrantLabel": "このページの権限: ",
         "parentPageGrantLabel": "親のページの権限: ",

+ 9 - 2
packages/app/public/static/locales/zh_CN/admin/admin.json

@@ -538,6 +538,7 @@
     }
   },
   "audit_log_management": {
+    "user": "用户",
     "username": "帐号",
     "date": "日期",
     "action": "行动",
@@ -545,13 +546,19 @@
     "url": "URL",
     "settings": "设置",
     "return": "返回",
+    "clear": "清除搜索标准",
+    "reload": "重新加载",
     "activity_expiration_date": "审计日志的到期日",
     "activity_expiration_date_explain": "创建的审计日志会在环境变量中设置的从创建时间算起的秒数后自动删除",
     "fixed_by_env_var": "这是由env var 修复的 <code>{{key}}={{value}}</code>.",
     "available_action_list": "搜索/查看 所有可用的行动",
-    "available_action_list_explain": "可以在审计日志中 搜索/查看 的行动列表",
+    "available_action_list_explain": "在当前配置中可以搜索/查看的行动列表",
     "action_list": "行动清单",
-    "disable_mode_explain": "审计日志当前已禁用。 要启用它,请将环境变量 <code>AUDIT_LOG_ENABLED</code> 设置为 true。"
+    "disable_mode_explain": "审计日志当前已禁用。 要启用它,请将环境变量 <code>AUDIT_LOG_ENABLED</code> 设置为 true。",
+    "docs_url": {
+      "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
+    }
+
   },
   "audit_log_action_category": {
     "Page": "页面",

+ 1 - 0
packages/app/public/static/locales/zh_CN/translation.json

@@ -1096,6 +1096,7 @@
       "no_grant_available": "无法找到可选择的权限列表。 请先修改父页的权限,然后再试一次。",
       "need_to_fix_grant": "为了正确使用该功能,需要修改与该页面相关的权限。 <br> 请从以下选项中选择进行更改。",
       "grant_label": {
+        "public": "向公众提供",
         "isForbidden": "无权查看的机构",
         "currentPageGrantLabel": "本页的权限: ",
         "parentPageGrantLabel": "父页的权限: ",

+ 3 - 0
packages/app/src/client/interfaces/clearable.ts

@@ -0,0 +1,3 @@
+export interface IClearable {
+  clear: () => void,
+}

+ 11 - 2
packages/app/src/components/Admin/AuditLog/ActivityTable.tsx

@@ -1,5 +1,7 @@
 import React, { FC } from 'react';
 
+import { pagePathUtils } from '@growi/core';
+import { UserPicture } from '@growi/ui';
 import { format } from 'date-fns';
 import { useTranslation } from 'react-i18next';
 
@@ -21,7 +23,7 @@ export const ActivityTable : FC<Props> = (props: Props) => {
       <table className="table table-default table-bordered table-user-list">
         <thead>
           <tr>
-            <th scope="col">{t('admin:audit_log_management.username')}</th>
+            <th scope="col">{t('admin:audit_log_management.user')}</th>
             <th scope="col">{t('admin:audit_log_management.date')}</th>
             <th scope="col">{t('admin:audit_log_management.action')}</th>
             <th scope="col">{t('admin:audit_log_management.ip')}</th>
@@ -32,7 +34,14 @@ export const ActivityTable : FC<Props> = (props: Props) => {
           {props.activityList.map((activity) => {
             return (
               <tr data-testid="activity-table" key={activity._id}>
-                <td>{activity.snapshot?.username}</td>
+                <td>
+                  { activity.user != null && (
+                    <>
+                      <UserPicture user={activity.user} className="picture rounded-circle" />
+                      <a className="ml-2" href={pagePathUtils.userPageRoot(activity.user)}>{activity.snapshot?.username}</a>
+                    </>
+                  )}
+                </td>
                 <td>{formatDate(activity.createdAt)}</td>
                 <td>{t(`admin:audit_log_action.${activity.action}`)}</td>
                 <td>{activity.ip}</td>

+ 18 - 2
packages/app/src/components/Admin/AuditLog/AuditLogSettings.tsx

@@ -3,6 +3,7 @@ import React, { FC, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { Collapse } from 'reactstrap';
 
+import { AllSupportedActions } from '~/interfaces/activity';
 import { useActivityExpirationSeconds, useAuditLogAvailableActions } from '~/stores/context';
 
 export const AuditLogSettings: FC = () => {
@@ -34,8 +35,23 @@ export const AuditLogSettings: FC = () => {
         />
       </p>
 
-      <h4 className="mt-4">{t('admin:audit_log_management.available_action_list')}</h4>
-      <p className="form-text text-muted">{t('admin:audit_log_management.available_action_list_explain')}</p>
+      <h4 className="mt-4">
+        {t('admin:audit_log_management.available_action_list')}
+        <span className="badge badge-pill badge-secondary ml-2">
+          {`${availableActions.length} / ${AllSupportedActions.length}`}
+        </span>
+        <a
+          className="ml-2"
+          href={t('admin:audit_log_management.docs_url.log_type')}
+          target="_blank"
+          rel="noopener noreferrer"
+        >
+          <i className="icon-fw icon-question" aria-hidden="true"></i>
+        </a>
+      </h4>
+      <p className="form-text text-muted">
+        {t('admin:audit_log_management.available_action_list_explain')}
+      </p>
       <p className="mt-1">
         <button type="button" className="btn btn-link p-0" aria-expanded="false" onClick={() => setIsExpandActionList(!isExpandActionList)}>
           <i className={`fa fa-fw fa-arrow-right ${isExpandActionList ? 'fa-rotate-90' : ''}`}></i>

+ 18 - 3
packages/app/src/components/Admin/AuditLog/SearchUsernameTypeahead.tsx

@@ -1,10 +1,11 @@
 import React, {
-  FC, Fragment, useState, useCallback,
+  Fragment, useState, useCallback, useRef, ForwardRefRenderFunction, forwardRef, useImperativeHandle,
 } from 'react';
 
 import { AsyncTypeahead, Menu, MenuItem } from 'react-bootstrap-typeahead';
 import { useTranslation } from 'react-i18next';
 
+import { IClearable } from '~/client/interfaces/clearable';
 import { useSWRxUsernames } from '~/stores/user';
 
 
@@ -25,10 +26,12 @@ type Props = {
   onChange: (text: string[]) => void
 }
 
-export const SearchUsernameTypeahead: FC<Props> = (props: Props) => {
+const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Props> = ((props: Props, ref) => {
   const { onChange } = props;
   const { t } = useTranslation();
 
+  const typeaheadRef = useRef<IClearable>(null);
+
   /*
    * State
    */
@@ -96,6 +99,15 @@ export const SearchUsernameTypeahead: FC<Props> = (props: Props) => {
     );
   }, []);
 
+  useImperativeHandle(ref, () => ({
+    clear() {
+      const instance = typeaheadRef?.current;
+      if (instance != null) {
+        instance.clear();
+      }
+    },
+  }));
+
   return (
     <div className="input-group mr-2">
       <div className="input-group-prepend">
@@ -104,6 +116,7 @@ export const SearchUsernameTypeahead: FC<Props> = (props: Props) => {
         </span>
       </div>
       <AsyncTypeahead
+        ref={typeaheadRef}
         id="search-username-typeahead-asynctypeahead"
         multiple
         delay={400}
@@ -119,4 +132,6 @@ export const SearchUsernameTypeahead: FC<Props> = (props: Props) => {
       />
     </div>
   );
-};
+});
+
+export const SearchUsernameTypeahead = forwardRef(SearchUsernameTypeaheadSubstance);

+ 32 - 8
packages/app/src/components/Admin/AuditLogManagement.tsx

@@ -1,8 +1,11 @@
-import React, { FC, useState, useCallback } from 'react';
+import React, {
+  FC, useState, useCallback, useRef,
+} from 'react';
 
 import { format } from 'date-fns';
 import { useTranslation } from 'react-i18next';
 
+import { IClearable } from '~/client/interfaces/clearable';
 import { toastError } from '~/client/util/apiNotification';
 import { SupportedActionType } from '~/interfaces/activity';
 import { useSWRxActivity } from '~/stores/activity';
@@ -17,7 +20,6 @@ import { DateRangePicker } from './AuditLog/DateRangePicker';
 import { SearchUsernameTypeahead } from './AuditLog/SearchUsernameTypeahead';
 import { SelectActionDropdown } from './AuditLog/SelectActionDropdown';
 
-
 const formatDate = (date: Date | null) => {
   if (date == null) {
     return '';
@@ -30,8 +32,9 @@ const PAGING_LIMIT = 10;
 export const AuditLogManagement: FC = () => {
   const { t } = useTranslation();
 
+  const typeaheadRef = useRef<IClearable>(null);
+
   const { data: auditLogAvailableActionsData } = useAuditLogAvailableActions();
-  const auditLogAvailableActions = auditLogAvailableActionsData != null ? auditLogAvailableActionsData : [];
 
   /*
    * State
@@ -43,7 +46,7 @@ export const AuditLogManagement: FC = () => {
   const [endDate, setEndDate] = useState<Date | null>(null);
   const [selectedUsernames, setSelectedUsernames] = useState<string[]>([]);
   const [actionMap, setActionMap] = useState(
-    new Map<SupportedActionType, boolean>(auditLogAvailableActions.map(action => [action, true])),
+    new Map<SupportedActionType, boolean>(auditLogAvailableActionsData != null ? auditLogAvailableActionsData.map(action => [action, true]) : []),
   );
 
   /*
@@ -94,6 +97,18 @@ export const AuditLogManagement: FC = () => {
     setSelectedUsernames(usernames);
   }, []);
 
+  const clearButtonPushedHandler = useCallback(() => {
+    setActivePage(1);
+    setStartDate(null);
+    setEndDate(null);
+    setSelectedUsernames([]);
+    typeaheadRef.current?.clear();
+
+    if (auditLogAvailableActionsData != null) {
+      setActionMap(new Map<SupportedActionType, boolean>(auditLogAvailableActionsData.map(action => [action, true])));
+    }
+  }, [setActivePage, setStartDate, setEndDate, setSelectedUsernames, setActionMap, auditLogAvailableActionsData]);
+
   const reloadButtonPushedHandler = useCallback(() => {
     setActivePage(1);
     mutateActivity();
@@ -128,6 +143,7 @@ export const AuditLogManagement: FC = () => {
         <>
           <div className="form-inline mb-3">
             <SearchUsernameTypeahead
+              ref={typeaheadRef}
               onChange={setUsernamesHandler}
             />
 
@@ -139,14 +155,22 @@ export const AuditLogManagement: FC = () => {
 
             <SelectActionDropdown
               actionMap={actionMap}
-              availableActions={auditLogAvailableActions}
+              availableActions={auditLogAvailableActionsData || []}
               onChangeAction={actionCheckboxChangedHandler}
               onChangeMultipleAction={multipleActionCheckboxChangedHandler}
             />
 
-            <button type="button" className="btn ml-auto grw-btn-reload" onClick={reloadButtonPushedHandler}>
-              <i className="icon icon-reload" />
-            </button>
+            <div className="ml-auto">
+              <button type="button" className="btn btn-outline-secondary btn-sm mr-2" onClick={clearButtonPushedHandler}>
+                <span className="icon-refresh mr-1" />
+                {t('admin:audit_log_management.clear')}
+              </button>
+
+              <button type="button" className="btn btn-outline-secondary btn-sm" onClick={reloadButtonPushedHandler}>
+                <i className="icon icon-reload mr-1" />
+                {t('admin:audit_log_management.reload')}
+              </button>
+            </div>
           </div>
 
           <p

+ 2 - 1
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -11,6 +11,7 @@ import {
 import { IPageOperationProcessData } from '~/interfaces/page-operation';
 import { useSWRxPageInfo } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
+import { shouldRecoverPagePaths } from '~/utils/page-operation';
 
 const logger = loggerFactory('growi:cli:PageItemControl');
 
@@ -136,7 +137,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
 
     // PathRecovery
     // Todo: It is wanted to find a better way to pass operationProcessData to PageItemControl
-    const shouldShowPathRecoveryButton = operationProcessData?.Rename != null ? operationProcessData?.Rename.isProcessable : false;
+    const shouldShowPathRecoveryButton = operationProcessData != null ? shouldRecoverPagePaths(operationProcessData) : false;
 
     contents = (
       <>

+ 1 - 0
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -126,6 +126,7 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
       <DropdownItem
         onClick={() => openAccessoriesModal(PageAccessoriesModalContents.ShareLink)}
         disabled={isGuestUser || isSharedUser || isLinkSharingDisabled}
+        data-testid="open-page-accessories-modal-btn-with-share-link-management-data-tab"
         className="grw-page-control-dropdown-item"
       >
         <span className="grw-page-control-dropdown-icon">

+ 2 - 6
packages/app/src/components/Navbar/SubNavButtons.tsx

@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
 
 import { toggleBookmark, toggleLike, toggleSubscribe } from '~/client/services/page-operation';
 import {
-  IPageInfoAll, IPageToDeleteWithMeta, IPageToRenameWithMeta, isIPageInfoForEntity, isIPageInfoForOperation,
+  IPageInfoForOperation, IPageToDeleteWithMeta, IPageToRenameWithMeta, isIPageInfoForEntity, isIPageInfoForOperation,
 } from '~/interfaces/page';
 import { useIsGuestUser } from '~/stores/context';
 import { IPageForPageDuplicateModal } from '~/stores/modal';
@@ -35,7 +35,7 @@ type SubNavButtonsSubstanceProps = CommonProps & {
   shareLinkId?: string | null,
   revisionId: string | null,
   path?: string | null,
-  pageInfo: IPageInfoAll,
+  pageInfo: IPageInfoForOperation,
 }
 
 const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element => {
@@ -140,10 +140,6 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
     onClickDeleteMenuItem(pageToDelete);
   }, [onClickDeleteMenuItem, pageId, pageInfo, path, revisionId]);
 
-  if (!isIPageInfoForOperation(pageInfo)) {
-    return <></>;
-  }
-
 
   const {
     sumOfLikers, sumOfSeenUsers, isLiked, bookmarkCount, isBookmarked,

+ 5 - 1
packages/app/src/components/Page/FixPageGrantAlert.tsx

@@ -76,6 +76,10 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
       return t('fix_page_grant.modal.grant_label.isForbidden');
     }
 
+    if (grantData.grant === 1) {
+      return t('fix_page_grant.modal.grant_label.public');
+    }
+
     if (grantData.grant === 4) {
       return t('fix_page_grant.modal.radio_btn.only_me');
     }
@@ -87,7 +91,7 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
       return `${t('fix_page_grant.modal.radio_btn.grant_group')}: (${grantData.grantedGroup.name})`;
     }
 
-    throw Error('cannnot get grant label'); // this error can't be throwed
+    throw Error('cannot get grant label'); // this error can't be throwed
   }, [t]);
 
   const renderGrantDataLabel = useCallback(() => {

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

@@ -86,7 +86,7 @@ class ShareLink extends React.Component {
     const { t } = this.props;
 
     return (
-      <div className="container p-0">
+      <div className="container p-0" data-testid="share-link-management">
         <h3 className="grw-modal-head d-flex pb-2">
           { t('share_links.share_link_list') }
           <button className="btn btn-danger ml-auto " type="button" onClick={this.deleteAllLinksButtonHandler}>{t('delete_all')}</button>

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

@@ -20,7 +20,7 @@ import { IPageForPageDuplicateModal } from '~/stores/modal';
 import { useSWRxPageChildren } from '~/stores/page-listing';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
-
+import { shouldRecoverPagePaths } from '~/utils/page-operation';
 
 import ClosableTextInput, { AlertInfo, AlertType } from '../../Common/ClosableTextInput';
 import CountBadge from '../../Common/CountBadge';
@@ -410,7 +410,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
 
   // Rename process
   // Icon that draw attention from users for some actions
-  const shouldShowAttentionIcon = !!page.processData?.Rename?.isProcessable;
+  const shouldShowAttentionIcon = page.processData != null ? shouldRecoverPagePaths(page.processData) : false;
 
   return (
     <div

+ 2 - 0
packages/app/src/interfaces/activity.ts

@@ -376,6 +376,8 @@ export const SmallActionGroup = {
   ACTION_USER_LOGOUT,
   ACTION_PAGE_CREATE,
   ACTION_PAGE_DELETE,
+  ACTION_PAGE_DELETE_COMPLETELY,
+  ACTION_PAGE_EMPTY_TRASH,
 } as const;
 
 // SmallActionGroup + Action by all General Users - PAGE_VIEW

+ 15 - 4
packages/app/src/interfaces/page-operation.ts

@@ -6,10 +6,21 @@ export const PageActionType = {
   Revert: 'Revert',
   NormalizeParent: 'NormalizeParent',
 } as const;
-export type PageActionType = typeof PageActionType[keyof typeof PageActionType]
-export type IPageOperationProcessData = Partial<{
-  [key in PageActionType]: {isProcessable: boolean}
-}>
+export type PageActionType = typeof PageActionType[keyof typeof PageActionType];
+
+export const PageActionStage = {
+  Main: 'Main',
+  Sub: 'Sub',
+} as const;
+export type PageActionStage = typeof PageActionStage[keyof typeof PageActionStage];
+
+export type IPageOperationProcessData = {
+  [key in PageActionType]?: {
+    [PageActionStage.Main]?: { isProcessable: boolean },
+    [PageActionStage.Sub]?: { isProcessable: boolean },
+  }
+}
+
 export type IPageOperationProcessInfo = {
   [pageId: string]: IPageOperationProcessData,
 }

+ 7 - 6
packages/app/src/migrations/20211129125654-initialize-private-legacy-pages-named-query.js

@@ -1,8 +1,8 @@
+import { getMongoUri, mongoOptions } from '@growi/core';
 import mongoose from 'mongoose';
 
-import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
-import NamedQuery from '~/server/models/named-query';
 import { SearchDelegatorName } from '~/interfaces/named-query';
+import NamedQuery from '~/server/models/named-query';
 import loggerFactory from '~/utils/logger';
 
 
@@ -13,10 +13,11 @@ module.exports = {
     mongoose.connect(getMongoUri(), mongoOptions);
 
     try {
-      await NamedQuery.insertMany({
-        name: SearchDelegatorName.PRIVATE_LEGACY_PAGES,
-        delegatorName: SearchDelegatorName.PRIVATE_LEGACY_PAGES,
-      });
+      await NamedQuery.updateOne(
+        { name: SearchDelegatorName.PRIVATE_LEGACY_PAGES },
+        { delegatorName: SearchDelegatorName.PRIVATE_LEGACY_PAGES },
+        { upsert: true },
+      );
     }
     catch (err) {
       logger.error('Failed to migrate named query for private legacy pages search delagator.', err);

+ 2 - 1
packages/app/src/server/crowi/index.js

@@ -10,13 +10,14 @@ import mongoose from 'mongoose';
 
 import pkg from '^/package.json';
 
+import { PageActionType } from '~/interfaces/page-operation';
 import CdnResourcesService from '~/services/cdn-resources-service';
 import Xss from '~/services/xss';
 import loggerFactory from '~/utils/logger';
 import { projectRoot } from '~/utils/project-dir-utils';
 
 import Activity from '../models/activity';
-import PageOperation, { PageActionType } from '../models/page-operation';
+import PageOperation from '../models/page-operation';
 import PageRedirect from '../models/page-redirect';
 import Tag from '../models/tag';
 import UserGroup from '../models/user-group';

+ 0 - 12
packages/app/src/server/models/activity.ts

@@ -126,18 +126,6 @@ activitySchema.statics.updateByParameters = async function(activityId: string, p
   return activity;
 };
 
-activitySchema.statics.getPaginatedActivity = async function(limit: number, offset: number, query) {
-  const paginateResult = await this.paginate(
-    query,
-    {
-      limit,
-      offset,
-      sort: { createdAt: -1 },
-    },
-  );
-  return paginateResult;
-};
-
 activitySchema.statics.findSnapshotUsernamesByUsernameRegexWithTotalCount = async function(
     q: string, option: { sortOpt: number | string, offset: number, limit: number},
 ): Promise<{usernames: string[], totalCount: number}> {

+ 1 - 16
packages/app/src/server/models/page-operation.ts

@@ -4,6 +4,7 @@ import mongoose, {
   Schema, Model, Document, QueryOptions, FilterQuery,
 } from 'mongoose';
 
+import { PageActionType, PageActionStage } from '~/interfaces/page-operation';
 import {
   IPageForResuming, IUserForResuming, IOptionsForResuming,
 } from '~/server/models/interfaces/page-operation';
@@ -18,22 +19,6 @@ const logger = loggerFactory('growi:models:page-operation');
 type IObjectId = mongoose.Types.ObjectId;
 const ObjectId = mongoose.Schema.Types.ObjectId;
 
-export const PageActionType = {
-  Rename: 'Rename',
-  Duplicate: 'Duplicate',
-  Delete: 'Delete',
-  DeleteCompletely: 'DeleteCompletely',
-  Revert: 'Revert',
-  NormalizeParent: 'NormalizeParent',
-} as const;
-export type PageActionType = typeof PageActionType[keyof typeof PageActionType];
-
-export const PageActionStage = {
-  Main: 'Main',
-  Sub: 'Sub',
-} as const;
-export type PageActionStage = typeof PageActionStage[keyof typeof PageActionStage];
-
 /*
  * Main Schema
  */

+ 6 - 5
packages/app/src/server/models/password-reset-order.ts

@@ -1,11 +1,12 @@
+import crypto from 'crypto';
+
+import { getOrCreateModel } from '@growi/core';
+import { addMinutes } from 'date-fns';
 import mongoose, {
   Schema, Model, Document,
 } from 'mongoose';
-
-import { addMinutes } from 'date-fns';
 import uniqueValidator from 'mongoose-unique-validator';
-import crypto from 'crypto';
-import { getOrCreateModel } from '@growi/core';
+
 
 const ObjectId = mongoose.Schema.Types.ObjectId;
 
@@ -20,7 +21,7 @@ export interface IPasswordResetOrder {
 }
 
 export interface PasswordResetOrderDocument extends IPasswordResetOrder, Document {
-  isExpired(): Promise<boolean>
+  isExpired(): boolean
   revokeOneTimeToken(): Promise<void>
 }
 

+ 1 - 1
packages/app/src/server/models/user-registration-order.ts

@@ -17,7 +17,7 @@ export interface IUserRegistrationOrder {
 }
 
 export interface UserRegistrationOrderDocument extends IUserRegistrationOrder, Document {
-  isExpired(): Promise<boolean>
+  isExpired(): boolean
   revokeOneTimeToken(): Promise<void>
 }
 

+ 26 - 3
packages/app/src/server/routes/apiv3/activity.ts

@@ -2,12 +2,13 @@ import { parseISO, addMinutes, isValid } from 'date-fns';
 import express, { Request, Router } from 'express';
 import { query } from 'express-validator';
 
-import { ISearchFilter } from '~/interfaces/activity';
+import { IActivity, ISearchFilter } from '~/interfaces/activity';
 import Activity from '~/server/models/activity';
 import loggerFactory from '~/utils/logger';
 
 import Crowi from '../../crowi';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
+import { serializeUserSecurely } from '../../models/serializers/user-serializer';
 
 import { ApiV3Response } from './interfaces/apiv3-response';
 
@@ -92,8 +93,30 @@ module.exports = (crowi: Crowi): Router => {
     }
 
     try {
-      const paginationResult = await Activity.getPaginatedActivity(limit, offset, query);
-      return res.apiv3({ paginationResult });
+      const paginateResult = await Activity.paginate(
+        query,
+        {
+          limit,
+          offset,
+          sort: { createdAt: -1 },
+          populate: 'user',
+        },
+      );
+
+      const User = crowi.model('User');
+      const serializedDocs = paginateResult.docs.map((doc: IActivity) => {
+        if (doc.user != null && doc.user instanceof User) {
+          doc.user = serializeUserSecurely(doc.user);
+        }
+        return doc;
+      });
+
+      const serializedPaginationResult = {
+        ...paginateResult,
+        docs: serializedDocs,
+      };
+
+      return res.apiv3({ serializedPaginationResult });
     }
     catch (err) {
       logger.error('Failed to get paginated activity', err);

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

@@ -575,8 +575,14 @@ module.exports = (crowi) => {
       return res.apiv3Err(new ErrorV3(msg, code), 403);
     }
 
+    const pageOp = await crowi.pageOperationService.getRenameSubOperationByPageId(page._id);
+    if (pageOp == null) {
+      const msg = 'PageOperation document for Rename Sub operation not found.';
+      const code = 'document_not_found';
+      return res.apiv3Err(new ErrorV3(msg, code), 404);
+    }
+
     try {
-      const pageOp = await crowi.pageOperationService.getRenameSubOperationByPageId(page._id);
       await crowi.pageService.resumeRenameSubOperation(page, pageOp);
     }
     catch (err) {

+ 15 - 5
packages/app/src/server/service/page-operation.ts

@@ -1,7 +1,9 @@
 import { pagePathUtils } from '@growi/core';
 
-import { IPageOperationProcessInfo, IPageOperationProcessData } from '~/interfaces/page-operation';
-import PageOperation, { PageActionType, PageActionStage, PageOperationDocument } from '~/server/models/page-operation';
+import {
+  IPageOperationProcessInfo, IPageOperationProcessData, PageActionType, PageActionStage,
+} from '~/interfaces/page-operation';
+import PageOperation, { PageOperationDocument } from '~/server/models/page-operation';
 import loggerFactory from '~/utils/logger';
 
 import { ObjectIdLike } from '../interfaces/mongoose-utils';
@@ -26,9 +28,10 @@ class PageOperationService {
   }
 
   async init(): Promise<void> {
-    // cleanup PageOperation documents except ones with actionType: Rename
+    // cleanup PageOperation documents except ones with { actionType: Rename, actionStage: Sub }
     const types = [Duplicate, Delete, DeleteCompletely, Revert, NormalizeParent];
     await PageOperation.deleteByActionTypes(types);
+    await PageOperation.deleteMany({ actionType: PageActionType.Rename, actionStage: PageActionStage.Main });
   }
 
   /**
@@ -137,12 +140,19 @@ class PageOperationService {
       const isProcessable = pageOp.isProcessable();
 
       // processData for processInfo
-      const processData: IPageOperationProcessData = { [actionType]: { isProcessable } };
+      const mainProcessableInfo = pageOp.actionStage === PageActionStage.Main ? { isProcessable } : undefined;
+      const subProcessableInfo = pageOp.actionStage === PageActionStage.Sub ? { isProcessable } : undefined;
+      const processData: IPageOperationProcessData = {
+        [actionType]: {
+          [PageActionStage.Main]: mainProcessableInfo,
+          [PageActionStage.Sub]: subProcessableInfo,
+        },
+      };
 
       // Merge processData if other processData exist
       if (processInfo[pageId] != null) {
         const otherProcessData = processInfo[pageId];
-        processInfo[pageId] = { ...otherProcessData, ...processData };
+        processInfo[pageId] = Object.assign(otherProcessData, processData);
         return;
       }
       // add new process data to processInfo

+ 88 - 9
packages/app/src/server/service/page.ts

@@ -15,7 +15,9 @@ import {
 import {
   PageDeleteConfigValue, IPageDeleteConfigValueToProcessValidation,
 } from '~/interfaces/page-delete-config';
-import { IPageOperationProcessInfo, IPageOperationProcessData } from '~/interfaces/page-operation';
+import {
+  IPageOperationProcessInfo, IPageOperationProcessData, PageActionStage, PageActionType,
+} from '~/interfaces/page-operation';
 import { IUserHasId } from '~/interfaces/user';
 import { PageMigrationErrorData, SocketEventName, UpdateDescCountRawData } from '~/interfaces/websocket';
 import {
@@ -27,7 +29,7 @@ import { prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
 
 import { ObjectIdLike } from '../interfaces/mongoose-utils';
 import { PathAlreadyExistsError } from '../models/errors';
-import PageOperation, { PageActionStage, PageActionType, PageOperationDocument } from '../models/page-operation';
+import PageOperation, { PageOperationDocument } from '../models/page-operation';
 import { PageRedirectModel } from '../models/page-redirect';
 import { serializePageSecurely } from '../models/serializers/page-serializer';
 import Subscription from '../models/subscription';
@@ -401,7 +403,19 @@ class PageService {
       logger.error('Failed to create PageOperation document.', err);
       throw err;
     }
-    const renamedPage = await this.renameMainOperation(page, newPagePath, user, options, pageOp._id);
+
+    let renamedPage: PageDocument | null = null;
+    try {
+      renamedPage = await this.renameMainOperation(page, newPagePath, user, options, pageOp._id);
+    }
+    catch (err) {
+      logger.error('Error occurred while running renameMainOperation', err);
+
+      // cleanup
+      await PageOperation.deleteOne({ _id: pageOp._id });
+
+      throw err;
+    }
 
     return renamedPage;
   }
@@ -996,7 +1010,20 @@ class PageService {
         logger.error('Failed to create PageOperation document.', err);
         throw err;
       }
-      this.duplicateRecursivelyMainOperation(page, newPagePath, user, pageOp._id);
+
+      (async() => {
+        try {
+          await this.duplicateRecursivelyMainOperation(page, newPagePath, user, pageOp._id);
+        }
+        catch (err) {
+          logger.error('Error occurred while running duplicateRecursivelyMainOperation.', err);
+
+          // cleanup
+          await PageOperation.deleteOne({ _id: pageOp._id });
+
+          throw err;
+        }
+      })();
     }
 
     const result = serializePageSecurely(duplicatedTarget);
@@ -1387,7 +1414,19 @@ class PageService {
       /*
        * Resumable Operation
        */
-      this.deleteRecursivelyMainOperation(page, user, pageOp._id);
+      (async() => {
+        try {
+          await this.deleteRecursivelyMainOperation(page, user, pageOp._id);
+        }
+        catch (err) {
+          logger.error('Error occurred while running deleteRecursivelyMainOperation.', err);
+
+          // cleanup
+          await PageOperation.deleteOne({ _id: pageOp._id });
+
+          throw err;
+        }
+      })();
     }
 
     return deletedPage;
@@ -1703,7 +1742,19 @@ class PageService {
       /*
        * Main Operation
        */
-      this.deleteCompletelyRecursivelyMainOperation(page, user, options, pageOp._id);
+      (async() => {
+        try {
+          await this.deleteCompletelyRecursivelyMainOperation(page, user, options, pageOp._id);
+        }
+        catch (err) {
+          logger.error('Error occurred while running deleteCompletelyRecursivelyMainOperation.', err);
+
+          // cleanup
+          await PageOperation.deleteOne({ _id: pageOp._id });
+
+          throw err;
+        }
+      })();
     }
 
     return;
@@ -1911,7 +1962,19 @@ class PageService {
       /*
        * Resumable Operation
        */
-      this.revertRecursivelyMainOperation(page, user, options, pageOp._id);
+      (async() => {
+        try {
+          await this.revertRecursivelyMainOperation(page, user, options, pageOp._id);
+        }
+        catch (err) {
+          logger.error('Error occurred while running revertRecursivelyMainOperation.', err);
+
+          // cleanup
+          await PageOperation.deleteOne({ _id: pageOp._id });
+
+          throw err;
+        }
+      })();
     }
 
     return updatedPage;
@@ -2295,7 +2358,19 @@ class PageService {
       throw err;
     }
 
-    this.normalizeParentRecursivelyMainOperation(page, user, pageOp._id);
+    (async() => {
+      try {
+        await this.normalizeParentRecursivelyMainOperation(page, user, pageOp._id);
+      }
+      catch (err) {
+        logger.error('Error occurred while running normalizeParentRecursivelyMainOperation.', err);
+
+        // cleanup
+        await PageOperation.deleteOne({ _id: pageOp._id });
+
+        throw err;
+      }
+    })();
   }
 
   async normalizeParentByPageIdsRecursively(pageIds: ObjectIdLike[], user): Promise<void> {
@@ -2480,7 +2555,11 @@ class PageService {
       }
       catch (err) {
         errorPagePaths.push(page.path);
-        logger.err('Failed to run normalizeParentRecursivelyMainOperation.', err);
+        logger.error('Failed to run normalizeParentRecursivelyMainOperation.', err);
+
+        // cleanup
+        await PageOperation.deleteOne({ _id: pageOp._id });
+
         throw err;
       }
     }

+ 3 - 0
packages/app/src/server/util/rate-limiter.ts

@@ -20,8 +20,10 @@ const generateApiRateLimitConfigFromEndpoint = (envVar: NodeJS.ProcessEnv, targe
     }
     const methodKey = `API_RATE_LIMIT_${target}_METHODS`;
     const maxRequestsKey = `API_RATE_LIMIT_${target}_MAX_REQUESTS`;
+    const usersPerIpProspectionKey = `API_RATE_LIMIT_${target}_USERS_PER_IP`;
     const method = envVar[methodKey] ?? 'ALL';
     const maxRequests = Number(envVar[maxRequestsKey]);
+    const usersPerIpProspection = Number(envVar[usersPerIpProspectionKey]);
 
     if (endpoint == null || maxRequests == null) {
       return;
@@ -30,6 +32,7 @@ const generateApiRateLimitConfigFromEndpoint = (envVar: NodeJS.ProcessEnv, targe
     const config = {
       method,
       maxRequests,
+      usersPerIpProspection,
     };
 
     apiRateLimitConfig[endpoint] = config;

+ 4 - 2
packages/app/src/services/renderer/markdown-it/link-by-relative-path.ts

@@ -5,14 +5,16 @@ const PATTERN_RELATIVE_PATH = new RegExp(/^(\.{1,2})(\/.*)?$/);
 
 export default class LinkerByRelativePathConfigurer {
 
-  pagePath: string
+  pagePath: string;
 
   constructor(pagePath: string) {
     this.pagePath = pagePath;
   }
 
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-  configure(md, pagePath: string): void {
+  configure(md): void {
+    const pagePath = this.pagePath;
+
     // Remember old renderer, if overridden, or proxy to default renderer
     const defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) {
       return self.renderToken(tokens, idx, options);

+ 1 - 1
packages/app/src/stores/activity.ts

@@ -13,6 +13,6 @@ export const useSWRxActivity = (limit?: number, offset?: number, searchFilter?:
   return useSWRImmutable(
     auditLogEnabled ? ['/activity', limit, offset, stringifiedSearchFilter] : null,
     (endpoint, limit, offset, stringifiedSearchFilter) => apiv3Get(endpoint, { limit, offset, searchFilter: stringifiedSearchFilter })
-      .then(result => result.data.paginationResult),
+      .then(result => result.data.serializedPaginationResult),
   );
 };

+ 5 - 0
packages/app/src/utils/page-operation.ts

@@ -0,0 +1,5 @@
+import { IPageOperationProcessData } from '~/interfaces/page-operation';
+
+export const shouldRecoverPagePaths = (processData: IPageOperationProcessData): boolean => {
+  return processData.Rename?.Sub != null ? processData.Rename.Sub.isProcessable : false;
+};

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

@@ -139,6 +139,17 @@ context('Page Accessories Modal', () => {
      cy.getByTestid('page-attachment').should('be.visible')
      cy.screenshot(`${ssPrefix}-open-page-attachment-data-bootstrap4`);
   });
+  it('Share Link Management 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-share-link-management-data-tab').click();
+   });
+
+   cy.getByTestid('page-accessories-modal').should('be.visible');
+   cy.getByTestid('share-link-management').should('be.visible');
+   cy.screenshot(`${ssPrefix}-open-share-link-management-bootstrap4`);
+  });
 
 });
 

+ 1 - 1
packages/app/test/integration/service/v5.page.test.ts

@@ -1,7 +1,7 @@
 import { addSeconds } from 'date-fns';
 import mongoose from 'mongoose';
 
-import { PageActionStage, PageActionType } from '../../../src/server/models/page-operation';
+import { PageActionStage, PageActionType } from '../../../src/interfaces/page-operation';
 import { getInstance } from '../setup-crowi';
 
 

+ 1 - 1
packages/app/test/integration/service/v5.public-page.test.ts

@@ -2,7 +2,7 @@
 import { advanceTo } from 'jest-date-mock';
 import mongoose from 'mongoose';
 
-import { PageActionType, PageActionStage } from '../../../src/server/models/page-operation';
+import { PageActionType, PageActionStage } from '../../../src/interfaces/page-operation';
 import Tag from '../../../src/server/models/tag';
 import { getInstance } from '../setup-crowi';
 

+ 1 - 1
packages/codemirror-textlint/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/codemirror-textlint",
-  "version": "5.1.0",
+  "version": "5.1.1-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "scripts": {

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "5.1.0",
+  "version": "5.1.1-RC.0",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-attachment-refs",
-  "version": "5.1.0",
+  "version": "5.1.1-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-lsx",
-  "version": "5.1.0",
+  "version": "5.1.1-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-pukiwiki-like-linker/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-pukiwiki-like-linker",
-  "version": "5.1.0",
+  "version": "5.1.1-RC.0",
   "description": "GROWI plugin to add PukiwikiLikeLinker",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slack",
-  "version": "5.1.0",
+  "version": "5.1.1-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",

+ 3 - 3
packages/slackbot-proxy/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "5.1.0",
+  "version": "5.1.1-slackbot-proxy.0",
   "license": "MIT",
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -25,7 +25,7 @@
   },
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^5.1.0",
+    "@growi/slack": "^5.1.1-RC.0",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",
@@ -58,7 +58,7 @@
     "browser-bunyan": "^1.6.3",
     "eslint-plugin-regex": "^1.8.0",
     "morgan": "^1.10.0",
-    "ts-node-dev": "^1.1.6",
+    "ts-node-dev": "^2.0.0",
     "tsc-alias": "^1.2.9"
   }
 }

+ 1 - 0
packages/ui/.gitignore

@@ -0,0 +1 @@
+/dist

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "5.1.0",
+  "version": "5.1.1-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [

+ 104 - 13
yarn.lock

@@ -1734,6 +1734,13 @@
   resolved "https://registry.yarnpkg.com/@browser-bunyan/levels/-/levels-1.6.0.tgz#3a50b8118254aa2ac26caf9d2aafa72d157e374b"
   integrity sha512-wte6nXXZH62Y/RGysYRlOkKxuJn+4S8xEamMF0fDncxxy0SriCHYwGPyWGF0FWYWmRzbZuEkp7dNebBf9Xfeeg==
 
+"@cspotcode/source-map-support@^0.8.0":
+  version "0.8.1"
+  resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
+  integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
+  dependencies:
+    "@jridgewell/trace-mapping" "0.3.9"
+
 "@cypress/request@^2.88.10":
   version "2.88.10"
   resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce"
@@ -2155,6 +2162,24 @@
     comment-json "^4.1.0"
     find-up "^4.1.0"
 
+"@jridgewell/resolve-uri@^3.0.3":
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
+  integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
+
+"@jridgewell/sourcemap-codec@^1.4.10":
+  version "1.4.14"
+  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
+  integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
+
+"@jridgewell/trace-mapping@0.3.9":
+  version "0.3.9"
+  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
+  integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
+  dependencies:
+    "@jridgewell/resolve-uri" "^3.0.3"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+
 "@jsdevtools/ono@^7.1.3":
   version "7.1.3"
   resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796"
@@ -3621,6 +3646,26 @@
   resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
   integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
 
+"@tsconfig/node10@^1.0.7":
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
+  integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==
+
+"@tsconfig/node12@^1.0.7":
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
+  integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==
+
+"@tsconfig/node14@^1.0.0":
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
+  integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
+
+"@tsconfig/node16@^1.0.2":
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e"
+  integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
+
 "@tsed/common@^6.43.0":
   version "6.43.0"
   resolved "https://registry.yarnpkg.com/@tsed/common/-/common-6.43.0.tgz#43d55a414c3ef24cd4e8f2adbf91935e08749f16"
@@ -4636,6 +4681,11 @@ acorn-walk@^7.1.1:
   resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
   integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
 
+acorn-walk@^8.1.1:
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
+  integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
+
 acorn@^6.4.1:
   version "6.4.2"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
@@ -4651,6 +4701,11 @@ acorn@^8.2.4:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c"
   integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==
 
+acorn@^8.4.1:
+  version "8.8.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
+  integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
+
 add-stream@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa"
@@ -14550,6 +14605,11 @@ minimist@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
 
+minimist@^1.2.6:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
+  integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
+
 minimist@~0.0.1:
   version "0.0.10"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
@@ -19652,7 +19712,7 @@ source-map-resolve@^0.5.0:
     source-map-url "^0.4.0"
     urix "^0.1.0"
 
-source-map-support@0.5.19, source-map-support@^0.5.12, source-map-support@^0.5.17:
+source-map-support@0.5.19, source-map-support@^0.5.12:
   version "0.5.19"
   resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
   integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
@@ -19660,6 +19720,14 @@ source-map-support@0.5.19, source-map-support@^0.5.12, source-map-support@^0.5.1
     buffer-from "^1.0.0"
     source-map "^0.6.0"
 
+source-map-support@^0.5.17:
+  version "0.5.21"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
+  integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
 source-map-support@^0.5.6:
   version "0.5.12"
   resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599"
@@ -21252,24 +21320,42 @@ ts-loader@^8.3.0:
     micromatch "^4.0.0"
     semver "^7.3.4"
 
-ts-node-dev@^1.1.6:
-  version "1.1.6"
-  resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.1.6.tgz#ee2113718cb5a92c1c8f4229123ad6afbeba01f8"
-  integrity sha512-RTUi7mHMNQospArGz07KiraQcdgUVNXKsgO2HAi7FoiyPMdTDqdniB6K1dqyaIxT7c9v/VpSbfBZPS6uVpaFLQ==
+ts-node-dev@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-2.0.0.tgz#bdd53e17ab3b5d822ef519928dc6b4a7e0f13065"
+  integrity sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==
   dependencies:
     chokidar "^3.5.1"
-    dateformat "~1.0.4-1.2.3"
     dynamic-dedupe "^0.3.0"
-    minimist "^1.2.5"
+    minimist "^1.2.6"
     mkdirp "^1.0.4"
     resolve "^1.0.0"
     rimraf "^2.6.1"
     source-map-support "^0.5.12"
     tree-kill "^1.2.2"
-    ts-node "^9.0.0"
+    ts-node "^10.4.0"
     tsconfig "^7.0.0"
 
-ts-node@^9.0.0, ts-node@^9.1.1:
+ts-node@^10.4.0, ts-node@^10.9.1:
+  version "10.9.1"
+  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b"
+  integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==
+  dependencies:
+    "@cspotcode/source-map-support" "^0.8.0"
+    "@tsconfig/node10" "^1.0.7"
+    "@tsconfig/node12" "^1.0.7"
+    "@tsconfig/node14" "^1.0.0"
+    "@tsconfig/node16" "^1.0.2"
+    acorn "^8.4.1"
+    acorn-walk "^8.1.1"
+    arg "^4.1.0"
+    create-require "^1.1.0"
+    diff "^4.0.1"
+    make-error "^1.1.1"
+    v8-compile-cache-lib "^3.0.1"
+    yn "3.1.1"
+
+ts-node@^9.1.1:
   version "9.1.1"
   resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d"
   integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==
@@ -21490,10 +21576,10 @@ typeorm@^0.2.31:
     yargs "^16.2.0"
     zen-observable-ts "^1.0.0"
 
-typescript@~4.6:
-  version "4.6.4"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9"
-  integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==
+typescript@~4.7:
+  version "4.7.4"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
+  integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
 
 typpy@2.3.11:
   version "2.3.11"
@@ -21974,6 +22060,11 @@ uuid@^3.1.0, uuid@^3.3.2:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
 
+v8-compile-cache-lib@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
+  integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
+
 v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"