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

Merge branch 'dev/7.0.x' into feat/132682-create-todays-page

ryoji-s 2 лет назад
Родитель
Сommit
f035a4f6e8
64 измененных файлов с 760 добавлено и 265 удалено
  1. 18 1
      CHANGELOG.md
  2. 1 1
      apps/app/docker/README.md
  3. 2 0
      apps/app/package.json
  4. 2 3
      apps/app/src/client/services/user-ui-settings.ts
  5. 1 1
      apps/app/src/client/util/apiv1-client.ts
  6. 1 1
      apps/app/src/client/util/apiv3-client.ts
  7. 0 0
      apps/app/src/components/Common/PageViewLayout.module.scss
  8. 1 1
      apps/app/src/components/Common/PageViewLayout.tsx
  9. 1 1
      apps/app/src/components/Layout/BasicLayout.tsx
  10. 0 32
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  11. 10 9
      apps/app/src/components/Navbar/GrowiNavbarBottom.module.scss
  12. 6 7
      apps/app/src/components/Navbar/GrowiNavbarBottom.tsx
  13. 1 1
      apps/app/src/components/Page/PageView.tsx
  14. 6 0
      apps/app/src/components/PageComment/CommentEditor.tsx
  15. 44 4
      apps/app/src/components/PageControls/PageControls.tsx
  16. 7 1
      apps/app/src/components/PageEditor/PageEditor.tsx
  17. 2 2
      apps/app/src/components/PageRenameModal.tsx
  18. 15 20
      apps/app/src/components/PageSideContents/PageSideContents.tsx
  19. 6 20
      apps/app/src/components/PageTags/PageTags.tsx
  20. 11 12
      apps/app/src/components/PageTags/RenderTagLabels.tsx
  21. 48 27
      apps/app/src/components/PageTags/TagEditModal.tsx
  22. 7 6
      apps/app/src/components/PageTags/TagsInput.tsx
  23. 1 1
      apps/app/src/components/ShareLinkPageView.tsx
  24. 25 6
      apps/app/src/components/Sidebar/Sidebar.module.scss
  25. 16 11
      apps/app/src/components/Sidebar/Sidebar.tsx
  26. 1 1
      apps/app/src/components/Sidebar/SidebarHead/ToggleCollapseButton.tsx
  27. 1 3
      apps/app/src/components/Sidebar/SidebarNav/PrimaryItems.tsx
  28. 1 1
      apps/app/src/migrations/20221219011829-remove-basic-auth-related-config.js
  29. 1 1
      apps/app/src/migrations/20230213090921-remove-presentation-configurations.js
  30. 1 1
      apps/app/src/migrations/20230731075753-add_installed_date_to_config.js
  31. 34 0
      apps/app/src/migrations/20231102012742-clean-user-ui-settings-collection.js
  32. 10 8
      apps/app/src/pages/[[...path]].page.tsx
  33. 18 4
      apps/app/src/pages/me/[[...path]].page.tsx
  34. 49 1
      apps/app/src/stores/modal.tsx
  35. 47 14
      apps/app/src/stores/ui.tsx
  36. 2 2
      apps/app/src/utils/logger/index.ts
  37. 8 1
      apps/app/tsconfig.json
  38. 4 2
      apps/slackbot-proxy/package.json
  39. 8 1
      apps/slackbot-proxy/tsconfig.json
  40. 1 1
      packages/core/src/remark-plugins/util/option-parser.ts
  41. 0 7
      packages/core/src/utils/objectid-utils.ts
  42. 4 4
      packages/core/src/utils/page-utils.ts
  43. 1 0
      packages/editor/package.json
  44. 1 0
      packages/editor/src/@types/emoji-mart.d.ts
  45. 3 0
      packages/editor/src/@types/scss.d.ts
  46. 1 1
      packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx
  47. 146 4
      packages/editor/src/components/CodeMirrorEditor/Toolbar/EmojiButton.tsx
  48. 8 3
      packages/editor/src/components/CodeMirrorEditor/Toolbar/Toolbar.tsx
  49. 1 1
      packages/editor/src/components/CodeMirrorEditorMain.tsx
  50. 8 2
      packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts
  51. 75 0
      packages/editor/src/services/extensions/emojiAutocompletionSettings.ts
  52. 1 0
      packages/editor/src/stores/index.ts
  53. 27 0
      packages/editor/src/stores/use-resolved-theme.ts
  54. 3 0
      packages/editor/vite.config.ts
  55. 1 1
      packages/pluginkit/src/v4/server/utils/template/scan.ts
  56. 4 1
      packages/presentation/tsconfig.json
  57. 8 1
      packages/remark-attachment-refs/tsconfig.json
  58. 8 1
      packages/remark-drawio/tsconfig.json
  59. 1 1
      packages/remark-lsx/src/client/stores/lsx/lsx.ts
  60. 8 1
      packages/remark-lsx/tsconfig.json
  61. 3 0
      packages/slack/package.json
  62. 1 1
      packages/slack/src/utils/check-communicable.ts
  63. 1 5
      tsconfig.base.json
  64. 28 22
      yarn.lock

+ 18 - 1
CHANGELOG.md

@@ -1,9 +1,26 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.2.1...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.2.2...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v6.2.2](https://github.com/weseek/growi/compare/v6.2.1...v6.2.2) - 2023-10-30
+
+### 🚀 Improvement
+
+- imprv: Printing styles (#8195) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: Show liker counts in lsx (#8194) @yuki-takei
+
+### 🧰 Maintenance
+
+- ci(deps-dev): bump postcss from 8.4.26 to 8.4.31 (#8142) @dependabot
+- ci(deps): bump cypress-io/github-action from 5 to 6 (#8051) @dependabot
+- ci(deps): bump amannn/action-semantic-pull-request from 5.0.2 to 5.3.0 (#8127) @dependabot
+- ci(deps): bump aws-actions/configure-aws-credentials from 2 to 4 (#8128) @dependabot
+
 ## [v6.2.1](https://github.com/weseek/growi/compare/v6.2.0...v6.2.1) - 2023-10-03
 
 ### BREAKING CHANGES

+ 1 - 1
apps/app/docker/README.md

@@ -11,7 +11,7 @@ Supported tags and respective Dockerfile links
 ------------------------------------------------
 
 * [`7.0.0`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.0/apps/app/docker/Dockerfile)
-* [`6.2.1`, `6.2`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.1/apps/app/docker/Dockerfile)
+* [`6.2.2`, `6.2`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.2/apps/app/docker/Dockerfile)
 * [`6.1.15`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.15/apps/app/docker/Dockerfile)
 
 

+ 2 - 0
apps/app/package.json

@@ -218,6 +218,8 @@
     "@types/express": "^4.17.11",
     "@types/jest": "^29.5.2",
     "@types/react-scroll": "^1.8.4",
+    "@types/throttle-debounce": "^5.0.1",
+    "@types/url-join": "^4.0.2",
     "autoprefixer": "^9.0.0",
     "babel-loader": "^8.2.5",
     "bootstrap": "^5.3.1",

+ 2 - 3
apps/app/src/client/services/user-ui-settings.ts

@@ -17,12 +17,11 @@ const _putUserUISettingsInBulk = (): Promise<AxiosResponse<IUserUISettings>> =>
 
 const _putUserUISettingsInBulkDebounced = debounce(1500, _putUserUISettingsInBulk);
 
-type ScheduleToPutFunction = (settings: Partial<IUserUISettings>) => Promise<AxiosResponse<IUserUISettings>>;
-export const scheduleToPut: ScheduleToPutFunction = (settings: Partial<IUserUISettings>): Promise<AxiosResponse<IUserUISettings>> => {
+export const scheduleToPut = (settings: Partial<IUserUISettings>): void => {
   settingsForBulk = {
     ...settingsForBulk,
     ...settings,
   };
 
-  return _putUserUISettingsInBulkDebounced();
+  _putUserUISettingsInBulkDebounced();
 };

+ 1 - 1
apps/app/src/client/util/apiv1-client.ts

@@ -1,4 +1,4 @@
-import * as urljoin from 'url-join';
+import urljoin from 'url-join';
 
 import axios from '~/utils/axios';
 

+ 1 - 1
apps/app/src/client/util/apiv3-client.ts

@@ -1,6 +1,6 @@
 // eslint-disable-next-line no-restricted-imports
 import { AxiosResponse } from 'axios';
-import * as urljoin from 'url-join';
+import urljoin from 'url-join';
 
 // eslint-disable-next-line no-restricted-imports
 

+ 0 - 0
apps/app/src/components/Layout/PageViewLayout.module.scss → apps/app/src/components/Common/PageViewLayout.module.scss


+ 1 - 1
apps/app/src/components/Layout/PageViewLayout.tsx → apps/app/src/components/Common/PageViewLayout.tsx

@@ -25,7 +25,7 @@ export const PageViewLayout = (props: Props): JSX.Element => {
                 <div className="flex-grow-1 flex-basis-0 mw-0">
                   {children}
                 </div>
-                <div className="grw-side-contents-container col-lg-3  d-edit-none" data-vrt-blackout-side-contents>
+                <div className="grw-side-contents-container col-lg-3  d-edit-none d-print-none" data-vrt-blackout-side-contents>
                   <div className="grw-side-contents-sticky-container">
                     {sideContents}
                   </div>

+ 1 - 1
apps/app/src/components/Layout/BasicLayout.tsx

@@ -33,7 +33,7 @@ type Props = {
 
 export const BasicLayout = ({ children, className }: Props): JSX.Element => {
   return (
-    <RawLayout className={className ?? ''}>
+    <RawLayout className={`${className ?? ''}`}>
       <DndProvider backend={HTML5Backend}>
 
         <div className="page-wrapper flex-row">

+ 0 - 32
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -202,12 +202,6 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
   const { data: isAbleToChangeEditorMode } = useIsAbleToChangeEditorMode();
 
-  // TODO: implement tags for editor
-  // refs: https://redmine.weseek.co.jp/issues/132125
-  // eslint-disable-next-line max-len
-  // const { data: tagsForEditors, mutate: mutatePageTagsForEditors, sync: syncPageTagsForEditors } = usePageTagsForEditors(!isSharedPage ? currentPage?._id : undefined);
-  // const { data: templateTagData } = useTemplateTagData();
-
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openRenameModal } = usePageRenameModal();
   const { open: openDeleteModal } = usePageDeleteModal();
@@ -217,36 +211,10 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const grant = currentPage?.grant ?? grantData?.grant;
   const grantUserGroupId = currentPage?.grantedGroup?._id ?? grantData?.grantedGroup?.id;
 
-  // TODO: implement tags for editor
-  // refs: https://redmine.weseek.co.jp/issues/132125
-  // useEffect(() => {
-  //   // Run only when tagsInfoData has been updated
-  //   if (templateTagData == null) {
-  //     syncPageTagsForEditors();
-  //   }
-  //   // eslint-disable-next-line react-hooks/exhaustive-deps
-  // }, [tagsInfoData?.tags]);
-
-  // TODO: implement tags for editor
-  // refs: https://redmine.weseek.co.jp/issues/132125
-  // useEffect(() => {
-  //   if (pageId === null && templateTagData != null) {
-  //     mutatePageTagsForEditors(templateTagData);
-  //   }
-  // }, [pageId, mutatePageTagsForEditors, templateTagData, mutateSWRTagsInfo]);
-
   const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false);
 
   const { isLinkSharingDisabled } = props;
 
-  // TODO: implement tags for editor
-  // refs: https://redmine.weseek.co.jp/issues/132125
-  // const tagsUpdatedHandlerForEditMode = useCallback((newTags: string[]): void => {
-  //   // It will not be reflected in the DB until the page is refreshed
-  //   mutatePageTagsForEditors(newTags);
-  //   return;
-  // }, [mutatePageTagsForEditors]);
-
   const duplicateItemClickedHandler = useCallback(async(page: IPageForPageDuplicateModal) => {
     const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => {
       router.push(toPath);

+ 10 - 9
apps/app/src/components/Navbar/GrowiNavbarBottom.module.scss

@@ -2,19 +2,18 @@
 @use '~/styles/mixins';
 
 .grw-navbar-bottom :global {
-  height: var.$grw-navbar-bottom-height;
-
   // apply transition
   transition-property: bottom;
   @include mixins.apply-navigation-transition();
-}
 
-.grw-navbar-bottom {
-  &:global(.grw-navbar-bottom-drawer-opened) {
-    bottom: #{-1 * var.$grw-navbar-bottom-height};
+  .navbar {
+    height: var.$grw-navbar-bottom-height;
   }
 }
 
+.grw-navbar-bottom-drawer-opened {
+  bottom: #{-1 * var.$grw-navbar-bottom-height};
+}
 
 // centering icons
 .grw-navbar-bottom :global {
@@ -25,7 +24,9 @@
 }
 
 // == Colors
-.grw-navbar-bottom {
-  background-color: rgba(var(--bs-body-bg-rgb), 0.7);
-  backdrop-filter: blur(35px);
+.grw-navbar-bottom :global {
+  .navbar {
+    background-color: rgba(var(--bs-body-bg-rgb), 0.7);
+    backdrop-filter: blur(35px);
+  }
 }

+ 6 - 7
apps/app/src/components/Navbar/GrowiNavbarBottom.tsx

@@ -18,13 +18,12 @@ export const GrowiNavbarBottom = (): JSX.Element => {
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: isSearchPage } = useIsSearchPage();
 
-  const additionalClasses = [styles['grw-navbar-bottom']];
-  if (isDrawerOpened) {
-    additionalClasses.push('grw-navbar-bottom-drawer-opened');
-  }
-
   return (
-    <div className="d-md-none d-edit-none fixed-bottom">
+    <div className={`
+      ${styles['grw-navbar-bottom']}
+      ${isDrawerOpened ? styles['grw-navbar-bottom-drawer-opened'] : ''}
+      d-md-none d-edit-none d-print-none fixed-bottom`}
+    >
 
       { !isDeviceLargerThanMd && !isSearchPage && (
         <div id="grw-global-search-collapse" className="grw-global-search collapse bg-dark">
@@ -34,7 +33,7 @@ export const GrowiNavbarBottom = (): JSX.Element => {
         </div>
       ) }
 
-      <div className={`navbar navbar-expand px-4 px-sm-5 ${additionalClasses.join(' ')}`}>
+      <div className="navbar navbar-expand px-4 px-sm-5">
 
         <ul className="navbar-nav flex-grow-1 d-flex align-items-center justify-content-between">
           <li className="nav-item">

+ 1 - 1
apps/app/src/components/Page/PageView.tsx

@@ -18,7 +18,7 @@ import { useIsMobile } from '~/stores/ui';
 
 import type { CommentsProps } from '../Comments';
 import { PagePathNavSticky } from '../Common/PagePathNav';
-import { PageViewLayout } from '../Layout/PageViewLayout';
+import { PageViewLayout } from '../Common/PageViewLayout';
 import { PageAlerts } from '../PageAlert/PageAlerts';
 import { PageContentFooter } from '../PageContentFooter';
 import type { PageSideContentsProps } from '../PageSideContents';

+ 6 - 0
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -2,6 +2,7 @@ import React, {
   useCallback, useState, useRef, useEffect,
 } from 'react';
 
+import { useResolvedThemeForEditor } from '@growi/editor';
 import { UserPicture } from '@growi/ui/dist/components';
 import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
@@ -19,6 +20,7 @@ import {
 } from '~/stores/context';
 import { useSWRxSlackChannels, useIsSlackEnabled, useIsEnabledUnsavedWarning } from '~/stores/editor';
 import { useCurrentPagePath } from '~/stores/page';
+import { useNextThemes } from '~/stores/use-next-themes';
 
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import { NotAvailableForGuest } from '../NotAvailableForGuest';
@@ -76,6 +78,10 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     increment: incrementEditingCommentsNum,
     decrement: decrementEditingCommentsNum,
   } = useSWRxEditingCommentsNum();
+  const { mutate: mutateResolvedTheme } = useResolvedThemeForEditor();
+
+  const { resolvedTheme } = useNextThemes();
+  mutateResolvedTheme(resolvedTheme);
 
   const [isReadyToUse, setIsReadyToUse] = useState(!isForNewComment);
   const [comment, setComment] = useState(commentBody ?? '');

+ 44 - 4
apps/app/src/components/PageControls/PageControls.tsx

@@ -14,9 +14,10 @@ import {
 } from '~/client/services/page-operation';
 import { toastError } from '~/client/util/toastr';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
-import type { IPageForPageDuplicateModal } from '~/stores/modal';
+import { useTagEditModal, type IPageForPageDuplicateModal } from '~/stores/modal';
+import { EditorMode, useEditorMode } from '~/stores/ui';
 
-import { useSWRxPageInfo } from '../../stores/page';
+import { useSWRxPageInfo, useSWRxTagsInfo } from '../../stores/page';
 import { useSWRxUsersList } from '../../stores/user';
 import {
   AdditionalMenuItemsRendererProps, ForceHideMenuItems, MenuItemType,
@@ -31,6 +32,26 @@ import SubscribeButton from './SubscribeButton';
 
 import styles from './PageControls.module.scss';
 
+type TagsProps = {
+  onClickEditTagsButton: () => void,
+}
+
+const Tags = (props: TagsProps): JSX.Element => {
+  const { onClickEditTagsButton } = props;
+
+  return (
+    <div className="grw-taglabels-container d-flex align-items-center">
+      <button
+        type="button"
+        className="btn btn-link btn-edit-tags text-muted border border-secondary p-1 d-flex align-items-center"
+        onClick={onClickEditTagsButton}
+      >
+        <i className="icon-tag me-2" />
+        Tags
+      </button>
+    </div>
+  );
+};
 
 type WideViewMenuItemProps = AdditionalMenuItemsRendererProps & {
   onClickMenuItem: (newValue: boolean) => void,
@@ -84,6 +105,7 @@ type PageControlsSubstanceProps = CommonProps & {
   path?: string | null,
   pageInfo: IPageInfoForOperation,
   expandContentWidth?: boolean,
+  onClickEditTagsButton: () => void,
 }
 
 const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element => {
@@ -91,11 +113,12 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
     pageInfo,
     pageId, revisionId, path, shareLinkId, expandContentWidth,
     disableSeenUserInfoPopover, showPageControlDropdown, forceHideMenuItems, additionalMenuItemRenderer,
-    onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem, onClickSwitchContentWidth,
+    onClickEditTagsButton, onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem, onClickSwitchContentWidth,
   } = props;
 
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
+  const { data: editorMode } = useEditorMode();
 
   const { mutate: mutatePageInfo } = useSWRxPageInfo(pageId, shareLinkId);
 
@@ -214,8 +237,15 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
     MenuItemType.REVERT,
   ];
 
+  const isViewMode = editorMode === EditorMode.View;
+
   return (
     <div className={`grw-page-controls ${styles['grw-page-controls']} d-flex`} style={{ gap: '2px' }}>
+      {revisionId != null && !isViewMode && (
+        <Tags
+          onClickEditTagsButton={onClickEditTagsButton}
+        />
+      )}
       {revisionId != null && (
         <SubscribeButton
           status={pageInfo.subscriptionStatus}
@@ -266,7 +296,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
 type PageControlsProps = CommonProps & {
   pageId: string,
   shareLinkId?: string | null,
-  revisionId?: string | null,
+  revisionId?: string,
   path?: string | null,
   expandContentWidth?: boolean,
 };
@@ -278,6 +308,15 @@ export const PageControls = memo((props: PageControlsProps): JSX.Element => {
   } = props;
 
   const { data: pageInfo, error } = useSWRxPageInfo(pageId ?? null, shareLinkId);
+  const { data: tagsInfoData } = useSWRxTagsInfo(pageId);
+  const { open: openTagEditModal } = useTagEditModal();
+
+  const onClickEditTagsButton = useCallback(() => {
+    if (tagsInfoData == null || revisionId == null) {
+      return;
+    }
+    openTagEditModal(tagsInfoData.tags, pageId, revisionId);
+  }, [pageId, revisionId, tagsInfoData, openTagEditModal]);
 
   if (error != null) {
     return <></>;
@@ -294,6 +333,7 @@ export const PageControls = memo((props: PageControlsProps): JSX.Element => {
       pageId={pageId}
       revisionId={revisionId ?? null}
       path={path}
+      onClickEditTagsButton={onClickEditTagsButton}
       onClickDuplicateMenuItem={onClickDuplicateMenuItem}
       onClickRenameMenuItem={onClickRenameMenuItem}
       onClickDeleteMenuItem={onClickDeleteMenuItem}

+ 7 - 1
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -8,7 +8,8 @@ import nodePath from 'path';
 import type { IPageHasId } from '@growi/core';
 import { pathUtils } from '@growi/core/dist/utils';
 import {
-  CodeMirrorEditorMain, GlobalCodeMirrorEditorKey, useCodeMirrorEditorIsolated, AcceptedUploadFileType,
+  CodeMirrorEditorMain, GlobalCodeMirrorEditorKey, AcceptedUploadFileType,
+  useCodeMirrorEditorIsolated, useResolvedThemeForEditor,
 } from '@growi/editor';
 import detectIndent from 'detect-indent';
 import { useTranslation } from 'next-i18next';
@@ -48,6 +49,7 @@ import {
   EditorMode,
   useEditorMode, useSelectedGrant,
 } from '~/stores/ui';
+import { useNextThemes } from '~/stores/use-next-themes';
 import { useGlobalSocket } from '~/stores/websocket';
 import loggerFactory from '~/utils/logger';
 
@@ -123,9 +125,13 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const { mutate: mutateIsConflict } = useIsConflict();
 
+  const { mutate: mutateResolvedTheme } = useResolvedThemeForEditor();
+
   const saveOrUpdate = useSaveOrUpdate();
   const updateStateAfterSave = useUpdateStateAfterSave(pageId, { supressEditingMarkdownMutation: true });
 
+  const { resolvedTheme } = useNextThemes();
+  mutateResolvedTheme(resolvedTheme);
 
   // TODO: remove workaround
   // for https://redmine.weseek.co.jp/issues/125923

+ 2 - 2
apps/app/src/components/PageRenameModal.tsx

@@ -152,12 +152,12 @@ const PageRenameModal = (): JSX.Element => {
   }, [checkExistPaths]);
 
   const checkIsUsersHomepageDebounce = useMemo(() => {
-    const checkIsPagePathRenameable = () => {
+    const checkIsPagePathRenameable = (pageNameInput: string) => {
       setIsMatchedWithUserHomepagePath(isUsersHomepage(pageNameInput));
     };
 
     return debounce(1000, checkIsPagePathRenameable);
-  }, [isUsersHomepage, pageNameInput]);
+  }, [isUsersHomepage]);
 
   useEffect(() => {
     if (isOpened && page != null && pageNameInput !== page.data.path) {

+ 15 - 20
apps/app/src/components/PageSideContents/PageSideContents.tsx

@@ -6,11 +6,8 @@ import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import { scroller } from 'react-scroll';
 
-import { useUpdateStateAfterSave } from '~/client/services/page-operation';
-import { apiPost } from '~/client/util/apiv1-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
-import { useDescendantsPageListModal } from '~/stores/modal';
+import { useDescendantsPageListModal, useTagEditModal } from '~/stores/modal';
 import { useSWRxPageInfo, useSWRxTagsInfo } from '~/stores/page';
 import { useIsAbleToShowTagLabel } from '~/stores/ui';
 
@@ -45,22 +42,14 @@ const Tags = (props: TagsProps): JSX.Element => {
   const { data: showTagLabel } = useIsAbleToShowTagLabel();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
+  const { open: openTagEditModal } = useTagEditModal();
 
-  const updateStateAfterSave = useUpdateStateAfterSave(pageId);
-
-  const tagsUpdatedHandler = useCallback(async(newTags: string[]) => {
-    try {
-      await apiPost('/tags.update', { pageId, revisionId, tags: newTags });
-
-      updateStateAfterSave?.();
-
-      toastSuccess('updated tags successfully');
+  const onClickEditTagsButton = useCallback(() => {
+    if (tagsInfoData == null) {
+      return;
     }
-    catch (err) {
-      toastError(err);
-    }
-
-  }, [pageId, revisionId, updateStateAfterSave]);
+    openTagEditModal(tagsInfoData.tags, pageId, revisionId);
+  }, [pageId, revisionId, tagsInfoData, openTagEditModal]);
 
   if (!showTagLabel) {
     return <></>;
@@ -71,7 +60,13 @@ const Tags = (props: TagsProps): JSX.Element => {
   return (
     <div className="grw-taglabels-container">
       { tagsInfoData?.tags != null
-        ? <PageTags tags={tagsInfoData.tags} isTagLabelsDisabled={isTagLabelsDisabled ?? false} tagsUpdateInvoked={tagsUpdatedHandler} />
+        ? (
+          <PageTags
+            tags={tagsInfoData.tags}
+            isTagLabelsDisabled={isTagLabelsDisabled}
+            onClickEditTagsButton={onClickEditTagsButton}
+          />
+        )
         : <PageTagsSkeleton />
       }
     </div>
@@ -104,7 +99,7 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
       {/* Tags */}
       <Tags pageId={page._id} revisionId={getIdForRef(page.revision)} />
 
-      <div className={`${styles['grw-page-accessories-controls']} d-flex flex-column gap-2 d-print-none`}>
+      <div className={`${styles['grw-page-accessories-controls']} d-flex flex-column gap-2`}>
         {/* Page list */}
         {!isSharedUser && (
           <div className="d-flex" data-testid="pageListButton">

+ 6 - 20
apps/app/src/components/PageTags/PageTags.tsx

@@ -1,9 +1,8 @@
-import React, { FC, useState } from 'react';
+import React, { FC } from 'react';
 
 import { Skeleton } from '../Skeleton';
 
 import RenderTagLabels from './RenderTagLabels';
-import TagEditModal from './TagEditModal';
 
 import styles from './TagLabels.module.scss';
 
@@ -11,6 +10,7 @@ type Props = {
   tags?: string[],
   isTagLabelsDisabled: boolean,
   tagsUpdateInvoked?: (tags: string[]) => Promise<void> | void,
+  onClickEditTagsButton: () => void,
 }
 
 export const PageTagsSkeleton = (): JSX.Element => {
@@ -18,17 +18,9 @@ export const PageTagsSkeleton = (): JSX.Element => {
 };
 
 export const PageTags:FC<Props> = (props: Props) => {
-  const { tags, isTagLabelsDisabled, tagsUpdateInvoked } = props;
-
-  const [isTagEditModalShown, setIsTagEditModalShown] = useState(false);
-
-  const openEditorModal = () => {
-    setIsTagEditModalShown(true);
-  };
-
-  const closeEditorModal = () => {
-    setIsTagEditModalShown(false);
-  };
+  const {
+    tags, isTagLabelsDisabled, onClickEditTagsButton,
+  } = props;
 
   if (tags == null) {
     return <PageTagsSkeleton />;
@@ -41,16 +33,10 @@ export const PageTags:FC<Props> = (props: Props) => {
       <div className={`${styles['grw-tag-labels']} grw-tag-labels d-flex align-items-center ${printNoneClass}`} data-testid="grw-tag-labels">
         <RenderTagLabels
           tags={tags}
-          openEditorModal={openEditorModal}
           isTagLabelsDisabled={isTagLabelsDisabled}
+          onClickEditTagsButton={onClickEditTagsButton}
         />
       </div>
-      <TagEditModal
-        tags={tags}
-        isOpen={isTagEditModalShown}
-        onClose={closeEditorModal}
-        onTagsUpdated={tagsUpdateInvoked}
-      />
     </>
   );
 };

+ 11 - 12
apps/app/src/components/PageTags/RenderTagLabels.tsx

@@ -10,22 +10,17 @@ import { NotAvailableForReadOnlyUser } from '../NotAvailableForReadOnlyUser';
 type RenderTagLabelsProps = {
   tags: string[],
   isTagLabelsDisabled: boolean,
-  openEditorModal?: () => void,
+  onClickEditTagsButton: () => void,
 }
 
 const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
-  const { tags, isTagLabelsDisabled, openEditorModal } = props;
+  const {
+    tags, isTagLabelsDisabled, onClickEditTagsButton,
+  } = props;
   const { t } = useTranslation();
 
   const { pushState } = useKeywordManager();
 
-  function openEditorHandler() {
-    if (openEditorModal == null) {
-      return;
-    }
-    openEditorModal();
-  }
-
   const isTagsEmpty = tags.length === 0;
 
   return (
@@ -46,10 +41,14 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
         <NotAvailableForReadOnlyUser>
           <div id="edit-tags-btn-wrapper-for-tooltip" className="d-print-none">
             <a
-              className={`btn btn-link btn-edit-tags text-muted p-0 d-flex align-items-center ${isTagsEmpty && 'no-tags'} ${isTagLabelsDisabled && 'disabled'}`}
-              onClick={openEditorHandler}
+              className={
+                `btn btn-link btn-edit-tags text-muted d-flex align-items-center
+                ${isTagsEmpty && 'no-tags'}
+                ${isTagLabelsDisabled && 'disabled'}`
+              }
+              onClick={onClickEditTagsButton}
             >
-              { isTagsEmpty && <>{ t('Add tags for this page') }</>}
+              {isTagsEmpty && <>{ t('Add tags for this page') }</>}
               <i className={`icon-plus ${isTagsEmpty && 'ms-1'}`} />
             </a>
           </div>

+ 48 - 27
apps/app/src/components/PageTags/TagEditModal.tsx

@@ -1,49 +1,62 @@
-import React, { useState, useEffect, useCallback } from 'react';
+import React, {
+  useState, useCallback, useEffect,
+} from 'react';
 
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
+import { useUpdateStateAfterSave } from '~/client/services/page-operation';
+import { apiPost } from '~/client/util/apiv1-client';
+import { toastError, toastSuccess } from '~/client/util/toastr';
+import { useTagEditModal, type TagEditModalStatus } from '~/stores/modal';
+
 import { TagsInput } from './TagsInput';
 
-type Props = {
-  tags: string[],
-  isOpen: boolean,
-  onClose?: () => void,
-  onTagsUpdated?: (tags: string[]) => Promise<void> | void,
-};
+type TagEditModalSubstanceProps = {
+  tagEditModalData: TagEditModalStatus,
+  closeTagEditModal: () => void,
+}
 
-function TagEditModal(props: Props): JSX.Element {
-  const { onClose, onTagsUpdated } = props;
+const TagEditModalSubstance: React.FC<TagEditModalSubstanceProps> = (props: TagEditModalSubstanceProps) => {
+  const { tagEditModalData, closeTagEditModal } = props;
+  const { t } = useTranslation();
+
+  const initTags = tagEditModalData.tags;
+  const isOpen = tagEditModalData.isOpen;
+  const pageId = tagEditModalData.pageId;
+  const revisionId = tagEditModalData.revisionId;
+  const updateStateAfterSave = useUpdateStateAfterSave(pageId);
 
   const [tags, setTags] = useState<string[]>([]);
-  const { t } = useTranslation();
 
+  // use to take initTags when redirect to other page
   useEffect(() => {
-    setTags(props.tags);
-  }, [props.tags]);
+    setTags(initTags);
+  }, [initTags]);
 
-  const closeModalHandler = useCallback(() => {
-    onClose?.();
-  }, [onClose]);
+  const handleSubmit = useCallback(async() => {
 
-  const handleSubmit = useCallback(() => {
-    if (onTagsUpdated == null) {
-      return;
-    }
+    try {
+      await apiPost('/tags.update', { pageId, revisionId, tags });
+      updateStateAfterSave?.();
 
-    onTagsUpdated(tags);
-    closeModalHandler();
-  }, [closeModalHandler, onTagsUpdated, tags]);
+      toastSuccess('updated tags successfully');
+      closeTagEditModal();
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [closeTagEditModal, tags, pageId, revisionId, updateStateAfterSave]);
 
   return (
-    <Modal isOpen={props.isOpen} toggle={closeModalHandler} id="edit-tag-modal" autoFocus={false}>
-      <ModalHeader tag="h4" toggle={closeModalHandler} className="bg-primary text-light">
+    <Modal isOpen={isOpen} toggle={closeTagEditModal} id="edit-tag-modal" autoFocus={false}>
+      <ModalHeader tag="h4" toggle={closeTagEditModal} className="bg-primary text-light">
         {t('tag_edit_modal.edit_tags')}
       </ModalHeader>
       <ModalBody>
-        <TagsInput tags={tags} onTagsUpdated={tags => setTags(tags)} autoFocus />
+        <TagsInput tags={initTags} onTagsUpdated={tags => setTags(tags)} autoFocus />
       </ModalBody>
       <ModalFooter>
         <button type="button" className="btn btn-primary" onClick={handleSubmit}>
@@ -53,6 +66,14 @@ function TagEditModal(props: Props): JSX.Element {
     </Modal>
   );
 
-}
+};
 
-export default TagEditModal;
+export const TagEditModal: React.FC = () => {
+  const { data: tagEditModalData, close: closeTagEditModal } = useTagEditModal();
+
+  if (!tagEditModalData?.isOpen) {
+    return <></>;
+  }
+
+  return <TagEditModalSubstance tagEditModalData={tagEditModalData} closeTagEditModal={closeTagEditModal} />;
+};

+ 7 - 6
apps/app/src/components/PageTags/TagsInput.tsx

@@ -22,8 +22,9 @@ type Props = {
 
 export const TagsInput: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
-  const tagsInputRef = useRef<TypeaheadInstance>(null);
+  const { tags, autoFocus, onTagsUpdated } = props;
 
+  const tagsInputRef = useRef<TypeaheadInstance>(null);
   const [resultTags, setResultTags] = useState<string[]>([]);
   const [searchQuery, setSearchQuery] = useState('');
 
@@ -32,10 +33,10 @@ export const TagsInput: FC<Props> = (props: Props) => {
   const isLoading = error == null && tagsSearch === undefined;
 
   const changeHandler = useCallback((selected: string[]) => {
-    if (props.onTagsUpdated != null) {
-      props.onTagsUpdated(selected);
+    if (onTagsUpdated != null) {
+      onTagsUpdated(selected);
     }
-  }, [props]);
+  }, [onTagsUpdated]);
 
   const searchHandler = useCallback(async(query: string) => {
     const tagsSearchData = tagsSearch?.tags || [];
@@ -64,7 +65,7 @@ export const TagsInput: FC<Props> = (props: Props) => {
         id="tag-typeahead-asynctypeahead"
         ref={tagsInputRef}
         caseSensitive={false}
-        defaultSelected={props.tags ?? []}
+        defaultSelected={tags}
         isLoading={isLoading}
         minLength={1}
         multiple
@@ -74,7 +75,7 @@ export const TagsInput: FC<Props> = (props: Props) => {
         onKeyDown={keyDownHandler}
         options={resultTags} // Search result (Some tag names)
         placeholder={t('tag_edit_modal.tags_input.tag_name')}
-        autoFocus={props.autoFocus}
+        autoFocus={autoFocus}
       />
     </div>
   );

+ 1 - 1
apps/app/src/components/ShareLinkPageView.tsx

@@ -11,7 +11,7 @@ import { useViewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 
 import { PagePathNavSticky } from './Common/PagePathNav';
-import { PageViewLayout } from './Layout/PageViewLayout';
+import { PageViewLayout } from './Common/PageViewLayout';
 import RevisionRenderer from './Page/RevisionRenderer';
 import ShareLinkAlert from './Page/ShareLinkAlert';
 import type { PageSideContentsProps } from './PageSideContents';

+ 25 - 6
apps/app/src/components/Sidebar/Sidebar.module.scss

@@ -5,10 +5,6 @@
 
 .grw-sidebar :global {
   top: 0;
-
-  .sidebar-contents-container {
-    backdrop-filter: blur(20px);
-  }
 }
 
 
@@ -69,6 +65,7 @@
         transform: translateX(-100%);
       }
       &.open {
+        z-index: bs.$zindex-modal;
         transform: translateX(0);
       }
     }
@@ -81,7 +78,18 @@
     --bs-border-color: var(--grw-highlight-200);
 
     .sidebar-contents-container {
-      background-color: rgba(var(--grw-highlight-100-rgb), .5);
+      background-color: color-mix(in srgb, var(--grw-highlight-100), var(--bs-body-bg));
+    }
+  }
+  // frosted glass effect in collapsed mode
+  .grw-sidebar {
+    &:global {
+      &.grw-sidebar-collapsed {
+        .sidebar-contents-container {
+          background-color: rgba(var(--grw-highlight-100-rgb), .5);
+          backdrop-filter: blur(20px);
+        }
+      }
     }
   }
 }
@@ -92,7 +100,18 @@
     --bs-border-color: var(--grw-highlight-800);
 
     .sidebar-contents-container {
-      background-color: rgba(var(--grw-highlight-800-rgb), .5);
+      background-color: color-mix(in srgb, var(--grw-highlight-800), var(--bs-body-bg));
+    }
+  }
+  // frosted glass effect in collapsed mode
+  .grw-sidebar {
+    &:global {
+      &.grw-sidebar-collapsed {
+        .sidebar-contents-container {
+          background-color: rgba(var(--grw-highlight-800-rgb), .5);
+          backdrop-filter: blur(20px);
+        }
+      }
     }
   }
 }

+ 16 - 11
apps/app/src/components/Sidebar/Sidebar.tsx

@@ -5,7 +5,6 @@ import React, {
 
 import dynamic from 'next/dynamic';
 
-import { scheduleToPut } from '~/client/services/user-ui-settings';
 import { SidebarMode } from '~/interfaces/ui';
 import {
   useDrawerOpened,
@@ -42,8 +41,8 @@ const ResizableContainer = memo((props: ResizableContainerProps): JSX.Element =>
 
   const { isDrawerMode, isCollapsedMode, isDockMode } = useSidebarMode();
   const { mutate: mutateDrawerOpened } = useDrawerOpened();
-  const { data: currentProductNavWidth, mutate: mutateProductNavWidth } = useCurrentProductNavWidth();
-  const { mutate: mutatePreferCollapsedMode } = usePreferCollapsedMode();
+  const { data: currentProductNavWidth, mutateAndSave: mutateProductNavWidth } = useCurrentProductNavWidth();
+  const { mutateAndSave: mutatePreferCollapsedMode } = usePreferCollapsedMode();
   const { mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
 
   const [resizableAreaWidth, setResizableAreaWidth] = useState<number|undefined>(undefined);
@@ -54,13 +53,11 @@ const ResizableContainer = memo((props: ResizableContainerProps): JSX.Element =>
 
   const resizeDoneHandler = useCallback((newWidth: number) => {
     mutateProductNavWidth(newWidth, false);
-    scheduleToPut({ preferCollapsedModeByUser: false, currentProductNavWidth: newWidth });
   }, [mutateProductNavWidth]);
 
   const collapsedByResizableAreaHandler = useCallback(() => {
     mutatePreferCollapsedMode(true);
     mutateCollapsedContentsOpened(false);
-    scheduleToPut({ preferCollapsedModeByUser: true });
   }, [mutateCollapsedContentsOpened, mutatePreferCollapsedMode]);
 
 
@@ -156,21 +153,29 @@ const DrawableContainer = memo((props: DrawableContainerProps): JSX.Element => {
 
   const { className, children } = props;
 
-  const { data: isDrawerOpened } = useDrawerOpened();
+  const { data: isDrawerOpened, mutate } = useDrawerOpened();
 
   const openClass = `${isDrawerOpened ? 'open' : ''}`;
 
   return (
-    <div className={`${className} ${openClass}`}>
-      {children}
-    </div>
+    <>
+      <div className={`${className} ${openClass}`}>
+        {children}
+      </div>
+      { isDrawerOpened && (
+        <div className="modal-backdrop fade show" onClick={() => mutate(false)} />
+      ) }
+    </>
   );
 });
 
 
 export const Sidebar = (): JSX.Element => {
 
-  const { data: sidebarMode, isDrawerMode, isDockMode } = useSidebarMode();
+  const {
+    data: sidebarMode,
+    isDrawerMode, isCollapsedMode, isDockMode,
+  } = useSidebarMode();
 
   // css styles
   const grwSidebarClass = styles['grw-sidebar'];
@@ -198,7 +203,7 @@ export const Sidebar = (): JSX.Element => {
       { sidebarMode != null && !isDockMode() && <AppTitleOnSubnavigation /> }
       <DrawableContainer className={`${grwSidebarClass} ${modeClass} border-end vh-100`} data-testid="grw-sidebar">
         <ResizableContainer>
-          { sidebarMode != null && isDockMode() && <AppTitleOnSidebarHead /> }
+          { sidebarMode != null && !isCollapsedMode() && <AppTitleOnSidebarHead /> }
           <SidebarHead />
           <CollapsibleContainer Nav={SidebarNav} className="border-top">
             <SidebarContents />

+ 1 - 1
apps/app/src/components/Sidebar/SidebarHead/ToggleCollapseButton.tsx

@@ -12,7 +12,7 @@ export const ToggleCollapseButton = memo((): JSX.Element => {
 
   const { isDrawerMode, isCollapsedMode } = useSidebarMode();
   const { data: isDrawerOpened, mutate: mutateDrawerOpened } = useDrawerOpened();
-  const { mutate: mutatePreferCollapsedMode } = usePreferCollapsedMode();
+  const { mutateAndSave: mutatePreferCollapsedMode } = usePreferCollapsedMode();
   const { mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
 
   const toggleDrawer = useCallback(() => {

+ 1 - 3
apps/app/src/components/Sidebar/SidebarNav/PrimaryItems.tsx

@@ -2,7 +2,6 @@ import { FC, memo, useCallback } from 'react';
 
 import dynamic from 'next/dynamic';
 
-import { scheduleToPut } from '~/client/services/user-ui-settings';
 import { SidebarContentsType, SidebarMode } from '~/interfaces/ui';
 import { useCollapsedContentsOpened, useCurrentSidebarContents, useSidebarMode } from '~/stores/ui';
 
@@ -41,13 +40,12 @@ const PrimaryItem: FC<PrimaryItemProps> = (props: PrimaryItemProps) => {
     onHover,
   } = props;
 
-  const { data: currentContents, mutate: mutateContents } = useCurrentSidebarContents();
+  const { data: currentContents, mutateAndSave: mutateContents } = useCurrentSidebarContents();
 
   const indicatorClass = useIndicator(sidebarMode, contents === currentContents);
 
   const selectThisItem = useCallback(() => {
     mutateContents(contents, false);
-    scheduleToPut({ currentSidebarContents: contents });
   }, [contents, mutateContents]);
 
   const itemClickedHandler = useCallback(() => {

+ 1 - 1
apps/app/src/migrations/20221219011829-remove-basic-auth-related-config.js

@@ -4,7 +4,7 @@ import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
 
-const logger = loggerFactory('growi:remove-basic-auth-related-config');
+const logger = loggerFactory('growi:migrate:remove-basic-auth-related-config');
 
 const mongoose = require('mongoose');
 

+ 1 - 1
apps/app/src/migrations/20230213090921-remove-presentation-configurations.js

@@ -4,7 +4,7 @@ import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
 
-const logger = loggerFactory('growi:remove-presentation-configurations');
+const logger = loggerFactory('growi:migrate:remove-presentation-configurations');
 
 const mongoose = require('mongoose');
 

+ 1 - 1
apps/app/src/migrations/20230731075753-add_installed_date_to_config.js

@@ -4,7 +4,7 @@ import { getModelSafely, getMongoUri, mongoOptions } from '~/server/util/mongoos
 import loggerFactory from '~/utils/logger';
 
 
-const logger = loggerFactory('growi:migration:add-installed-date-to-config');
+const logger = loggerFactory('growi:migrate:add-installed-date-to-config');
 
 const mongoose = require('mongoose');
 

+ 34 - 0
apps/app/src/migrations/20231102012742-clean-user-ui-settings-collection.js

@@ -0,0 +1,34 @@
+// eslint-disable-next-line import/no-named-as-default
+import UserUISettings from '~/server/models/user-ui-settings';
+import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:migrate:clean-user-ui-settings-collection');
+
+const mongoose = require('mongoose');
+
+module.exports = {
+  async up() {
+    logger.info('Apply migration');
+    mongoose.connect(getMongoUri(), mongoOptions);
+
+    await UserUISettings.updateMany(
+      {},
+      {
+        $unset: {
+          isSidebarCollapsed: '',
+          preferDrawerModeByUser: '',
+          preferDrawerModeOnEditByUser: '',
+        },
+      },
+      { strict: false },
+    );
+
+    logger.info('Migration has successfully applied');
+  },
+
+  async down() {
+    // No rollback
+  },
+};

+ 10 - 8
apps/app/src/pages/[[...path]].page.tsx

@@ -20,7 +20,7 @@ import Head from 'next/head';
 import { useRouter } from 'next/router';
 import superjson from 'superjson';
 
-import { useLayoutFluidClassNameByPage, useEditorModeClassName } from '~/client/services/layout';
+import { useEditorModeClassName, useLayoutFluidClassNameByPage } from '~/client/services/layout';
 import { PageView } from '~/components/Page/PageView';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript'; import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { EditorConfig } from '~/interfaces/editor-settings';
@@ -76,6 +76,7 @@ const TemplateModal = dynamic(() => import('../components/TemplateModal').then(m
 const LinkEditModal = dynamic(() => import('../components/PageEditor/LinkEditModal').then(mod => mod.LinkEditModal), { ssr: false });
 const PageStatusAlert = dynamic(() => import('../components/PageStatusAlert').then(mod => mod.PageStatusAlert), { ssr: false });
 const QuestionnaireModalManager = dynamic(() => import('~/features/questionnaire/client/components/QuestionnaireModalManager'), { ssr: false });
+const TagEditModal = dynamic(() => import('../components/PageTags/TagEditModal').then(mod => mod.TagEditModal), { ssr: false });
 
 const logger = loggerFactory('growi:pages:all');
 
@@ -343,21 +344,21 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   );
 };
 
+
+const BasicLayoutWithEditor = ({ children }: { children?: ReactNode }): JSX.Element => {
+  const editorModeClassName = useEditorModeClassName();
+  return <BasicLayout className={editorModeClassName}>{children}</BasicLayout>;
+};
+
 type LayoutProps = Props & {
   children?: ReactNode
 }
 
 const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
-  const className = useEditorModeClassName();
-
   // init sidebar config with UserUISettings and sidebarConfig
   useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
-  return (
-    <BasicLayout className={className}>
-      {children}
-    </BasicLayout>
-  );
+  return <BasicLayoutWithEditor>{children}</BasicLayoutWithEditor>;
 };
 
 Page.getLayout = function getLayout(page: React.ReactElement<Props>) {
@@ -376,6 +377,7 @@ Page.getLayout = function getLayout(page: React.ReactElement<Props>) {
       <QuestionnaireModalManager />
       <TemplateModal />
       <LinkEditModal />
+      <TagEditModal />
     </>
   );
 };

+ 18 - 4
apps/app/src/pages/me/[[...path]].page.tsx

@@ -1,7 +1,7 @@
-import React, { useMemo } from 'react';
+import React, { type ReactNode, useMemo } from 'react';
 
 import type { IUserHasId } from '@growi/core';
-import {
+import type {
   GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 import { useTranslation } from 'next-i18next';
@@ -135,12 +135,26 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
   );
 };
 
-MePage.getLayout = function getLayout(page) {
+
+type LayoutProps = Props & {
+  children?: ReactNode
+}
+
+const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
+
   return (
-    <BasicLayout>{page}</BasicLayout>
+    <BasicLayout>
+      {children}
+    </BasicLayout>
   );
 };
 
+MePage.getLayout = function getLayout(page) {
+  return <Layout {...page.props}>{page}</Layout>;
+};
+
 async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;

+ 49 - 1
apps/app/src/stores/modal.tsx

@@ -700,7 +700,7 @@ export const useDeleteAttachmentModal = (): SWRResponse<DeleteAttachmentModalSta
   const open = useCallback((attachment: IAttachmentHasId, remove: Remove) => {
     mutate({ isOpened: true, attachment, remove });
   }, [mutate]);
-  const close = useCallback((): void => {
+  const close = useCallback(() => {
     mutate({ isOpened: false });
   }, [mutate]);
 
@@ -773,3 +773,51 @@ export const usePageSelectModal = (
     close: () => swrResponse.mutate({ isOpened: false }),
   };
 };
+
+/*
+* TagEditModal
+*/
+export type TagEditModalStatus = {
+  isOpen: boolean,
+  tags: string[],
+  pageId: string,
+  revisionId: string,
+}
+
+type TagEditModalUtils = {
+  open(tags: string[], pageId: string, revisionId: string): Promise<void>,
+  close(): Promise<void>,
+}
+
+export const useTagEditModal = (): SWRResponse<TagEditModalStatus, Error> & TagEditModalUtils => {
+  const initialStatus: TagEditModalStatus = useMemo(() => {
+    return {
+      isOpen: false,
+      tags: [],
+      pageId: '',
+      revisionId: '',
+    };
+  }, []);
+
+  const swrResponse = useStaticSWR<TagEditModalStatus, Error>('TagEditModal', undefined, { fallbackData: initialStatus });
+  const { mutate } = swrResponse;
+
+  const open = useCallback(async(tags: string[], pageId: string, revisionId: string) => {
+    mutate({
+      isOpen: true,
+      tags,
+      pageId,
+      revisionId,
+    });
+  }, [mutate]);
+
+  const close = useCallback(async() => {
+    mutate(initialStatus);
+  }, [initialStatus, mutate]);
+
+  return {
+    ...swrResponse,
+    open,
+    close,
+  };
+};

+ 47 - 14
apps/app/src/stores/ui.tsx

@@ -3,18 +3,19 @@ import {
 } from 'react';
 
 import { PageGrant, type Nullable } from '@growi/core';
-import { type SWRResponseWithUtils, useSWRStatic } from '@growi/core/dist/swr';
+import { type SWRResponseWithUtils, useSWRStatic, withUtils } from '@growi/core/dist/swr';
 import { pagePathUtils, isClient, isServer } from '@growi/core/dist/utils';
 import { Breakpoint } from '@growi/ui/dist/interfaces';
 import { addBreakpointListener, cleanupBreakpointListener } from '@growi/ui/dist/utils';
 import type { HtmlElementNode } from 'rehype-toc';
 import type SimpleBar from 'simplebar-react';
 import {
-  useSWRConfig, type SWRResponse, type Key,
+  useSWRConfig, type SWRResponse, type Key, KeyedMutator, MutatorOptions,
 } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 import type { IFocusable } from '~/client/interfaces/focusable';
+import { scheduleToPut } from '~/client/services/user-ui-settings';
 import type { IPageGrantData } from '~/interfaces/page';
 import { SidebarContentsType, SidebarMode } from '~/interfaces/ui';
 import type { UpdateDescCountData } from '~/interfaces/websocket';
@@ -95,8 +96,6 @@ export const EditorModeHash = {
 } as const;
 export type EditorModeHash = typeof EditorModeHash[keyof typeof EditorModeHash];
 
-export const isEditorModeHash = (hash: string): hash is EditorModeHash => Object.values<string>(EditorModeHash).includes(hash);
-
 const updateHashByEditorMode = (newEditorMode: EditorMode) => {
   const { pathname, search } = window.location;
 
@@ -251,26 +250,60 @@ export const useIsDeviceLargerThanXl = (): SWRResponse<boolean, Error> => {
 };
 
 
-export const useCurrentSidebarContents = (initialData?: SidebarContentsType): SWRResponse<SidebarContentsType, Error> => {
-  return useSWRStatic('sidebarContents', initialData, { fallbackData: SidebarContentsType.TREE });
-};
+type MutateAndSaveUserUISettings<Data> = (data: Data, opts?: boolean | MutatorOptions<Data>) => Promise<Data | undefined>;
+type MutateAndSaveUserUISettingsUtils<Data> = {
+  mutateAndSave: MutateAndSaveUserUISettings<Data>;
+}
+
+export const useCurrentSidebarContents = (
+    initialData?: SidebarContentsType,
+): SWRResponseWithUtils<MutateAndSaveUserUISettingsUtils<SidebarContentsType>, SidebarContentsType> => {
+  const swrResponse = useSWRStatic('sidebarContents', initialData, { fallbackData: SidebarContentsType.TREE });
+
+  const { mutate } = swrResponse;
 
-export const useCurrentProductNavWidth = (initialData?: number): SWRResponse<number, Error> => {
-  return useSWRStatic('productNavWidth', initialData, { fallbackData: 320 });
+  const mutateAndSave: MutateAndSaveUserUISettings<SidebarContentsType> = useCallback((data, opts?) => {
+    scheduleToPut({ currentSidebarContents: data });
+    return mutate(data, opts);
+  }, [mutate]);
+
+  return withUtils(swrResponse, { mutateAndSave });
 };
 
-export const useDrawerOpened = (isOpened?: boolean): SWRResponse<boolean, Error> => {
-  return useSWRStatic('isDrawerOpened', isOpened, { fallbackData: false });
+export const useCurrentProductNavWidth = (initialData?: number): SWRResponseWithUtils<MutateAndSaveUserUISettingsUtils<number>, number> => {
+  const swrResponse = useSWRStatic('productNavWidth', initialData, { fallbackData: 320 });
+
+  const { mutate } = swrResponse;
+
+  const mutateAndSave: MutateAndSaveUserUISettings<number> = useCallback((data, opts?) => {
+    scheduleToPut({ currentProductNavWidth: data });
+    return mutate(data, opts);
+  }, [mutate]);
+
+  return withUtils(swrResponse, { mutateAndSave });
 };
 
-export const usePreferCollapsedMode = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useSWRStatic('isPreferCollapsedMode', initialData, { fallbackData: false });
+export const usePreferCollapsedMode = (initialData?: boolean): SWRResponseWithUtils<MutateAndSaveUserUISettingsUtils<boolean>, boolean> => {
+  const swrResponse = useSWRStatic('isPreferCollapsedMode', initialData, { fallbackData: false });
+
+  const { mutate } = swrResponse;
+
+  const mutateAndSave: MutateAndSaveUserUISettings<boolean> = useCallback((data, opts?) => {
+    scheduleToPut({ preferCollapsedModeByUser: data });
+    return mutate(data, opts);
+  }, [mutate]);
+
+  return withUtils(swrResponse, { mutateAndSave });
 };
 
-export const useCollapsedContentsOpened = (initialData?: boolean): SWRResponse<boolean, Error> => {
+export const useCollapsedContentsOpened = (initialData?: boolean): SWRResponse<boolean> => {
   return useSWRStatic('isCollapsedContentsOpened', initialData, { fallbackData: false });
 };
 
+export const useDrawerOpened = (isOpened?: boolean): SWRResponse<boolean, Error> => {
+  return useSWRStatic('isDrawerOpened', isOpened, { fallbackData: false });
+};
+
 type DetectSidebarModeUtils = {
   isDrawerMode(): boolean
   isCollapsedMode(): boolean

+ 2 - 2
apps/app/src/utils/logger/index.ts

@@ -1,11 +1,11 @@
 import Logger from 'bunyan';
-import { createLogger } from 'universal-bunyan';
+import { createLogger, type UniversalBunyanConfig } from 'universal-bunyan';
 
 import configForDev from '^/config/logger/config.dev';
 import configForProd from '^/config/logger/config.prod';
 
 const isProduction = process.env.NODE_ENV === 'production';
-const config = isProduction ? configForProd : configForDev;
+const config = (isProduction ? configForProd : configForDev) as UniversalBunyanConfig;
 
 const loggerFactory = function(name: string): Logger {
   return createLogger({

+ 8 - 1
apps/app/tsconfig.json

@@ -15,7 +15,14 @@
       "~/*": ["./src/*"],
       "^/*": ["./*"],
       "debug": ["./src/server/utils/logger/alias-for-debug"]
-    }
+    },
+
+    /* TODO: remove below flags for strict checking */
+    "strict": false,
+    "strictNullChecks": true,
+    "strictBindCallApply": true,
+    "noImplicitAny": false,
+    "noImplicitOverride": true
   },
   "include": [
     "next-env.d.ts",

+ 4 - 2
apps/slackbot-proxy/package.json

@@ -17,8 +17,10 @@
     "start:prod": "cross-env NODE_ENV=production node -r dotenv-flow/config dist/index.js",
     "postbuild": "yarn cp:public && yarn cp:views && yarn cp:bootstrap",
     "predev": "yarn cp:bootstrap:dev",
-    "lint": "yarn eslint src --ext .ts",
-    "lint:fix": "yarn eslint src --ext .ts --fix",
+    "lint:js": "yarn eslint src/**/*.{js,ts}",
+    "lint:styles": "stylelint --allow-empty-input src/**/*.scss src/**/*.css",
+    "lint:typecheck": "tsc",
+    "lint": "run-p lint:*",
     "version": "yarn version --no-git-tag-version --preid=slackbot-proxy"
   },
   "// comments for dependencies": {

+ 8 - 1
apps/slackbot-proxy/tsconfig.json

@@ -7,7 +7,14 @@
     "baseUrl": ".",
     "paths": {
       "~/*": ["./src/*"]
-    }
+    },
+
+    /* TODO: remove below flags for strict checking */
+    "strict": false,
+    "strictNullChecks": true,
+    "strictBindCallApply": true,
+    "noImplicitAny": false,
+    "noImplicitOverride": true
   },
   "include": [
     "src"

+ 1 - 1
packages/core/src/remark-plugins/util/option-parser.ts

@@ -36,7 +36,7 @@ export class OptionParser {
 
     // determine start
     let start;
-    let end;
+    let end = -1;
 
     // has operator
     if (match[3] != null) {

+ 0 - 7
packages/core/src/utils/objectid-utils.ts

@@ -1,12 +1,5 @@
 import ObjectId from 'bson-objectid';
 
-import { isServer } from './browser-utils';
-
-// Workaround to avoid https://github.com/williamkapke/bson-objectid/issues/50
-if (isServer()) {
-  global._Buffer = Buffer;
-}
-
 export function isValidObjectId(id: string | ObjectId | null | undefined): boolean {
   if (id == null) {
     return false;

+ 4 - 4
packages/core/src/utils/page-utils.ts

@@ -1,3 +1,5 @@
+import { IPage } from '..';
+
 import { isTopPage } from './page-path-utils/is-top-page';
 
 // const GRANT_PUBLIC = 1;
@@ -14,8 +16,7 @@ const STATUS_DELETED = 'deleted';
  * @param page Page
  * @returns boolean
  */
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-export const isOnTree = (page): boolean => {
+export const isOnTree = (page: IPage): boolean => {
   const { path, parent } = page;
 
   if (isTopPage(path)) {
@@ -38,8 +39,7 @@ export const isOnTree = (page): boolean => {
  * @param page PageDocument
  * @returns boolean
  */
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-export const isPageNormalized = (page): boolean => {
+export const isPageNormalized = (page: IPage): boolean => {
   const { grant, status } = page;
 
   if (grant === GRANT_RESTRICTED || grant === GRANT_SPECIFIED) {

+ 1 - 0
packages/editor/package.json

@@ -32,6 +32,7 @@
     "@uiw/react-codemirror": "^4.21.8",
     "bootstrap": "^5.3.1",
     "codemirror": "^6.0.1",
+    "emoji-mart": "npm:panta82-emoji-mart@^3.0.1",
     "eslint-plugin-react-refresh": "^0.4.1",
     "react-dropzone": "^14.2.3",
     "react-hook-form": "^7.45.4",

+ 1 - 0
packages/editor/src/@types/emoji-mart.d.ts

@@ -0,0 +1 @@
+declare module 'emoji-mart';

+ 3 - 0
packages/editor/src/@types/declaration.d.ts → packages/editor/src/@types/scss.d.ts

@@ -1,2 +1,5 @@
 // prevent TS2307: Cannot find module './xxx.module.scss' or its corresponding type declarations.
 declare module '*.scss';
+
+// prevent TS7016: Could not find a declaration file for module 'emoji-mart'.
+declare module 'emoji-mart';

+ 1 - 1
packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx

@@ -105,7 +105,7 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
   return (
     <div {...getRootProps()} className="flex-expand-vert">
       <CodeMirrorEditorContainer ref={containerRef} />
-      <Toolbar onFileOpen={open} acceptedFileType={acceptedFileType} />
+      <Toolbar editorKey={editorKey} onFileOpen={open} acceptedFileType={acceptedFileType} />
     </div>
   );
 };

+ 146 - 4
packages/editor/src/components/CodeMirrorEditor/Toolbar/EmojiButton.tsx

@@ -1,7 +1,149 @@
-export const EmojiButton = (): JSX.Element => {
+import {
+  FC, useState, useCallback, CSSProperties,
+} from 'react';
+
+import { Picker } from 'emoji-mart';
+import i18n from 'i18next';
+import { Modal } from 'reactstrap';
+
+import { useCodeMirrorEditorIsolated, useResolvedThemeForEditor } from '../../../stores';
+
+import 'emoji-mart/css/emoji-mart.css';
+
+type Props = {
+  editorKey: string,
+}
+
+type Translation = {
+  search: string
+  clear: string
+  notfound: string
+  skintext: string
+  categories: object
+  categorieslabel: string
+  skintones: object
+  title: string
+}
+
+// TODO: https://redmine.weseek.co.jp/issues/133681
+const getEmojiTranslation = (): Translation => {
+
+  const categories: { [key: string]: string } = {};
+  [
+    'search',
+    'recent',
+    'smileys',
+    'people',
+    'nature',
+    'foods',
+    'activity',
+    'places',
+    'objects',
+    'symbols',
+    'flags',
+    'custom',
+  ].forEach((category) => {
+    categories[category] = i18n.t(`emoji.categories.${category}`);
+  });
+
+  const skintones: { [key: string]: string} = {};
+  (Array.from(Array(6).keys())).forEach((tone) => {
+    skintones[tone + 1] = i18n.t(`emoji.skintones.${tone + 1}`);
+  });
+
+  const translation = {
+    search: i18n.t('emoji.search'),
+    clear: i18n.t('emoji.clear'),
+    notfound: i18n.t('emoji.notfound'),
+    skintext: i18n.t('emoji.skintext'),
+    categories,
+    categorieslabel: i18n.t('emoji.categorieslabel'),
+    skintones,
+    title: i18n.t('emoji.title'),
+  };
+
+  return translation;
+};
+
+const translation = getEmojiTranslation();
+
+export const EmojiButton: FC<Props> = (props) => {
+  const { editorKey } = props;
+
+  const [isOpen, setIsOpen] = useState(false);
+
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey);
+  const { data: resolvedTheme } = useResolvedThemeForEditor();
+
+  const view = codeMirrorEditor?.view;
+  const cursorIndex = view?.state.selection.main.head;
+  const toggle = () => setIsOpen(!isOpen);
+
+  const selectEmoji = useCallback((emoji: { colons: string }): void => {
+
+    if (cursorIndex == null || !isOpen) {
+      return;
+    }
+
+    view?.dispatch({
+      changes: {
+        from: cursorIndex,
+        insert: emoji.colons,
+      },
+    });
+
+    toggle();
+  }, [cursorIndex, isOpen, toggle, view]);
+
+  const setStyle = useCallback((): CSSProperties => {
+    if (view == null || cursorIndex == null || !isOpen) {
+      return {};
+    }
+
+    const offset = 20;
+    const emojiPickerHeight = 420;
+    const cursorRect = view.coordsAtPos(cursorIndex);
+    const editorRect = view.dom.getBoundingClientRect();
+
+    if (cursorRect == null) {
+      return {};
+    }
+
+    // Emoji Picker bottom position exceed editor's bottom position
+    if (cursorRect.bottom + emojiPickerHeight > editorRect.bottom) {
+      return {
+        top: editorRect.bottom - emojiPickerHeight,
+        left: cursorRect.left + offset,
+        position: 'fixed',
+      };
+    }
+    return {
+      top: cursorRect.top + offset,
+      left: cursorRect.left + offset,
+      position: 'fixed',
+    };
+  }, [cursorIndex, isOpen, view]);
+
   return (
-    <button type="button" className="btn btn-toolbar-button">
-      <span className="material-symbols-outlined fs-5">emoji_emotions</span>
-    </button>
+    <>
+      <button type="button" className="btn btn-toolbar-button" onClick={toggle}>
+        <span className="material-symbols-outlined fs-5">emoji_emotions</span>
+      </button>
+      { isOpen
+      && (
+        <div className="mb-2 d-none d-md-block">
+          <Modal isOpen={isOpen} toggle={toggle} backdropClassName="emoji-picker-modal" fade={false}>
+            <Picker
+              onSelect={selectEmoji}
+              i18n={translation}
+              title={translation.title}
+              emojiTooltip
+              style={setStyle()}
+              theme={resolvedTheme}
+            />
+          </Modal>
+        </div>
+      )}
+    </>
   );
 };

+ 8 - 3
packages/editor/src/components/CodeMirrorEditor/Toolbar/Toolbar.tsx

@@ -1,5 +1,7 @@
 import { memo } from 'react';
 
+import { AcceptedUploadFileType } from '../../../consts';
+
 import { AttachmentsDropup } from './AttachmentsDropup';
 import { DiagramButton } from './DiagramButton';
 import { EmojiButton } from './EmojiButton';
@@ -7,23 +9,26 @@ import { TableButton } from './TableButton';
 import { TemplateButton } from './TemplateButton';
 import { TextFormatTools } from './TextFormatTools';
 
-import { AcceptedUploadFileType } from 'src/consts';
 
 import styles from './Toolbar.module.scss';
 
 type Props = {
+  editorKey: string,
   onFileOpen: () => void,
   acceptedFileType: AcceptedUploadFileType
 }
 
 export const Toolbar = memo((props: Props): JSX.Element => {
 
-  const { onFileOpen, acceptedFileType } = props;
+  const { editorKey, onFileOpen, acceptedFileType } = props;
+
   return (
     <div className={`d-flex gap-2 p-2 codemirror-editor-toolbar ${styles['codemirror-editor-toolbar']}`}>
       <AttachmentsDropup onFileOpen={onFileOpen} acceptedFileType={acceptedFileType} />
       <TextFormatTools />
-      <EmojiButton />
+      <EmojiButton
+        editorKey={editorKey}
+      />
       <TableButton />
       <DiagramButton />
       <TemplateButton />

+ 1 - 1
packages/editor/src/components/CodeMirrorEditorMain.tsx

@@ -13,7 +13,6 @@ const additionalExtensions: Extension[] = [
   scrollPastEnd(),
 ];
 
-
 type Props = {
   onChange?: (value: string) => void,
   onSave?: () => void,
@@ -60,6 +59,7 @@ export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
     return cleanupFunction;
   }, [codeMirrorEditor, onSave]);
 
+
   return (
     <CodeMirrorEditor
       editorKey={GlobalCodeMirrorEditorKey.MAIN}

+ 8 - 2
packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts

@@ -10,6 +10,8 @@ import { tags } from '@lezer/highlight';
 import { useCodeMirror, type UseCodeMirror } from '@uiw/react-codemirror';
 import deepmerge from 'ts-deepmerge';
 
+import { emojiAutocompletionSettings } from '../../extensions/emojiAutocompletionSettings';
+
 import { useAppendExtensions, type AppendExtensions } from './utils/append-extensions';
 import { useFocus, type Focus } from './utils/focus';
 import { useGetDoc, type GetDoc } from './utils/get-doc';
@@ -49,15 +51,19 @@ const defaultExtensions: Extension[] = [
   Prec.lowest(keymap.of(defaultKeymap)),
   syntaxHighlighting(markdownHighlighting),
   Prec.lowest(syntaxHighlighting(defaultHighlightStyle)),
+  emojiAutocompletionSettings,
 ];
 
+
 export const useCodeMirrorEditor = (props?: UseCodeMirror): UseCodeMirrorEditor => {
 
-  const mergedProps = useMemo<UseCodeMirror>(() => {
+  const mergedProps = useMemo(() => {
     return deepmerge(
       props ?? {},
       {
-        extensions: defaultExtensions,
+        extensions: [
+          defaultExtensions,
+        ],
         // Reset settings of react-codemirror.
         // Extensions are defined first will be used if they have the same priority.
         // If extensions conflict, disable them here.

+ 75 - 0
packages/editor/src/services/extensions/emojiAutocompletionSettings.ts

@@ -0,0 +1,75 @@
+import { type CompletionContext, type Completion, autocompletion } from '@codemirror/autocomplete';
+import { syntaxTree } from '@codemirror/language';
+import { emojiIndex } from 'emoji-mart';
+import emojiData from 'emoji-mart/data/all.json';
+
+const getEmojiDataArray = (): string[] => {
+  const rawEmojiDataArray = emojiData.categories;
+
+  const emojiCategoriesData = [
+    'people',
+    'nature',
+    'foods',
+    'activity',
+    'places',
+    'objects',
+    'symbols',
+    'flags',
+  ];
+
+  const fixedEmojiDataArray: string[] = [];
+
+  emojiCategoriesData.forEach((value) => {
+    const tempArray = rawEmojiDataArray.find(obj => obj.id === value)?.emojis;
+
+    if (tempArray == null) {
+      return;
+    }
+
+    fixedEmojiDataArray.push(...tempArray);
+  });
+
+  return fixedEmojiDataArray;
+};
+
+const emojiDataArray = getEmojiDataArray();
+
+const emojiOptions = emojiDataArray.map(
+  tag => ({ label: `:${tag}:`, type: tag }),
+);
+
+const TWO_OR_MORE_WORD_CHARACTERS_REGEX = /:\w{2,}$/;
+
+
+// EmojiAutocompletion is activated when two characters are entered into the editor.
+const emojiAutocompletion = (context: CompletionContext) => {
+  const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
+  const textBefore = context.state.sliceDoc(nodeBefore.from, context.pos);
+  const emojiBefore = TWO_OR_MORE_WORD_CHARACTERS_REGEX.exec(textBefore);
+
+  if (!emojiBefore && !context.explicit) return null;
+
+  return {
+    from: emojiBefore ? nodeBefore.from + emojiBefore.index : context.pos,
+    options: emojiOptions,
+    validFor: TWO_OR_MORE_WORD_CHARACTERS_REGEX,
+  };
+};
+
+export const emojiAutocompletionSettings = autocompletion({
+  addToOptions: [{
+    render: (completion: Completion) => {
+      const emojiName = completion.type ?? '';
+      const emojiData = emojiIndex.emojis[emojiName];
+
+      const emoji = emojiData.native ?? emojiData[1].native;
+
+      const element = document.createElement('span');
+      element.innerHTML = emoji;
+      return element;
+    },
+    position: 20,
+  }],
+  icons: false,
+  override: [emojiAutocompletion],
+});

+ 1 - 0
packages/editor/src/stores/index.ts

@@ -1 +1,2 @@
 export * from './codemirror-editor';
+export * from './use-resolved-theme';

+ 27 - 0
packages/editor/src/stores/use-resolved-theme.ts

@@ -0,0 +1,27 @@
+import { useCallback } from 'react';
+
+import { ColorScheme } from '@growi/core';
+import { useSWRStatic } from '@growi/core/dist/swr';
+import type { SWRResponse } from 'swr';
+import { mutate } from 'swr';
+
+type ResolvedThemeStatus = {
+  themeData: ColorScheme,
+}
+
+type ResolvedThemeUtils = {
+  mutateResolvedThemeForEditor(resolvedTheme: ColorScheme): void
+}
+
+export const useResolvedThemeForEditor = (): SWRResponse<ResolvedThemeStatus, Error> & ResolvedThemeUtils => {
+  const swrResponse = useSWRStatic<ResolvedThemeStatus, Error>('resolvedTheme');
+
+  const mutateResolvedThemeForEditor = useCallback((resolvedTheme: ColorScheme) => {
+    mutate('resolvedTheme', { themeData: resolvedTheme });
+  }, []);
+
+  return {
+    ...swrResponse,
+    mutateResolvedThemeForEditor,
+  };
+};

+ 3 - 0
packages/editor/vite.config.ts

@@ -50,6 +50,9 @@ export default defineConfig({
         preserveModules: true,
         preserveModulesRoot: 'src',
       },
+      external: [
+        'emoji-mart/css/emoji-mart.css',
+      ],
     },
   },
 });

+ 1 - 1
packages/pluginkit/src/v4/server/utils/template/scan.ts

@@ -52,7 +52,7 @@ export const scanTemplate = async(
         id: templateId,
         locale,
         isValid: false,
-        invalidReason: err.message,
+        invalidReason: (err as Error).message,
       });
     }
   }

+ 4 - 1
packages/presentation/tsconfig.json

@@ -6,7 +6,10 @@
 
     "baseUrl": ".",
     "paths": {
-    }
+    },
+
+    /* TODO: remove below flags for strict checking */
+    "noImplicitAny": false
   },
   "include": [
     "src"

+ 8 - 1
packages/remark-attachment-refs/tsconfig.json

@@ -6,7 +6,14 @@
     "baseUrl": ".",
     "paths": {
       "~/*": ["./src/*"]
-    }
+    },
+
+    /* TODO: remove below flags for strict checking */
+    "strict": false,
+    "strictNullChecks": true,
+    "strictBindCallApply": true,
+    "noImplicitAny": false,
+    "noImplicitOverride": true
   },
   "include": [
     "src"

+ 8 - 1
packages/remark-drawio/tsconfig.json

@@ -2,7 +2,14 @@
   "$schema": "http://json.schemastore.org/tsconfig",
   "extends": "../../tsconfig.base.json",
   "compilerOptions": {
-    "jsx": "react-jsx"
+    "jsx": "react-jsx",
+
+    /* TODO: remove below flags for strict checking */
+    "strict": false,
+    "strictNullChecks": true,
+    "strictBindCallApply": true,
+    "noImplicitAny": false,
+    "noImplicitOverride": true
   },
   "include": [
     "src"

+ 1 - 1
packages/remark-lsx/src/client/stores/lsx/lsx.ts

@@ -27,7 +27,7 @@ export const useSWRxLsx = (
           : null;
       }
       catch (err) {
-        parseError = err;
+        parseError = err as Error;
       }
 
       // the first loading

+ 8 - 1
packages/remark-lsx/tsconfig.json

@@ -6,7 +6,14 @@
 
     "types": [
       "vitest/globals"
-    ]
+    ],
+
+    /* TODO: remove below flags for strict checking */
+    "strict": false,
+    "strictNullChecks": true,
+    "strictBindCallApply": true,
+    "noImplicitAny": false,
+    "noImplicitOverride": true
   },
   "include": [
     "src"

+ 3 - 0
packages/slack/package.json

@@ -50,6 +50,9 @@
   "dependencies": {
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
+    "@types/bunyan": "^1.8.10",
+    "@types/http-errors": "^2.0.3",
+    "@types/url-join": "^4.0.2",
     "axios": "^0.24.0",
     "browser-bunyan": "^1.6.3",
     "bunyan": "^1.8.15",

+ 1 - 1
packages/slack/src/utils/check-communicable.ts

@@ -86,7 +86,7 @@ export const getConnectionStatus = async(token:string): Promise<ConnectionStatus
     status.workspaceName = await retrieveWorkspaceName(client);
   }
   catch (err) {
-    status.error = err;
+    status.error = err as Error;
   }
 
   return status;

+ 1 - 5
tsconfig.base.json

@@ -13,11 +13,7 @@
     "lib": ["dom", "dom.iterable", "esnext"],
 
     /* Strict Type-Checking Options */
-    // "strict": true,
-    "strictNullChecks": true,
-    "strictBindCallApply": true,
-    "noImplicitAny": false,
-    "noImplicitOverride": true,
+    "strict": true,
 
     /* Additional Checks */
     "noUnusedLocals": false,

+ 28 - 22
yarn.lock

@@ -2658,6 +2658,9 @@
   dependencies:
     "@slack/oauth" "^2.0.1"
     "@slack/web-api" "^6.2.4"
+    "@types/bunyan" "^1.8.10"
+    "@types/http-errors" "^2.0.3"
+    "@types/url-join" "^4.0.2"
     axios "^0.24.0"
     browser-bunyan "^1.6.3"
     bunyan "^1.8.15"
@@ -3943,6 +3946,13 @@
     "@types/express" "*"
     "@types/node" "*"
 
+"@types/bunyan@^1.8.10":
+  version "1.8.10"
+  resolved "https://registry.yarnpkg.com/@types/bunyan/-/bunyan-1.8.10.tgz#60f2d297c3d29fd3b85c54f28a48b99d61686fe0"
+  integrity sha512-A82U/3EAdWX89f+dfysGiRvbeoLuRLV3i6SLg3HuNK4Yf+dHOqdbxT70vQUwvD3DOr2Dvpcv9dRX4ipTf0LpEg==
+  dependencies:
+    "@types/node" "*"
+
 "@types/cache-manager@^3.4.0":
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/@types/cache-manager/-/cache-manager-3.4.0.tgz#414136ea3807a8cd071b8f20370c5df5dbffd382"
@@ -4078,6 +4088,11 @@
     "@types/react" "*"
     hoist-non-react-statics "^3.3.0"
 
+"@types/http-errors@^2.0.3":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.3.tgz#c54e61f79b3947d040f150abd58f71efb422ff62"
+  integrity sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==
+
 "@types/is-stream@^1.1.0":
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/@types/is-stream/-/is-stream-1.1.0.tgz#b84d7bb207a210f2af9bed431dc0fbe9c4143be1"
@@ -4342,11 +4357,21 @@
   dependencies:
     "@types/node" "*"
 
+"@types/throttle-debounce@^5.0.1":
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/@types/throttle-debounce/-/throttle-debounce-5.0.1.tgz#8ce917e41580b2cf16f8ee840e227947f4152b04"
+  integrity sha512-/fifasjlhpz/r4YsH0r0ZXJvivXFB3F6bmezMnqgsn/NK/fYJn7vN84k7eYn/oALu/aenXo+t8Pv+QlkS6iYBg==
+
 "@types/unist@*", "@types/unist@^2.0.0":
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
   integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==
 
+"@types/url-join@^4.0.2":
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/@types/url-join/-/url-join-4.0.2.tgz#e8774924c7f492626ee3309baf6697f80e1414df"
+  integrity sha512-uv54MkAtQ4B5Qm20LmMN7tAdczqRenu1K6Sf7PHCygqylVJlRwjpUE5OGofqxdXGH3QJUu+qvDZzPadz5EOjxA==
+
 "@types/warning@^3.0.0":
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52"
@@ -5855,16 +5880,11 @@ cjs-module-lexer@^1.0.0:
   resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40"
   integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==
 
-classnames@^2.0.0:
+classnames@^2.0.0, classnames@^2.2.0, classnames@^2.2.3, classnames@^2.2.6:
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
   integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
 
-classnames@^2.2.0, classnames@^2.2.3, classnames@^2.2.6:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
-  integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
-
 clean-stack@^2.0.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
@@ -6312,12 +6332,7 @@ core-js-pure@^3.20.2:
   resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.23.1.tgz#0b27e4c3ad46178b84e790dbbb81987218ab82ad"
   integrity sha512-3qNgf6TqI3U1uhuSYRzJZGfFd4T+YlbyVPl+jgRiKjdZopvG4keZQwWZDAWpu1UH9nCgTpUzIV3GFawC7cJsqg==
 
-core-js@^3, core-js@^3.0.1, core-js@^3.2.1:
-  version "3.23.3"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.23.3.tgz#3b977612b15da6da0c9cc4aec487e8d24f371112"
-  integrity sha512-oAKwkj9xcWNBAvGbT//WiCdOMpb9XQG92/Fe3ABFM/R16BsHgePG00mFOgKf7IsCtfj8tA1kHtf/VwErhriz5Q==
-
-core-js@^3.6.5:
+core-js@^3, core-js@^3.0.1, core-js@^3.2.1, core-js@^3.6.5:
   version "3.33.0"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.0.tgz#70366dbf737134761edb017990cf5ce6c6369c40"
   integrity sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw==
@@ -13179,16 +13194,7 @@ postcss@^7.0.0:
     picocolors "^0.2.1"
     source-map "^0.6.1"
 
-postcss@^8.3.11, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.25:
-  version "8.4.26"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.26.tgz#1bc62ab19f8e1e5463d98cf74af39702a00a9e94"
-  integrity sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==
-  dependencies:
-    nanoid "^3.3.6"
-    picocolors "^1.0.0"
-    source-map-js "^1.0.2"
-
-postcss@^8.4.31:
+postcss@^8.3.11, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.25, postcss@^8.4.31:
   version "8.4.31"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
   integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==