Browse Source

make forum paging query param based

Klaudiusz Dembler 4 years ago
parent
commit
138f688e38

+ 10 - 26
pioneer/packages/joy-forum/src/CategoryList.tsx

@@ -3,7 +3,6 @@ import { Link } from 'react-router-dom';
 import ReactMarkdown from 'react-markdown';
 import { Table, Dropdown, Button, Segment, Label } from 'semantic-ui-react';
 import styled from 'styled-components';
-import { History } from 'history';
 import orderBy from 'lodash/orderBy';
 import BN from 'bn.js';
 
@@ -11,7 +10,7 @@ import { Option, bool } from '@polkadot/types';
 import { CategoryId, Category, ThreadId, Thread } from '@joystream/types/forum';
 import { ViewThread } from './ViewThread';
 import { MutedSpan } from '@polkadot/joy-utils/MutedText';
-import { UrlHasIdProps, CategoryCrumbs, Pagination, ThreadsPerPage } from './utils';
+import { UrlHasIdProps, CategoryCrumbs, Pagination, ThreadsPerPage, usePagination } from './utils';
 import Section from '@polkadot/joy-utils/Section';
 import { JoyWarn } from '@polkadot/joy-utils/JoyStatus';
 import { withForumCalls } from './calls';
@@ -100,9 +99,7 @@ function CategoryActions (props: CategoryActionsProps) {
 
 type InnerViewCategoryProps = {
   category?: Category;
-  page?: number;
   preview?: boolean;
-  history?: History;
 };
 
 type ViewCategoryProps = InnerViewCategoryProps & {
@@ -114,7 +111,7 @@ const CategoryPreviewRow = styled(Table.Row)`
 `;
 
 function InnerViewCategory (props: InnerViewCategoryProps) {
-  const { history, category, page = 1, preview = false } = props;
+  const { category, preview = false } = props;
 
   if (!category) {
     return <em>Loading...</em>;
@@ -154,10 +151,6 @@ function InnerViewCategory (props: InnerViewCategoryProps) {
     );
   }
 
-  if (!history) {
-    return <em>Error: <code>history</code> property was not found.</em>;
-  }
-
   const renderSubCategoriesAndThreads = () => <>
     {category.archived &&
       <JoyWarn title={'This category is archived.'}>
@@ -181,7 +174,7 @@ function InnerViewCategory (props: InnerViewCategoryProps) {
     }
 
     <Section title={`Threads (${category.num_direct_unmoderated_threads.toString()})`}>
-      <CategoryThreads category={category} page={page} history={history} />
+      <CategoryThreads category={category} />
     </Section>
   </>;
 
@@ -205,8 +198,6 @@ const ViewCategory = withForumCalls<ViewCategoryProps>(
 
 type InnerCategoryThreadsProps = {
   category: Category;
-  page: number;
-  history: History;
 };
 
 type CategoryThreadsProps = ApiProps & InnerCategoryThreadsProps & {
@@ -214,7 +205,8 @@ type CategoryThreadsProps = ApiProps & InnerCategoryThreadsProps & {
 };
 
 function InnerCategoryThreads (props: CategoryThreadsProps) {
-  const { api, category, nextThreadId, page, history } = props;
+  const { api, category, nextThreadId } = props;
+  const [currentPage, setCurrentPage] = usePagination();
 
   if (!category.hasUnmoderatedThreads) {
     return <em>No threads in this category</em>;
@@ -273,20 +265,16 @@ function InnerCategoryThreads (props: CategoryThreadsProps) {
     return <em>No threads in this category</em>;
   }
 
-  const onPageChange = (activePage?: string | number) => {
-    history.push(`/forum/categories/${category.id.toString()}/page/${activePage}`);
-  };
-
   const itemsPerPage = ThreadsPerPage;
-  const minIdx = (page - 1) * itemsPerPage;
+  const minIdx = (currentPage - 1) * itemsPerPage;
   const maxIdx = minIdx + itemsPerPage - 1;
 
   const pagination =
     <Pagination
-      currentPage={page}
+      currentPage={currentPage}
       totalItems={threadCount}
       itemsPerPage={itemsPerPage}
-      onPageChange={onPageChange}
+      onPageChange={setCurrentPage}
     />;
 
   const pageOfItems = threads
@@ -321,21 +309,17 @@ export const CategoryThreads = withMulti(
 );
 
 type ViewCategoryByIdProps = UrlHasIdProps & {
-  history: History;
   match: {
     params: {
       id: string;
-      page?: string;
     };
   };
 };
 
 export function ViewCategoryById (props: ViewCategoryByIdProps) {
-  const { history, match: { params: { id, page: pageStr } } } = props;
+  const { match: { params: { id } } } = props;
   try {
-    // tslint:disable-next-line:radix
-    const page = pageStr ? parseInt(pageStr) : 1;
-    return <ViewCategory id={new CategoryId(id)} page={page} history={history} />;
+    return <ViewCategory id={new CategoryId(id)} />;
   } catch (err) {
     return <em>Invalid category ID: {id}</em>;
   }

+ 17 - 0
pioneer/packages/joy-forum/src/LegacyPagingRedirect.tsx

@@ -0,0 +1,17 @@
+import React from 'react';
+import { useLocation, Redirect } from 'react-router-dom';
+
+export const LegacyPagingRedirect: React.FC = () => {
+  const { pathname } = useLocation();
+  const parsingRegexp = /(.+)\/page\/(\d+)/;
+  const groups = parsingRegexp.exec(pathname);
+  if (!groups) {
+    return <em>Failed to parse the URL</em>;
+  }
+
+  const basePath = groups[1];
+  const page = groups[2];
+  const search = new URLSearchParams();
+  search.set('page', page);
+  return <Redirect to={{ pathname: basePath, search: search.toString() }} />;
+};

+ 9 - 31
pioneer/packages/joy-forum/src/ViewThread.tsx

@@ -3,11 +3,10 @@ import { Link } from 'react-router-dom';
 import ReactMarkdown from 'react-markdown';
 import styled from 'styled-components';
 import { Table, Button, Label } from 'semantic-ui-react';
-import { History } from 'history';
 import BN from 'bn.js';
 
 import { Category, Thread, ThreadId, Post, PostId } from '@joystream/types/forum';
-import { Pagination, RepliesPerPage, CategoryCrumbs, TimeAgoDate } from './utils';
+import { Pagination, RepliesPerPage, CategoryCrumbs, TimeAgoDate, usePagination } from './utils';
 import { ViewReply } from './ViewReply';
 import { Moderate } from './Moderate';
 import { MutedSpan } from '@polkadot/joy-utils/MutedText';
@@ -80,9 +79,7 @@ const ThreadInfoMemberPreview = styled(MemberPreview)`
 type InnerViewThreadProps = {
   category: Category;
   thread: Thread;
-  page?: number;
   preview?: boolean;
-  history?: History;
 };
 
 type ViewThreadProps = ApiProps & InnerViewThreadProps & {
@@ -91,7 +88,8 @@ type ViewThreadProps = ApiProps & InnerViewThreadProps & {
 
 function InnerViewThread (props: ViewThreadProps) {
   const [showModerateForm, setShowModerateForm] = useState(false);
-  const { history, category, thread, page = 1, preview = false } = props;
+  const { category, thread, preview = false } = props;
+  const [currentPage, setCurrentPage] = usePagination();
 
   if (!thread) {
     return <em>Loading thread details...</em>;
@@ -138,10 +136,6 @@ function InnerViewThread (props: ViewThreadProps) {
     );
   }
 
-  if (!history) {
-    return <em>History propoerty is undefined</em>;
-  }
-
   const { api, nextPostId } = props;
   const [loaded, setLoaded] = useState(false);
   const [posts, setPosts] = useState(new Array<Post>());
@@ -183,20 +177,16 @@ function InnerViewThread (props: ViewThreadProps) {
       return <em>Loading posts...</em>;
     }
 
-    const onPageChange = (activePage?: string | number) => {
-      history.push(`/forum/threads/${id.toString()}/page/${activePage}`);
-    };
-
     const itemsPerPage = RepliesPerPage;
-    const minIdx = (page - 1) * RepliesPerPage;
+    const minIdx = (currentPage - 1) * RepliesPerPage;
     const maxIdx = minIdx + RepliesPerPage - 1;
 
     const pagination =
       <Pagination
-        currentPage={page}
+        currentPage={currentPage}
         totalItems={totalPostsInThread}
         itemsPerPage={itemsPerPage}
-        onPageChange={onPageChange}
+        onPageChange={setCurrentPage}
       />;
 
     const pageOfItems = posts
@@ -293,27 +283,15 @@ export const ViewThread = withMulti(
 );
 
 type ViewThreadByIdProps = ApiProps & {
-  history: History;
   match: {
     params: {
       id: string;
-      page?: string;
     };
   };
 };
 
 function InnerViewThreadById (props: ViewThreadByIdProps) {
-  const { api, history, match: { params: { id, page: pageStr } } } = props;
-
-  let page = 1;
-  if (pageStr) {
-    try {
-      // tslint:disable-next-line:radix
-      page = parseInt(pageStr);
-    } catch (err) {
-      console.log('Failed to parse page number form URL');
-    }
-  }
+  const { api, match: { params: { id } } } = props;
 
   let threadId: ThreadId;
   try {
@@ -340,7 +318,7 @@ function InnerViewThreadById (props: ViewThreadByIdProps) {
     };
 
     loadThreadAndCategory();
-  }, [id, page]);
+  }, [id]);
 
   // console.log({ threadId: id, page });
 
@@ -356,7 +334,7 @@ function InnerViewThreadById (props: ViewThreadByIdProps) {
     return <em>{ 'Thread\'s category was not found' }</em>;
   }
 
-  return <ViewThread id={threadId} category={category} thread={thread} page={page} history={history} />;
+  return <ViewThread id={threadId} category={category} thread={thread} />;
 }
 
 export const ViewThreadById = withApi(InnerViewThreadById);

+ 7 - 3
pioneer/packages/joy-forum/src/index.tsx

@@ -15,6 +15,7 @@ import { NewThread, EditThread } from './EditThread';
 import { NewReply, EditReply } from './EditReply';
 import { CategoryList, ViewCategoryById } from './CategoryList';
 import { ViewThreadById } from './ViewThread';
+import { LegacyPagingRedirect } from './LegacyPagingRedirect';
 
 const ForumContentWrapper = styled.main`
   padding-top: 1.5rem;
@@ -30,22 +31,25 @@ class App extends React.PureComponent<Props> {
         <ForumSudoProvider>
           <ForumContentWrapper className='forum--App'>
             <Switch>
+              {/* routes for handling legacy format of forum paging within the routing path */}
+              {/* translate page param to search query */}
+              <Route path={`${basePath}/categories/:id/page/:page`} component={LegacyPagingRedirect} />
+              <Route path={`${basePath}/threads/:id/page/:page`} component={LegacyPagingRedirect} />
+
               {/* <Route path={`${basePath}/sudo`} component={EditForumSudo} /> */}
               {/* <Route path={`${basePath}/categories/new`} component={NewCategory} /> */}
+
               <Route path={`${basePath}/categories/:id/newSubcategory`} component={NewSubcategory} />
               <Route path={`${basePath}/categories/:id/newThread`} component={NewThread} />
               <Route path={`${basePath}/categories/:id/edit`} component={EditCategory} />
-              <Route path={`${basePath}/categories/:id/page/:page`} component={ViewCategoryById} />
               <Route path={`${basePath}/categories/:id`} component={ViewCategoryById} />
               <Route path={`${basePath}/categories`} component={CategoryList} />
 
               <Route path={`${basePath}/threads/:id/reply`} component={NewReply} />
               <Route path={`${basePath}/threads/:id/edit`} component={EditThread} />
-              <Route path={`${basePath}/threads/:id/page/:page`} component={ViewThreadById} />
               <Route path={`${basePath}/threads/:id`} component={ViewThreadById} />
 
               <Route path={`${basePath}/replies/:id/edit`} component={EditReply} />
-              {/* <Route path={`${basePath}/replies/:id`} component={ViewReplyById} /> */}
 
               <Route component={CategoryList} />
             </Switch>

+ 58 - 1
pioneer/packages/joy-forum/src/utils.tsx

@@ -1,4 +1,5 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
+import { useHistory, useLocation } from 'react-router';
 import { Link } from 'react-router-dom';
 import { Breadcrumb, Pagination as SuiPagination } from 'semantic-ui-react';
 import styled from 'styled-components';
@@ -145,3 +146,59 @@ export type UrlHasIdProps = {
     };
   };
 };
+
+type QueryValueType = string | null;
+type QuerySetValueType = (value?: QueryValueType | number) => void;
+type QueryReturnType = [QueryValueType, QuerySetValueType];
+
+export const useQueryParam = (queryParam: string): QueryReturnType => {
+  const { pathname, search } = useLocation();
+  const history = useHistory();
+  const [value, setValue] = useState<QueryValueType>(null);
+
+  useEffect(() => {
+    const params = new URLSearchParams(search);
+    const paramValue = params.get(queryParam);
+    if (paramValue !== value) {
+      setValue(paramValue);
+    }
+  }, [search, setValue, queryParam]);
+
+  const setParam: QuerySetValueType = (rawValue) => {
+    let parsedValue: string | null;
+    if (!rawValue && rawValue !== 0) {
+      parsedValue = null;
+    } else {
+      parsedValue = rawValue.toString();
+    }
+
+    const params = new URLSearchParams(search);
+    if (parsedValue) {
+      params.set(queryParam, parsedValue);
+    } else {
+      params.delete(queryParam);
+    }
+
+    setValue(parsedValue);
+    history.push({ pathname, search: params.toString() });
+  };
+
+  return [value, setParam];
+};
+
+export const usePagination = (): [number, QuerySetValueType] => {
+  const [rawCurrentPage, setCurrentPage] = useQueryParam('page');
+
+  let currentPage = 1;
+  if (rawCurrentPage) {
+    const parsedPage = Number.parseInt(rawCurrentPage);
+    if (!Number.isNaN(parsedPage)) {
+      currentPage = parsedPage;
+    } else {
+      // eslint-disable-next-line no-console
+      console.warn('Failed to parse URL page idx');
+    }
+  }
+
+  return [currentPage, setCurrentPage];
+};