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

Merge branch 'dev/5.0.x' into feat/page-rename-v5

Shun Miyazawa 4 лет назад
Родитель
Сommit
f5f3876257
35 измененных файлов с 364 добавлено и 294 удалено
  1. 7 0
      packages/app/resource/locales/en_US/translation.json
  2. 7 0
      packages/app/resource/locales/ja_JP/translation.json
  3. 7 0
      packages/app/resource/locales/zh_CN/translation.json
  4. 2 0
      packages/app/src/client/app.jsx
  5. 0 84
      packages/app/src/components/BookmarkButton.jsx
  6. 27 38
      packages/app/src/components/BookmarkButtons.tsx
  7. 1 1
      packages/app/src/components/Common/ClosableTextInput.tsx
  8. 16 9
      packages/app/src/components/Common/Dropdown/PageItemControl.tsx
  9. 26 0
      packages/app/src/components/DuplicatePage.tsx
  10. 16 0
      packages/app/src/components/IdenticalPathPage.tsx
  11. 2 1
      packages/app/src/components/Navbar/SubNavButtons.tsx
  12. 1 1
      packages/app/src/components/Page/PageListItem.tsx
  13. 4 2
      packages/app/src/components/Page/RevisionRenderer.jsx
  14. 6 4
      packages/app/src/components/PageReactionButtons.tsx
  15. 67 25
      packages/app/src/components/Sidebar/PageTree/Item.tsx
  16. 2 2
      packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx
  17. 3 0
      packages/app/src/interfaces/bookmark-info.ts
  18. 0 7
      packages/app/src/interfaces/bookmarks.ts
  19. 3 2
      packages/app/src/server/routes/page.js
  20. 5 3
      packages/app/src/server/service/page.ts
  21. 6 0
      packages/app/src/server/views/layout-growi/identical-path-page-list.html
  22. 0 22
      packages/app/src/server/views/layout-growi/select-go-to-page.html
  23. 1 0
      packages/app/src/stores/bookmark.ts
  24. 0 21
      packages/app/src/stores/bookmarks.tsx
  25. 14 11
      packages/app/src/styles/_page-tree.scss
  26. 10 27
      packages/app/src/styles/theme/_apply-colors-dark.scss
  27. 9 26
      packages/app/src/styles/theme/_apply-colors-light.scss
  28. 1 1
      packages/app/src/styles/theme/_apply-colors.scss
  29. 19 1
      packages/app/src/styles/theme/christmas.scss
  30. 18 0
      packages/app/src/styles/theme/future.scss
  31. 20 2
      packages/app/src/styles/theme/island.scss
  32. 18 0
      packages/app/src/styles/theme/kibela.scss
  33. 42 0
      packages/app/src/styles/theme/mixins/_list-group.scss
  34. 1 1
      packages/app/src/styles/theme/mono-blue.scss
  35. 3 3
      packages/app/src/styles/theme/wood.scss

+ 7 - 0
packages/app/resource/locales/en_US/translation.json

@@ -112,6 +112,8 @@
   "Wiki Management Home Page": "Wiki Management Home Page",
   "App Settings": "App Settings",
   "V5 Page Migration": "V5 Page Migration",
+  "GROWI.5.0_new_schema": "GROWI.5.0 new schema",
+  "See_more_detail_on_new_schema": "See more detail on <a href='#'>{{url}}</a> <i class='icon-share-alt'></i> ",
   "Site URL settings": "Site URL settings",
   "Markdown Settings": "Markdown Settings",
   "Customize": "Customize",
@@ -971,5 +973,10 @@
   },
   "pagetree": {
     "private_legacy_pages": "Private Legacy Pages"
+  },
+  "duplicated_page_alert" : {
+    "same_page_name_exists": "Same page name exits as「{{pageName}}」",
+    "same_page_name_exists_at_path" : "Same page name as {{pageName}} exists at {{path}} ",
+    "select_page_to_see" : "Select a page to see"
   }
 }

+ 7 - 0
packages/app/resource/locales/ja_JP/translation.json

@@ -112,6 +112,8 @@
   "Wiki Management Home Page": "Wiki管理トップ",
   "App Settings": "アプリ設定",
   "V5 Page Migration": "V5 ページマイグレーション",
+  "GROWI.5.0_new_schema": "GROWI.5.0における新スキーマについて",
+  "See_more_detail_on_new_schema": "詳しくは<a href='#'>{{url}}</a><i class='icon-share-alt'></i>を参照ください。",
   "Site URL settings": "サイトURL設定",
   "Markdown Settings": "マークダウン設定",
   "Customize": "カスタマイズ",
@@ -964,5 +966,10 @@
   },
   "pagetree": {
     "private_legacy_pages": "待避所"
+  },
+  "duplicated_page_alert" : {
+    "same_page_name_exists": "ページ名 「{{pageName}}」が重複しています",
+    "same_page_name_exists_at_path" : "”{{path}}” において ”{{pageName}}”というページは複数存在しています。",
+    "select_page_to_see" : "以下から遷移するページを選択してください。"
   }
 }

+ 7 - 0
packages/app/resource/locales/zh_CN/translation.json

@@ -120,6 +120,8 @@
 	"Wiki Management Home Page": "Wiki管理首页",
 	"App Settings": "系统设置",
   "V5 Page Migration": "V5 Page Migration",
+  "GROWI.5.0_new_schema": "GROWI.5.0 new schema",
+  "See_more_detail_on_new_schema": "更多详情请见<a href='#'>{{url}}</a> <i class='icon-share-alt'></i> ",
 	"Site URL settings": "主页URL设置",
 	"Markdown Settings": "Markdown设置",
 	"Customize": "页面定制",
@@ -974,5 +976,10 @@
   },
   "pagetree": {
     "private_legacy_pages": "私人遗留页面"
+  },
+  "duplicated_page_alert" : {
+    "same_page_name_exists": "页面名称「{{pageName}}」是重复的",
+    "same_page_name_exists_at_path" : "在”{{path}}” 中,有不止一个名为”{{pageName}}”的页面",
+    "select_page_to_see" : "请在下面选择你想去的页面。"
   }
 }

+ 2 - 0
packages/app/src/client/app.jsx

@@ -42,6 +42,7 @@ import Fab from '../components/Fab';
 import PersonalSettings from '../components/Me/PersonalSettings';
 import GrowiSubNavigation from '../components/Navbar/GrowiSubNavigation';
 import GrowiSubNavigationSwitcher from '../components/Navbar/GrowiSubNavigationSwitcher';
+import IdenticalPathPage from '~/components/IdenticalPathPage';
 
 import ContextExtractor from '~/client/services/ContextExtractor';
 import PageContainer from '~/client/services/PageContainer';
@@ -88,6 +89,7 @@ Object.assign(componentMappings, {
 
   'search-page': <SearchPage crowi={appContainer} />,
   'all-in-app-notifications': <InAppNotificationPage />,
+  'identical-path-page-list': <IdenticalPathPage />,
 
   // 'revision-history': <PageHistory pageId={pageId} />,
   'tags-page': <TagsList crowi={appContainer} />,

+ 0 - 84
packages/app/src/components/BookmarkButton.jsx

@@ -1,84 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { UncontrolledTooltip } from 'reactstrap';
-import { useTranslation } from 'react-i18next';
-import { withUnstatedContainers } from './UnstatedUtils';
-
-import AppContainer from '~/client/services/AppContainer';
-
-class LegacyBookmarkButton extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.handleClick = this.handleClick.bind(this);
-  }
-
-  async handleClick() {
-
-    if (this.props.onBookMarkClicked == null) {
-      return;
-    }
-    this.props.onBookMarkClicked();
-  }
-
-  render() {
-    const {
-      appContainer, t, isBookmarked, hideTotalNumber, sumOfBookmarks,
-    } = this.props;
-    const { isGuestUser } = appContainer;
-
-    return (
-      <>
-        <button
-          type="button"
-          id="bookmark-button"
-          onClick={this.handleClick}
-          className={`btn btn-bookmark border-0
-          ${isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
-        >
-          <i className={`fa ${isBookmarked ? 'fa-bookmark' : 'fa-bookmark-o'}`}></i>
-          { !hideTotalNumber && (
-            <span className="total-bookmarks ml-3">
-              {sumOfBookmarks && (
-                sumOfBookmarks
-              )}
-            </span>
-          ) }
-        </button>
-
-        {isGuestUser && (
-          <UncontrolledTooltip placement="top" target="bookmark-button" fade={false}>
-            {t('Not available for guest')}
-          </UncontrolledTooltip>
-        )}
-      </>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const LegacyBookmarkButtonWrapper = withUnstatedContainers(LegacyBookmarkButton, [AppContainer]);
-
-LegacyBookmarkButton.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  isBookmarked: PropTypes.bool.isRequired,
-
-  hideTotalNumber: PropTypes.bool,
-  sumOfBookmarks: PropTypes.number,
-  t: PropTypes.func.isRequired,
-  onBookMarkClicked: PropTypes.func,
-};
-
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-const BookmarkButton = (props) => {
-  const { t } = useTranslation();
-  return <LegacyBookmarkButtonWrapper t={t} {...props}></LegacyBookmarkButtonWrapper>;
-};
-
-export default BookmarkButton;

+ 27 - 38
packages/app/src/components/BookmarkButtons.tsx

@@ -1,49 +1,35 @@
 import React, { FC, useState } from 'react';
 
-import { Types } from 'mongoose';
 import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
 import { useTranslation } from 'react-i18next';
 
+import { IUser } from '../interfaces/user';
+
 import UserPictureList from './User/UserPictureList';
-import { toastError } from '~/client/util/apiNotification';
 import { useIsGuestUser } from '~/stores/context';
-import { useSWRxBookmarksInfo } from '~/stores/bookmarks';
-import { apiv3Put } from '~/client/util/apiv3-client';
 
 interface Props {
-  pageId: Types.ObjectId
+  hideTotalNumber?: boolean
+  isBookmarked: boolean
+  sumOfBookmarks: number
+  bookmarkedUsers: IUser[]
+  onBookMarkClicked: ()=>void;
 }
 
-const BookmarkButton: FC<Props> = (props: Props) => {
+const BookmarkButtons: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
-  const { pageId } = props;
 
   const [isPopoverOpen, setIsPopoverOpen] = useState(false);
 
   const { data: isGuestUser } = useIsGuestUser();
-  const { data: bookmarksInfo, mutate } = useSWRxBookmarksInfo(pageId);
-
-  const isBookmarked = bookmarksInfo?.isBookmarked != null ? bookmarksInfo.isBookmarked : false;
-  const sumOfBookmarks = bookmarksInfo?.sumOfBookmarks != null ? bookmarksInfo.sumOfBookmarks : 0;
-  const bookmarkedUsers = bookmarksInfo?.bookmarkedUsers != null ? bookmarksInfo.bookmarkedUsers : [];
 
   const togglePopover = () => {
     setIsPopoverOpen(!isPopoverOpen);
   };
 
   const handleClick = async() => {
-    if (isGuestUser) {
-      return;
-    }
-
-    try {
-      const res = await apiv3Put('/bookmarks', { pageId, bool: !isBookmarked });
-      if (res) {
-        mutate();
-      }
-    }
-    catch (err) {
-      toastError(err);
+    if (props.onBookMarkClicked != null) {
+      props.onBookMarkClicked();
     }
   };
 
@@ -54,9 +40,9 @@ const BookmarkButton: FC<Props> = (props: Props) => {
         id="bookmark-button"
         onClick={handleClick}
         className={`btn btn-bookmark border-0
-          ${isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
+          ${props.isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
       >
-        <i className="icon-star"></i>
+        <i className={`fa ${props.isBookmarked ? 'fa-bookmark' : 'fa-bookmark-o'}`}></i>
       </button>
 
       {isGuestUser && (
@@ -65,19 +51,22 @@ const BookmarkButton: FC<Props> = (props: Props) => {
         </UncontrolledTooltip>
       )}
 
-      <button type="button" id="po-total-bookmarks" className={`btn btn-bookmark border-0 total-bookmarks ${isBookmarked ? 'active' : ''}`}>
-        {sumOfBookmarks}
-      </button>
-
-      <Popover placement="bottom" isOpen={isPopoverOpen} target="po-total-bookmarks" toggle={togglePopover} trigger="legacy">
-        <PopoverBody className="seen-user-popover">
-          <div className="px-2 text-right user-list-content text-truncate text-muted">
-            {bookmarkedUsers.length ? <UserPictureList users={bookmarkedUsers} /> : t('No users have bookmarked yet')}
-          </div>
-        </PopoverBody>
-      </Popover>
+      { !props.hideTotalNumber && (
+        <>
+          <button type="button" id="po-total-bookmarks" className={`btn btn-bookmark border-0 total-bookmarks ${props.isBookmarked ? 'active' : ''}`}>
+            {props.sumOfBookmarks}
+          </button>
+          <Popover placement="bottom" isOpen={isPopoverOpen} target="po-total-bookmarks" toggle={togglePopover} trigger="legacy">
+            <PopoverBody className="seen-user-popover">
+              <div className="px-2 text-right user-list-content text-truncate text-muted">
+                {props.bookmarkedUsers.length ? <UserPictureList users={props.bookmarkedUsers} /> : t('No users have bookmarked yet')}
+              </div>
+            </PopoverBody>
+          </Popover>
+        </>
+      ) }
     </div>
   );
 };
 
-export default BookmarkButton;
+export default BookmarkButtons;

+ 1 - 1
packages/app/src/components/Common/ClosableTextInput.tsx

@@ -96,7 +96,7 @@ const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextI
       <input
         ref={inputRef}
         type="text"
-        className="form-control mt-1"
+        className="form-control"
         placeholder={props.placeholder}
         name="input"
         onChange={onChangeHandler}

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

@@ -1,4 +1,4 @@
-import React, { FC, useState } from 'react';
+import React, { FC, useState, useCallback } from 'react';
 import {
   Dropdown, DropdownMenu, DropdownToggle, DropdownItem,
 } from 'reactstrap';
@@ -15,23 +15,30 @@ type PageItemControlProps = {
   page: Partial<IPageHasId>
   isEnableActions: boolean
   isDeletable: boolean
-  onClickDeleteButton?: (pageId: string) => void
+  onClickDeleteButtonHandler?: (pageId: string) => void
+  onClickRenameButtonHandler?: (pageId: string) => void
 }
 
 const PageItemControl: FC<PageItemControlProps> = (props: PageItemControlProps) => {
 
   const {
-    page, isEnableActions, onClickDeleteButton, isDeletable,
+    page, isEnableActions, onClickDeleteButtonHandler, isDeletable, onClickRenameButtonHandler,
   } = props;
   const { t } = useTranslation('');
   const [isOpen, setIsOpen] = useState(false);
   const { data: bookmarkInfo, error: bookmarkInfoError, mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(page._id, isOpen);
 
-  const deleteButtonHandler = () => {
-    if (onClickDeleteButton != null && page._id != null) {
-      onClickDeleteButton(page._id);
+  const deleteButtonClickedHandler = useCallback(() => {
+    if (onClickDeleteButtonHandler != null && page._id != null) {
+      onClickDeleteButtonHandler(page._id);
     }
-  };
+  }, [onClickDeleteButtonHandler, page._id]);
+
+  const renameButtonClickedHandler = useCallback(() => {
+    if (onClickRenameButtonHandler != null && page._id != null) {
+      onClickRenameButtonHandler(page._id);
+    }
+  }, [onClickRenameButtonHandler, page._id]);
 
 
   const bookmarkToggleHandler = (async() => {
@@ -105,7 +112,7 @@ const PageItemControl: FC<PageItemControlProps> = (props: PageItemControlProps)
           </DropdownItem>
         )}
         {isEnableActions && (
-          <DropdownItem onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
+          <DropdownItem onClick={renameButtonClickedHandler}>
             <i className="icon-fw  icon-action-redo"></i>
             {t('Move/Rename')}
           </DropdownItem>
@@ -113,7 +120,7 @@ const PageItemControl: FC<PageItemControlProps> = (props: PageItemControlProps)
         {isDeletable && isEnableActions && (
           <>
             <DropdownItem divider />
-            <DropdownItem className="text-danger pt-2" onClick={deleteButtonHandler}>
+            <DropdownItem className="text-danger pt-2" onClick={deleteButtonClickedHandler}>
               <i className="icon-fw icon-trash"></i>
               {t('Delete')}
             </DropdownItem>

+ 26 - 0
packages/app/src/components/DuplicatePage.tsx

@@ -0,0 +1,26 @@
+import React, { FC } from 'react';
+import { DevidedPagePath } from '@growi/core';
+import { useTranslation } from 'react-i18next';
+
+
+type DuplicatePageAlertProps = {
+  path : string,
+}
+
+const DuplicatePageAlert : FC<DuplicatePageAlertProps> = (props: DuplicatePageAlertProps) => {
+  const { path } = props;
+  const { t } = useTranslation();
+  const devidedPath = new DevidedPagePath(path);
+
+  return (
+    <div className="alert alert-warning py-3">
+      <h5 className="font-weight-bold mt-1">{t('duplicated_page_alert.same_page_name_exists', { pageName: devidedPath.latter })}</h5>
+      <p>
+        {t('duplicated_page_alert.same_page_name_exists_at_path',
+          { path: devidedPath.isFormerRoot ? '/' : devidedPath.former, pageName: devidedPath.latter })}<br />
+        <p dangerouslySetInnerHTML={{ __html: t('See_more_detail_on_new_schema', { url: t('GROWI.5.0_new_schema') }) }} />
+      </p>
+      <p className="mb-1">{t('duplicated_page_alert.select_page_to_see')}</p>
+    </div>
+  );
+};

+ 16 - 0
packages/app/src/components/IdenticalPathPage.tsx

@@ -0,0 +1,16 @@
+import React, { FC } from 'react';
+
+type IdenticalPathPageProps= {
+  // add props and types here
+}
+const IdenticalPathPage:FC<IdenticalPathPageProps> = (props:IdenticalPathPageProps) => {
+  return (
+    <div>
+      {/* Todo: show alert */}
+      {/* Todo: show identical path page list */}
+      IdenticalPathPageList
+    </div>
+  );
+};
+
+export default IdenticalPathPage;

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

@@ -74,7 +74,7 @@ const SubNavButtons: FC<SubNavButtonsProps> = (props: SubNavButtonsProps) => {
   }
 
   const { sumOfLikers, isLiked } = pageInfo;
-  const { sumOfBookmarks, isBookmarked } = bookmarkInfo;
+  const { sumOfBookmarks, isBookmarked, bookmarkedUsers } = bookmarkInfo;
 
   return (
     <div className="d-flex" style={{ gap: '2px' }}>
@@ -91,6 +91,7 @@ const SubNavButtons: FC<SubNavButtonsProps> = (props: SubNavButtonsProps) => {
             onLikeClicked={likeClickhandler}
             sumOfBookmarks={sumOfBookmarks}
             isBookmarked={isBookmarked}
+            bookmarkedUsers={bookmarkedUsers}
             onBookMarkClicked={bookmarkClickHandler}
           >
           </PageReactionButtons>

+ 1 - 1
packages/app/src/components/Page/PageListItem.tsx

@@ -115,7 +115,7 @@ const PageListItem: FC<Props> = memo((props:Props) => {
               <div className="item-control ml-auto">
                 <PageItemControl
                   page={pageData}
-                  onClickDeleteButton={props.onClickDeleteButton}
+                  onClickDeleteButtonHandler={props.onClickDeleteButton}
                   isEnableActions={isEnableActions}
                   isDeletable={!isTopPage(pageData.path)}
                 />

+ 4 - 2
packages/app/src/components/Page/RevisionRenderer.jsx

@@ -64,9 +64,11 @@ class LegacyRevisionRenderer extends React.PureComponent {
    */
   getHighlightedBody(body, keywords) {
     const normalizedKeywordsArray = [];
-    // !!TODO!!: care double quote
     // !!TODO!!: add test code
-    keywords.replace(/"/g, '').split(/[\u{20}\u{3000}]/u).forEach((keyword, i) => { // split by both full-with and half-width space
+    // Separate keywords
+    // - Surrounded by double quotation
+    // - Split by both full-width and half-width spaces
+    [...keywords.match(/"[^"]+"|[^\u{20}\u{3000}]+/ug)].forEach((keyword, i) => {
       if (keyword === '') {
         return;
       }

+ 6 - 4
packages/app/src/components/PageReactionButtons.tsx

@@ -1,7 +1,7 @@
 import React, { FC } from 'react';
 import LikeButtons from './LikeButtons';
 import { IUser } from '../interfaces/user';
-import BookmarkButton from './BookmarkButton';
+import BookmarkButtons from './BookmarkButtons';
 
 type Props = {
   isCompactMode?: boolean,
@@ -13,13 +13,14 @@ type Props = {
 
   isBookmarked: boolean,
   sumOfBookmarks: number,
+  bookmarkedUsers: IUser[]
   onBookMarkClicked: ()=>void,
 }
 
 
 const PageReactionButtons : FC<Props> = (props: Props) => {
   const {
-    isCompactMode, sumOfLikers, isLiked, likers, onLikeClicked, sumOfBookmarks, isBookmarked, onBookMarkClicked,
+    isCompactMode, sumOfLikers, isLiked, likers, onLikeClicked, sumOfBookmarks, isBookmarked, bookmarkedUsers, onBookMarkClicked,
   } = props;
 
 
@@ -33,13 +34,14 @@ const PageReactionButtons : FC<Props> = (props: Props) => {
         likers={likers}
       >
       </LikeButtons>
-      <BookmarkButton
+      <BookmarkButtons
         hideTotalNumber={isCompactMode}
         sumOfBookmarks={sumOfBookmarks}
         isBookmarked={isBookmarked}
+        bookmarkedUsers={bookmarkedUsers}
         onBookMarkClicked={onBookMarkClicked}
       >
-      </BookmarkButton>
+      </BookmarkButtons>
     </>
   );
 };

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

@@ -45,25 +45,34 @@ type ItemControlProps = {
   page: Partial<IPageHasId>
   isEnableActions: boolean
   isDeletable: boolean
-  onClickDeleteButtonHandler?(): void
-  onClickPlusButtonHandler?(): void
+  onClickPlusButton?(): void
+  onClickDeleteButton?(): void
+  onClickRenameButton?(): void
 }
 
 const ItemControl: FC<ItemControlProps> = memo((props: ItemControlProps) => {
   const onClickPlusButton = () => {
-    if (props.onClickPlusButtonHandler == null) {
+    if (props.onClickPlusButton == null) {
       return;
     }
 
-    props.onClickPlusButtonHandler();
+    props.onClickPlusButton();
   };
 
-  const onClickDeleteButton = () => {
-    if (props.onClickDeleteButtonHandler == null) {
+  const onClickDeleteButtonHandler = () => {
+    if (props.onClickDeleteButton == null) {
       return;
     }
 
-    props.onClickDeleteButtonHandler();
+    props.onClickDeleteButton();
+  };
+
+  const onClickRenameButtonHandler = () => {
+    if (props.onClickRenameButton == null) {
+      return;
+    }
+
+    props.onClickRenameButton();
   };
 
   if (props.page == null) {
@@ -72,7 +81,13 @@ const ItemControl: FC<ItemControlProps> = memo((props: ItemControlProps) => {
 
   return (
     <>
-      <PageItemControl page={props.page} onClickDeleteButton={onClickDeleteButton} isEnableActions={props.isEnableActions} isDeletable={props.isDeletable} />
+      <PageItemControl
+        page={props.page}
+        onClickDeleteButtonHandler={onClickDeleteButtonHandler}
+        isEnableActions={props.isEnableActions}
+        isDeletable={props.isDeletable}
+        onClickRenameButtonHandler={onClickRenameButtonHandler}
+      />
       <button
         type="button"
         className="border-0 rounded grw-btn-page-management p-0"
@@ -105,12 +120,11 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
 
   const [currentChildren, setCurrentChildren] = useState(children);
   const [isOpen, setIsOpen] = useState(_isOpen);
-
   const [isNewPageInputShown, setNewPageInputShown] = useState(false);
+  const [isRenameInputShown, setRenameInputShown] = useState(false);
 
   const { data, error } = useSWRxPageChildren(isOpen ? page._id : null);
 
-
   const [{ isDragging }, drag] = useDrag(() => ({
     type: 'PAGE_TREE',
     item: { page },
@@ -151,7 +165,11 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
     setIsOpen(!isOpen);
   }, [isOpen]);
 
-  const onClickDeleteButtonHandler = useCallback(() => {
+  const onClickPlusButton = useCallback(() => {
+    setNewPageInputShown(true);
+  }, []);
+
+  const onClickDeleteButton = useCallback(() => {
     if (onClickDeleteByPage == null) {
       return;
     }
@@ -171,6 +189,23 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
     onClickDeleteByPage(pageToDelete);
   }, [page, onClickDeleteByPage]);
 
+
+  const onClickRenameButton = useCallback(() => {
+    setRenameInputShown(true);
+  }, []);
+
+  // TODO: make a put request to pages/title
+  const onPressEnterForRenameHandler = () => {
+    toastWarning(t('search_result.currently_not_implemented'));
+    setRenameInputShown(false);
+  };
+
+  // TODO: go to create page page
+  const onPressEnterForCreateHandler = () => {
+    toastWarning(t('search_result.currently_not_implemented'));
+    setNewPageInputShown(false);
+  };
+
   const inputValidator = (title: string | null): AlertInfo | null => {
     if (title == null || title === '') {
       return {
@@ -182,11 +217,6 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
     return null;
   };
 
-  // TODO: go to create page page
-  const onPressEnterHandler = () => {
-    toastWarning(t('search_result.currently_not_implemented'));
-  };
-
   // didMount
   useEffect(() => {
     if (hasChildren()) setIsOpen(true);
@@ -215,9 +245,9 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
 
   return (
     <div className={`grw-pagetree-item-container ${isOver ? 'grw-pagetree-is-over' : ''}`}>
-      <div
+      <li
         ref={(c) => { drag(c); drop(c) }}
-        className={`grw-pagetree-item d-flex align-items-center pr-1 ${page.isTarget ? 'grw-pagetree-is-target' : ''}`}
+        className={`list-group-item list-group-item-action border-0 py-1 d-flex align-items-center  ${page.isTarget ? 'grw-pagetree-is-target' : ''}`}
       >
         <button
           type="button"
@@ -228,29 +258,41 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
             <TriangleIcon />
           </div>
         </button>
-        <a href={page._id} className="grw-pagetree-title-anchor flex-grow-1">
-          <p className={`text-truncate m-auto ${page.isEmpty && 'text-muted'}`}>{nodePath.basename(page.path as string) || '/'}</p>
-        </a>
+        { isRenameInputShown && (
+          <ClosableTextInput
+            isShown
+            placeholder={t('Input page name')}
+            onClickOutside={() => { setRenameInputShown(false) }}
+            onPressEnter={onPressEnterForRenameHandler}
+            inputValidator={inputValidator}
+          />
+        )}
+        { !isRenameInputShown && (
+          <a href={page._id} className="grw-pagetree-title-anchor flex-grow-1">
+            <p className={`text-truncate m-auto ${page.isEmpty && 'text-muted'}`}>{nodePath.basename(page.path as string) || '/'}</p>
+          </a>
+        )}
         <div className="grw-pagetree-count-wrapper">
           <ItemCount />
         </div>
         <div className="grw-pagetree-control d-none">
           <ItemControl
             page={page}
-            onClickDeleteButtonHandler={onClickDeleteButtonHandler}
-            onClickPlusButtonHandler={() => { setNewPageInputShown(true) }}
+            onClickPlusButton={onClickPlusButton}
+            onClickDeleteButton={onClickDeleteButton}
+            onClickRenameButton={onClickRenameButton}
             isEnableActions={isEnableActions}
             isDeletable={!page.isEmpty && !isTopPage(page.path as string)}
           />
         </div>
-      </div>
+      </li>
 
       {isEnableActions && (
         <ClosableTextInput
           isShown={isNewPageInputShown}
           placeholder={t('Input page name')}
           onClickOutside={() => { setNewPageInputShown(false) }}
-          onPressEnter={onPressEnterHandler}
+          onPressEnter={onPressEnterForCreateHandler}
           inputValidator={inputValidator}
         />
       )}

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

@@ -67,7 +67,7 @@ const renderByInitialNode = (
     initialNode: ItemNode, DeleteModal: JSX.Element, isEnableActions: boolean, targetPathOrId?: string, onClickDeleteByPage?: (page: IPageForPageDeleteModal) => void,
 ): JSX.Element => {
   return (
-    <div className="grw-pagetree p-3">
+    <ul className="grw-pagetree list-group p-3">
       <Item
         key={initialNode.page.path}
         targetPathOrId={targetPathOrId}
@@ -77,7 +77,7 @@ const renderByInitialNode = (
         onClickDeleteByPage={onClickDeleteByPage}
       />
       {DeleteModal}
-    </div>
+    </ul>
   );
 };
 

+ 3 - 0
packages/app/src/interfaces/bookmark-info.ts

@@ -1,4 +1,7 @@
+import { IUser } from '~/interfaces/user';
+
 export type IBookmarkInfo = {
   sumOfBookmarks: number;
   isBookmarked: boolean,
+  bookmarkedUsers: IUser[]
 };

+ 0 - 7
packages/app/src/interfaces/bookmarks.ts

@@ -1,7 +0,0 @@
-import { IUser } from '~/interfaces/user';
-
-export interface IBookmarksInfo {
-  isBookmarked: boolean
-  sumOfBookmarks: number
-  bookmarkedUsers: IUser[]
-}

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

@@ -613,8 +613,9 @@ module.exports = function(crowi, app) {
     const { redirectFrom } = req.query;
 
     if (pages.length >= 2) {
-      // pass only redirectFrom since it is not sure whether the query params are related to the pages
-      return res.render('layout-growi/select-go-to-page', { pages, redirectFrom });
+      return res.render('layout-growi/identical-path-page-list', {
+        pages, redirectFrom,
+      });
     }
 
     if (pages.length === 1) {

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

@@ -1108,7 +1108,9 @@ class PageService {
                 },
                 {
                   $project: {
-                    revision: { $substr: ['$body', 0, MAX_LENGTH] },
+                    // What is $substrCP?
+                    // see: https://stackoverflow.com/questions/43556024/mongodb-error-substrbytes-invalid-range-ending-index-is-in-the-middle-of-a-ut/43556249
+                    revision: { $substrCP: ['$body', 0, MAX_LENGTH] },
                   },
                 },
               ],
@@ -1380,7 +1382,7 @@ class PageService {
           const filter: any = {
             // regexr.com/6889f
             // ex. /parent/any_child OR /any_level1
-            path: { $regex: new RegExp(`^${parentPath}(\\/[^/]+)\\/?$`, 'gi') },
+            path: { $regex: new RegExp(`^${parentPath}(\\/[^/]+)\\/?$`, 'i') },
           };
           if (grant != null) {
             filter.grant = grant;
@@ -1407,7 +1409,7 @@ class PageService {
           }
 
           // finish migration
-          if (res.result.nModified === 0) { // TODO: find the best property to count updated documents
+          if (res.result.nModified === 0 && res.result.nMatched === 0) {
             shouldContinue = false;
             logger.error('Migration is unable to continue', 'parentPaths:', parentPaths, 'bulkWriteResult:', res);
           }

+ 6 - 0
packages/app/src/server/views/layout-growi/identical-path-page-list.html

@@ -0,0 +1,6 @@
+{% extends 'base/layout.html' %}
+
+{% block content_main %}
+<div id="grw-fav-sticky-trigger" class="sticky-top"></div>
+<div id="identical-path-page-list"></div>
+{% endblock %}

+ 0 - 22
packages/app/src/server/views/layout-growi/select-go-to-page.html

@@ -1,22 +0,0 @@
-{% extends 'base/layout.html' %}
-
-<!-- WIP -->
-
-{% block content_main %}
-  <div>ContentMain</div>
-  <div>
-    {% for page in pages %}
-      <li>{{page._id.toString()}}: {{page.path}}</li>
-    {% endfor %}
-  </div>
-  <br>
-  <div>redirectFrom: {{redirectFrom}}</div>
-{% endblock %}
-
-{% block content_footer %}
-  <div>Footer</div>
-{% endblock %}
-
-{% block body_end %}
-  <div>BodyEnd</div>
-{% endblock %}

+ 1 - 0
packages/app/src/stores/bookmark.ts

@@ -11,6 +11,7 @@ export const useSWRBookmarkInfo = (pageId: string | null | undefined, isOpen = f
       return {
         sumOfBookmarks: response.data.sumOfBookmarks,
         isBookmarked: response.data.isBookmarked,
+        bookmarkedUsers: response.data.bookmarkedUsers,
       };
     }),
   );

+ 0 - 21
packages/app/src/stores/bookmarks.tsx

@@ -1,21 +0,0 @@
-import useSWR, { SWRResponse } from 'swr';
-
-import { Types } from 'mongoose';
-import { apiv3Get } from '~/client/util/apiv3-client';
-
-import { IBookmarksInfo } from '~/interfaces/bookmarks';
-
-
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const useSWRxBookmarksInfo = <Data, Error>(pageId: Types.ObjectId):SWRResponse<IBookmarksInfo, Error> => {
-  return useSWR(
-    ['/bookmarks/info', pageId],
-    (endpoint, pageId) => apiv3Get(endpoint, { pageId }).then((response) => {
-      return {
-        isBookmarked: response.data.isBookmarked,
-        sumOfBookmarks: response.data.sumOfBookmarks,
-        bookmarkedUsers: response.data.bookmarkedUsers,
-      };
-    }),
-  );
-};

+ 14 - 11
packages/app/src/styles/_page-tree.scss

@@ -5,7 +5,7 @@ $grw-pagetree-item-padding-left: 10px;
 .grw-pagetree {
   min-height: calc(100vh - ($grw-navbar-height + $grw-navbar-border-width + $grw-sidebar-content-header-height + $grw-sidebar-content-footer-height));
 
-  .grw-pagetree-item {
+  .list-group-item {
     &:hover {
       .grw-pagetree-control {
         display: flex !important;
@@ -47,54 +47,57 @@ $grw-pagetree-item-padding-left: 10px;
 
   // To realize a hierarchical structure, set multiplied padding-left to each pagetree-item
   > .grw-pagetree-item-container {
+    > .list-group-item {
+      padding-left: 0;
+    }
     > .grw-pagetree-item-children {
       > .grw-pagetree-item-container {
-        > .grw-pagetree-item {
+        > .list-group-item {
           padding-left: $grw-pagetree-item-padding-left;
         }
         > .grw-pagetree-item-children {
           > .grw-pagetree-item-container {
-            > .grw-pagetree-item {
+            > .list-group-item {
               padding-left: $grw-pagetree-item-padding-left * 2;
             }
             > .grw-pagetree-item-children {
               > .grw-pagetree-item-container {
-                > .grw-pagetree-item {
+                > .list-group-item {
                   padding-left: $grw-pagetree-item-padding-left * 3;
                 }
                 > .grw-pagetree-item-children {
                   > .grw-pagetree-item-container {
-                    > .grw-pagetree-item {
+                    > .list-group-item {
                       padding-left: $grw-pagetree-item-padding-left * 4;
                     }
                     > .grw-pagetree-item-children {
                       > .grw-pagetree-item-container {
-                        > .grw-pagetree-item {
+                        > .list-group-item {
                           padding-left: $grw-pagetree-item-padding-left * 5;
                         }
                         > .grw-pagetree-item-children {
                           > .grw-pagetree-item-container {
-                            > .grw-pagetree-item {
+                            > .list-group-item {
                               padding-left: $grw-pagetree-item-padding-left * 6;
                             }
                             > .grw-pagetree-item-children {
                               > .grw-pagetree-item-container {
-                                > .grw-pagetree-item {
+                                > .list-group-item {
                                   padding-left: $grw-pagetree-item-padding-left * 7;
                                 }
                                 > .grw-pagetree-item-children {
                                   > .grw-pagetree-item-container {
-                                    > .grw-pagetree-item {
+                                    > .list-group-item {
                                       padding-left: $grw-pagetree-item-padding-left * 8;
                                     }
                                     > .grw-pagetree-item-children {
                                       > .grw-pagetree-item-container {
-                                        > .grw-pagetree-item {
+                                        > .list-group-item {
                                           padding-left: $grw-pagetree-item-padding-left * 9;
                                         }
                                         .grw-pagetree-item-children {
                                           > .grw-pagetree-item-container {
-                                            > .grw-pagetree-item {
+                                            > .list-group-item {
                                               padding-left: $grw-pagetree-item-padding-left * 10;
                                             }
                                           }

+ 10 - 27
packages/app/src/styles/theme/_apply-colors-dark.scss

@@ -2,7 +2,7 @@
 $color-list: $color-global !default;
 $bgcolor-list: $bgcolor-global !default;
 $color-list-hover: $color-global !default;
-$bgcolor-list-hover: lighten($bgcolor-global, 3%) !default;
+$bgcolor-list-hover: lighten($bgcolor-global, 8%) !default;
 $color-list-active: $color-reversal !default;
 $bgcolor-list-active: $primary !default;
 $bgcolor-subnav: lighten($bgcolor-global, 3%) !default;
@@ -257,32 +257,15 @@ ul.pagination {
 
   // Pagetree
   .grw-pagetree {
-    .grw-pagetree-is-over {
-      background: $bgcolor-list-hover;
-    }
-    .grw-pagetree-item {
-      &.grw-pagetree-is-target {
-        background: $bgcolor-list-hover;
-      }
-
-      .grw-pagetree-count {
-        background: $bgcolor-sidebar-list-group;
-      }
-
-      .grw-pagetree-button {
-        &:not(:hover) {
-          svg {
-            fill: $gray-500;
-          }
-        }
-      }
-      &:hover {
-        background: $bgcolor-list-hover;
-      }
-      &:active {
-        background: lighten($bgcolor-list-hover, 5%);
-      }
-    }
+    @include override-list-group-item-for-pagetree(
+      $color-list,
+      $bgcolor-sidebar-list-group,
+      $color-list-hover,
+      $bgcolor-list-hover,
+      $color-list-active,
+      lighten($bgcolor-list-hover, 5%),
+      $gray-500
+    );
   }
 }
 

+ 9 - 26
packages/app/src/styles/theme/_apply-colors-light.scss

@@ -170,32 +170,15 @@ $border-color: $border-color-global;
 
   // Pagetree
   .grw-pagetree {
-    .grw-pagetree-is-over {
-      background: $bgcolor-list-hover;
-    }
-    .grw-pagetree-item {
-      &.grw-pagetree-is-target {
-        background: $bgcolor-list-hover;
-      }
-
-      .grw-pagetree-count {
-        background: $bgcolor-sidebar-list-group;
-      }
-
-      .grw-pagetree-button {
-        &:not(:hover) {
-          svg {
-            fill: $gray-400;
-          }
-        }
-      }
-      &:hover {
-        background: $bgcolor-list-hover;
-      }
-      &:active {
-        background: $bgcolor-list-active;
-      }
-    }
+    @include override-list-group-item-for-pagetree(
+      $color-list,
+      $bgcolor-sidebar-list-group,
+      $color-list-hover,
+      $bgcolor-list-hover,
+      $color-list-active,
+      $bgcolor-list-active,
+      $gray-400
+    );
   }
 }
 

+ 1 - 1
packages/app/src/styles/theme/_apply-colors.scss

@@ -311,7 +311,7 @@ ul.pagination {
   }
 
   .grw-pagetree {
-    .grw-pagetree-item {
+    .list-group-item {
       .grw-pagetree-title-anchor {
         color: inherit;
       }

+ 19 - 1
packages/app/src/styles/theme/christmas.scss

@@ -62,7 +62,7 @@ html[dark] {
   // $color-list: $color-global;
   $bgcolor-list: transparent;
   // $color-list-hover: $color-reversal;
-  $color-list-active: white;
+  $color-list-active: $themelight;
   $bgcolor-list-active: $themecolor;
 
   // Navbar
@@ -181,4 +181,22 @@ html[dark] {
       @include btn-page-editor-mode-manager(darken($subthemecolor, 15%), lighten($subthemecolor, 35%), lighten($subthemecolor, 45%));
     }
   }
+
+  /*
+ * GROWI Sidebar
+ */
+  .grw-sidebar {
+    // Pagetree
+    .grw-pagetree {
+      @include override-list-group-item-for-pagetree(
+        $color-list,
+        $bgcolor-sidebar-list-group,
+        $color-list-hover,
+        $bgcolor-list-hover,
+        $color-list-active,
+        $bgcolor-list-hover,
+        $gray-400
+      );
+    }
+  }
 }

+ 18 - 0
packages/app/src/styles/theme/future.scss

@@ -109,4 +109,22 @@ html[dark] {
     color: #95abba;
     background-color: #1f1f22;
   }
+
+  /*
+ * GROWI Sidebar
+ */
+  .grw-sidebar {
+    // Pagetree
+    .grw-pagetree {
+      @include override-list-group-item-for-pagetree(
+        $color-list,
+        $bgcolor-sidebar-list-group,
+        $color-list-hover,
+        $bgcolor-list-hover,
+        $color-list-active,
+        lighten($bgcolor-list-hover, 5%),
+        $gray-600
+      );
+    }
+  }
 }

+ 20 - 2
packages/app/src/styles/theme/island.scss

@@ -29,9 +29,9 @@ html[dark] {
   // $color-list: $color-global;
   // $bgcolor-list: lighten($color-themelight, 10%);
   // $color-list-hover: ;
-  // $bgcolor-list-hover: ;
+  $bgcolor-list-hover: $color-themelight;
   $color-list-active: $color-global;
-  // $bgcolor-list-active: $primary;
+  $bgcolor-list-active: $primary;
 
   // Table colors
   // $color-table: #; // optional
@@ -118,4 +118,22 @@ html[dark] {
       color: $color-reversal;
     }
   }
+
+  /*
+   * GROWI Sidebar
+  */
+  .grw-sidebar {
+    // Pagetree
+    .grw-pagetree {
+      @include override-list-group-item-for-pagetree(
+        $color-list,
+        $bgcolor-sidebar-list-group,
+        $color-list-hover,
+        $bgcolor-list-hover,
+        $color-list-active,
+        lighten($bgcolor-list-hover, 5%),
+        $gray-400
+      );
+    }
+  }
 }

+ 18 - 0
packages/app/src/styles/theme/kibela.scss

@@ -109,4 +109,22 @@ html[dark] {
       @include btn-page-editor-mode-manager(darken($primary, 15%), lighten($primary, 45%), lighten($primary, 50%));
     }
   }
+
+  /*
+ * GROWI Sidebar
+ */
+  .grw-sidebar {
+    // Pagetree
+    .grw-pagetree {
+      @include override-list-group-item-for-pagetree(
+        $color-list,
+        $bgcolor-sidebar-list-group,
+        $color-list-hover,
+        $bgcolor-list-hover,
+        $color-list-active,
+        lighten($bgcolor-list-active, 55%),
+        $gray-400
+      );
+    }
+  }
 }

+ 42 - 0
packages/app/src/styles/theme/mixins/_list-group.scss

@@ -17,3 +17,45 @@
     }
   }
 }
+
+@mixin override-list-group-item-for-pagetree(
+  $color,
+  $bgcolor,
+  $color-hover: $color,
+  $bgcolor-hover: $bgcolor,
+  $color-active: $color,
+  $bgcolor-active: $bgcolor,
+  $button-color
+) {
+  .grw-pagetree-is-over {
+    background: $bgcolor-hover;
+  }
+  .list-group-item {
+    color: $color;
+    background-color: transparent;
+    border-color: $border-color-global;
+
+    &.grw-pagetree-is-target {
+      background: $bgcolor-active;
+    }
+    .grw-pagetree-count {
+      background: $bgcolor;
+    }
+    .grw-pagetree-button {
+      &:not(:hover) {
+        svg {
+          fill: $button-color;
+        }
+      }
+    }
+
+    &.list-group-item-action {
+      &:hover {
+        background-color: $bgcolor-hover;
+      }
+      &:active {
+        background-color: $bgcolor-active;
+      }
+    }
+  }
+}

+ 1 - 1
packages/app/src/styles/theme/mono-blue.scss

@@ -32,7 +32,7 @@ html[light] {
   // $color-list: $color-global;
   $bgcolor-list: transparent;
   $color-list-hover: $color-search;
-  $bgcolor-list-hover: darken($bgcolor-global, 3%);
+  $bgcolor-list-hover: lighten($primary, 70%);
   // $color-list-active: $color-reversal;
   // $bgcolor-list-active: $primary;
 

+ 3 - 3
packages/app/src/styles/theme/wood.scss

@@ -66,9 +66,9 @@ html[dark] {
   // $color-list: $color-global;
   $bgcolor-list: transparent;
   $color-list-hover: $gray-100;
-  $bgcolor-list-hover: darken($bgcolor-global, 3%);
+  $bgcolor-list-hover: lighten($primary, 40%);
   // $color-list-active: $color-reversal;
-  // $bgcolor-list-active: $primary;
+  $bgcolor-list-active: lighten($primary, 30%);
 
   // Table colors
   // $color-table: #; // optional
@@ -88,7 +88,7 @@ html[dark] {
   $color-editor-icons: $color-global;
 
   // Sidebar
-  $bgcolor-sidebar: transparent;
+  $bgcolor-sidebar: $themecolor;
   // Sidebar contents
   $color-sidebar-context: #9d7406;
   $bgcolor-sidebar-context: transparent;