Kaynağa Gözat

Merge pull request #6387 from weseek/master

Release v5.1.2
Yuki Takei 3 yıl önce
ebeveyn
işleme
a1b2f1311e
44 değiştirilmiş dosya ile 725 ekleme ve 631 silme
  1. 1 1
      lerna.json
  2. 2 2
      package.json
  3. 1 0
      packages/app/config/ci/.env.local.for-ci
  4. 1 0
      packages/app/config/webpack.common.js
  5. 9 9
      packages/app/package.json
  6. 1 2
      packages/app/public/static/locales/en_US/admin/admin.json
  7. 2 0
      packages/app/public/static/locales/en_US/translation.json
  8. 1 2
      packages/app/public/static/locales/ja_JP/admin/admin.json
  9. 2 0
      packages/app/public/static/locales/ja_JP/translation.json
  10. 1 2
      packages/app/public/static/locales/zh_CN/admin/admin.json
  11. 2 0
      packages/app/public/static/locales/zh_CN/translation.json
  12. 1 0
      packages/app/regconfig.json
  13. 23 0
      packages/app/src/client/services/page-operation.ts
  14. 28 21
      packages/app/src/components/Admin/AuditLog/DateRangePicker.tsx
  15. 8 11
      packages/app/src/components/Admin/AuditLogManagement.tsx
  16. 18 9
      packages/app/src/components/Common/Dropdown/PageItemControl.tsx
  17. 72 2
      packages/app/src/components/Navbar/SubNavButtons.tsx
  18. 14 1
      packages/app/src/components/Page/RevisionRenderer.tsx
  19. 0 108
      packages/app/src/components/PageAccessoriesModalControl.jsx
  20. 0 404
      packages/app/src/components/PageEditor/Editor.jsx
  21. 362 0
      packages/app/src/components/PageEditor/Editor.tsx
  22. 1 1
      packages/app/src/components/PageList/PageListItemL.tsx
  23. 2 1
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  24. 0 1
      packages/app/src/components/SearchPage/SearchResultList.tsx
  25. 9 14
      packages/app/src/components/Sidebar/CustomSidebar.tsx
  26. 17 0
      packages/app/src/interfaces/editor-methods.ts
  27. 2 0
      packages/app/src/interfaces/page.ts
  28. 4 1
      packages/app/src/server/models/obsolete-page.js
  29. 1 0
      packages/app/src/server/models/page.ts
  30. 27 2
      packages/app/src/server/routes/apiv3/page.js
  31. 7 1
      packages/app/src/server/service/page.ts
  32. 9 1
      packages/app/src/server/views/layout/layout.html
  33. 3 0
      packages/app/src/styles/_admin.scss
  34. 3 2
      packages/app/test/cypress/integration/20-basic-features/access-to-page.spec.ts
  35. 3 0
      packages/app/test/cypress/integration/21-basic-features-for-guest/access-to-page.spec.ts
  36. 1 1
      packages/codemirror-textlint/package.json
  37. 1 1
      packages/core/package.json
  38. 1 1
      packages/plugin-attachment-refs/package.json
  39. 1 1
      packages/plugin-lsx/package.json
  40. 1 1
      packages/plugin-pukiwiki-like-linker/package.json
  41. 1 1
      packages/slack/package.json
  42. 2 2
      packages/slackbot-proxy/package.json
  43. 1 1
      packages/ui/package.json
  44. 79 24
      yarn.lock

+ 1 - 1
lerna.json

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

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "5.1.1",
+  "version": "5.1.2-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -75,7 +75,7 @@
     "reg-notify-github-plugin": "^0.11.1",
     "reg-notify-slack-plugin": "^0.11.0",
     "reg-publish-s3-plugin": "^0.11.0",
-    "reg-suit": "^0.11.1",
+    "reg-suit": "^0.12.1",
     "rewire": "^5.0.0",
     "shipjs": "^0.24.1",
     "stylelint": "^14.2.0",

+ 1 - 0
packages/app/config/ci/.env.local.for-ci

@@ -1 +1,2 @@
 FORMAT_NODE_LOG=true
+MATHJAX=1

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

@@ -76,6 +76,7 @@ module.exports = (options) => {
     },
     node: {
       fs: 'empty',
+      module: 'empty',
     },
     module: {
       rules: options.module.rules.concat([

+ 9 - 9
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "5.1.1",
+  "version": "5.1.2-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -59,16 +59,16 @@
   "dependencies": {
     "@aws-sdk/client-s3": "^3.58.0",
     "@aws-sdk/s3-request-presigner": "^3.58.0",
-    "@browser-bunyan/console-formatted-stream": "^1.6.2",
+    "@browser-bunyan/console-formatted-stream": "^1.8.0",
     "@elastic/elasticsearch6": "npm:@elastic/elasticsearch@^6.8.8",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.1.1",
-    "@growi/plugin-attachment-refs": "^5.1.1",
-    "@growi/plugin-lsx": "^5.1.1",
-    "@growi/plugin-pukiwiki-like-linker": "^5.1.1",
-    "@growi/slack": "^5.1.1",
+    "@growi/codemirror-textlint": "^5.1.2-RC.0",
+    "@growi/plugin-attachment-refs": "^5.1.2-RC.0",
+    "@growi/plugin-lsx": "^5.1.2-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^5.1.2-RC.0",
+    "@growi/slack": "^5.1.2-RC.0",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
@@ -82,7 +82,7 @@
     "axios": "^0.24.0",
     "axios-retry": "^3.2.4",
     "body-parser": "^1.18.2",
-    "browser-bunyan": "^1.6.3",
+    "browser-bunyan": "^1.8.0",
     "bunyan": "^1.8.15",
     "check-node-version": "^4.1.0",
     "compression": "^1.7.4",
@@ -171,7 +171,7 @@
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^5.1.1",
+    "@growi/ui": "^5.1.2-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",

+ 1 - 2
packages/app/public/static/locales/en_US/admin/admin.json

@@ -537,8 +537,7 @@
     "url": "URL",
     "settings": "Settings",
     "return": "Return",
-    "clear": "Clear search criteria",
-    "reload": "Reload",
+    "clear": "Clear",
     "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>.",

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

@@ -164,6 +164,7 @@
   "No bookmarks yet": "No bookmarks yet",
   "add_bookmark": "Add to Bookmarks",
   "remove_bookmark": "Remove from Bookmarks",
+  "wide_view": "Wide View",
   "Recent Created": "Recent Created",
   "Recent Changes": "Recent Changes",
   "Page Tree": "Page Tree",
@@ -537,6 +538,7 @@
     "create_failed": "Failed to create {{target}}",
     "update_successed": "Succeeded to update {{target}}",
     "update_failed": "Failed to update {{target}}",
+    "file_upload_failed": "File upload failed.",
     "initialize_successed": "Succeeded to initialize {{target}}",
     "give_user_admin": "Succeeded to give {{username}} admin",
     "remove_user_admin": "Succeeded to remove {{username}} admin",

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

@@ -536,8 +536,7 @@
     "url": "URL",
     "settings": "設定",
     "return": "戻る",
-    "clear": "検索条件のクリア",
-    "reload": "再読み込み",
+    "clear": "クリア",
     "activity_expiration_date": "監査ログの有効期限",
     "activity_expiration_date_explain": "作成された監査ログは、作成時間から環境変数に設定した秒数後に自動的に削除されます",
     "fixed_by_env_var": "環境変数により固定されています <code>{{key}}={{value}}</code>.",

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

@@ -166,6 +166,7 @@
   "No bookmarks yet": "No bookmarks yet",
   "add_bookmark": "ブックマークに追加",
   "remove_bookmark": "ブックマークから削除",
+  "wide_view": "ワイドビュー",
   "Recent Created": "最新の作成",
   "Recent Changes": "最新の変更",
   "Page Tree": "ページツリー",
@@ -537,6 +538,7 @@
     "create_failed": "{{target}}の作成に失敗しました",
     "update_successed": "{{target}}を更新しました",
     "update_failed": "{{target}}の更新に失敗しました",
+    "file_upload_failed": "ファイルのアップロードに失敗しました",
     "initialize_successed": "{{target}}を初期化しました",
     "give_user_admin": "{{username}}を管理者に設定しました",
     "remove_user_admin": "{{username}}を管理者から外しました",

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

@@ -546,8 +546,7 @@
     "url": "URL",
     "settings": "设置",
     "return": "返回",
-    "clear": "清除搜索标准",
-    "reload": "重新加载",
+    "clear": "清除",
     "activity_expiration_date": "审计日志的到期日",
     "activity_expiration_date_explain": "创建的审计日志会在环境变量中设置的从创建时间算起的秒数后自动删除",
     "fixed_by_env_var": "这是由env var 修复的 <code>{{key}}={{value}}</code>.",

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

@@ -172,6 +172,7 @@
   "No bookmarks yet": "暂无书签",
   "add_bookmark": "添加到书签",
   "remove_bookmark": "从书签中删除",
+  "wide_view": "视野开阔",
 	"Recent Created": "最新创建",
   "Recent Changes": "最新修改",
   "Page Tree": "页面树",
@@ -515,6 +516,7 @@
     "create_failed": "Failed to create {{target}}",
 		"update_successed": "Succeeded to update {{target}}",
     "update_failed": "Failed to update {{target}}",
+    "file_upload_failed": "文件上传失败",
     "initialize_successed": "Succeeded to initialize {{target}}",
 		"give_user_admin": "Succeeded to give {{username}} admin",
     "remove_user_admin": "Succeeded to remove {{username}} admin ",

+ 1 - 0
packages/app/regconfig.json

@@ -13,6 +13,7 @@
     "reg-notify-github-plugin": {
       "prCommentBehavior": "new",
       "setCommitStatus": false,
+      "shortDescription": true,
       "clientId": "$REG_NOTIFY_GITHUB_PLUGIN_CLIENTID"
     },
     "reg-notify-slack-plugin": {

+ 23 - 0
packages/app/src/client/services/page-operation.ts

@@ -37,6 +37,29 @@ export const toggleBookmark = async(pageId: string, currentValue?: boolean): Pro
   }
 };
 
+// Utility to update body class
+const updateBodyClassByView = (expandContentWidth: boolean): void => {
+  const bodyClasses = document.body.classList;
+  const isLayoutFluid = bodyClasses.contains('growi-layout-fluid');
+
+  if (expandContentWidth && !isLayoutFluid) {
+    bodyClasses.add('growi-layout-fluid');
+  }
+  else if (isLayoutFluid) {
+    bodyClasses.remove('growi-layout-fluid');
+  }
+};
+
+export const updateContentWidth = async(pageId: string, newValue: boolean): Promise<void> => {
+  try {
+    await apiv3Put(`/page/${pageId}/content-width`, { expandContentWidth: newValue });
+    updateBodyClassByView(newValue);
+  }
+  catch (err) {
+    toastError(err);
+  }
+};
+
 export const bookmark = async(pageId: string): Promise<void> => {
   try {
     await apiv3Put('/bookmarks', { pageId, bool: true });

+ 28 - 21
packages/app/src/components/Admin/AuditLog/DateRangePicker.tsx

@@ -1,33 +1,42 @@
-import React, {
-  FC, useRef, forwardRef, useCallback,
-} from 'react';
+import React, { FC, forwardRef, useCallback } from 'react';
 
+import { addDays, format } from 'date-fns';
 import DatePicker from 'react-datepicker';
 import 'react-datepicker/dist/react-datepicker.css';
 
-import { useTranslation } from 'react-i18next';
-
 
 type CustomInputProps = {
-  buttonRef: React.Ref<HTMLButtonElement>
-  onClick?: () => void;
+  value?: string
+  onChange?: () => void
+  onFocus?: () => void
 }
 
-const CustomInput = forwardRef<HTMLButtonElement, CustomInputProps>((props: CustomInputProps) => {
-  const { t } = useTranslation();
+const CustomInput = forwardRef<HTMLInputElement, CustomInputProps>((props: CustomInputProps, ref) => {
+  const dateFormat = 'MM/dd/yyyy';
+  const date = new Date();
+  const placeholder = `${format(date, dateFormat)} - ${format(addDays(date, 1), dateFormat)}`;
+
   return (
-    <button
-      type="button"
-      className="btn btn-outline-secondary dropdown-toggle"
-      ref={props.buttonRef}
-      onClick={props.onClick}
-    >
-      <i className="fa fa-fw fa-calendar" /> {t('admin:audit_log_management.date')}
-    </button>
+    <div className="input-group admin-audit-log">
+      <div className="input-group-prepend">
+        <span className="input-group-text">
+          <i className="fa fa-fw fa-calendar" />
+        </span>
+      </div>
+      <input
+        ref={ref}
+        type="text"
+        value={props?.value}
+        onFocus={props?.onFocus}
+        onChange={props?.onChange}
+        placeholder={placeholder}
+        className="form-control date-range-picker"
+        aria-describedby="basic-addon1"
+      />
+    </div>
   );
 });
 
-
 type DateRangePickerProps = {
   startDate: Date | null
   endDate: Date | null
@@ -37,8 +46,6 @@ type DateRangePickerProps = {
 export const DateRangePicker: FC<DateRangePickerProps> = (props: DateRangePickerProps) => {
   const { startDate, endDate, onChange } = props;
 
-  const buttonRef = useRef(null);
-
   const changeHandler = useCallback((dateList: Date[] | null[]) => {
     if (onChange != null) {
       const [start, end] = dateList;
@@ -59,7 +66,7 @@ export const DateRangePicker: FC<DateRangePickerProps> = (props: DateRangePicker
         startDate={startDate}
         endDate={endDate}
         onChange={changeHandler}
-        customInput={<CustomInput buttonRef={buttonRef} />}
+        customInput={<CustomInput />}
       />
     </div>
   );

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

@@ -135,6 +135,11 @@ export const AuditLogManagement: FC = () => {
         <span>
           {isSettingPage ? t('AuditLog Settings') : t('AuditLog')}
         </span>
+        { !isSettingPage && (
+          <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={reloadButtonPushedHandler}>
+            <i className="icon icon-reload"></i>
+          </button>
+        )}
       </h2>
 
       {isSettingPage ? (
@@ -160,17 +165,9 @@ export const AuditLogManagement: FC = () => {
               onChangeMultipleAction={multipleActionCheckboxChangedHandler}
             />
 
-            <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>
+            <button type="button" className="btn btn-link" onClick={clearButtonPushedHandler}>
+              {t('admin:audit_log_management.clear')}
+            </button>
           </div>
 
           <p

+ 18 - 9
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -23,6 +23,7 @@ export const MenuItemType = {
   DELETE: 'delete',
   REVERT: 'revert',
   PATH_RECOVERY: 'pathRecovery',
+  SWITCH_CONTENT_WIDTH: 'switch_content_width',
 } as const;
 export type MenuItemType = typeof MenuItemType[keyof typeof MenuItemType];
 
@@ -42,6 +43,7 @@ type CommonProps = {
   onClickRevertMenuItem?: (pageId: string) => Promise<void> | void,
   onClickPathRecoveryMenuItem?: (pageId: string) => Promise<void> | void,
 
+  additionalMenuItemOnTopRenderer?: React.FunctionComponent<AdditionalMenuItemsRendererProps>,
   additionalMenuItemRenderer?: React.FunctionComponent<AdditionalMenuItemsRendererProps>,
   isInstantRename?: boolean,
   alignRight?: boolean,
@@ -58,13 +60,14 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
   const { t } = useTranslation('');
 
   const {
-    pageId, isLoading,
-    pageInfo, isEnableActions, forceHideMenuItems, operationProcessData,
-    onClickBookmarkMenuItem, onClickRenameMenuItem, onClickDuplicateMenuItem, onClickDeleteMenuItem, onClickRevertMenuItem, onClickPathRecoveryMenuItem,
-    additionalMenuItemRenderer: AdditionalMenuItems, isInstantRename, alignRight,
+    pageId, isLoading, pageInfo, isEnableActions, forceHideMenuItems, operationProcessData,
+    onClickBookmarkMenuItem, onClickRenameMenuItem, onClickDuplicateMenuItem, onClickDeleteMenuItem,
+    onClickRevertMenuItem, onClickPathRecoveryMenuItem,
+    additionalMenuItemOnTopRenderer: AdditionalMenuItemsOnTop,
+    additionalMenuItemRenderer: AdditionalMenuItems,
+    isInstantRename, alignRight,
   } = props;
 
-
   // eslint-disable-next-line react-hooks/rules-of-hooks
   const bookmarkItemClickedHandler = useCallback(async() => {
     if (!isIPageInfoForOperation(pageInfo) || onClickBookmarkMenuItem == null) {
@@ -149,8 +152,15 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
           </DropdownItem>
         ) }
 
+        { AdditionalMenuItemsOnTop && (
+          <>
+            <AdditionalMenuItemsOnTop pageInfo={pageInfo} />
+            <DropdownItem divider />
+          </>
+        ) }
+
         {/* Bookmark */}
-        { !forceHideMenuItems?.includes(MenuItemType.BOOKMARK) && isEnableActions && !pageInfo.isEmpty && isIPageInfoForOperation(pageInfo) && (
+        { !forceHideMenuItems?.includes(MenuItemType.BOOKMARK) && isEnableActions && isIPageInfoForOperation(pageInfo) && (
           <DropdownItem
             onClick={bookmarkItemClickedHandler}
             className="grw-page-control-dropdown-item"
@@ -252,9 +262,8 @@ type PageItemControlSubstanceProps = CommonProps & {
 export const PageItemControlSubstance = (props: PageItemControlSubstanceProps): JSX.Element => {
 
   const {
-    pageId, pageInfo: presetPageInfo, fetchOnInit,
-    children,
-    onClickBookmarkMenuItem, onClickRenameMenuItem, onClickDuplicateMenuItem, onClickDeleteMenuItem, onClickPathRecoveryMenuItem,
+    pageId, pageInfo: presetPageInfo, fetchOnInit, children, onClickBookmarkMenuItem, onClickRenameMenuItem,
+    onClickDuplicateMenuItem, onClickDeleteMenuItem, onClickPathRecoveryMenuItem,
   } = props;
 
   const [isOpen, setIsOpen] = useState(false);

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

@@ -1,6 +1,12 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useMemo } from 'react';
 
-import { toggleBookmark, toggleLike, toggleSubscribe } from '~/client/services/page-operation';
+import { useTranslation } from 'react-i18next';
+import { DropdownItem } from 'reactstrap';
+
+import {
+  toggleBookmark, toggleLike, toggleSubscribe, updateContentWidth,
+} from '~/client/services/page-operation';
+import { toastError } from '~/client/util/apiNotification';
 import {
   IPageInfoForOperation, IPageToDeleteWithMeta, IPageToRenameWithMeta, isIPageInfoForEntity, isIPageInfoForOperation,
 } from '~/interfaces/page';
@@ -19,6 +25,43 @@ import SubscribeButton from '../SubscribeButton';
 import SeenUserInfo from '../User/SeenUserInfo';
 
 
+type WideViewMenuItemProps = AdditionalMenuItemsRendererProps & {
+  onClickMenuItem: (newValue: boolean) => void,
+}
+
+const WideViewMenuItem = (props: WideViewMenuItemProps): JSX.Element => {
+  const { t } = useTranslation();
+
+  const {
+    pageInfo, onClickMenuItem,
+  } = props;
+
+  if (!isIPageInfoForEntity(pageInfo)) {
+    return <></>;
+  }
+
+  return (
+    <DropdownItem
+      onClick={() => onClickMenuItem(!pageInfo.expandContentWidth)}
+      className="grw-page-control-dropdown-item"
+    >
+      <div className="custom-control custom-switch ml-1">
+        <input
+          id="switchContentWidth"
+          className="custom-control-input"
+          type="checkbox"
+          checked={pageInfo.expandContentWidth}
+          onChange={() => {}}
+        />
+        <label className="custom-control-label" htmlFor="switchContentWidth">
+          { t('wide_view') }
+        </label>
+      </div>
+    </DropdownItem>
+  );
+};
+
+
 type CommonProps = {
   isCompactMode?: boolean,
   disableSeenUserInfoPopover?: boolean,
@@ -140,6 +183,32 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
     onClickDeleteMenuItem(pageToDelete);
   }, [onClickDeleteMenuItem, pageId, pageInfo, path, revisionId]);
 
+  const switchContentWidthClickHandler = useCallback(async(newValue: boolean) => {
+    if (isGuestUser == null || isGuestUser) {
+      return;
+    }
+    if (!isIPageInfoForEntity(pageInfo)) {
+      return;
+    }
+    try {
+      await updateContentWidth(pageId, newValue);
+      mutatePageInfo();
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [isGuestUser, mutatePageInfo, pageId, pageInfo]);
+
+  const wideviewMenuItemRenderer = useMemo(() => {
+    if (!isIPageInfoForEntity(pageInfo)) {
+      return undefined;
+    }
+    return props => <WideViewMenuItem {...props} onClickMenuItem={switchContentWidthClickHandler} />;
+  }, [pageInfo, switchContentWidthClickHandler]);
+
+  if (!isIPageInfoForOperation(pageInfo)) {
+    return <></>;
+  }
 
   const {
     sumOfLikers, sumOfSeenUsers, isLiked, bookmarkCount, isBookmarked,
@@ -188,6 +257,7 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
           pageInfo={pageInfo}
           isEnableActions={!isGuestUser}
           forceHideMenuItems={forceHideMenuItemsWithBookmark}
+          additionalMenuItemOnTopRenderer={wideviewMenuItemRenderer}
           additionalMenuItemRenderer={additionalMenuItemRenderer}
           onClickRenameMenuItem={renameMenuItemClickHandler}
           onClickDuplicateMenuItem={duplicateMenuItemClickHandler}

+ 14 - 1
packages/app/src/components/Page/RevisionRenderer.tsx

@@ -2,6 +2,7 @@ import React, {
   useCallback, useEffect, useMemo, useState,
 } from 'react';
 
+import AppContainer from '~/client/services/AppContainer';
 import { blinkElem } from '~/client/util/blink-section-header';
 import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
 import { CustomWindow } from '~/interfaces/global';
@@ -9,6 +10,8 @@ import GrowiRenderer from '~/services/renderer/growi-renderer';
 import { useEditorSettings } from '~/stores/editor';
 import loggerFactory from '~/utils/logger';
 
+import { withUnstatedContainers } from '../UnstatedUtils';
+
 import RevisionBody from './RevisionBody';
 
 
@@ -84,6 +87,7 @@ function getHighlightedBody(body: string, _keywords: string | string[]): string
 
 
 type Props = {
+  appContainer: AppContainer,
   growiRenderer: GrowiRenderer,
   markdown: string,
   pagePath: string,
@@ -154,9 +158,13 @@ const RevisionRenderer = (props: Props): JSX.Element => {
 
   }, [currentRenderingContext, interceptorManager, renderHtml]);
 
+  const config = props.appContainer.getConfig();
+  const isMathJaxEnabled = !!config.env.MATHJAX;
+
   return (
     <RevisionBody
       html={html}
+      isMathJaxEnabled={isMathJaxEnabled}
       additionalClassName={props.additionalClassName}
       renderMathJaxOnInit
     />
@@ -164,4 +172,9 @@ const RevisionRenderer = (props: Props): JSX.Element => {
 
 };
 
-export default RevisionRenderer;
+/**
+   * Wrapper component for using unstated
+   */
+const RevisionRendererWrapper = withUnstatedContainers(RevisionRenderer, [AppContainer]);
+
+export default RevisionRendererWrapper;

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

@@ -1,108 +0,0 @@
-import React, { Fragment, useMemo } from 'react';
-
-import PropTypes from 'prop-types';
-import { useTranslation } from 'react-i18next';
-import { UncontrolledTooltip } from 'reactstrap';
-
-import { useCurrentPageId } from '~/stores/context';
-
-import AttachmentIcon from './Icons/AttachmentIcon';
-import HistoryIcon from './Icons/HistoryIcon';
-import PageListIcon from './Icons/PageListIcon';
-import ShareLinkIcon from './Icons/ShareLinkIcon';
-import TimeLineIcon from './Icons/TimeLineIcon';
-import { withUnstatedContainers } from './UnstatedUtils';
-
-
-const PageAccessoriesModalControl = (props) => {
-  const { t } = useTranslation();
-  const {
-    pageAccessoriesContainer, isGuestUser, isSharedUser,
-  } = props;
-  const isLinkSharingDisabled = pageAccessoriesContainer.appContainer.config.disableLinkSharing;
-
-  const { data: pageId } = useCurrentPageId();
-
-  const accessoriesBtnList = useMemo(() => {
-    return [
-      {
-        name: 'pagelist',
-        Icon: <PageListIcon />,
-        disabled: isSharedUser,
-        i18n: t('page_list'),
-      },
-      {
-        name: 'timeline',
-        Icon: <TimeLineIcon />,
-        disabled: isSharedUser,
-        i18n: t('Timeline View'),
-      },
-      {
-        name: 'pageHistory',
-        Icon: <HistoryIcon />,
-        disabled: isGuestUser || isSharedUser,
-        i18n: t('History'),
-      },
-      {
-        name: 'attachment',
-        Icon: <AttachmentIcon />,
-        i18n: t('attachment_data'),
-      },
-      {
-        name: 'shareLink',
-        Icon: <ShareLinkIcon />,
-        disabled: isGuestUser || isSharedUser || isLinkSharingDisabled,
-        i18n: t('share_links.share_link_management'),
-      },
-    ];
-  }, [t, isGuestUser, isSharedUser, isLinkSharingDisabled]);
-
-  return (
-    <div className="grw-page-accessories-control d-flex flex-nowrap align-items-center justify-content-end justify-content-lg-between">
-      {accessoriesBtnList.map((accessory) => {
-
-        let tooltipMessage;
-        if (accessory.disabled) {
-          tooltipMessage = t('Not available for guest');
-          if (accessory.name === 'shareLink' && isLinkSharingDisabled) {
-            tooltipMessage = t('Link sharing is disabled');
-          }
-        }
-        else {
-          tooltipMessage = accessory.i18n;
-        }
-
-        return (
-          <Fragment key={accessory.name}>
-            <div id={`shareLink-btn-wrapper-for-tooltip-for-${accessory.name}`}>
-              <button
-                type="button"
-                className={`btn btn-link grw-btn-page-accessories ${accessory.disabled ? 'disabled' : ''}`}
-                onClick={() => pageAccessoriesContainer.openPageAccessoriesModal(accessory.name)}
-              >
-                {accessory.Icon}
-              </button>
-            </div>
-            <UncontrolledTooltip placement="top" target={`shareLink-btn-wrapper-for-tooltip-for-${accessory.name}`} fade={false}>
-              {tooltipMessage}
-            </UncontrolledTooltip>
-          </Fragment>
-        );
-      })}
-    </div>
-  );
-};
-
-PageAccessoriesModalControl.propTypes = {
-  pageAccessoriesContainer: PropTypes.any,
-
-  isGuestUser: PropTypes.bool.isRequired,
-  isSharedUser: PropTypes.bool.isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const PageAccessoriesModalControlWrapper = withUnstatedContainers(PageAccessoriesModalControl, []);
-
-export default PageAccessoriesModalControlWrapper;

+ 0 - 404
packages/app/src/components/PageEditor/Editor.jsx

@@ -1,404 +0,0 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
-import Dropzone from 'react-dropzone';
-import {
-  Modal, ModalHeader, ModalBody,
-} from 'reactstrap';
-
-import { useDefaultIndentSize } from '~/stores/context';
-import { useEditorSettings } from '~/stores/editor';
-
-import AbstractEditor from './AbstractEditor';
-import Cheatsheet from './Cheatsheet';
-import CodeMirrorEditor from './CodeMirrorEditor';
-import pasteHelper from './PasteHelper';
-import TextAreaEditor from './TextAreaEditor';
-
-
-class Editor extends AbstractEditor {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      isComponentDidMount: false,
-      dropzoneActive: false,
-      isUploading: false,
-      isCheatsheetModalShown: false,
-    };
-
-    this.getEditorSubstance = this.getEditorSubstance.bind(this);
-
-    this.pasteFilesHandler = this.pasteFilesHandler.bind(this);
-
-    this.dragEnterHandler = this.dragEnterHandler.bind(this);
-    this.dragLeaveHandler = this.dragLeaveHandler.bind(this);
-    this.dropHandler = this.dropHandler.bind(this);
-
-    this.showMarkdownHelp = this.showMarkdownHelp.bind(this);
-    this.addAttachmentHandler = this.addAttachmentHandler.bind(this);
-
-    this.getAcceptableType = this.getAcceptableType.bind(this);
-    this.getDropzoneClassName = this.getDropzoneClassName.bind(this);
-    this.renderDropzoneOverlay = this.renderDropzoneOverlay.bind(this);
-  }
-
-  componentDidMount() {
-    this.setState({ isComponentDidMount: true });
-  }
-
-  getEditorSubstance() {
-    return this.props.isMobile
-      ? this.taEditor
-      : this.cmEditor;
-  }
-
-  /**
-   * @inheritDoc
-   */
-  forceToFocus() {
-    this.getEditorSubstance().forceToFocus();
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setValue(newValue) {
-    this.getEditorSubstance().setValue(newValue);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setGfmMode(bool) {
-    this.getEditorSubstance().setGfmMode(bool);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setCaretLine(line) {
-    this.getEditorSubstance().setCaretLine(line);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  setScrollTopByLine(line) {
-    this.getEditorSubstance().setScrollTopByLine(line);
-  }
-
-  /**
-   * @inheritDoc
-   */
-  insertText(text) {
-    this.getEditorSubstance().insertText(text);
-  }
-
-  /**
-   * remove overlay and set isUploading to false
-   */
-  terminateUploadingState() {
-    this.setState({
-      dropzoneActive: false,
-      isUploading: false,
-    });
-  }
-
-  /**
-   * dispatch onUpload event
-   */
-  dispatchUpload(files) {
-    if (this.props.onUpload != null) {
-      this.props.onUpload(files);
-    }
-  }
-
-  /**
-   * get acceptable(uploadable) file type
-   */
-  getAcceptableType() {
-    let accept = 'null'; // reject all
-    if (this.props.isUploadable) {
-      if (!this.props.isUploadableFile) {
-        accept = 'image/*'; // image only
-      }
-      else {
-        accept = ''; // allow all
-      }
-    }
-
-    return accept;
-  }
-
-  pasteFilesHandler(event) {
-    const items = event.clipboardData.items || event.clipboardData.files || [];
-
-    // abort if length is not 1
-    if (items.length < 1) {
-      return;
-    }
-
-    for (let i = 0; i < items.length; i++) {
-      try {
-        const file = items[i].getAsFile();
-        // check file type (the same process as Dropzone)
-        if (file != null && pasteHelper.isAcceptableType(file, this.getAcceptableType())) {
-          this.dispatchUpload(file);
-          this.setState({ isUploading: true });
-        }
-      }
-      catch (e) {
-        this.logger.error(e);
-      }
-    }
-  }
-
-  dragEnterHandler(event) {
-    const dataTransfer = event.dataTransfer;
-
-    // do nothing if contents is not files
-    if (!dataTransfer.types.includes('Files')) {
-      return;
-    }
-
-    this.setState({ dropzoneActive: true });
-  }
-
-  dragLeaveHandler() {
-    this.setState({ dropzoneActive: false });
-  }
-
-  dropHandler(accepted, rejected) {
-    // rejected
-    if (accepted.length !== 1) { // length should be 0 or 1 because `multiple={false}` is set
-      this.setState({ dropzoneActive: false });
-      return;
-    }
-
-    const file = accepted[0];
-    this.dispatchUpload(file);
-    this.setState({ isUploading: true });
-  }
-
-  showMarkdownHelp() {
-    this.setState({ isCheatsheetModalShown: true });
-  }
-
-  addAttachmentHandler() {
-    this.dropzone.open();
-  }
-
-  getDropzoneClassName(isDragAccept, isDragReject) {
-    let className = 'dropzone';
-    if (!this.props.isUploadable) {
-      className += ' dropzone-unuploadable';
-    }
-    else {
-      className += ' dropzone-uploadable';
-
-      if (this.props.isUploadableFile) {
-        className += ' dropzone-uploadablefile';
-      }
-    }
-
-    // uploading
-    if (this.state.isUploading) {
-      className += ' dropzone-uploading';
-    }
-
-    if (isDragAccept) {
-      className += ' dropzone-accepted';
-    }
-
-    if (isDragReject) {
-      className += ' dropzone-rejected';
-    }
-
-    return className;
-  }
-
-  renderDropzoneOverlay() {
-    return (
-      <div className="overlay overlay-dropzone-active">
-        {this.state.isUploading
-          && (
-            <span className="overlay-content">
-              <div className="speeding-wheel d-inline-block"></div>
-              <span className="sr-only">Uploading...</span>
-            </span>
-          )
-        }
-        {!this.state.isUploading && <span className="overlay-content"></span>}
-      </div>
-    );
-  }
-
-  renderNavbar() {
-    return (
-      <div className="m-0 navbar navbar-default navbar-editor" style={{ minHeight: 'unset' }}>
-        <ul className="pl-2 nav nav-navbar">
-          { this.getNavbarItems() != null && this.getNavbarItems().map((item, idx) => {
-            // eslint-disable-next-line react/no-array-index-key
-            return <li key={`navbarItem-${idx}`}>{item}</li>;
-          }) }
-        </ul>
-      </div>
-    );
-  }
-
-  getNavbarItems() {
-    // set navbar items(react elements) here that are common in CodeMirrorEditor or TextAreaEditor
-    const navbarItems = [];
-
-    // concat common items and items specific to CodeMirrorEditor or TextAreaEditor
-    return navbarItems.concat(this.getEditorSubstance().getNavbarItems());
-  }
-
-  renderCheatsheetModal() {
-    const hideCheatsheetModal = () => {
-      this.setState({ isCheatsheetModalShown: false });
-    };
-
-    return (
-      <Modal isOpen={this.state.isCheatsheetModalShown} toggle={hideCheatsheetModal} className="modal-gfm-cheatsheet">
-        <ModalHeader tag="h4" toggle={hideCheatsheetModal} className="bg-primary text-light">
-          <i className="icon-fw icon-question" />Markdown help
-        </ModalHeader>
-        <ModalBody>
-          <Cheatsheet />
-        </ModalBody>
-      </Modal>
-    );
-  }
-
-
-  render() {
-    const flexContainer = {
-      height: '100%',
-      display: 'flex',
-      flexDirection: 'column',
-    };
-
-    const {
-      isMobile,
-      indentSize,
-    } = this.props;
-
-    return (
-      <>
-        <div style={flexContainer} className="editor-container">
-          <Dropzone
-            ref={(c) => { this.dropzone = c }}
-            accept={this.getAcceptableType()}
-            noClick
-            noKeyboard
-            multiple={false}
-            onDragLeave={this.dragLeaveHandler}
-            onDrop={this.dropHandler}
-          >
-            {({
-              getRootProps,
-              getInputProps,
-              isDragAccept,
-              isDragReject,
-            }) => {
-              return (
-                <div className={this.getDropzoneClassName(isDragAccept, isDragReject)} {...getRootProps()}>
-                  { this.state.dropzoneActive && this.renderDropzoneOverlay() }
-
-                  { this.state.isComponentDidMount && this.renderNavbar() }
-
-                  {/* for PC */}
-                  { !isMobile && (
-                    // eslint-disable-next-line arrow-body-style
-                    <CodeMirrorEditor
-                      ref={(c) => { this.cmEditor = c }}
-                      indentSize={indentSize}
-                      onPasteFiles={this.pasteFilesHandler}
-                      onDragEnter={this.dragEnterHandler}
-                      onMarkdownHelpButtonClicked={this.showMarkdownHelp}
-                      onAddAttachmentButtonClicked={this.addAttachmentHandler}
-                      {...this.props}
-                    />
-                  )}
-
-                  {/* for mobile */}
-                  { isMobile && (
-                    <TextAreaEditor
-                      ref={(c) => { this.taEditor = c }}
-                      onPasteFiles={this.pasteFilesHandler}
-                      onDragEnter={this.dragEnterHandler}
-                      {...this.props}
-                    />
-                  )}
-
-                  <input {...getInputProps()} />
-                </div>
-              );
-            }}
-          </Dropzone>
-
-          { this.props.isUploadable
-            && (
-              <button
-                type="button"
-                className="btn btn-outline-secondary btn-block btn-open-dropzone"
-                onClick={this.addAttachmentHandler}
-              >
-                <i className="icon-paper-clip" aria-hidden="true"></i>&nbsp;
-                Attach files
-                <span className="d-none d-sm-inline">
-                &nbsp;by dragging &amp; dropping,&nbsp;
-                  <span className="btn-link">selecting them</span>,&nbsp;
-                  or pasting from the clipboard.
-                </span>
-
-              </button>
-            )
-          }
-
-          { this.renderCheatsheetModal() }
-
-        </div>
-      </>
-    );
-  }
-
-}
-
-Editor.propTypes = Object.assign({
-  noCdn: PropTypes.bool,
-  // this value is markdown
-  value: PropTypes.string,
-  isMobile: PropTypes.bool,
-  isUploadable: PropTypes.bool,
-  isUploadableFile: PropTypes.bool,
-  onChange: PropTypes.func,
-  onUpload: PropTypes.func,
-  editorSettings: PropTypes.object.isRequired,
-  indentSize: PropTypes.number,
-}, AbstractEditor.propTypes);
-
-
-const EditorWrapper = React.forwardRef((props, ref) => {
-  const { data: editorSettings } = useEditorSettings();
-  const { data: defaultIndentSize } = useDefaultIndentSize();
-
-  if (editorSettings == null) {
-    return <></>;
-  }
-
-  return (
-    <Editor
-      ref={ref}
-      {...props}
-      editorSettings={editorSettings}
-      // eslint-disable-next-line react/prop-types
-      indentSize={props.indentSize ?? defaultIndentSize}
-    />
-  );
-});
-
-export default EditorWrapper;

+ 362 - 0
packages/app/src/components/PageEditor/Editor.tsx

@@ -0,0 +1,362 @@
+import React, {
+  useState, useRef, useImperativeHandle, useCallback, useMemo,
+} from 'react';
+
+import Dropzone from 'react-dropzone';
+import { useTranslation } from 'react-i18next';
+import {
+  Modal, ModalHeader, ModalBody,
+} from 'reactstrap';
+
+import { toastError } from '~/client/util/apiNotification';
+import { useDefaultIndentSize } from '~/stores/context';
+import { useEditorSettings } from '~/stores/editor';
+import { useIsMobile } from '~/stores/ui';
+
+import { IEditorMethods } from '../../interfaces/editor-methods';
+
+import Cheatsheet from './Cheatsheet';
+import CodeMirrorEditor from './CodeMirrorEditor';
+import pasteHelper from './PasteHelper';
+import TextAreaEditor from './TextAreaEditor';
+
+
+type EditorPropsType = {
+  value?: string,
+  isGfmMode?: boolean,
+  noCdn?: boolean,
+  isUploadable?: boolean,
+  isUploadableFile?: boolean,
+  onChange?: () => void,
+  onUpload?: (file) => void,
+  indentSize?: number,
+  onScrollCursorIntoView?: (line: number) => void,
+  onSave?: () => Promise<void>,
+  onPasteFiles?: (event: Event) => void,
+  onCtrlEnter?: (event: Event) => void,
+}
+
+type DropzoneRef = {
+  open: () => void
+}
+
+const Editor = React.forwardRef((props: EditorPropsType, ref): JSX.Element => {
+  const {
+    onUpload, isUploadable, isUploadableFile, indentSize, isGfmMode = true,
+  } = props;
+
+  const [dropzoneActive, setDropzoneActive] = useState(false);
+  const [isUploading, setIsUploading] = useState(false);
+  const [isCheatsheetModalShown, setIsCheatsheetModalShown] = useState(false);
+
+  const { t } = useTranslation();
+  const { data: editorSettings } = useEditorSettings();
+  const { data: defaultIndentSize } = useDefaultIndentSize();
+  const { data: isMobile } = useIsMobile();
+
+  const dropzoneRef = useRef<DropzoneRef>(null);
+  const cmEditorRef = useRef<CodeMirrorEditor>(null);
+  const taEditorRef = useRef<TextAreaEditor>(null);
+
+  const editorSubstance = isMobile ? taEditorRef.current : cmEditorRef.current;
+
+  const methods: Partial<IEditorMethods> = useMemo(() => {
+    return {
+      forceToFocus: () => {
+        if (editorSubstance == null) { return }
+        editorSubstance.forceToFocus();
+      },
+      setValue: (newValue: string) => {
+        if (editorSubstance == null) { return }
+        editorSubstance.setValue(newValue);
+      },
+      setGfmMode: (bool: boolean) => {
+        if (editorSubstance == null) { return }
+        editorSubstance.setGfmMode(bool);
+      },
+      setCaretLine: (line: number) => {
+        if (editorSubstance == null) { return }
+        editorSubstance.setCaretLine(line);
+      },
+      setScrollTopByLine: (line: number) => {
+        if (editorSubstance == null) { return }
+        editorSubstance.setScrollTopByLine(line);
+      },
+      insertText: (text: string) => {
+        if (editorSubstance == null) { return }
+        editorSubstance.insertText(text);
+      },
+      getNavbarItems: (): JSX.Element[] => {
+        if (editorSubstance == null) { return [] }
+        // concat common items and items specific to CodeMirrorEditor or TextAreaEditor
+        const navbarItems = editorSubstance.getNavbarItems() ?? [];
+        return navbarItems;
+      },
+    };
+  }, [editorSubstance]);
+
+  // methods for ref
+  useImperativeHandle(ref, () => ({
+    forceToFocus: methods.forceToFocus,
+    setValue: methods.setValue,
+    setGfmMode: methods.setGfmMode,
+    setCaretLine: methods.setCaretLine,
+    setScrollTopByLine: methods.setScrollTopByLine,
+    insertText: methods.insertText,
+    /**
+   * remove overlay and set isUploading to false
+   */
+    terminateUploadingState: () => {
+      setDropzoneActive(false);
+      setIsUploading(false);
+    },
+  }));
+
+  /**
+   * dispatch onUpload event
+   */
+  const dispatchUpload = useCallback((files) => {
+    if (onUpload != null) {
+      onUpload(files);
+    }
+  }, [onUpload]);
+
+  /**
+   * get acceptable(uploadable) file type
+   */
+  const getAcceptableType = useCallback(() => {
+    let accept = 'null'; // reject all
+    if (isUploadable) {
+      if (!isUploadableFile) {
+        accept = 'image/*'; // image only
+      }
+      else {
+        accept = ''; // allow all
+      }
+    }
+
+    return accept;
+  }, [isUploadable, isUploadableFile]);
+
+  const pasteFilesHandler = useCallback((event) => {
+    const items = event.clipboardData.items || event.clipboardData.files || [];
+
+    toastError(t('toaster.file_upload_failed'));
+
+    // abort if length is not 1
+    if (items.length < 1) {
+      return;
+    }
+
+    for (let i = 0; i < items.length; i++) {
+      try {
+        const file = items[i].getAsFile();
+        // check file type (the same process as Dropzone)
+        if (file != null && pasteHelper.isAcceptableType(file, getAcceptableType())) {
+          dispatchUpload(file);
+          setIsUploading(true);
+        }
+      }
+      catch (e) {
+        toastError(t('toaster.file_upload_failed'));
+      }
+    }
+  }, [dispatchUpload, getAcceptableType, t]);
+
+  const dragEnterHandler = useCallback((event) => {
+    const dataTransfer = event.dataTransfer;
+
+    // do nothing if contents is not files
+    if (!dataTransfer.types.includes('Files')) {
+      return;
+    }
+
+    setDropzoneActive(true);
+  }, []);
+
+  const dropHandler = useCallback((accepted) => {
+    // rejected
+    if (accepted.length !== 1) { // length should be 0 or 1 because `multiple={false}` is set
+      setDropzoneActive(false);
+      return;
+    }
+
+    const file = accepted[0];
+    dispatchUpload(file);
+    setIsUploading(true);
+  }, [dispatchUpload]);
+
+  const addAttachmentHandler = useCallback(() => {
+    if (dropzoneRef.current == null) { return }
+    dropzoneRef.current.open();
+  }, []);
+
+  const getDropzoneClassName = useCallback((isDragAccept: boolean, isDragReject: boolean) => {
+    let className = 'dropzone';
+    if (!isUploadable) {
+      className += ' dropzone-unuploadable';
+    }
+    else {
+      className += ' dropzone-uploadable';
+
+      if (isUploadableFile) {
+        className += ' dropzone-uploadablefile';
+      }
+    }
+
+    // uploading
+    if (isUploading) {
+      className += ' dropzone-uploading';
+    }
+
+    if (isDragAccept) {
+      className += ' dropzone-accepted';
+    }
+
+    if (isDragReject) {
+      className += ' dropzone-rejected';
+    }
+
+    return className;
+  }, [isUploadable, isUploading, isUploadableFile]);
+
+  const renderDropzoneOverlay = useCallback(() => {
+    return (
+      <div className="overlay overlay-dropzone-active">
+        {isUploading
+          && (
+            <span className="overlay-content">
+              <div className="speeding-wheel d-inline-block"></div>
+              <span className="sr-only">Uploading...</span>
+            </span>
+          )
+        }
+        {!isUploading && <span className="overlay-content"></span>}
+      </div>
+    );
+  }, [isUploading]);
+
+  const renderNavbar = useCallback(() => {
+    return (
+      <div className="m-0 navbar navbar-default navbar-editor" style={{ minHeight: 'unset' }}>
+        <ul className="pl-2 nav nav-navbar">
+          { methods.getNavbarItems?.().map((item, idx) => {
+            // eslint-disable-next-line react/no-array-index-key
+            return <li key={`navbarItem-${idx}`}>{item}</li>;
+          }) }
+        </ul>
+      </div>
+    );
+  }, [methods]);
+
+  const renderCheatsheetModal = useCallback(() => {
+    const hideCheatsheetModal = () => {
+      setIsCheatsheetModalShown(false);
+    };
+
+    return (
+      <Modal isOpen={isCheatsheetModalShown} toggle={hideCheatsheetModal} className="modal-gfm-cheatsheet">
+        <ModalHeader tag="h4" toggle={hideCheatsheetModal} className="bg-primary text-light">
+          <i className="icon-fw icon-question" />Markdown help
+        </ModalHeader>
+        <ModalBody>
+          <Cheatsheet />
+        </ModalBody>
+      </Modal>
+    );
+  }, [isCheatsheetModalShown]);
+
+  if (editorSettings == null) {
+    return <></>;
+  }
+
+  const flexContainer: React.CSSProperties = {
+    height: '100%',
+    display: 'flex',
+    flexDirection: 'column',
+  };
+
+  return (
+    <>
+      <div style={flexContainer} className="editor-container">
+        <Dropzone
+          ref={dropzoneRef}
+          accept={getAcceptableType()}
+          noClick
+          noKeyboard
+          multiple={false}
+          onDragLeave={() => { setDropzoneActive(false) }}
+          onDrop={dropHandler}
+        >
+          {({
+            getRootProps,
+            getInputProps,
+            isDragAccept,
+            isDragReject,
+          }) => {
+            return (
+              <div className={getDropzoneClassName(isDragAccept, isDragReject)} {...getRootProps()}>
+                { dropzoneActive && renderDropzoneOverlay() }
+
+                { renderNavbar() }
+
+                {/* for PC */}
+                { !isMobile && (
+                  // eslint-disable-next-line arrow-body-style
+                  <CodeMirrorEditor
+                    ref={cmEditorRef}
+                    indentSize={indentSize ?? defaultIndentSize}
+                    onPasteFiles={pasteFilesHandler}
+                    onDragEnter={dragEnterHandler}
+                    onMarkdownHelpButtonClicked={() => { setIsCheatsheetModalShown(true) }}
+                    onAddAttachmentButtonClicked={addAttachmentHandler}
+                    editorSettings={editorSettings}
+                    isGfmMode={isGfmMode}
+                    {...props}
+                  />
+                )}
+
+                {/* for mobile */}
+                { isMobile && (
+                  <TextAreaEditor
+                    ref={taEditorRef}
+                    onPasteFiles={pasteFilesHandler}
+                    onDragEnter={dragEnterHandler}
+                    {...props}
+                  />
+                )}
+
+                <input {...getInputProps()} />
+              </div>
+            );
+          }}
+        </Dropzone>
+
+        { isUploadable
+          && (
+            <button
+              type="button"
+              className="btn btn-outline-secondary btn-block btn-open-dropzone"
+              onClick={addAttachmentHandler}
+            >
+              <i className="icon-paper-clip" aria-hidden="true"></i>&nbsp;
+              Attach files
+              <span className="d-none d-sm-inline">
+              &nbsp;by dragging &amp; dropping,&nbsp;
+                <span className="btn-link">selecting them</span>,&nbsp;
+                or pasting from the clipboard.
+              </span>
+
+            </button>
+          )
+        }
+
+        { renderCheatsheetModal() }
+
+      </div>
+    </>
+  );
+});
+
+
+export default Editor;

+ 1 - 1
packages/app/src/components/PageList/PageListItemL.tsx

@@ -99,7 +99,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
   const lastUpdateDate = format(new Date(pageData.updatedAt), 'yyyy/MM/dd HH:mm:ss');
 
   useEffect(() => {
-    if (isIPageInfoForEntity(pageInfo) && pageInfo != null) {
+    if (isIPageInfoForEntity(pageInfo)) {
       // likerCount
       setLikerCount(pageInfo.likerIds?.length ?? 0);
       // bookmarkCount

+ 2 - 1
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -21,7 +21,7 @@ import { useFullTextSearchTermManager } from '~/stores/search';
 
 
 import AppContainer from '../../client/services/AppContainer';
-import { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
+import { AdditionalMenuItemsRendererProps, ForceHideMenuItems, MenuItemType } from '../Common/Dropdown/PageItemControl';
 import { GrowiSubNavigation } from '../Navbar/GrowiSubNavigation';
 import { SubNavButtons } from '../Navbar/SubNavButtons';
 import RevisionLoader from '../Page/RevisionLoader';
@@ -176,6 +176,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
       ? page.revision
       : page.revision._id;
 
+
     return (
       <div className="d-flex flex-column align-items-end justify-content-center py-md-2">
         <SubNavButtons

+ 0 - 1
packages/app/src/components/SearchPage/SearchResultList.tsx

@@ -126,7 +126,6 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
     advanceFts();
   };
 
-
   return (
     <ul data-testid="search-result-list" className="page-list-ul list-group list-group-flush">
       { (injectedPages ?? pages).map((page, i) => {

+ 9 - 14
packages/app/src/components/Sidebar/CustomSidebar.tsx

@@ -1,13 +1,12 @@
 import React, { FC } from 'react';
 
 import AppContainer from '~/client/services/AppContainer';
-import loggerFactory from '~/utils/logger';
+import { IRevision } from '~/interfaces/revision';
 import { useSWRxPageByPath } from '~/stores/page';
+import { useCustomSidebarRenderer } from '~/stores/renderer';
+import loggerFactory from '~/utils/logger';
 
-import { withUnstatedContainers } from '../UnstatedUtils';
 import RevisionRenderer from '../Page/RevisionRenderer';
-import { IRevision } from '~/interfaces/revision';
-import { useCustomSidebarRenderer } from '~/stores/renderer';
 
 const logger = loggerFactory('growi:cli:CustomSidebar');
 
@@ -26,9 +25,7 @@ type Props = {
   appContainer: AppContainer,
 };
 
-const CustomSidebar: FC<Props> = (props: Props) => {
-
-  const { appContainer } = props;
+const CustomSidebar: FC<Props> = () => {
 
   const { data: renderer } = useCustomSidebarRenderer();
 
@@ -41,6 +38,9 @@ const CustomSidebar: FC<Props> = (props: Props) => {
   const isLoading = page === undefined && error == null;
   const markdown = (page?.revision as IRevision | undefined)?.body;
 
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const RevisionRendererAny: any = RevisionRenderer;
+
   return (
     <>
       <div className="grw-sidebar-content-header p-3 d-flex">
@@ -64,7 +64,7 @@ const CustomSidebar: FC<Props> = (props: Props) => {
       {
         (!isLoading && markdown != null) && (
           <div className="p-3">
-            <RevisionRenderer
+            <RevisionRendererAny
               growiRenderer={renderer}
               markdown={markdown}
               pagePath="/Sidebar"
@@ -83,9 +83,4 @@ const CustomSidebar: FC<Props> = (props: Props) => {
   );
 };
 
-/**
- * Wrapper component for using unstated
- */
-const CustomSidebarWrapper = withUnstatedContainers(CustomSidebar, [AppContainer]);
-
-export default CustomSidebarWrapper;
+export default CustomSidebar;

+ 17 - 0
packages/app/src/interfaces/editor-methods.ts

@@ -0,0 +1,17 @@
+export interface IEditorMethods {
+  forceToFocus: () => void,
+  setValue: (newValue: string) => void,
+  setGfmMode: (bool: boolean) => void,
+  setCaretLine: (line: number) => void,
+  setScrollTopByLine: (line: number) => void,
+  getStrFromBol(): void,
+  getStrToEol: () => void,
+  getStrFromBolToSelectedUpperPos: () => void,
+  replaceBolToCurrentPos: (text: string) => void,
+  replaceLine: (text: string) => void,
+  insertText: (text: string) => void,
+  insertLinebreak: () => void,
+  dispatchSave: () => void,
+  dispatchPasteFiles: (event: Event) => void,
+  getNavbarItems: () => JSX.Element[],
+}

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

@@ -29,6 +29,7 @@ export interface IPage {
   pageIdOnHackmd: string,
   revisionHackmdSynced: Ref<IRevision>,
   hasDraftOnHackmd: boolean,
+  expandContentWidth?: boolean,
   deleteUser: Ref<IUser>,
   deletedAt: Date,
 }
@@ -61,6 +62,7 @@ export type IPageInfoForEntity = IPageInfo & {
   likerIds: string[],
   sumOfSeenUsers: number,
   seenUserIds: string[],
+  expandContentWidth?: boolean,
 }
 
 export type IPageInfoForOperation = IPageInfoForEntity & {

+ 4 - 1
packages/app/src/server/models/obsolete-page.js

@@ -683,6 +683,7 @@ export const getPageSchema = (crowi) => {
     const Revision = crowi.model('Revision');
     const format = options.format || 'markdown';
     const grantUserGroupId = options.grantUserGroupId || null;
+    const expandContentWidth = crowi.configManager.getConfig('crowi', 'customize:isContainerFluid');
 
     // sanitize path
     path = crowi.xss.process(path); // eslint-disable-line no-param-reassign
@@ -704,7 +705,9 @@ export const getPageSchema = (crowi) => {
     page.creator = user;
     page.lastUpdateUser = user;
     page.status = STATUS_PUBLISHED;
-
+    if (expandContentWidth != null) {
+      page.expandContentWidth = expandContentWidth;
+    }
     await validateAppliedScope(user, grant, grantUserGroupId);
     page.applyScope(user, grant, grantUserGroupId);
 

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

@@ -102,6 +102,7 @@ const schema = new Schema<PageDocument, PageModel>({
   pageIdOnHackmd: { type: String },
   revisionHackmdSynced: { type: ObjectId, ref: 'Revision' }, // the revision that is synced to HackMD
   hasDraftOnHackmd: { type: Boolean }, // set true if revision and revisionHackmdSynced are same but HackMD document has modified
+  expandContentWidth: { type: Boolean },
   updatedAt: { type: Date, default: Date.now }, // Do not use timetamps for updatedAt because it breaks 'updateMetadata: false' option
   deleteUser: { type: ObjectId, ref: 'User' },
   deletedAt: { type: Date },

+ 27 - 2
packages/app/src/server/routes/apiv3/page.js

@@ -18,7 +18,6 @@ const router = express.Router();
 const { convertToNewAffiliationPath, isTopPage } = pagePathUtils;
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
-
 /**
  * @swagger
  *  tags:
@@ -166,8 +165,9 @@ module.exports = (crowi) => {
   const certifySharedPage = require('../../middlewares/certify-shared-page')(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);
 
+  const configManager = crowi.configManager;
+
   const globalNotificationService = crowi.getGlobalNotificationService();
-  const socketIoService = crowi.socketIoService;
   const { Page, GlobalNotificationSetting, Bookmark } = crowi.models;
   const { pageService, exportService } = crowi;
 
@@ -220,6 +220,9 @@ module.exports = (crowi) => {
     subscribeStatus: [
       query('pageId').isString(),
     ],
+    contentWidth: [
+      body('expandContentWidth').isBoolean(),
+    ],
   };
 
   /**
@@ -817,5 +820,27 @@ module.exports = (crowi) => {
     }
   });
 
+
+  router.put('/:pageId/content-width', accessTokenParser, loginRequiredStrictly, csrf,
+    validator.contentWidth, apiV3FormValidator, async(req, res) => {
+      const { pageId } = req.params;
+      const { expandContentWidth } = req.body;
+
+      const isContainerFluidBySystem = configManager.getConfig('crowi', 'customize:isContainerFluid');
+
+      try {
+        const updateQuery = expandContentWidth === isContainerFluidBySystem
+          ? { $unset: { expandContentWidth } } // remove if the specified value is the same to the system's one
+          : { $set: { expandContentWidth } };
+
+        const page = await Page.updateOne({ _id: pageId }, updateQuery);
+        return res.apiv3({ page });
+      }
+      catch (err) {
+        logger.error('update-content-width-failed', err);
+        return res.apiv3Err(err, 500);
+      }
+    });
+
   return router;
 };

+ 7 - 1
packages/app/src/server/service/page.ts

@@ -2173,6 +2173,7 @@ class PageService {
 
     const likers = page.liker.slice(0, 15) as Ref<IUserHasId>[];
     const seenUsers = page.seenUsers.slice(0, 15) as Ref<IUserHasId>[];
+    const expandContentWidth = page.expandContentWidth ?? this.crowi.configManager.getConfig('crowi', 'customize:isContainerFluid');
 
     return {
       isV5Compatible: isTopPage(page.path) || page.parent != null,
@@ -2185,6 +2186,7 @@ class PageService {
       isDeletable: isMovable,
       isAbleToDeleteCompletely: false,
       isRevertible: isTrashPage(page.path),
+      expandContentWidth,
     };
 
   }
@@ -3354,6 +3356,8 @@ class PageService {
   async create(path: string, body: string, user, options: PageCreateOptions = {}): Promise<PageDocument> {
     const Page = mongoose.model('Page') as unknown as PageModel;
 
+    const expandContentWidth = this.crowi.configManager.getConfig('crowi', 'customize:isContainerFluid');
+
     // Switch method
     const isV5Compatible = this.crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
     if (!isV5Compatible) {
@@ -3400,7 +3404,9 @@ class PageService {
       const parent = await this.getParentAndFillAncestorsByUser(user, path);
       page.parent = parent._id;
     }
-
+    if (expandContentWidth != null) {
+      page.expandContentWidth = expandContentWidth;
+    }
     // Save
     let savedPage = await page.save();
 

+ 9 - 1
packages/app/src/server/views/layout/layout.html

@@ -61,9 +61,17 @@
 {% block html_body %}
 {% set additionalBodyClasses = []; %}
 {% block html_additional_body_classes %}{% endblock %}
-{% if getConfig('crowi', 'customize:isContainerFluid') %}
+
+{% if page.expandContentWidth !== undefined %}
+  {% set isContainerFluid = page.expandContentWidth; %}
+{% else %}
+  {% set isContainerFluid = getConfig('crowi', 'customize:isContainerFluid'); %}
+{% endif %}
+
+{% if isContainerFluid  %}
   {% set additionalBodyClasses = additionalBodyClasses|push('growi-layout-fluid') %}
 {% endif %}
+
 <body
   class="{% block html_base_css %}{% endblock %} growi {{ additionalBodyClasses|join(' ') }}"
   data-plugin-enabled="{{ getConfig('crowi', 'plugin:isEnabledPlugins') }}"

+ 3 - 0
packages/app/src/styles/_admin.scss

@@ -233,6 +233,9 @@ $slack-work-space-name-card-border: #efc1f6;
       max-height: 500px;
       overflow-y: auto;
     }
+    .date-range-picker {
+      width: 188px;
+    }
   }
 
   #layoutOptions {

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

@@ -26,6 +26,9 @@ context('Access to page', () => {
 
   it('/Sandbox/Math is successfully loaded', () => {
     cy.visit('/Sandbox/Math');
+
+    cy.get('mjx-container').should('be.visible');
+
     cy.screenshot(`${ssPrefix}-sandbox-math`);
   });
 
@@ -163,8 +166,6 @@ context('Access to /me/all-in-app-notifications', () => {
     cy.get('.notification-wrapper > a').click();
     cy.get('.notification-wrapper > .dropdown-menu > a').click();
 
-    cy.get('#all-in-app-notifications').should('be.visible');
-
     cy.screenshot(`${ssPrefix}-see-all`, { capture: 'viewport' });
 
     cy.get('.grw-custom-nav-tab > div > ul > li:nth-child(2) > a').click();

+ 3 - 0
packages/app/test/cypress/integration/21-basic-features-for-guest/access-to-page.spec.ts

@@ -22,6 +22,9 @@ context('Access to page by guest', () => {
 
   it('/Sandbox/Math is successfully loaded', () => {
     cy.visit('/Sandbox/Math');
+
+    cy.get('mjx-container').should('be.visible');
+
     cy.screenshot(`${ssPrefix}-sandbox-math`);
   });
 

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/codemirror-textlint",
-  "version": "5.1.1",
+  "version": "5.1.2-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.1",
+  "version": "5.1.2-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.1",
+  "version": "5.1.2-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.1",
+  "version": "5.1.2-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.1",
+  "version": "5.1.2-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.1",
+  "version": "5.1.2-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "5.1.1",
+  "version": "5.1.2-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.1",
+    "@growi/slack": "^5.1.2-RC.0",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",

+ 1 - 1
packages/ui/package.json

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

+ 79 - 24
yarn.lock

@@ -1715,6 +1715,13 @@
   dependencies:
     "@browser-bunyan/levels" "^1.6.0"
 
+"@browser-bunyan/console-formatted-stream@^1.8.0":
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/@browser-bunyan/console-formatted-stream/-/console-formatted-stream-1.8.0.tgz#dda9dcab6ce445cbf2911045709930757e5d48c1"
+  integrity sha512-Lg5SC2uXrvZ6aLwLZT6SErfN1Is4NcrTOb5km4BW/BfL8Lv0CfpsYuhuD7ltdURL6awTYBUiT+BwhKw1Xd9glQ==
+  dependencies:
+    "@browser-bunyan/levels" "^1.8.0"
+
 "@browser-bunyan/console-plain-stream@^1.6.0":
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/@browser-bunyan/console-plain-stream/-/console-plain-stream-1.6.0.tgz#295404482150e7693846ccb07045676218bcc911"
@@ -1722,6 +1729,13 @@
   dependencies:
     "@browser-bunyan/levels" "^1.6.0"
 
+"@browser-bunyan/console-plain-stream@^1.8.0":
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/@browser-bunyan/console-plain-stream/-/console-plain-stream-1.8.0.tgz#18cd8fe879a0f576cf84c4fa4647e86cd3feea3e"
+  integrity sha512-S0WNsH5zvMfkbayIx90wANGHQ8l3Bvd7mjgy95/bYmUzcI+Mwkv2eJcSufdTP/MbdHBhjv/lEdLDOXEPBi+w3A==
+  dependencies:
+    "@browser-bunyan/levels" "^1.8.0"
+
 "@browser-bunyan/console-raw-stream@^1.6.0":
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/@browser-bunyan/console-raw-stream/-/console-raw-stream-1.6.0.tgz#255f4734c064dc046fe7896353982c563e2ec150"
@@ -1729,11 +1743,23 @@
   dependencies:
     "@browser-bunyan/levels" "^1.6.0"
 
+"@browser-bunyan/console-raw-stream@^1.8.0":
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/@browser-bunyan/console-raw-stream/-/console-raw-stream-1.8.0.tgz#5d0438139bbffd9ed779241df6ae7e5f3a2a7b0c"
+  integrity sha512-6M/xEiNckbFslQMaS1BHAxvuvN1Wtbh/aq4UzQD3fjEPFCxtubvf4KyzwPxUXA5CXq7leVZ+cibEUCRBsm5bzg==
+  dependencies:
+    "@browser-bunyan/levels" "^1.8.0"
+
 "@browser-bunyan/levels@^1.6.0":
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/@browser-bunyan/levels/-/levels-1.6.0.tgz#3a50b8118254aa2ac26caf9d2aafa72d157e374b"
   integrity sha512-wte6nXXZH62Y/RGysYRlOkKxuJn+4S8xEamMF0fDncxxy0SriCHYwGPyWGF0FWYWmRzbZuEkp7dNebBf9Xfeeg==
 
+"@browser-bunyan/levels@^1.8.0":
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/@browser-bunyan/levels/-/levels-1.8.0.tgz#1c0a98d04284e0620e8ee414d7ce43385080a5cf"
+  integrity sha512-f9oSDik8kAl+4rhVyHqIr012P1boHFUKc7D9nzA5+lDsFoP90UQnDwpseqBdF2mTaWYju10E7h+GdH8u+7MHOQ==
+
 "@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"
@@ -5792,6 +5818,16 @@ browser-bunyan@^1.6.3:
     "@browser-bunyan/console-raw-stream" "^1.6.0"
     "@browser-bunyan/levels" "^1.6.0"
 
+browser-bunyan@^1.8.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/browser-bunyan/-/browser-bunyan-1.8.0.tgz#6b9662fea571c642fce80ad002d62e3ea1453393"
+  integrity sha512-Et1TaRUm8m2oy4OTi69g0qAM8wqpofACUgkdBnj1Kq2aC8Wpl8w+lNevebPG6zKH2w0Aq+BHiAXWwjm0/QbkaQ==
+  dependencies:
+    "@browser-bunyan/console-formatted-stream" "^1.8.0"
+    "@browser-bunyan/console-plain-stream" "^1.8.0"
+    "@browser-bunyan/console-raw-stream" "^1.8.0"
+    "@browser-bunyan/levels" "^1.8.0"
+
 browser-or-node@>=1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/browser-or-node/-/browser-or-node-1.2.1.tgz#cd65172da6a7fd689c7a650d326bd2ad145419a7"
@@ -11594,10 +11630,10 @@ inquirer@7.1.0:
     strip-ansi "^6.0.0"
     through "^2.3.6"
 
-inquirer@8.1.5:
-  version "8.1.5"
-  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.1.5.tgz#2dc5159203c826d654915b5fe6990fd17f54a150"
-  integrity sha512-G6/9xUqmt/r+UvufSyrPpt84NYwhKZ9jLsgMbQzlx804XErNupor8WQdBnBRrXmBfTPpuwf1sV+ss2ovjgdXIg==
+inquirer@8.2.1:
+  version "8.2.1"
+  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.1.tgz#e00022e3e8930a92662f760f020686530a84671d"
+  integrity sha512-pxhBaw9cyTFMjwKtkjePWDhvwzvrNGAw7En4hottzlPvz80GZaMZthdDU35aA6/f5FRZf3uhE057q8w1DE3V2g==
   dependencies:
     ansi-escapes "^4.2.1"
     chalk "^4.1.1"
@@ -11609,7 +11645,7 @@ inquirer@8.1.5:
     mute-stream "0.0.8"
     ora "^5.4.1"
     run-async "^2.4.0"
-    rxjs "^7.2.0"
+    rxjs "^7.5.5"
     string-width "^4.1.0"
     strip-ansi "^6.0.0"
     through "^2.3.6"
@@ -18278,14 +18314,14 @@ reg-publish-s3-plugin@^0.11.0:
     reg-suit-util "^0.11.0"
     uuid "^8.3.0"
 
-reg-suit-core@^0.11.1:
-  version "0.11.1"
-  resolved "https://registry.yarnpkg.com/reg-suit-core/-/reg-suit-core-0.11.1.tgz#e554fab4da79a6caf2c8a312fadbdc4539a1583e"
-  integrity sha512-v3U6c8Mn8f9pz44YrnvxCLCRUWDs4t86/55XfBtxt3LGB+QxN9ekK2dNRPk67UsX3OZoB1n1dSjAJONTlWpNPw==
+reg-suit-core@^0.12.1:
+  version "0.12.1"
+  resolved "https://registry.yarnpkg.com/reg-suit-core/-/reg-suit-core-0.12.1.tgz#511f63d2053a5bb76181d994074bbbb6ca432848"
+  integrity sha512-lc8MSax1CAZVJgps3EjhRIsHjfUFZun0FjC+FOwjKXohWOq+z0HfI8+IfCjhMHNbkG2kIa/YcnQF6Zd0/X/Usw==
   dependencies:
     cpx "^1.5.0"
     reg-cli "^0.17.0"
-    reg-suit-util "^0.11.0"
+    reg-suit-util "^0.12.1"
     rimraf "^3.0.2"
 
 reg-suit-util@^0.11.0:
@@ -18307,16 +18343,35 @@ reg-suit-util@^0.11.0:
     mime-types "^2.1.27"
     mkdirp "^1.0.4"
 
-reg-suit@^0.11.1:
-  version "0.11.1"
-  resolved "https://registry.yarnpkg.com/reg-suit/-/reg-suit-0.11.1.tgz#2bc7180873cf793724825eb492e5396a2d95daa3"
-  integrity sha512-tGCPEoQhHcUn4oZGj5nu26yaFEpksPHUwGPoKpBxSV5ldmwS/zY13KyJZaetlZh/IV7HTxLP/i9TXBlhzc3QWw==
+reg-suit-util@^0.12.1:
+  version "0.12.1"
+  resolved "https://registry.yarnpkg.com/reg-suit-util/-/reg-suit-util-0.12.1.tgz#ceec40cf116ec4986d151b0af96d9fe49a60fb9e"
+  integrity sha512-w/cLYCBX8ULDsSZEJHArOuaWQms/YErFFhMsnKClvuf/mlvRQgok/zKcksaYoyAQVe/seY+/SRnHdPVtw5YViQ==
+  dependencies:
+    "@types/cli-progress" "^3.8.0"
+    "@types/cli-spinner" "^0.2.0"
+    "@types/glob" "^7.1.3"
+    "@types/lodash" "^4.14.161"
+    "@types/mime-types" "^2.1.0"
+    "@types/mkdirp" "^1.0.1"
+    chalk "^4.1.0"
+    cli-progress "^3.8.2"
+    cli-spinner "^0.2.6"
+    glob "^7.1.6"
+    lodash "^4.17.20"
+    mime-types "^2.1.27"
+    mkdirp "^1.0.4"
+
+reg-suit@^0.12.1:
+  version "0.12.1"
+  resolved "https://registry.yarnpkg.com/reg-suit/-/reg-suit-0.12.1.tgz#c6897c1e909d932673dc9ed93f0ce544b8c0262f"
+  integrity sha512-aXVcRK7fVE582F+iv3VEJEsnJ3g7CfF1pjeSS+WbObCYBN+FucO3eMMq8AvgYDwhH7oc0eTBofW7Nc0Ko2zumg==
   dependencies:
     cp-file "9.1.0"
     ignore "5.1.8"
-    inquirer "8.1.5"
-    reg-suit-core "^0.11.1"
-    reg-suit-util "^0.11.0"
+    inquirer "8.2.1"
+    reg-suit-core "^0.12.1"
+    reg-suit-util "^0.12.1"
     yargs "17.0.1"
 
 regenerator-runtime@^0.11.0:
@@ -18797,13 +18852,6 @@ rxjs@^6.5.3, rxjs@^6.6.0:
   dependencies:
     tslib "^1.9.0"
 
-rxjs@^7.2.0:
-  version "7.5.2"
-  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.2.tgz#11e4a3a1dfad85dbf7fb6e33cbba17668497490b"
-  integrity sha512-PwDt186XaL3QN5qXj/H9DGyHhP3/RYYgZZwqBv9Tv8rsAaiwFH1IsJJlcgD37J7UW5a6O67qX0KWKS3/pu0m4w==
-  dependencies:
-    tslib "^2.1.0"
-
 rxjs@^7.4.0:
   version "7.5.1"
   resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.1.tgz#af73df343cbcab37628197f43ea0c8256f54b157"
@@ -18811,6 +18859,13 @@ rxjs@^7.4.0:
   dependencies:
     tslib "^2.1.0"
 
+rxjs@^7.5.5:
+  version "7.5.6"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.6.tgz#0446577557862afd6903517ce7cae79ecb9662bc"
+  integrity sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==
+  dependencies:
+    tslib "^2.1.0"
+
 safe-buffer@5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"