2
0
Эх сурвалжийг харах

Merge branch 'dev/5.0.x' into imprv/81836-86485-duplicateAlert

* dev/5.0.x: (34 commits)
  change file name
  remove unnecessary props
  modify duplicate-page
  add attributes for ContextExtractor
  show duplicate page
  cleanup
  fix lint
  useCallback
  cleanup & add todo
  fix name
  rename
  create onClickPlusButton func
  rename func
  remove marigin
  rename func
  update placeholder
  cleanup
  clicking on anything other than input will close input
  rename state
  creating  rename operation
  ...
Mao 4 жил өмнө
parent
commit
4b8976a63c

+ 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 my-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>

+ 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>
     </>
   );
 };

+ 64 - 22
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);
@@ -228,17 +258,29 @@ 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)}
           />
@@ -250,7 +292,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
           isShown={isNewPageInputShown}
           placeholder={t('Input page name')}
           onClickOutside={() => { setNewPageInputShown(false) }}
-          onPressEnter={onPressEnterHandler}
+          onPressEnter={onPressEnterForCreateHandler}
           inputValidator={inputValidator}
         />
       )}

+ 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) {

+ 6 - 7
packages/app/src/server/service/page.js

@@ -891,7 +891,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] },
                   },
                 },
               ],
@@ -1164,15 +1166,12 @@ class PageService {
 
           // modify to adjust for RegExp
           let parentPath = parent.path === '/' ? '' : parent.path;
-          // inject \ before brackets
-          ['(', ')', '[', ']', '{', '}'].forEach((bracket) => {
-            parentPath = parentPath.replace(bracket, `\\${bracket}`);
-          });
+          parentPath = escapeStringRegexp(parentPath);
 
           const filter = {
             // regexr.com/6889f
             // ex. /parent/any_child OR /any_level1
-            path: { $regex: new RegExp(`^${parentPath}(\\/[^/]+)\\/?$`, 'g') },
+            path: { $regex: new RegExp(`^${parentPath}(\\/[^/]+)\\/?$`, 'i') },
           };
           if (grant != null) {
             filter.grant = grant;
@@ -1199,7 +1198,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,
-      };
-    }),
-  );
-};