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

Merge pull request #4854 from weseek/fix/pt-for-guest-user

fix: Pt for guest user
Yuki Takei 4 лет назад
Родитель
Сommit
fe39c43d37

+ 38 - 20
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -6,13 +6,14 @@ import { useTranslation } from 'react-i18next';
 import { IPageHasId } from '~/interfaces/page';
 
 type PageItemControlProps = {
-  page: Partial<IPageHasId>,
-  onClickDeleteButton?: (pageId: string) => void,
+  page: Partial<IPageHasId>
+  isEnableActions: boolean
+  onClickDeleteButton?: (pageId: string) => void
 }
 
 const PageItemControl: FC<PageItemControlProps> = (props: PageItemControlProps) => {
 
-  const { page, onClickDeleteButton } = props;
+  const { page, isEnableActions, onClickDeleteButton } = props;
   const { t } = useTranslation('');
 
   const deleteButtonHandler = () => {
@@ -48,23 +49,40 @@ const PageItemControl: FC<PageItemControlProps> = (props: PageItemControlProps)
           TODO: add function to the following buttons like using modal or others
           ref: https://estoc.weseek.co.jp/redmine/issues/79026
         */}
-        <button className="dropdown-item" type="button" onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
-          <i className="icon-fw icon-star"></i>
-          {t('Add to bookmark')}
-        </button>
-        <button className="dropdown-item" type="button" onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
-          <i className="icon-fw icon-docs"></i>
-          {t('Duplicate')}
-        </button>
-        <button className="dropdown-item" type="button" onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
-          <i className="icon-fw  icon-action-redo"></i>
-          {t('Move/Rename')}
-        </button>
-        <div className="dropdown-divider"></div>
-        <button className="dropdown-item text-danger pt-2" type="button" onClick={deleteButtonHandler}>
-          <i className="icon-fw icon-trash"></i>
-          {t('Delete')}
-        </button>
+
+        {/* TODO: show dropdown when permalink section is implemented */}
+        {!isEnableActions && (
+          <p className="dropdown-item">
+            {t('search_result.currently_not_implemented')}
+          </p>
+        )}
+        {isEnableActions && (
+          <button className="dropdown-item" type="button" onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
+            <i className="icon-fw icon-star"></i>
+            {t('Add to bookmark')}
+          </button>
+        )}
+        {isEnableActions && (
+          <button className="dropdown-item" type="button" onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
+            <i className="icon-fw icon-docs"></i>
+            {t('Duplicate')}
+          </button>
+        )}
+        {isEnableActions && (
+          <button className="dropdown-item" type="button" onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
+            <i className="icon-fw  icon-action-redo"></i>
+            {t('Move/Rename')}
+          </button>
+        )}
+        {isEnableActions && (
+          <>
+            <div className="dropdown-divider"></div>
+            <button className="dropdown-item text-danger pt-2" type="button" onClick={deleteButtonHandler}>
+              <i className="icon-fw icon-trash"></i>
+              {t('Delete')}
+            </button>
+          </>
+        )}
       </div>
     </>
   );

+ 18 - 2
packages/app/src/components/SearchPage.jsx

@@ -13,6 +13,7 @@ import SearchResultList from './SearchPage/SearchResultList';
 import SearchControl from './SearchPage/SearchControl';
 import { CheckboxType, SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
 import PageDeleteModal from './PageDeleteModal';
+import { useIsGuestUser } from '~/stores/context';
 
 export const specificPathNames = {
   user: '/user',
@@ -283,6 +284,7 @@ class SearchPage extends React.Component {
     return (
       <SearchResultList
         pages={this.state.searchResults || []}
+        isEnableActions={!this.props.isGuestUser}
         focusedSearchResultData={this.state.focusedSearchResultData}
         selectedPagesIdList={this.state.selectedPagesIdList || []}
         searchResultCount={this.state.searchResultCount}
@@ -345,16 +347,30 @@ class SearchPage extends React.Component {
 /**
  * Wrapper component for using unstated
  */
-const SearchPageWrapper = withUnstatedContainers(SearchPage, [AppContainer]);
+const SearchPageHOCWrapper = withTranslation()(withUnstatedContainers(SearchPage, [AppContainer]));
 
 SearchPage.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   query: PropTypes.object,
+  isGuestUser: PropTypes.bool.isRequired,
 };
 SearchPage.defaultProps = {
   // pollInterval: 1000,
   query: SearchPage.getQueryByLocation(window.location || {}),
 };
 
-export default withTranslation()(SearchPageWrapper);
+const SearchPageFCWrapper = (props) => {
+  const { data: isGuestUser } = useIsGuestUser();
+
+  /*
+   * dependencies
+   */
+  if (isGuestUser == null) {
+    return null;
+  }
+
+  return <SearchPageHOCWrapper {...props} isGuestUser={isGuestUser} />;
+};
+
+export default SearchPageFCWrapper;

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

@@ -7,6 +7,7 @@ import { IPageSearchResultData } from '../../interfaces/search';
 type Props = {
   pages: IPageSearchResultData[],
   selectedPagesIdList: Set<string>
+  isEnableActions: boolean,
   searchResultCount?: number,
   activePage?: number,
   pagingLimit?: number,
@@ -19,7 +20,7 @@ type Props = {
 }
 
 const SearchResultList: FC<Props> = (props:Props) => {
-  const { focusedSearchResultData, selectedPagesIdList } = props;
+  const { focusedSearchResultData, selectedPagesIdList, isEnableActions } = props;
 
   const focusedPageId = (focusedSearchResultData != null && focusedSearchResultData.pageData != null) ? focusedSearchResultData.pageData._id : '';
   return (
@@ -31,6 +32,7 @@ const SearchResultList: FC<Props> = (props:Props) => {
           <SearchResultListItem
             key={page.pageData._id}
             page={page}
+            isEnableActions={isEnableActions}
             onClickSearchResultItem={props.onClickSearchResultItem}
             onClickCheckbox={props.onClickCheckbox}
             isChecked={isChecked}

+ 3 - 2
packages/app/src/components/SearchPage/SearchResultListItem.tsx

@@ -13,6 +13,7 @@ type Props = {
   page: IPageSearchResultData,
   isSelected: boolean,
   isChecked: boolean,
+  isEnableActions: boolean,
   onClickCheckbox?: (pageId: string) => void,
   onClickSearchResultItem?: (pageId: string) => void,
   onClickDeleteButton?: (pageId: string) => void,
@@ -21,7 +22,7 @@ type Props = {
 const SearchResultListItem: FC<Props> = (props:Props) => {
   const {
     // todo: refactoring variable name to clear what changed
-    page: { pageData, pageMeta }, isSelected, onClickSearchResultItem, onClickCheckbox, isChecked,
+    page: { pageData, pageMeta }, isSelected, onClickSearchResultItem, onClickCheckbox, isChecked, isEnableActions,
   } = props;
 
   // Add prefix 'id_' in pageId, because scrollspy of bootstrap doesn't work when the first letter of id attr of target component is numeral.
@@ -77,7 +78,7 @@ const SearchResultListItem: FC<Props> = (props:Props) => {
               </div>
               {/* doropdown icon includes page control buttons */}
               <div className="ml-auto">
-                <PageItemControl page={pageData} onClickDeleteButton={props.onClickDeleteButton} />
+                <PageItemControl page={pageData} onClickDeleteButton={props.onClickDeleteButton} isEnableActions={isEnableActions} />
               </div>
             </div>
             <div className="my-2">

+ 14 - 3
packages/app/src/components/Sidebar/PageTree.tsx

@@ -2,7 +2,9 @@ import React, { FC, memo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
-import { useCurrentPagePath, useCurrentPageId, useTargetAndAncestors } from '~/stores/context';
+import {
+  useCurrentPagePath, useCurrentPageId, useTargetAndAncestors, useIsGuestUser,
+} from '~/stores/context';
 
 import ItemsTree from './PageTree/ItemsTree';
 import PrivateLegacyPages from './PageTree/PrivateLegacyPages';
@@ -12,16 +14,24 @@ import { IPageForPageDeleteModal } from '../PageDeleteModal';
 const PageTree: FC = memo(() => {
   const { t } = useTranslation();
 
+  const { data: isGuestUser } = useIsGuestUser();
   const { data: currentPath } = useCurrentPagePath();
   const { data: targetId } = useCurrentPageId();
   const { data: targetAndAncestorsData } = useTargetAndAncestors();
 
-  const { data: migrationStatus } = useSWRxV5MigrationStatus();
+  const { data: migrationStatus } = useSWRxV5MigrationStatus(!isGuestUser);
 
   // for delete modal
   const [isDeleteModalOpen, setDeleteModalOpen] = useState(false);
   const [pagesToDelete, setPagesToDelete] = useState<IPageForPageDeleteModal[]>([]);
 
+  /*
+   * dependencies
+   */
+  if (isGuestUser == null) {
+    return null;
+  }
+
   const onClickDeleteByPage = (page: IPageForPageDeleteModal) => {
     setDeleteModalOpen(true);
     setPagesToDelete([page]);
@@ -41,6 +51,7 @@ const PageTree: FC = memo(() => {
 
       <div className="grw-sidebar-content-body">
         <ItemsTree
+          isEnableActions={!isGuestUser}
           targetPath={path}
           targetId={targetId}
           targetAndAncestorsData={targetAndAncestorsData}
@@ -55,7 +66,7 @@ const PageTree: FC = memo(() => {
 
       <div className="grw-sidebar-content-footer">
         {
-          migrationStatus?.migratablePagesCount != null && migrationStatus.migratablePagesCount !== 0 && (
+          !isGuestUser && migrationStatus?.migratablePagesCount != null && migrationStatus.migratablePagesCount !== 0 && (
             <PrivateLegacyPages />
           )
         }

+ 15 - 9
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -15,6 +15,7 @@ import TriangleIcon from '~/components/Icons/TriangleIcon';
 
 
 interface ItemProps {
+  isEnableActions: boolean
   itemNode: ItemNode
   targetId?: string
   isOpen?: boolean
@@ -37,6 +38,7 @@ const markTarget = (children: ItemNode[], targetId?: string): void => {
 
 type ItemControlProps = {
   page: Partial<IPageHasId>
+  isEnableActions: boolean
   onClickDeleteButtonHandler?(): void
   onClickPlusButtonHandler?(): void
 }
@@ -64,7 +66,7 @@ const ItemControl: FC<ItemControlProps> = memo((props: ItemControlProps) => {
 
   return (
     <>
-      <PageItemControl page={props.page} onClickDeleteButton={onClickDeleteButton} />
+      <PageItemControl page={props.page} onClickDeleteButton={onClickDeleteButton} isEnableActions={props.isEnableActions} />
       <button
         type="button"
         className="btn-link nav-link border-0 rounded grw-btn-page-management py-0"
@@ -89,7 +91,7 @@ const ItemCount: FC = () => {
 const Item: FC<ItemProps> = (props: ItemProps) => {
   const { t } = useTranslation();
   const {
-    itemNode, targetId, isOpen: _isOpen = false, onClickDeleteByPage,
+    itemNode, targetId, isOpen: _isOpen = false, onClickDeleteByPage, isEnableActions,
   } = props;
 
   const { page, children } = itemNode;
@@ -200,21 +202,25 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
             page={page}
             onClickDeleteButtonHandler={onClickDeleteButtonHandler}
             onClickPlusButtonHandler={() => { setNewPageInputShown(true) }}
+            isEnableActions={isEnableActions}
           />
         </div>
       </div>
 
-      <ClosableTextInput
-        isShown={isNewPageInputShown}
-        placeholder={t('Input title')}
-        onClickOutside={() => { setNewPageInputShown(false) }}
-        onPressEnter={onPressEnterHandler}
-        inputValidator={inputValidator}
-      />
+      {!isEnableActions && (
+        <ClosableTextInput
+          isShown={isNewPageInputShown}
+          placeholder={t('Input title')}
+          onClickOutside={() => { setNewPageInputShown(false) }}
+          onPressEnter={onPressEnterHandler}
+          inputValidator={inputValidator}
+        />
+      )}
       {
         isOpen && hasChildren() && currentChildren.map(node => (
           <Item
             key={node.page._id}
+            isEnableActions={isEnableActions}
             itemNode={node}
             isOpen={false}
             onClickDeleteByPage={onClickDeleteByPage}

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

@@ -43,6 +43,7 @@ const generateInitialNodeAfterResponse = (ancestorsChildren: Record<string, Part
 };
 
 type ItemsTreeProps = {
+  isEnableActions: boolean
   targetPath: string
   targetId?: string
   targetAndAncestorsData?: TargetAndAncestors
@@ -57,11 +58,18 @@ type ItemsTreeProps = {
 }
 
 const renderByInitialNode = (
-    initialNode: ItemNode, DeleteModal: JSX.Element, targetId?: string, onClickDeleteByPage?: (page: IPageForPageDeleteModal) => void,
+    initialNode: ItemNode, DeleteModal: JSX.Element, isEnableActions: boolean, targetId?: string, onClickDeleteByPage?: (page: IPageForPageDeleteModal) => void,
 ): JSX.Element => {
   return (
     <div className="grw-pagetree p-3">
-      <Item key={initialNode.page.path} targetId={targetId} itemNode={initialNode} isOpen onClickDeleteByPage={onClickDeleteByPage} />
+      <Item
+        key={initialNode.page.path}
+        targetId={targetId}
+        itemNode={initialNode}
+        isOpen
+        isEnableActions={isEnableActions}
+        onClickDeleteByPage={onClickDeleteByPage}
+      />
       {DeleteModal}
     </div>
   );
@@ -74,7 +82,7 @@ const renderByInitialNode = (
 const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
   const {
     targetPath, targetId, targetAndAncestorsData, isDeleteModalOpen, pagesToDelete, isAbleToDeleteCompletely, isDeleteCompletelyModal, onCloseDelete,
-    onClickDeleteByPage,
+    onClickDeleteByPage, isEnableActions,
   } = props;
 
   const { data: ancestorsChildrenData, error: error1 } = useSWRxPageAncestorsChildren(targetPath);
@@ -101,7 +109,7 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
    */
   if (ancestorsChildrenData != null && rootPageData != null) {
     const initialNode = generateInitialNodeAfterResponse(ancestorsChildrenData.ancestorsChildren, new ItemNode(rootPageData.rootPage));
-    return renderByInitialNode(initialNode, DeleteModal, targetId, onClickDeleteByPage);
+    return renderByInitialNode(initialNode, DeleteModal, isEnableActions, targetId, onClickDeleteByPage);
   }
 
   /*
@@ -109,7 +117,7 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
    */
   if (targetAndAncestorsData != null) {
     const initialNode = generateInitialNodeBeforeResponse(targetAndAncestorsData.targetAndAncestors);
-    return renderByInitialNode(initialNode, DeleteModal, targetId, onClickDeleteByPage);
+    return renderByInitialNode(initialNode, DeleteModal, isEnableActions, targetId, onClickDeleteByPage);
   }
 
   return null;

+ 4 - 5
packages/app/src/server/routes/apiv3/page-listing.ts

@@ -34,14 +34,13 @@ const validator = {
  */
 export default (crowi: Crowi): Router => {
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
-  // Do not use loginRequired with isGuestAllowed true since page tree may show private page titles
-  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
+  const loginRequired = require('../../middlewares/login-required')(crowi, true);
   const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
 
   const router = express.Router();
 
 
-  router.get('/root', accessTokenParser, loginRequiredStrictly, async(req: AuthorizedRequest, res: ApiV3Response) => {
+  router.get('/root', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
     const Page: PageModel = crowi.model('Page');
 
     let rootPage;
@@ -56,7 +55,7 @@ export default (crowi: Crowi): Router => {
   });
 
   // eslint-disable-next-line max-len
-  router.get('/ancestors-children', accessTokenParser, loginRequiredStrictly, ...validator.pagePathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response): Promise<any> => {
+  router.get('/ancestors-children', accessTokenParser, loginRequired, ...validator.pagePathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response): Promise<any> => {
     const { path } = req.query;
 
     const Page: PageModel = crowi.model('Page');
@@ -76,7 +75,7 @@ export default (crowi: Crowi): Router => {
    * In most cases, using id should be prioritized
    */
   // eslint-disable-next-line max-len
-  router.get('/children', accessTokenParser, loginRequiredStrictly, validator.pageIdOrPathRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
+  router.get('/children', accessTokenParser, loginRequired, validator.pageIdOrPathRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
     const { id, path } = req.query;
 
     const Page: PageModel = crowi.model('Page');

+ 4 - 2
packages/app/src/stores/page-listing.tsx

@@ -45,9 +45,11 @@ export const useSWRxPageChildren = (
   );
 };
 
-export const useSWRxV5MigrationStatus = (): SWRResponse<V5MigrationStatus, Error> => {
+export const useSWRxV5MigrationStatus = (
+    shouldFetch = true,
+): SWRResponse<V5MigrationStatus, Error> => {
   return useSWR(
-    '/pages/v5-migration-status',
+    shouldFetch ? '/pages/v5-migration-status' : null,
     endpoint => apiv3Get(endpoint).then((response) => {
       return {
         migratablePagesCount: response.data.migratablePagesCount,