Browse Source

Merge pull request #4594 from weseek/feat/77524-80481-SubNavButtons-refactor

feat/#80481 SubNavButton refactor like button
Yuki Takei 4 years ago
parent
commit
5b98a73e7d

+ 0 - 16
packages/app/src/client/services/PageContainer.js

@@ -292,22 +292,6 @@ export default class PageContainer extends Container {
     await this.retrieveLikersAndSeenUsers();
   }
 
-  async toggleLike() {
-    {
-      const toggledIsLiked = !this.state.isLiked;
-      await this.appContainer.apiv3Put('/page/likes', { pageId: this.state.pageId, bool: toggledIsLiked });
-
-      await this.setState(state => ({
-        isLiked: toggledIsLiked,
-        sumOfLikers: toggledIsLiked ? state.sumOfLikers + 1 : state.sumOfLikers - 1,
-        likerIds: toggledIsLiked
-          ? [...this.state.likerIds, this.appContainer.currentUserId]
-          : state.likerIds.filter(id => id !== this.appContainer.currentUserId),
-      }));
-    }
-
-    await this.retrieveLikersAndSeenUsers();
-  }
 
   async retrieveLikersAndSeenUsers() {
     const { users } = await this.appContainer.apiGet('/users.list', { user_ids: [...this.state.likerIds, ...this.state.seenUserIds].join(',') });

+ 22 - 25
packages/app/src/components/LikeButtons.jsx

@@ -6,11 +6,11 @@ import { withTranslation } from 'react-i18next';
 import UserPictureList from './User/UserPictureList';
 import { withUnstatedContainers } from './UnstatedUtils';
 
-import { toastError } from '~/client/util/apiNotification';
 import AppContainer from '~/client/services/AppContainer';
-import PageContainer from '~/client/services/PageContainer';
 
-class LikeButtons extends React.Component {
+// TODO : user image not displayed in search page. Fix it.
+// task : https://estoc.weseek.co.jp/redmine/issues/81110
+class LegacyLikeButtons extends React.Component {
 
   constructor(props) {
     super(props);
@@ -30,28 +30,19 @@ class LikeButtons extends React.Component {
     }));
   }
 
-  async handleClick() {
-    const { appContainer, pageContainer } = this.props;
-    const { isGuestUser } = appContainer;
 
-    if (isGuestUser) {
+  handleClick() {
+    if (this.props.onLikeClicked == null) {
       return;
     }
-
-    try {
-      pageContainer.toggleLike();
-    }
-    catch (err) {
-      toastError(err);
-    }
+    this.props.onLikeClicked();
   }
 
   render() {
-    const { appContainer, pageContainer, t } = this.props;
-    const { isGuestUser } = appContainer;
     const {
-      state: { likers, sumOfLikers, isLiked },
-    } = pageContainer;
+      appContainer, likerIds, sumOfLikers, isLiked, t,
+    } = this.props;
+    const { isGuestUser } = appContainer;
 
     return (
       <div className="btn-group" role="group" aria-label="Like buttons">
@@ -76,7 +67,7 @@ class LikeButtons extends React.Component {
         <Popover placement="bottom" isOpen={this.state.isPopoverOpen} target="po-total-likes" toggle={this.togglePopover} trigger="legacy">
           <PopoverBody className="seen-user-popover">
             <div className="px-2 text-right user-list-content text-truncate text-muted">
-              {likers.length ? <UserPictureList users={likers} /> : t('No users have liked this yet.')}
+              {likerIds.length ? <UserPictureList users={likerIds} /> : t('No users have liked this yet.')}
             </div>
           </PopoverBody>
         </Popover>
@@ -89,14 +80,20 @@ class LikeButtons extends React.Component {
 /**
  * Wrapper component for using unstated
  */
-const LikeButtonsWrapper = withUnstatedContainers(LikeButtons, [AppContainer, PageContainer]);
+const LegacyLikeButtonsWrapper = withUnstatedContainers(LegacyLikeButtons, [AppContainer]);
 
-LikeButtons.propTypes = {
+LegacyLikeButtons.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-
+  onChangeInvoked: PropTypes.func,
+  pageId: PropTypes.string.isRequired,
+  likerIds: PropTypes.arrayOf(PropTypes.object).isRequired,
+  sumOfLikers: PropTypes.number.isRequired,
+  isLiked: PropTypes.bool.isRequired,
+  onLikeClicked: PropTypes.func,
   t: PropTypes.func.isRequired,
-  size: PropTypes.string,
+};
+const LikeButtons = (props) => {
+  return <LegacyLikeButtonsWrapper {...props}></LegacyLikeButtonsWrapper>;
 };
 
-export default withTranslation()(LikeButtonsWrapper);
+export default withTranslation()(LikeButtons);

+ 2 - 2
packages/app/src/components/Navbar/GrowiSubNavigation.jsx

@@ -7,7 +7,7 @@ import NavigationContainer from '~/client/services/NavigationContainer';
 import PageContainer from '~/client/services/PageContainer';
 
 import TagLabels from '../Page/TagLabels';
-import SubnavButtons from './SubNavButtons';
+import SubNavButtons from './SubNavButtons';
 import PageEditorModeManager from './PageEditorModeManager';
 
 import AuthorInfo from './AuthorInfo';
@@ -59,7 +59,7 @@ const GrowiSubNavigation = (props) => {
 
         <div className="d-flex flex-column align-items-end">
           <div className="d-flex">
-            <SubnavButtons isCompactMode={isCompactMode} />
+            <SubNavButtons isCompactMode={isCompactMode} pageId={pageId} />
           </div>
           <div className="mt-2">
             {pageContainer.isAbleToShowPageEditorModeManager && (

+ 0 - 80
packages/app/src/components/Navbar/SubNavButtons.jsx

@@ -1,80 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import AppContainer from '~/client/services/AppContainer';
-import NavigationContainer from '~/client/services/NavigationContainer';
-import PageContainer from '~/client/services/PageContainer';
-import { withUnstatedContainers } from '../UnstatedUtils';
-import loggerFactory from '~/utils/logger';
-
-import BookmarkButton from '../BookmarkButton';
-import LikeButtons from '../LikeButtons';
-import PageManagement from '../Page/PageManagement';
-
-const logger = loggerFactory('growi:SubnavButtons');
-
-const SubnavButtons = (props) => {
-  const {
-    appContainer, navigationContainer, pageContainer, isCompactMode,
-  } = props;
-
-  /* eslint-enable react/prop-types */
-
-  /* eslint-disable react/prop-types */
-  const PageReactionButtons = ({ pageContainer }) => {
-    const { pageId, isBookmarked, sumOfBookmarks } = pageContainer.state;
-
-    const onChangeInvoked = () => {
-      if (pageContainer.retrieveBookmarkInfo == null) { logger.error('retrieveBookmarkInfo is null') }
-      else { pageContainer.retrieveBookmarkInfo() }
-    };
-
-    return (
-      <>
-        {pageContainer.isAbleToShowLikeButtons && (
-          <span>
-            <LikeButtons />
-          </span>
-        )}
-        <span>
-          <BookmarkButton
-            pageId={pageId}
-            isBookmarked={isBookmarked}
-            sumOfBookmarks={sumOfBookmarks}
-            onChangeInvoked={onChangeInvoked}
-          />
-        </span>
-      </>
-    );
-  };
-  /* eslint-enable react/prop-types */
-
-  const { editorMode } = navigationContainer.state;
-  const isViewMode = editorMode === 'view';
-
-  return (
-    <>
-      {isViewMode && (
-        <>
-          {pageContainer.isAbleToShowPageReactionButtons && <PageReactionButtons appContainer={appContainer} pageContainer={pageContainer} />}
-          {pageContainer.isAbleToShowPageManagement && <PageManagement isCompactMode={isCompactMode} />}
-        </>
-      )}
-    </>
-  );
-};
-
-/**
- * Wrapper component for using unstated
- */
-const SubnavButtonsWrapper = withUnstatedContainers(SubnavButtons, [AppContainer, NavigationContainer, PageContainer]);
-
-
-SubnavButtons.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-
-  isCompactMode: PropTypes.bool,
-};
-
-export default SubnavButtonsWrapper;

+ 87 - 0
packages/app/src/components/Navbar/SubNavButtons.tsx

@@ -0,0 +1,87 @@
+import React, {
+  FC, useCallback,
+} from 'react';
+import AppContainer from '../../client/services/AppContainer';
+import NavigationContainer from '../../client/services/NavigationContainer';
+import { withUnstatedContainers } from '../UnstatedUtils';
+
+import PageReactionButtons from '../PageReactionButtons';
+import PageManagement from '../Page/PageManagement';
+import { useSWRPageInfo } from '../../stores/page';
+import { toastError } from '../../client/util/apiNotification';
+import { apiv3Put } from '../../client/util/apiv3-client';
+
+
+type SubNavButtonsProps= {
+  appContainer: AppContainer,
+  navigationContainer: NavigationContainer,
+  isCompactMode?: boolean,
+  pageId: string,
+}
+const SubNavButtons: FC<SubNavButtonsProps> = (props: SubNavButtonsProps) => {
+  const {
+    appContainer, navigationContainer, isCompactMode, pageId,
+  } = props;
+  const { editorMode } = navigationContainer.state;
+  const isViewMode = editorMode === 'view';
+  const { data: pageInfo, error: pageInfoError, mutate: mutatePageInfo } = useSWRPageInfo(pageId);
+
+  const likeClickhandler = useCallback(async() => {
+    const { isGuestUser } = appContainer;
+
+    if (isGuestUser) {
+      return;
+    }
+
+    try {
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      await apiv3Put('/page/likes', { pageId, bool: !pageInfo!.isLiked });
+      mutatePageInfo();
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [pageInfo]);
+
+
+  if (pageInfoError != null || pageInfo == null) {
+    return <></>;
+  }
+  const { sumOfLikers, likerIds, isLiked } = pageInfo;
+
+  return (
+    <>
+      {isViewMode && (
+        <PageReactionButtons
+          pageId={pageId}
+          sumOfLikers={sumOfLikers}
+          likerIds={likerIds}
+          isLiked={isLiked}
+          onLikeClicked={likeClickhandler}
+        >
+        </PageReactionButtons>
+      )}
+      {/*
+        TODO :
+        once 80335 is done, merge 77543 branch(parent of 80335) into 77524.
+        (pageContainer dependencies in bookmark, delete modal, rename etc are removed)
+        then place PageManagement here.
+        TASK: https://estoc.weseek.co.jp/redmine/issues/81076
+        CONDITION :isAbleToShowPageManagement = !isNotFoundPage && !isTrashPage && !isSharedUser
+      */}
+      {/* if (CONDITION) then <PageManagement isCompactMode> */}
+    </>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const SubNavButtonsUnstatedWrapper = withUnstatedContainers(SubNavButtons, [AppContainer, NavigationContainer]);
+
+// wrapping tsx component returned by withUnstatedContainers to avoid type error when this component used in other tsx components.
+const SubNavButtonsWrapper = (props) => {
+  return <SubNavButtonsUnstatedWrapper {...props}></SubNavButtonsUnstatedWrapper>;
+};
+
+export default SubNavButtonsWrapper;

+ 7 - 4
packages/app/src/components/Page/RevisionLoader.jsx

@@ -13,7 +13,7 @@ import RevisionRenderer from './RevisionRenderer';
 /**
  * Load data from server and render RevisionBody component
  */
-class RevisionLoader extends React.Component {
+class LegacyRevisionLoader extends React.Component {
 
   constructor(props) {
     super(props);
@@ -116,9 +116,9 @@ class RevisionLoader extends React.Component {
 /**
  * Wrapper component for using unstated
  */
-const RevisionLoaderWrapper = withUnstatedContainers(RevisionLoader, [AppContainer]);
+const LegacyRevisionLoaderWrapper = withUnstatedContainers(LegacyRevisionLoader, [AppContainer]);
 
-RevisionLoader.propTypes = {
+LegacyRevisionLoader.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
   growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
@@ -129,4 +129,7 @@ RevisionLoader.propTypes = {
   highlightKeywords: PropTypes.string,
 };
 
-export default RevisionLoaderWrapper;
+const RevisionLoader = (props) => {
+  return <LegacyRevisionLoaderWrapper {...props}></LegacyRevisionLoaderWrapper>;
+};
+export default RevisionLoader;

+ 39 - 0
packages/app/src/components/PageReactionButtons.tsx

@@ -0,0 +1,39 @@
+import React, { FC, useState, useEffect } from 'react';
+import LikeButtons from './LikeButtons';
+
+
+type Props = {
+  pageId: string,
+  sumOfLikers: number,
+  likerIds: string[],
+  isLiked: boolean,
+  onLikeClicked: (isLiked : boolean)=>void,
+}
+
+
+const PageReactionButtons : FC<Props> = (props: Props) => {
+  const {
+    pageId, sumOfLikers, likerIds, isLiked, onLikeClicked,
+  } = props;
+
+
+  return (
+    <>
+      <span>
+        <LikeButtons onLikeClicked={onLikeClicked} pageId={pageId} likerIds={likerIds} sumOfLikers={sumOfLikers} isLiked={isLiked}></LikeButtons>
+      </span>
+      <span>
+        {/*
+          TODO:
+          once 80335 is done, merge 77543 branch(parent of 80335) into 77524.
+          (pageContainer dependencies in bookmark, delete modal, rename etc are removed)
+          then place BookMarkButton here
+          TASK: https://estoc.weseek.co.jp/redmine/issues/81076
+        */}
+        {/* <BookmarkButton></BookmarkButton> */}
+      </span>
+    </>
+  );
+};
+
+export default PageReactionButtons;

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

@@ -30,7 +30,7 @@ class SearchPage extends React.Component {
       searchedKeyword: '',
       searchedPages: [],
       searchResultMeta: {},
-      focusedPage: {},
+      focusedPage: null,
       selectedPages: new Set(),
       searchResultCount: 0,
       activePage: 1,
@@ -163,7 +163,7 @@ class SearchPage extends React.Component {
           searchedPages: [],
           searchResultMeta: {},
           searchResultCount: 0,
-          focusedPage: {},
+          focusedPage: null,
           activePage: 1,
         });
       }

+ 8 - 7
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -4,25 +4,26 @@ import RevisionLoader from '../Page/RevisionLoader';
 import AppContainer from '../../client/services/AppContainer';
 import SearchResultContentSubNavigation from './SearchResultContentSubNavigation';
 
+// TODO : set focusedPage type to ?IPageSearchResultData once #80214 is merged
+// PR: https://github.com/weseek/growi/pull/4649
+
 type Props ={
   appContainer: AppContainer,
   searchingKeyword:string,
-  focusedPage : any,
+  focusedPage: null | any,
 }
+
+
 const SearchResultContent: FC<Props> = (props: Props) => {
   const page = props.focusedPage;
   if (page == null) return null;
-  // Temporaly workaround for lint error
-  // later needs to be fixed: RevisoinRender to typescriptcomponet
-  const RevisionLoaderTypeAny: any = RevisionLoader;
-  const SearchResultContentSubNavigationTypeAny: any = SearchResultContentSubNavigation;
   const growiRenderer = props.appContainer.getRenderer('searchresult');
   let showTags = false;
   if (page.tags != null && page.tags.length > 0) { showTags = true }
   return (
     <div key={page._id} className="search-result-page mb-5">
-      <SearchResultContentSubNavigationTypeAny pageId={page._id} path={page.path}></SearchResultContentSubNavigationTypeAny>
-      <RevisionLoaderTypeAny
+      <SearchResultContentSubNavigation pageId={page._id} path={page.path}></SearchResultContentSubNavigation>
+      <RevisionLoader
         growiRenderer={growiRenderer}
         pageId={page._id}
         pagePath={page.path}

+ 8 - 8
packages/app/src/components/SearchPage/SearchResultContentSubNavigation.tsx

@@ -2,6 +2,7 @@ import React, { FC } from 'react';
 import PagePathNav from '../PagePathNav';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '../../client/services/AppContainer';
+import SubNavButtons from '../Navbar/SubNavButtons';
 
 type Props = {
   appContainer:AppContainer
@@ -11,11 +12,11 @@ type Props = {
   isCompactMode?: boolean,
 }
 
+
 const SearchResultContentSubNavigation: FC<Props> = (props : Props) => {
   const {
     appContainer, pageId, path, isCompactMode, isSignleLineMode,
   } = props;
-
   const { isSharedUser } = appContainer;
   return (
     <div className={`grw-subnav container-fluid d-flex align-items-center justify-content-between ${isCompactMode ? 'grw-subnav-compact d-print-none' : ''}`}>
@@ -35,11 +36,7 @@ const SearchResultContentSubNavigation: FC<Props> = (props : Props) => {
       </div>
       {/* Right side */}
       <div className="d-flex">
-        {/* TODO: refactor SubNavButtons in a way that it can be used independently from pageContainer
-              TASK : #80481 https://estoc.weseek.co.jp/redmine/issues/80481
-              CONDITION reference: https://dev.growi.org/5fabddf8bbeb1a0048bcb9e9
-        */}
-        {/* <SubnavButtons isCompactMode={isCompactMode} /> */}
+        <SubNavButtons isCompactMode={isCompactMode} pageId={pageId}></SubNavButtons>
       </div>
     </div>
   );
@@ -49,7 +46,10 @@ const SearchResultContentSubNavigation: FC<Props> = (props : Props) => {
 /**
  * Wrapper component for using unstated
  */
-const SearchResultContentSubNavigationWrapper = withUnstatedContainers(SearchResultContentSubNavigation, [AppContainer]);
-
+const SearchResultContentSubNavigationUnstatedWrapper = withUnstatedContainers(SearchResultContentSubNavigation, [AppContainer]);
 
+// wrapping tsx component returned by withUnstatedContainers to avoid type error when this component used in other tsx components.
+const SearchResultContentSubNavigationWrapper = (props) => {
+  return <SearchResultContentSubNavigationUnstatedWrapper {...props}></SearchResultContentSubNavigationUnstatedWrapper>;
+};
 export default SearchResultContentSubNavigationWrapper;

+ 8 - 0
packages/app/src/interfaces/page-info.ts

@@ -0,0 +1,8 @@
+export type IPageInfo = {
+  sumOfLikers: number;
+  likerIds: string[];
+  seenUserIds: string[];
+  sumOfSeenUsers: number;
+  isSeen: boolean;
+  isLiked: boolean;
+};

+ 2 - 2
packages/app/src/server/models/page.js

@@ -370,7 +370,7 @@ module.exports = function(crowi) {
       }
       else {
         logger.debug('liker not updated');
-        return reject(self);
+        return reject(new Error('Already liked'));
       }
     }));
   };
@@ -391,7 +391,7 @@ module.exports = function(crowi) {
       }
       else {
         logger.debug('liker not updated');
-        return reject(self);
+        return reject(new Error('Already unliked'));
       }
     }));
   };

+ 18 - 4
packages/app/src/stores/page.tsx

@@ -1,10 +1,10 @@
 import useSWR, { SWRResponse } from 'swr';
 
-import { apiv3Get } from '~/client/util/apiv3-client';
-
-import { IPage } from '~/interfaces/page';
-import { IPagingResult } from '~/interfaces/paging-result';
+import { apiv3Get } from '../client/util/apiv3-client';
 
+import { IPage } from '../interfaces/page';
+import { IPagingResult } from '../interfaces/paging-result';
+import { IPageInfo } from '../interfaces/page-info';
 
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 export const useSWRxRecentlyUpdated = <Data, Error>(): SWRResponse<IPage[], Error> => {
@@ -31,3 +31,17 @@ export const useSWRxPageList = (
     }),
   );
 };
+
+
+export const useSWRPageInfo = (pageId: string): SWRResponse<IPageInfo, Error> => {
+  return useSWR(`/page/info?pageId=${pageId}`, endpoint => apiv3Get(endpoint).then((response) => {
+    return {
+      sumOfLikers: response.data.sumOfLikers,
+      likerIds: response.data.likerIds,
+      seenUserIds: response.data.seenUserIds,
+      sumOfSeenUsers: response.data.sumOfSeenUsers,
+      isSeen: response.data.isSeen,
+      isLiked: response.data?.isLiked,
+    };
+  }));
+};