Răsfoiți Sursa

Merge branch 'master' into feat/search-implement

Steven Fukase 4 ani în urmă
părinte
comite
da401a996f

+ 2 - 1
packages/app/src/client/services/AppContainer.js

@@ -31,8 +31,9 @@ export default class AppContainer extends Container {
       preferDarkModeByMediaQuery: false,
     };
 
+    // get csrf token from body element
+    // DO NOT REMOVE: uploading attachment data requires appContainer.csrfToken
     const body = document.querySelector('body');
-
     this.csrfToken = body.dataset.csrftoken;
 
     this.config = JSON.parse(document.getElementById('growi-context-hydrate').textContent || '{}');

+ 18 - 2
packages/app/src/client/util/apiv1-client.ts

@@ -4,6 +4,14 @@ import axios from '~/utils/axios';
 
 const apiv1Root = '/_api';
 
+// get csrf token from body element
+const body = document.querySelector('body');
+const csrfToken = body?.dataset.csrftoken;
+
+
+type ParamWithCsrfKey = {
+  _csrf: string,
+}
 
 class Apiv1ErrorHandler extends Error {
 
@@ -38,10 +46,18 @@ export async function apiGet(path: string, params: unknown = {}): Promise<unknow
   return apiRequest('get', path, { params });
 }
 
-export async function apiPost(path: string, params: unknown = {}): Promise<unknown> {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiPost(path: string, params: any & ParamWithCsrfKey = {}): Promise<unknown> {
+  if (params._csrf == null) {
+    params._csrf = csrfToken;
+  }
   return apiRequest('post', path, params);
 }
 
-export async function apiDelete(path: string, params: unknown = {}): Promise<unknown> {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiDelete(path: string, params: any & ParamWithCsrfKey = {}): Promise<unknown> {
+  if (params._csrf == null) {
+    params._csrf = csrfToken;
+  }
   return apiRequest('delete', path, { data: params });
 }

+ 21 - 3
packages/app/src/client/util/apiv3-client.ts

@@ -11,6 +11,15 @@ const apiv3Root = '/_api/v3';
 
 const logger = loggerFactory('growi:apiv3');
 
+// get csrf token from body element
+const body = document.querySelector('body');
+const csrfToken = body?.dataset.csrftoken;
+
+
+type ParamWithCsrfKey = {
+  _csrf: string,
+}
+
 const apiv3ErrorHandler = (_err) => {
   // extract api errors from general 400 err
   const err = _err.response ? _err.response.data.errors : _err;
@@ -41,16 +50,25 @@ export async function apiv3Get<T = any>(path: string, params: unknown = {}): Pro
 }
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-export async function apiv3Post<T = any>(path: string, params: unknown = {}): Promise<AxiosResponse<T>> {
+export async function apiv3Post<T = any>(path: string, params: any & ParamWithCsrfKey = {}): Promise<AxiosResponse<T>> {
+  if (params._csrf == null) {
+    params._csrf = csrfToken;
+  }
   return apiv3Request('post', path, params);
 }
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-export async function apiv3Put<T = any>(path: string, params: unknown = {}): Promise<AxiosResponse<T>> {
+export async function apiv3Put<T = any>(path: string, params: any & ParamWithCsrfKey = {}): Promise<AxiosResponse<T>> {
+  if (params._csrf == null) {
+    params._csrf = csrfToken;
+  }
   return apiv3Request('put', path, params);
 }
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-export async function apiv3Delete<T = any>(path: string, params: unknown = {}): Promise<AxiosResponse<T>> {
+export async function apiv3Delete<T = any>(path: string, params: any & ParamWithCsrfKey = {}): Promise<AxiosResponse<T>> {
+  if (params._csrf == null) {
+    params._csrf = csrfToken;
+  }
   return apiv3Request('delete', path, { params });
 }

+ 15 - 23
packages/app/src/components/PageList.jsx

@@ -8,39 +8,33 @@ import { withUnstatedContainers } from './UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 
+import { toastError } from '~/client/util/apiNotification';
+import { useSWRxPageList } from '~/stores/page';
+
 import PaginationWrapper from './PaginationWrapper';
 
 
 const PageList = (props) => {
   const { appContainer, pageContainer, t } = props;
   const { path } = pageContainer.state;
-  const [pages, setPages] = useState(null);
-  const [isLoading, setIsLoading] = useState(true);
 
   const [activePage, setActivePage] = useState(1);
-  const [totalPages, setTotalPages] = useState(0);
-  const [limit, setLimit] = useState(Infinity);
+
+  const { data: pagesListData, error } = useSWRxPageList(path, activePage);
 
   function setPageNumber(selectedPageNumber) {
     setActivePage(selectedPageNumber);
   }
 
-  const updatePageList = useCallback(async() => {
-    const page = activePage;
-    const res = await appContainer.apiv3Get('/pages/list', { path, page });
-
-    setPages(res.data.pages);
-    setIsLoading(false);
-    setTotalPages(res.data.totalCount);
-    setLimit(res.data.limit);
-  }, [appContainer, path, activePage]);
-
-  useEffect(() => {
-    updatePageList();
-  }, [updatePageList]);
 
+  // TODO: To be implemented in #79549
+  if (error != null) {
+    // toastError(error, 'Error occurred in PageList');
+    // eslint-disable-next-line no-console
+    console.log(error, 'Error occurred in PageList');
+  }
 
-  if (isLoading) {
+  if (pagesListData == null) {
     return (
       <div className="wiki">
         <div className="text-muted text-center">
@@ -51,7 +45,7 @@ const PageList = (props) => {
   }
 
   const liClasses = props.liClasses.join(' ');
-  const pageList = pages.map(page => (
+  const pageList = pagesListData.items.map(page => (
     <li key={page._id} className={liClasses}>
       <Page page={page} />
     </li>
@@ -81,14 +75,12 @@ const PageList = (props) => {
       <PaginationWrapper
         activePage={activePage}
         changePage={setPageNumber}
-        totalItemsCount={totalPages}
-        pagingLimit={limit}
+        totalItemsCount={pagesListData.totalCount}
+        pagingLimit={pagesListData.limit}
         align="center"
       />
     </div>
   );
-
-
 };
 
 const PageListWrapper = withUnstatedContainers(PageList, [AppContainer, PageContainer]);

+ 5 - 0
packages/app/src/interfaces/paging-result.ts

@@ -0,0 +1,5 @@
+export type IPagingResult<T> = {
+  items: T[],
+  totalCount: number,
+  limit: number,
+}

+ 70 - 0
packages/app/src/server/routes/apiv3/page.js

@@ -112,6 +112,52 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  *          bool:
  *            type: boolean
  *            description: boolean for like status
+ *
+ *      PageInfo:
+ *        description: PageInfo
+ *        type: object
+ *        required:
+ *          - isSeen
+ *          - sumOfLikers
+ *          - likerIds
+ *          - sumOfSeenUsers
+ *          - seenUserIds
+ *        properties:
+ *          isSeen:
+ *            type: boolean
+ *            description: Whether the page has ever been seen
+ *          isLiked:
+ *            type: boolean
+ *            description: Whether the page is liked by the logged in user
+ *          sumOfLikers:
+ *            type: number
+ *            description: Number of users who have liked the page
+ *          likerIds:
+ *            type: array
+ *            items:
+ *              type: string
+ *            description: Ids of users who have liked the page
+ *            example: ["5e07345972560e001761fa63"]
+ *          sumOfSeenUsers:
+ *            type: number
+ *            description: Number of users who have seen the page
+ *          seenUserIds:
+ *            type: array
+ *            items:
+ *              type: string
+ *            description: Ids of users who have seen the page
+ *            example: ["5e07345972560e001761fa63"]
+ *
+ *      PageParams:
+ *        description: PageParams
+ *        type: object
+ *        required:
+ *          - pageId
+ *        properties:
+ *          pageId:
+ *            type: string
+ *            description: page ID
+ *            example: 5e07345972560e001761fa63
  */
 module.exports = (crowi) => {
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
@@ -208,6 +254,30 @@ module.exports = (crowi) => {
     }
   });
 
+  /**
+   * @swagger
+   *
+   *    /page/info:
+   *      get:
+   *        tags: [Page]
+   *        summary: /page/info
+   *        description: Retrieve current page info
+   *        operationId: getPageInfo
+   *        requestBody:
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/PageParams'
+   *        responses:
+   *          200:
+   *            description: Successfully retrieved current page info.
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/PageInfo'
+   *          500:
+   *            description: Internal server error.
+   */
   router.get(('/info', loginRequired), async(req, res) => {
 
     try {

+ 20 - 0
packages/app/src/stores/page.tsx

@@ -3,6 +3,8 @@ import useSWR, { SWRResponse } from 'swr';
 import { apiv3Get } from '~/client/util/apiv3-client';
 
 import { IPage } from '~/interfaces/page';
+import { IPagingResult } from '~/interfaces/paging-result';
+
 
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 export const useSWRxRecentlyUpdated = <Data, Error>(): SWRResponse<IPage[], Error> => {
@@ -11,3 +13,21 @@ export const useSWRxRecentlyUpdated = <Data, Error>(): SWRResponse<IPage[], Erro
     endpoint => apiv3Get<{ pages: IPage[] }>(endpoint).then(response => response.data?.pages),
   );
 };
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export const useSWRxPageList = (
+    path: string,
+    pageNumber?: number,
+): SWRResponse<IPagingResult<IPage>, Error> => {
+  const page = pageNumber || 1;
+  return useSWR(
+    `/pages/list?path=${path}&page=${page}`,
+    endpoint => apiv3Get<{pages: IPage[], totalCount: number, limit: number}>(endpoint).then((response) => {
+      return {
+        items: response.data.pages,
+        totalCount: response.data.totalCount,
+        limit: response.data.limit,
+      };
+    }),
+  );
+};