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

Merge branch 'dev/7.0.x' into feat/wip-page

Shun Miyazawa 2 лет назад
Родитель
Сommit
8b37829282
31 измененных файлов с 152 добавлено и 188 удалено
  1. 2 2
      apps/app/public/static/locales/en_US/translation.json
  2. 2 2
      apps/app/public/static/locales/ja_JP/translation.json
  3. 2 2
      apps/app/public/static/locales/zh_CN/translation.json
  4. 3 5
      apps/app/src/components/DescendantsPageListModal.tsx
  5. 0 17
      apps/app/src/components/Icons/PageListIcon.jsx
  6. 0 19
      apps/app/src/components/Icons/TimeLineIcon.jsx
  7. 2 4
      apps/app/src/components/NotFoundPage.tsx
  8. 3 2
      apps/app/src/components/PageControls/PageControls.tsx
  9. 2 2
      apps/app/src/components/PageDuplicateModal.tsx
  10. 1 2
      apps/app/src/components/PageSideContents/PageSideContents.tsx
  11. 45 17
      apps/app/src/components/PageTags/PageTags.tsx
  12. 14 45
      apps/app/src/components/PageTags/RenderTagLabels.tsx
  13. 14 2
      apps/app/src/components/PageTags/TagLabels.module.scss
  14. 2 0
      apps/app/src/components/Sidebar/Bookmarks.tsx
  15. 8 5
      apps/app/src/components/Sidebar/Bookmarks/BookmarkContents.tsx
  16. 2 0
      apps/app/src/components/Sidebar/Custom/CustomSidebar.tsx
  17. 2 0
      apps/app/src/components/Sidebar/InAppNotification/InAppNotification.tsx
  18. 1 1
      apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.tsx
  19. 4 2
      apps/app/src/components/Sidebar/PageTree/PageTree.tsx
  20. 5 2
      apps/app/src/components/Sidebar/PageTreeItem/Ellipsis.tsx
  21. 2 0
      apps/app/src/components/Sidebar/RecentChanges/RecentChanges.tsx
  22. 2 2
      apps/app/src/components/Sidebar/Sidebar.module.scss
  23. 1 1
      apps/app/src/components/Sidebar/SidebarNav/PrimaryItems.tsx
  24. 2 0
      apps/app/src/components/Sidebar/Tag.tsx
  25. 5 5
      apps/app/src/components/TableOfContents.tsx
  26. 3 5
      apps/app/src/components/TrashPageList.tsx
  27. 0 5
      apps/app/src/server/routes/apiv3/page/index.js
  28. 0 8
      apps/app/src/server/service/page-grant.ts
  29. 7 2
      apps/app/src/server/service/page/index.ts
  30. 11 26
      apps/app/test/integration/service/page-grant.test.js
  31. 5 3
      packages/editor/src/services/list-util/markdown-list-util.ts

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

@@ -391,11 +391,11 @@
       "Recursively": "Recursively",
       "Duplicate without exist path": "Duplicate without exist path",
       "Same page already exists": "Same page already exists",
-      "Only duplicate user related resources": "Only duplicate user related resources"
+      "Only duplicate user related pages": "Only duplicate pages you can access"
     },
     "help": {
       "recursive": "Duplicate children of under this path recursively",
-      "only_user_related_resources": "This will only duplicate pages that the user has permission to view. If the page permission is set to \"Only specific groups\", only user related groups will be set to the page duplicate."
+      "only_inherit_user_related_groups": "If the page privilege is set to \"Only inside the group\", groups you do not belong to will lose access to the duplicated page"
     }
   },
   "duplicated_pages": "{{fromPath}} has been duplicated",

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

@@ -424,11 +424,11 @@
       "Recursively": "再帰的に複製",
       "Duplicate without exist path": "存在するパス以外を複製する",
       "Same page already exists": "同じページがすでに存在します",
-      "Only duplicate user related resources": "ユーザに関連のあるリソースのみを複製する"
+      "Only duplicate user related pages": "自分が閲覧可能なページのみを複製する"
     },
     "help": {
       "recursive": "配下のページも複製します",
-      "only_user_related_resources": "ユーザが閲覧可能なページのみを複製します。また、閲覧権限が「特定グループのみ」で設定されている場合、複製後のページにはユーザが所属するグループのみを閲覧可能なグループとして設定します。"
+      "only_inherit_user_related_groups": "閲覧権限が「特定グループのみ」で設定されている場合、複製されたページを閲覧可能なグループ一覧から、自分が所属していないものは取り除かれます"
     }
   },
   "duplicated_pages": "{{fromPath}} を複製しました",

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

@@ -381,11 +381,11 @@
       "Recursively": "Recursively",
       "Duplicate without exist path": "Duplicate without exist path",
       "Same page already exists": "Same page already exists",
-      "Only duplicate user related resources": "Only duplicate user related resources"
+      "Only duplicate user related pages": "Only duplicate pages you can access"
     },
     "help": {
       "recursive": "Duplicate children of under this path recursively",
-      "only_user_related_resources": "This will only duplicate pages that the user has permission to view. If the page permission is set to \"Only specific groups\", only user related groups will be set to the page duplicate."
+      "only_inherit_user_related_groups": "If the page privilege is set to \"Only inside the group\", groups you do not belong to will lose access to the duplicated page"
     }
   },
   "duplicated_pages": "{{fromPath}} 已重复",

+ 3 - 5
apps/app/src/components/DescendantsPageListModal.tsx

@@ -13,10 +13,8 @@ import { useDescendantsPageListModal } from '~/stores/modal';
 
 import { CustomNavTab } from './CustomNavigation/CustomNav';
 import CustomTabContent from './CustomNavigation/CustomTabContent';
-import { DescendantsPageListProps } from './DescendantsPageList';
+import type { DescendantsPageListProps } from './DescendantsPageList';
 import ExpandOrContractButton from './ExpandOrContractButton';
-import PageListIcon from './Icons/PageListIcon';
-import TimeLineIcon from './Icons/TimeLineIcon';
 
 import styles from './DescendantsPageListModal.module.scss';
 
@@ -46,7 +44,7 @@ export const DescendantsPageListModal = (): JSX.Element => {
   const navTabMapping = useMemo(() => {
     return {
       pagelist: {
-        Icon: PageListIcon,
+        Icon: () => <span className="material-symbols-outlined">subject</span>,
         Content: () => {
           if (status == null || status.path == null || !status.isOpened) {
             return <></>;
@@ -57,7 +55,7 @@ export const DescendantsPageListModal = (): JSX.Element => {
         isLinkEnabled: () => !isSharedUser,
       },
       timeline: {
-        Icon: TimeLineIcon,
+        Icon: () => <span className="material-symbols-outlined">timeline</span>,
         Content: () => {
           if (status == null || !status.isOpened) {
             return <></>;

+ 0 - 17
apps/app/src/components/Icons/PageListIcon.jsx

@@ -1,17 +0,0 @@
-import React from 'react';
-
-const PageList = () => (
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    viewBox="0 0 14 14"
-
-  >
-    <rect width="14" height="14" fillOpacity="0" />
-    <path d="M12.63,2.72H1.37a.54.54,0,0,1,0-1.08H12.63a.54.54,0,0,1,0,1.08Z" />
-    <path d="M11.82,5.94H1.37a.55.55,0,0,1,0-1.09H11.82a.55.55,0,1,1,0,1.09Z" />
-    <path d="M9.41,9.15h-8a.54.54,0,0,1,0-1.08h8a.54.54,0,0,1,0,1.08Z" />
-    <path d="M10.84,12.36H1.37a.54.54,0,1,1,0-1.08h9.47a.54.54,0,1,1,0,1.08Z" />
-  </svg>
-);
-
-export default PageList;

+ 0 - 19
apps/app/src/components/Icons/TimeLineIcon.jsx

@@ -1,19 +0,0 @@
-import React from 'react';
-
-const TimeLine = () => (
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    viewBox="0 0 14 14"
-
-  >
-    <rect width="14" height="14" fillOpacity="0" />
-    <path
-      d="M13.6,4.6a1.2,1.2,0,0,1-1.2,1.2,1,1,0,0,1-.3,0L10,7.89a1.1,1.1,0,0,1,0,.31,1.2,1.2,0,1,1-2.4,0,1.1,1.1,0,0,1,
-      0-.31L6.11,6.36a1.3,1.3,0,0,1-.62,0L2.75,9.1a1,1,0,0,1,0,.3A1.2,1.2,0,1,1,1.6,8.2a1,1,0,0,1,.3,0L4.64,
-      5.51a1.1,1.1,0,0,1,0-.31A1.2,1.2,0,0,1,7,5.2a1.1,1.1,0,0,1,0,.31L8.49,7a1.3,1.3,0,0,1,.62,0L11.25,4.9a1,
-      1,0,0,1-.05-.3,1.2,1.2,0,1,1,2.4,0Z"
-    />
-  </svg>
-);
-
-export default TimeLine;

+ 2 - 4
apps/app/src/components/NotFoundPage.tsx

@@ -4,8 +4,6 @@ import { useTranslation } from 'next-i18next';
 
 import CustomNavAndContents from './CustomNavigation/CustomNavAndContents';
 import { DescendantsPageList } from './DescendantsPageList';
-import PageListIcon from './Icons/PageListIcon';
-import TimeLineIcon from './Icons/TimeLineIcon';
 import { PageTimeline } from './PageTimeline';
 
 type NotFoundPageProps = {
@@ -20,12 +18,12 @@ const NotFoundPage = (props: NotFoundPageProps): JSX.Element => {
   const navTabMapping = useMemo(() => {
     return {
       pagelist: {
-        Icon: PageListIcon,
+        Icon: () => <span className="material-symbols-outlined">subject</span>,
         Content: () => <DescendantsPageList path={path} />,
         i18n: t('page_list'),
       },
       timeLine: {
-        Icon: TimeLineIcon,
+        Icon: () => <span className="material-symbols-outlined">timeline</span>,
         Content: PageTimeline,
         i18n: t('Timeline View'),
       },

+ 3 - 2
apps/app/src/components/PageControls/PageControls.tsx

@@ -20,8 +20,9 @@ import loggerFactory from '~/utils/logger';
 
 import { useSWRxPageInfo, useSWRxTagsInfo } from '../../stores/page';
 import { useSWRxUsersList } from '../../stores/user';
+import type { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import {
-  AdditionalMenuItemsRendererProps, ForceHideMenuItems, MenuItemType,
+  MenuItemType,
   PageItemControl,
 } from '../Common/Dropdown/PageItemControl';
 
@@ -45,7 +46,7 @@ const Tags = (props: TagsProps): JSX.Element => {
   const { onClickEditTagsButton } = props;
 
   return (
-    <div className="grw-taglabels-container d-flex align-items-center">
+    <div className="grw-tag-labels-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"

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

@@ -239,8 +239,8 @@ const PageDuplicateModal = (): JSX.Element => {
             onChange={() => setOnlyDuplicateUserRelatedResources(!onlyDuplicateUserRelatedResources)}
           />
           <label className="form-label form-check-label" htmlFor="cbOnlyDuplicateUserRelatedResources">
-            { t('modal_duplicate.label.Only duplicate user related resources') }
-            <p className="form-text text-muted my-0">{ t('modal_duplicate.help.only_user_related_resources') }</p>
+            { t('modal_duplicate.label.Only duplicate user related pages') }
+            <p className="form-text text-muted my-0">{ t('modal_duplicate.help.only_inherit_user_related_groups') }</p>
           </label>
         </div>
         <div className="mt-3">

+ 1 - 2
apps/app/src/components/PageSideContents/PageSideContents.tsx

@@ -58,7 +58,7 @@ const Tags = (props: TagsProps): JSX.Element => {
   const isTagLabelsDisabled = !!isGuestUser || !!isReadOnlyUser;
 
   return (
-    <div className="grw-taglabels-container">
+    <div className="grw-tag-labels-container">
       <PageTags
         tags={tagsInfoData.tags}
         isTagLabelsDisabled={isTagLabelsDisabled}
@@ -88,7 +88,6 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
   const isUsersHomepagePath = isUsersHomepage(pagePath);
   const isTrash = isTrashPage(pagePath);
 
-
   return (
     <>
       {/* Tags */}

+ 45 - 17
apps/app/src/components/PageTags/PageTags.tsx

@@ -1,6 +1,10 @@
 import type { FC } from 'react';
-import React from 'react';
+import React, { useState } from 'react';
 
+import { useTranslation } from 'next-i18next';
+
+import { NotAvailableForGuest } from '../NotAvailableForGuest';
+import { NotAvailableForReadOnlyUser } from '../NotAvailableForReadOnlyUser';
 import { Skeleton } from '../Skeleton';
 
 import RenderTagLabels from './RenderTagLabels';
@@ -22,6 +26,8 @@ export const PageTags:FC<Props> = (props: Props) => {
   const {
     tags, isTagLabelsDisabled, onClickEditTagsButton,
   } = props;
+  const [isHovered, setIsHovered] = useState(false);
+  const { t } = useTranslation();
 
   if (tags == null) {
     return <></>;
@@ -29,24 +35,46 @@ export const PageTags:FC<Props> = (props: Props) => {
 
   const printNoneClass = tags.length === 0 ? 'd-print-none' : '';
 
+  const onMouseEnterHandler = () => setIsHovered(true);
+  const onMouseLeaveHandler = () => setIsHovered(false);
+
   return (
-    <>
-      <div className={`${styles['grw-tag-labels']} grw-tag-labels d-flex align-items-center ${printNoneClass}`} data-testid="grw-tag-labels">
-        <button
-          type="button"
-          className={`btn btn-sm btn-outline-secondary rounded-pill mb-2 d-flex d-lg-none ${styles['grw-tag-icon-button']}`}
-          onClick={onClickEditTagsButton}
-        >
-          <span className="material-symbols-outlined">local_offer</span>
-        </button>
-        <div className="d-none d-lg-flex">
-          <RenderTagLabels
-            tags={tags}
-            isTagLabelsDisabled={isTagLabelsDisabled}
-            onClickEditTagsButton={onClickEditTagsButton}
-          />
+    <div className={`${styles['grw-tag-labels']} grw-tag-labels d-flex align-items-center mb-2 ${printNoneClass}`} data-testid="grw-tag-labels">
+      <div className="d-flex d-lg-none">
+        <NotAvailableForGuest>
+          <NotAvailableForReadOnlyUser>
+            <button
+              type="button"
+              className={`btn btn-sm btn-outline-secondary rounded-pill ${styles['grw-tag-icon-button']}`}
+              onClick={onClickEditTagsButton}
+            >
+              <span className="material-symbols-outlined">local_offer</span>
+            </button>
+          </NotAvailableForReadOnlyUser>
+        </NotAvailableForGuest>
+      </div>
+      <div className="d-none d-lg-flex row">
+        <div className="mb-2">
+          <button
+            id="edit-tags-btn-wrapper-for-tooltip"
+            type="button"
+            className="btn btn-link text-secondary p-0 border-0"
+            onMouseEnter={onMouseEnterHandler}
+            onMouseLeave={onMouseLeaveHandler}
+            onClick={onClickEditTagsButton}
+            disabled={isTagLabelsDisabled}
+          >
+            <span className="material-symbols-outlined me-1">local_offer</span>
+            <span className="me-2">{t('Tags')}</span>
+            {(isHovered && !isTagLabelsDisabled) && (
+              <span className="material-symbols-outlined p-0">edit</span>
+            )}
+          </button>
+        </div>
+        <div>
+          <RenderTagLabels tags={tags} />
         </div>
       </div>
-    </>
+    </div>
   );
 };

+ 14 - 45
apps/app/src/components/PageTags/RenderTagLabels.tsx

@@ -1,65 +1,34 @@
 import React from 'react';
 
-import { useTranslation } from 'next-i18next';
+import SimpleBar from 'simplebar-react';
 
 import { useKeywordManager } from '~/client/services/search-operation';
 
-import { NotAvailableForGuest } from '../NotAvailableForGuest';
-import { NotAvailableForReadOnlyUser } from '../NotAvailableForReadOnlyUser';
-
 type RenderTagLabelsProps = {
   tags: string[],
-  isTagLabelsDisabled: boolean,
-  onClickEditTagsButton: () => void,
 }
 
 const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
-  const {
-    tags, isTagLabelsDisabled, onClickEditTagsButton,
-  } = props;
-  const { t } = useTranslation();
+  const { tags } = props;
 
   const { pushState } = useKeywordManager();
 
-  const isTagsEmpty = tags.length === 0;
 
   return (
-    <>
-      {tags.map((tag) => {
-        return (
-          <a
-            key={tag}
-            type="button"
-            className="grw-tag badge me-2"
-            onClick={() => pushState(`tag:${tag}`)}
-          >
-            {tag}
-          </a>
-        );
-      })}
-      <NotAvailableForGuest>
-        <NotAvailableForReadOnlyUser>
-          <div id="edit-tags-btn-wrapper-for-tooltip" className="d-print-none">
-            <a
-              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') }</>}
-              <i className={`icon-plus ${isTagsEmpty && 'ms-1'}`} />
-            </a>
-          </div>
-        </NotAvailableForReadOnlyUser>
-      </NotAvailableForGuest>
-    </>
-
+    <SimpleBar className="grw-tag-simple-bar pe-1">
+      {tags.map(tag => (
+        <a
+          key={tag}
+          type="button"
+          className="grw-tag badge me-1 mb-1 text-truncate"
+          onClick={() => pushState(`tag:${tag}`)}
+        >
+          {tag}
+        </a>
+      ))}
+    </SimpleBar>
   );
-
 });
-
 RenderTagLabels.displayName = 'RenderTagLabels';
 
 export default RenderTagLabels;

+ 14 - 2
apps/app/src/components/PageTags/TagLabels.module.scss

@@ -8,8 +8,20 @@ $grw-tag-label-font-size: 12px;
     font-weight: normal;
     border-radius: bs.$border-radius;
   }
-  .material-symbols-outlined {
-    font-size: 2em;
+
+  .grw-tag-simple-bar {
+    width: 15.5rem;
+    max-height: 5rem;
+    .grw-tag{
+      max-width: 15rem;
+    }
+  }
+
+  // apply larger font when smaller than lg
+  @include bs.media-breakpoint-down(lg) {
+    .material-symbols-outlined {
+      font-size: 2em;
+    }
   }
 }
 

+ 2 - 0
apps/app/src/components/Sidebar/Bookmarks.tsx

@@ -13,6 +13,8 @@ export const Bookmarks = () : JSX.Element => {
 
   return (
     <>
+      {/* TODO : #139425 Match the space specification method to others */}
+      {/* ref.  https://redmine.weseek.co.jp/issues/139425 */}
       <div className="grw-sidebar-content-header p-3">
         <h3 className="mb-0">{t('Bookmarks')}</h3>
       </div>

+ 8 - 5
apps/app/src/components/Sidebar/Bookmarks/BookmarkContents.tsx

@@ -38,15 +38,18 @@ export const BookmarkContents = (): JSX.Element => {
   }, [mutateBookmarkFolders]);
 
   return (
-    <>
-      <div className="col-8 mb-2 ">
+    <div className="ms-3">
+      <div className="col-8 mb-2">
         <button
           type="button"
           className="btn btn-outline-secondary rounded-pill d-flex justify-content-start align-middle"
           onClick={onClickNewBookmarkFolder}
         >
-          <FolderPlusIcon />
-          <span className="mx-2 ">{t('bookmark_folder.new_folder')}</span>
+
+          <div className="d-flex align-items-center">
+            <FolderPlusIcon />
+            <span className="ms-2">{t('bookmark_folder.new_folder')}</span>
+          </div>
         </button>
       </div>
       {isCreateAction && (
@@ -58,6 +61,6 @@ export const BookmarkContents = (): JSX.Element => {
         </div>
       )}
       <BookmarkFolderTree isOperable userId={currentUser?._id} />
-    </>
+    </div>
   );
 };

+ 2 - 0
apps/app/src/components/Sidebar/Custom/CustomSidebar.tsx

@@ -18,6 +18,8 @@ export const CustomSidebar = (): JSX.Element => {
   const { mutate, isLoading } = useSWRxPageByPath('/Sidebar');
 
   return (
+    // TODO : #139425 Match the space specification method to others
+    // ref.  https://redmine.weseek.co.jp/issues/139425
     <div className="px-3">
       <div className="grw-sidebar-content-header py-3 d-flex">
         <h3 className="mb-0">

+ 2 - 0
apps/app/src/components/Sidebar/InAppNotification/InAppNotification.tsx

@@ -15,6 +15,8 @@ export const InAppNotification = (): JSX.Element => {
   const [isUnopendNotificationsVisible, setUnopendNotificationsVisible] = useState(false);
 
   return (
+    // TODO : #139425 Match the space specification method to others
+    // ref.  https://redmine.weseek.co.jp/issues/139425
     <div className="px-3">
       <div className="grw-sidebar-content-header py-3 d-flex">
         <h3 className="mb-0">

+ 1 - 1
apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.tsx

@@ -57,7 +57,7 @@ export const PageCreateButton = React.memo((): JSX.Element => {
 
   return (
     <div
-      className="d-flex flex-row"
+      className="d-flex flex-row mt-2"
       onMouseEnter={onMouseEnterHandler}
       onMouseLeave={onMouseLeaveHandler}
     >

+ 4 - 2
apps/app/src/components/Sidebar/PageTree/PageTree.tsx

@@ -16,8 +16,10 @@ export const PageTree = (): JSX.Element => {
   const { t } = useTranslation();
 
   return (
-    <div className="px-3">
-      <div className="grw-sidebar-content-header py-3 d-flex">
+    // TODO : #139425 Match the space specification method to others
+    // ref.  https://redmine.weseek.co.jp/issues/139425
+    <div className="pt-4 pb-3 px-3">
+      <div className="grw-sidebar-content-header d-flex">
         <h3 className="mb-0">{t('Page Tree')}</h3>
         <Suspense>
           <PageTreeHeader />

+ 5 - 2
apps/app/src/components/Sidebar/PageTreeItem/Ellipsis.tsx

@@ -1,5 +1,6 @@
+import type { FC } from 'react';
 import React, {
-  useCallback, useState, FC,
+  useCallback, useState,
 } from 'react';
 
 import nodePath from 'path';
@@ -125,10 +126,12 @@ export const Ellipsis: FC<TreeItemToolProps> = (props) => {
     }
   };
 
+  const hasChildren = page.descendantCount ? page.descendantCount > 0 : false;
+
   return (
     <>
       {isRenameInputShown ? (
-        <div className="flex-fill">
+        <div className={`position-absolute ${hasChildren ? 'ms-5' : 'ms-4'}`}>
           <NotDraggableForClosableTextInput>
             <ClosableTextInput
               value={nodePath.basename(page.path ?? '')}

+ 2 - 0
apps/app/src/components/Sidebar/RecentChanges/RecentChanges.tsx

@@ -18,6 +18,8 @@ export const RecentChanges = (): JSX.Element => {
   const [isSmall, setIsSmall] = useState(false);
 
   return (
+    // TODO : #139425 Match the space specification method to others
+    // ref.  https://redmine.weseek.co.jp/issues/139425
     <div className="px-3" data-testid="grw-recent-changes">
       <div className="grw-sidebar-content-header py-3 d-flex">
         <h3 className="mb-0 text-nowrap">{t('Recent Changes')}</h3>

+ 2 - 2
apps/app/src/components/Sidebar/Sidebar.module.scss

@@ -86,7 +86,7 @@
     &:global {
       &.grw-sidebar-collapsed {
         .sidebar-contents-container {
-          background-color: rgba(var(--grw-highlight-100-rgb), .5);
+          background-color: rgba(var(--grw-highlight-100-rgb), .8);
           backdrop-filter: blur(20px);
         }
       }
@@ -108,7 +108,7 @@
     &:global {
       &.grw-sidebar-collapsed {
         .sidebar-contents-container {
-          background-color: rgba(var(--grw-highlight-800-rgb), .5);
+          background-color: rgba(var(--grw-highlight-800-rgb), .8);
           backdrop-filter: blur(20px);
         }
       }

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

@@ -29,7 +29,7 @@ export const PrimaryItems = memo((props: Props) => {
 
   return (
     <div className={styles['grw-primary-items']}>
-      <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.TREE} label="Page Tree" iconName="format_list_bulleted" onHover={onItemHover} />
+      <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.TREE} label="Page Tree" iconName="list" onHover={onItemHover} />
       <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.CUSTOM} label="Custom Sidebar" iconName="code" onHover={onItemHover} />
       <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.RECENT} label="Recent Changes" iconName="update" onHover={onItemHover} />
       <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.BOOKMARKS} label="Bookmarks" iconName="bookmarks" onHover={onItemHover} />

+ 2 - 0
apps/app/src/components/Sidebar/Tag.tsx

@@ -44,6 +44,8 @@ const Tag: FC = () => {
 
   // todo: adjust design by XD
   return (
+    // TODO : #139425 Match the space specification method to others
+    // ref.  https://redmine.weseek.co.jp/issues/139425
     <div className="container-lg px-4 mb-5 pb-5" data-testid="grw-sidebar-content-tags">
       <div className="grw-sidebar-content-header py-3 d-flex">
         <h3 className="mb-0">{t('Tags')}</h3>

+ 5 - 5
apps/app/src/components/TableOfContents.tsx

@@ -11,7 +11,7 @@ import { StickyStretchableScroller } from './StickyStretchableScroller';
 
 import styles from './TableOfContents.module.scss';
 
-const { isUserPage: _isUserPage } = pagePathUtils;
+const { isUsersHomepage: _isUsersHomepage } = pagePathUtils;
 
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 const logger = loggerFactory('growi:TableOfContents');
@@ -19,7 +19,7 @@ const logger = loggerFactory('growi:TableOfContents');
 const TableOfContents = (): JSX.Element => {
   const { data: currentPagePath } = useCurrentPagePath();
 
-  const isUserPage = currentPagePath != null && _isUserPage(currentPagePath);
+  const isUsersHomePage = currentPagePath != null && _isUsersHomepage(currentPagePath);
 
   const { data: rendererOptions } = useTocOptions();
 
@@ -41,13 +41,13 @@ const TableOfContents = (): JSX.Element => {
     // get smaller bottom line of window height - .system-version height - margin 5px) and containerTop
     let bottom = Math.min(window.innerHeight - 20 - 5, parentBottom);
 
-    if (isUserPage) {
+    if (isUsersHomePage) {
       // raise the bottom line by the height and margin-top of UserContentLinks
-      bottom -= 45;
+      bottom -= 90;
     }
     // bottom - revisionToc top
     return bottom - (containerTop + containerPaddingTop);
-  }, [isUserPage, rendererOptions]);
+  }, [isUsersHomePage, rendererOptions]);
 
   return (
     <div id="revision-toc" className={`revision-toc ${styles['revision-toc']}`}>

+ 3 - 5
apps/app/src/components/TrashPageList.tsx

@@ -5,17 +5,15 @@ import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 
 import { toastSuccess } from '~/client/util/toastr';
-import { IPagingResult } from '~/interfaces/paging-result';
+import type { IPagingResult } from '~/interfaces/paging-result';
 import { useIsReadOnlyUser, useShowPageLimitationXL } from '~/stores/context';
 import { useEmptyTrashModal } from '~/stores/modal';
 import { useSWRxPageInfoForList, useSWRxPageList } from '~/stores/page-listing';
 
 import { MenuItemType } from './Common/Dropdown/PageItemControl';
 import CustomNavAndContents from './CustomNavigation/CustomNavAndContents';
-import { DescendantsPageListProps } from './DescendantsPageList';
+import type { DescendantsPageListProps } from './DescendantsPageList';
 import EmptyTrashButton from './EmptyTrashButton';
-import PageListIcon from './Icons/PageListIcon';
-
 
 const DescendantsPageList = dynamic<DescendantsPageListProps>(() => import('./DescendantsPageList').then(mod => mod.DescendantsPageList), { ssr: false });
 
@@ -83,7 +81,7 @@ export const TrashPageList = (): JSX.Element => {
   const navTabMapping = useMemo(() => {
     return {
       pagelist: {
-        Icon: PageListIcon,
+        Icon: () => <span className="material-symbols-outlined">subject</span>,
         Content: DescendantsPageListForTrash,
         i18n: t('page_list'),
       },

+ 0 - 5
apps/app/src/server/routes/apiv3/page/index.js

@@ -680,11 +680,6 @@ module.exports = (crowi) => {
     const { pageId } = req.params;
     const { grant, userRelatedGrantedGroups } = req.body;
 
-    // TODO: remove in https://redmine.weseek.co.jp/issues/136137
-    if (userRelatedGrantedGroups != null && userRelatedGrantedGroups.length > 1) {
-      return res.apiv3Err('Cannot grant multiple groups to page at the moment');
-    }
-
     const Page = crowi.model('Page');
 
     const page = await Page.findByIdAndViewer(pageId, req.user, null, false);

+ 0 - 8
apps/app/src/server/service/page-grant.ts

@@ -507,19 +507,11 @@ class PageGrantService implements IPageGrantService {
       grantedGroupIds?: IGrantedGroup[],
       shouldCheckDescendants = false,
       includeNotMigratedPages = false,
-      previousGrantedGroupIds?: IGrantedGroup[],
   ): Promise<boolean> {
     if (isTopPage(targetPath)) {
       return true;
     }
 
-    if (previousGrantedGroupIds != null) {
-      const isGrantChangeable = await this.validateGrantChange(user, previousGrantedGroupIds, grant, grantedGroupIds);
-      if (!isGrantChangeable) {
-        return false;
-      }
-    }
-
     const comparableAncestor = await this.generateComparableAncestor(targetPath, includeNotMigratedPages);
 
     if (!shouldCheckDescendants) { // checking the parent is enough

+ 7 - 2
apps/app/src/server/service/page/index.ts

@@ -2406,7 +2406,7 @@ class PageService implements IPageService {
         newChildGrantedGroups = this.getNewGrantedGroupsSyncronously(userRelatedGroups, userRelatedParentGrantedGroups, childPage);
       }
       const canChangeGrant = this.pageGrantService
-        .validateGrantChangeSyncronously(userRelatedGroups, childPage.grantedGroups, PageGrant.GRANT_USER_GROUP, newChildGrantedGroups);
+        .validateGrantChangeSyncronously(userRelatedGroups, childPage.grantedGroups, grant, newChildGrantedGroups);
       if (canChangeGrant) {
         operations.push({
           updateOne: {
@@ -4143,12 +4143,17 @@ class PageService implements IPageService {
     const shouldBeOnTree = grant !== PageGrant.GRANT_RESTRICTED;
     const isChildrenExist = await Page.count({ path: new RegExp(`^${escapeStringRegexp(addTrailingSlash(clonedPageData.path))}`), parent: { $ne: null } });
 
+    const isGrantChangeable = await this.pageGrantService.validateGrantChange(user, pageData.grantedGroups, grant, grantUserGroupIds);
+    if (!isGrantChangeable) {
+      throw Error('The selected grant or grantedGroup is not assignable to this page.');
+    }
+
     if (shouldBeOnTree) {
       let isGrantNormalized = false;
       try {
         const shouldCheckDescendants = !options.overwriteScopesOfDescendants;
         // eslint-disable-next-line max-len
-        isGrantNormalized = await this.pageGrantService.isGrantNormalized(user, clonedPageData.path, grant, grantedUserIds, grantUserGroupIds, shouldCheckDescendants, false, pageData.grantedGroups);
+        isGrantNormalized = await this.pageGrantService.isGrantNormalized(user, clonedPageData.path, grant, grantedUserIds, grantUserGroupIds, shouldCheckDescendants, false);
       }
       catch (err) {
         logger.error(`Failed to validate grant of page at "${clonedPageData.path}" of grant ${grant}:`, err);

+ 11 - 26
apps/app/test/integration/service/page-grant.test.js

@@ -542,72 +542,57 @@ describe('PageGrantService', () => {
     });
   });
 
-  describe('Test isGrantNormalized method with previousGrantedGroupIds given', () => {
+  describe('Test validateGrantChange method', () => {
     test('Should return true when Target: completely owned by User1 (belongs to all groups)', async() => {
-      const targetPath = pageMultipleGroupTreesAndUsersPath;
       const grant = Page.GRANT_PUBLIC;
-      const grantedUserIds = null;
       const grantedGroupIds = [];
-      const shouldCheckDescendants = false;
 
-      const result = await pageGrantService.isGrantNormalized(
-        user1, targetPath, grant, grantedUserIds, grantedGroupIds, shouldCheckDescendants, false, multipleGroupTreesAndUsersPage.grantedGroups,
+      const result = await pageGrantService.validateGrantChange(
+        user1, multipleGroupTreesAndUsersPage.grantedGroups, grant, grantedGroupIds,
       );
 
       expect(result).toBe(true);
     });
 
     test('Should return false when Target: partially owned by User2 (belongs to one of the groups), and change to public grant', async() => {
-      const targetPath = pageMultipleGroupTreesAndUsersPath;
       const grant = Page.GRANT_PUBLIC;
-      const grantedUserIds = null;
       const grantedGroupIds = [];
-      const shouldCheckDescendants = false;
 
-      const result = await pageGrantService.isGrantNormalized(
-        user2, targetPath, grant, grantedUserIds, grantedGroupIds, shouldCheckDescendants, false, multipleGroupTreesAndUsersPage.grantedGroups,
+      const result = await pageGrantService.validateGrantChange(
+        user2, multipleGroupTreesAndUsersPage.grantedGroups, grant, grantedGroupIds,
       );
 
       expect(result).toBe(false);
     });
 
     test('Should return false when Target: partially owned by User2 (belongs to one of the groups), and change to owner grant', async() => {
-      const targetPath = pageMultipleGroupTreesAndUsersPath;
       const grant = Page.GRANT_OWNER;
-      const grantedUserIds = [user2._id];
       const grantedGroupIds = [];
-      const shouldCheckDescendants = false;
 
-      const result = await pageGrantService.isGrantNormalized(
-        user2, targetPath, grant, grantedUserIds, grantedGroupIds, shouldCheckDescendants, false, multipleGroupTreesAndUsersPage.grantedGroups,
+      const result = await pageGrantService.validateGrantChange(
+        user2, multipleGroupTreesAndUsersPage.grantedGroups, grant, grantedGroupIds,
       );
 
       expect(result).toBe(false);
     });
 
     test('Should return false when Target: partially owned by User2 (belongs to one of the groups), and change to restricted grant', async() => {
-      const targetPath = pageMultipleGroupTreesAndUsersPath;
       const grant = Page.GRANT_RESTRICTED;
-      const grantedUserIds = null;
       const grantedGroupIds = [];
-      const shouldCheckDescendants = false;
 
-      const result = await pageGrantService.isGrantNormalized(
-        user2, targetPath, grant, grantedUserIds, grantedGroupIds, shouldCheckDescendants, false, multipleGroupTreesAndUsersPage.grantedGroups,
+      const result = await pageGrantService.validateGrantChange(
+        user2, multipleGroupTreesAndUsersPage.grantedGroups, grant, grantedGroupIds,
       );
 
       expect(result).toBe(false);
     });
 
     test('Should return false when Target: partially owned by User2, and change to group grant without any groups of user2', async() => {
-      const targetPath = pageMultipleGroupTreesAndUsersPath;
       const grant = Page.GRANT_USER_GROUP;
-      const grantedUserIds = null;
       const grantedGroupIds = [{ item: differentTreeGroup._id, type: GroupType.userGroup }];
-      const shouldCheckDescendants = false;
 
-      const result = await pageGrantService.isGrantNormalized(
-        user2, targetPath, grant, grantedUserIds, grantedGroupIds, shouldCheckDescendants, false, multipleGroupTreesAndUsersPage.grantedGroups,
+      const result = await pageGrantService.validateGrantChange(
+        user2, multipleGroupTreesAndUsersPage.grantedGroups, grant, grantedGroupIds,
       );
 
       expect(result).toBe(false);

+ 5 - 3
packages/editor/src/services/list-util/markdown-list-util.ts

@@ -1,7 +1,9 @@
 import type { EditorView } from '@codemirror/view';
 
-// https://regex101.com/r/7BN2fR/5
-const indentAndMarkRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/;
+// https://regex101.com/r/r9plEA/1
+const indentAndMarkRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]\s))(\s*)/;
+// https://regex101.com/r/HFYoFN/1
+const indentAndMarkOnlyRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/;
 
 const getBol = (editor: EditorView) => {
   const curPos = editor.state.selection.main.head;
@@ -26,7 +28,7 @@ export const adjustPasteData = (strFromBol: string, text: string): string => {
 
     const replacedLines = lines?.map((line, index) => {
 
-      if (index === 0 && strFromBol.match(indentAndMarkRE)) {
+      if (index === 0 && strFromBol.match(indentAndMarkOnlyRE)) {
         return line.replace(indentAndMarkRE, '');
       }