Browse Source

Merge pull request #8499 from weseek/feat/140685-switch-wip-page-visibility

feat: Switch wip page visibility (PageTree, RecentChanges)
Yuki Takei 2 years ago
parent
commit
75e0783857

+ 3 - 0
apps/app/public/static/locales/en_US/translation.json

@@ -834,5 +834,8 @@
     "publish_page": "Publish page",
     "publish_page": "Publish page",
     "success_publish_page": "Page has been published",
     "success_publish_page": "Page has been published",
     "fail_publish_page": "Failed to publish the Page"
     "fail_publish_page": "Failed to publish the Page"
+  },
+  "sidebar_header": {
+    "show_wip_page": "Show WIP"
   }
   }
 }
 }

+ 3 - 0
apps/app/public/static/locales/ja_JP/translation.json

@@ -867,5 +867,8 @@
     "publish_page": "WIP を解除",
     "publish_page": "WIP を解除",
     "success_publish_page": "WIP を解除しました",
     "success_publish_page": "WIP を解除しました",
     "fail_publish_page": "WIP を解除できませんでした"
     "fail_publish_page": "WIP を解除できませんでした"
+  },
+  "sidebar_header": {
+    "show_wip_page": "WIP を表示"
   }
   }
 }
 }

+ 3 - 0
apps/app/public/static/locales/zh_CN/translation.json

@@ -837,5 +837,8 @@
     "publish_page": "发布 WIP",
     "publish_page": "发布 WIP",
     "success_publish_page": "WIP 已停用",
     "success_publish_page": "WIP 已停用",
     "fail_publish_page": "无法停用 WIP"
     "fail_publish_page": "无法停用 WIP"
+  },
+  "sidebar_header": {
+    "show_wip_page": "显示 WIP"
   }
   }
 }
 }

+ 3 - 1
apps/app/src/components/ItemsTree/ItemsTree.tsx

@@ -91,6 +91,7 @@ const isSecondStageRenderingCondition = (condition: RenderingCondition|SecondSta
 type ItemsTreeProps = {
 type ItemsTreeProps = {
   isEnableActions: boolean
   isEnableActions: boolean
   isReadOnlyUser: boolean
   isReadOnlyUser: boolean
+  isWipPageShown?: boolean
   targetPath: string
   targetPath: string
   targetPathOrId?: Nullable<string>
   targetPathOrId?: Nullable<string>
   targetAndAncestorsData?: TargetAndAncestors
   targetAndAncestorsData?: TargetAndAncestors
@@ -103,7 +104,7 @@ type ItemsTreeProps = {
  */
  */
 export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
 export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   const {
   const {
-    targetPath, targetPathOrId, targetAndAncestorsData, isEnableActions, isReadOnlyUser, CustomTreeItem, onClickTreeItem,
+    targetPath, targetPathOrId, targetAndAncestorsData, isEnableActions, isReadOnlyUser, isWipPageShown, CustomTreeItem, onClickTreeItem,
   } = props;
   } = props;
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -281,6 +282,7 @@ export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
           itemNode={initialItemNode}
           itemNode={initialItemNode}
           isOpen
           isOpen
           isEnableActions={isEnableActions}
           isEnableActions={isEnableActions}
+          isWipPageShown={isWipPageShown}
           isReadOnlyUser={isReadOnlyUser}
           isReadOnlyUser={isReadOnlyUser}
           onRenamed={onRenamed}
           onRenamed={onRenamed}
           onClickDuplicateMenuItem={onClickDuplicateMenuItem}
           onClickDuplicateMenuItem={onClickDuplicateMenuItem}

+ 11 - 5
apps/app/src/components/Sidebar/PageTree/PageTree.tsx

@@ -1,9 +1,10 @@
-import { Suspense } from 'react';
+import { Suspense, useState } from 'react';
 
 
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
 import ItemsTreeContentSkeleton from '../../ItemsTree/ItemsTreeContentSkeleton';
 import ItemsTreeContentSkeleton from '../../ItemsTree/ItemsTreeContentSkeleton';
+
 import { PageTreeHeader } from './PageTreeSubstance';
 import { PageTreeHeader } from './PageTreeSubstance';
 
 
 const PageTreeContent = dynamic(
 const PageTreeContent = dynamic(
@@ -15,19 +16,24 @@ const PageTreeContent = dynamic(
 export const PageTree = (): JSX.Element => {
 export const PageTree = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
+  const [isWipPageShown, setIsWipPageShown] = useState(true);
+
   return (
   return (
     // TODO : #139425 Match the space specification method to others
     // TODO : #139425 Match the space specification method to others
     // ref.  https://redmine.weseek.co.jp/issues/139425
     // ref.  https://redmine.weseek.co.jp/issues/139425
-    <div className="pt-4 pb-3 px-3">
-      <div className="grw-sidebar-content-header d-flex">
+    <div className="px-3">
+      <div className="grw-sidebar-content-header py-3 d-flex">
         <h3 className="mb-0">{t('Page Tree')}</h3>
         <h3 className="mb-0">{t('Page Tree')}</h3>
         <Suspense>
         <Suspense>
-          <PageTreeHeader />
+          <PageTreeHeader
+            isWipPageShown={isWipPageShown}
+            onWipPageShownChange={() => { setIsWipPageShown(!isWipPageShown) }}
+          />
         </Suspense>
         </Suspense>
       </div>
       </div>
 
 
       <Suspense fallback={<ItemsTreeContentSkeleton />}>
       <Suspense fallback={<ItemsTreeContentSkeleton />}>
-        <PageTreeContent />
+        <PageTreeContent isWipPageShown={isWipPageShown} />
       </Suspense>
       </Suspense>
     </div>
     </div>
   );
   );

+ 40 - 2
apps/app/src/components/Sidebar/PageTree/PageTreeSubstance.tsx

@@ -1,6 +1,9 @@
 import React, { memo, useCallback } from 'react';
 import React, { memo, useCallback } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import {
+  UncontrolledButtonDropdown, DropdownMenu, DropdownToggle, DropdownItem,
+} from 'reactstrap';
 
 
 import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
@@ -12,8 +15,14 @@ import { SidebarHeaderReloadButton } from '../SidebarHeaderReloadButton';
 
 
 import { PrivateLegacyPagesLink } from './PrivateLegacyPagesLink';
 import { PrivateLegacyPagesLink } from './PrivateLegacyPagesLink';
 
 
+type HeaderProps = {
+  isWipPageShown: boolean,
+  onWipPageShownChange?: () => void
+}
+
+export const PageTreeHeader = memo(({ isWipPageShown, onWipPageShownChange }: HeaderProps) => {
+  const { t } = useTranslation();
 
 
-export const PageTreeHeader = memo(() => {
   const { mutate: mutateRootPage } = useSWRxRootPage({ suspense: true });
   const { mutate: mutateRootPage } = useSWRxRootPage({ suspense: true });
   useSWRxV5MigrationStatus({ suspense: true });
   useSWRxV5MigrationStatus({ suspense: true });
 
 
@@ -25,6 +34,29 @@ export const PageTreeHeader = memo(() => {
   return (
   return (
     <>
     <>
       <SidebarHeaderReloadButton onClick={() => mutate()} />
       <SidebarHeaderReloadButton onClick={() => mutate()} />
+
+      <UncontrolledButtonDropdown className="me-1">
+        <DropdownToggle color="transparent" className="p-0 border-0">
+          <span className="material-symbols-outlined">more_horiz</span>
+        </DropdownToggle>
+
+        <DropdownMenu container="body">
+          <DropdownItem onClick={onWipPageShownChange} className="">
+            <div className="form-check form-switch">
+              <input
+                id="wipPageVisibility"
+                className="form-check-input"
+                type="checkbox"
+                checked={isWipPageShown}
+                onChange={() => {}}
+              />
+              <label className="form-label form-check-label text-muted" htmlFor="wipPageVisibility">
+                {t('sidebar_header.show_wip_page')}
+              </label>
+            </div>
+          </DropdownItem>
+        </DropdownMenu>
+      </UncontrolledButtonDropdown>
     </>
     </>
   );
   );
 });
 });
@@ -44,7 +76,12 @@ const PageTreeUnavailable = () => {
   );
   );
 };
 };
 
 
-export const PageTreeContent = memo(() => {
+type PageTreeContentProps = {
+  isWipPageShown: boolean,
+}
+
+export const PageTreeContent = memo(({ isWipPageShown }: PageTreeContentProps) => {
+
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: currentPath } = useCurrentPagePath();
   const { data: currentPath } = useCurrentPagePath();
@@ -73,6 +110,7 @@ export const PageTreeContent = memo(() => {
       <ItemsTree
       <ItemsTree
         isEnableActions={!isGuestUser}
         isEnableActions={!isGuestUser}
         isReadOnlyUser={!!isReadOnlyUser}
         isReadOnlyUser={!!isReadOnlyUser}
+        isWipPageShown={isWipPageShown}
         targetPath={path}
         targetPath={path}
         targetPathOrId={targetPathOrId}
         targetPathOrId={targetPathOrId}
         targetAndAncestorsData={targetAndAncestorsData}
         targetAndAncestorsData={targetAndAncestorsData}

+ 1 - 0
apps/app/src/components/Sidebar/PageTreeItem/PageTreeItem.tsx

@@ -177,6 +177,7 @@ export const PageTreeItem: FC<TreeItemProps> = (props) => {
       isOpen={isOpen}
       isOpen={isOpen}
       isEnableActions={props.isEnableActions}
       isEnableActions={props.isEnableActions}
       isReadOnlyUser={props.isReadOnlyUser}
       isReadOnlyUser={props.isReadOnlyUser}
+      isWipPageShown={props.isWipPageShown}
       onClick={itemSelectedHandler}
       onClick={itemSelectedHandler}
       onClickDuplicateMenuItem={props.onClickDuplicateMenuItem}
       onClickDuplicateMenuItem={props.onClickDuplicateMenuItem}
       onClickDeleteMenuItem={props.onClickDeleteMenuItem}
       onClickDeleteMenuItem={props.onClickDeleteMenuItem}

+ 8 - 2
apps/app/src/components/Sidebar/RecentChanges/RecentChanges.tsx

@@ -16,6 +16,7 @@ export const RecentChanges = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const [isSmall, setIsSmall] = useState(false);
   const [isSmall, setIsSmall] = useState(false);
+  const [isWipPageShown, setIsWipPageShown] = useState(true);
 
 
   return (
   return (
     // TODO : #139425 Match the space specification method to others
     // TODO : #139425 Match the space specification method to others
@@ -24,12 +25,17 @@ export const RecentChanges = (): JSX.Element => {
       <div className="grw-sidebar-content-header py-3 d-flex">
       <div className="grw-sidebar-content-header py-3 d-flex">
         <h3 className="mb-0 text-nowrap">{t('Recent Changes')}</h3>
         <h3 className="mb-0 text-nowrap">{t('Recent Changes')}</h3>
         <Suspense>
         <Suspense>
-          <RecentChangesHeader isSmall={isSmall} onSizeChange={setIsSmall} />
+          <RecentChangesHeader
+            isSmall={isSmall}
+            onSizeChange={setIsSmall}
+            isWipPageShown={isWipPageShown}
+            onWipPageShownChange={() => { setIsWipPageShown(!isWipPageShown) }}
+          />
         </Suspense>
         </Suspense>
       </div>
       </div>
 
 
       <Suspense fallback={<RecentChangesContentSkeleton />}>
       <Suspense fallback={<RecentChangesContentSkeleton />}>
-        <RecentChangesContent isSmall={isSmall} />
+        <RecentChangesContent isWipPageShown={isWipPageShown} isSmall={isSmall} />
       </Suspense>
       </Suspense>
     </div>
     </div>
   );
   );

+ 0 - 1
apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.module.scss

@@ -1,7 +1,6 @@
 @use '~/styles/mixins' as *;
 @use '~/styles/mixins' as *;
 
 
 .grw-recent-changes-resize-button :global {
 .grw-recent-changes-resize-button :global {
-  font-size: 12px;
   line-height: normal;
   line-height: normal;
   transform: translateY(-2px);
   transform: translateY(-2px);
 
 

+ 54 - 17
apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx

@@ -7,6 +7,10 @@ import {
 } from '@growi/core';
 } from '@growi/core';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { UserPicture } from '@growi/ui/dist/components';
 import { UserPicture } from '@growi/ui/dist/components';
+import { useTranslation } from 'react-i18next';
+import {
+  DropdownItem, DropdownMenu, DropdownToggle, UncontrolledButtonDropdown,
+} from 'reactstrap';
 
 
 import { useKeywordManager } from '~/client/services/search-operation';
 import { useKeywordManager } from '~/client/services/search-operation';
 import { PagePathHierarchicalLink } from '~/components/Common/PagePathHierarchicalLink';
 import { PagePathHierarchicalLink } from '~/components/Common/PagePathHierarchicalLink';
@@ -116,8 +120,11 @@ const PageItem = memo(({ page, isSmall, onClickTag }: PageItemProps): JSX.Elemen
               { !dPagePath.isRoot && <FormerLink /> }
               { !dPagePath.isRoot && <FormerLink /> }
             </div>
             </div>
 
 
-            <h6 className={`col-12 ${isSmall ? 'mb-0 text-truncate' : 'mb-0'}`}>
+            <h6 className={`col-12 d-flex align-items-center ${isSmall ? 'mb-0 text-truncate' : 'mb-0'}`}>
               <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
               <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
+              { page.wip && (
+                <span className="wip-page-badge badge rounded-pill text-bg-secondary ms-2">WIP</span>
+              ) }
               {locked}
               {locked}
             </h6>
             </h6>
 
 
@@ -143,12 +150,17 @@ PageItem.displayName = 'PageItem';
 type HeaderProps = {
 type HeaderProps = {
   isSmall: boolean,
   isSmall: boolean,
   onSizeChange: (isSmall: boolean) => void,
   onSizeChange: (isSmall: boolean) => void,
+  isWipPageShown: boolean,
+  onWipPageShownChange: () => void,
 }
 }
 
 
 const PER_PAGE = 20;
 const PER_PAGE = 20;
-export const RecentChangesHeader = ({ isSmall, onSizeChange }: HeaderProps): JSX.Element => {
+export const RecentChangesHeader = ({
+  isSmall, onSizeChange, isWipPageShown, onWipPageShownChange,
+}: HeaderProps): JSX.Element => {
+  const { t } = useTranslation();
 
 
-  const { mutate } = useSWRINFxRecentlyUpdated(PER_PAGE, { suspense: true });
+  const { mutate } = useSWRINFxRecentlyUpdated(PER_PAGE, isWipPageShown, { suspense: true });
 
 
   const retrieveSizePreferenceFromLocalStorage = useCallback(() => {
   const retrieveSizePreferenceFromLocalStorage = useCallback(() => {
     if (window.localStorage.isRecentChangesSidebarSmall === 'true') {
     if (window.localStorage.isRecentChangesSidebarSmall === 'true') {
@@ -169,28 +181,53 @@ export const RecentChangesHeader = ({ isSmall, onSizeChange }: HeaderProps): JSX
   return (
   return (
     <>
     <>
       <SidebarHeaderReloadButton onClick={() => mutate()} />
       <SidebarHeaderReloadButton onClick={() => mutate()} />
-      <div className="d-flex align-items-center">
-        <div className={`grw-recent-changes-resize-button ${styles['grw-recent-changes-resize-button']} form-check form-switch ms-1`}>
-          <input
-            id="recentChangesResize"
-            className="form-check-input"
-            type="checkbox"
-            checked={isSmall}
-            onChange={changeSizeHandler}
-          />
-          <label className="form-label form-check-label" htmlFor="recentChangesResize" />
-        </div>
-      </div>
+
+      <UncontrolledButtonDropdown className="me-1">
+        <DropdownToggle color="transparent" className="p-0 border-0">
+          <span className="material-symbols-outlined">more_horiz</span>
+        </DropdownToggle>
+
+        <DropdownMenu container="body">
+          <DropdownItem onClick={changeSizeHandler}>
+            <div className={`${styles['grw-recent-changes-resize-button']} form-check form-switch`}>
+              <input
+                id="recentChangesResize"
+                className="form-check-input"
+                type="checkbox"
+                checked={isSmall}
+                onChange={() => {}}
+              />
+              <label className="form-label form-check-label text-muted" htmlFor="recentChangesResize" />
+            </div>
+          </DropdownItem>
+
+          <DropdownItem onClick={onWipPageShownChange}>
+            <div className="form-check form-switch">
+              <input
+                id="wipPageVisibility"
+                className="form-check-input"
+                type="checkbox"
+                checked={isWipPageShown}
+                onChange={() => {}}
+              />
+              <label className="form-label form-check-label text-muted" htmlFor="wipPageVisibility">
+                {t('sidebar_header.show_wip_page')}
+              </label>
+            </div>
+          </DropdownItem>
+        </DropdownMenu>
+      </UncontrolledButtonDropdown>
     </>
     </>
   );
   );
 };
 };
 
 
 type ContentProps = {
 type ContentProps = {
   isSmall: boolean,
   isSmall: boolean,
+  isWipPageShown: boolean,
 }
 }
 
 
-export const RecentChangesContent = ({ isSmall }: ContentProps): JSX.Element => {
-  const swrInifinitexRecentlyUpdated = useSWRINFxRecentlyUpdated(PER_PAGE, { suspense: true });
+export const RecentChangesContent = ({ isSmall, isWipPageShown }: ContentProps): JSX.Element => {
+  const swrInifinitexRecentlyUpdated = useSWRINFxRecentlyUpdated(PER_PAGE, isWipPageShown, { suspense: true });
   const { data } = swrInifinitexRecentlyUpdated;
   const { data } = swrInifinitexRecentlyUpdated;
 
 
   const { pushState } = useKeywordManager();
   const { pushState } = useKeywordManager();

+ 3 - 0
apps/app/src/components/Sidebar/SidebarContents.module.scss

@@ -11,4 +11,7 @@
     --bs-list-group-bg: transparent;
     --bs-list-group-bg: transparent;
   }
   }
 
 
+  .wip-page-badge {
+    --bs-badge-font-size: 0.5rem;
+  }
 }
 }

+ 6 - 2
apps/app/src/components/TreeItem/SimpleItem.tsx

@@ -61,7 +61,7 @@ const SimpleItemContent = ({ page }: { page: IPageForItem }) => {
           <div className="d-flex align-items-center">
           <div className="d-flex align-items-center">
             <span className={`text-truncate ${page.isEmpty && 'grw-sidebar-text-muted'}`}>{pageName}</span>
             <span className={`text-truncate ${page.isEmpty && 'grw-sidebar-text-muted'}`}>{pageName}</span>
             { page.wip && (
             { page.wip && (
-              <span className="badge rounded-pill text-bg-secondary ms-2">WIP</span>
+              <span className="wip-page-badge badge rounded-pill text-bg-secondary ms-2">WIP</span>
             )}
             )}
           </div>
           </div>
         </div>
         </div>
@@ -95,7 +95,7 @@ type SimpleItemProps = TreeItemProps & {
 export const SimpleItem: FC<SimpleItemProps> = (props) => {
 export const SimpleItem: FC<SimpleItemProps> = (props) => {
   const {
   const {
     itemNode, targetPathOrId, isOpen: _isOpen = false,
     itemNode, targetPathOrId, isOpen: _isOpen = false,
-    onRenamed, onClick, onClickDuplicateMenuItem, onClickDeleteMenuItem, isEnableActions, isReadOnlyUser,
+    onRenamed, onClick, onClickDuplicateMenuItem, onClickDeleteMenuItem, isEnableActions, isReadOnlyUser, isWipPageShown = true,
     itemRef, itemClass, mainClassName,
     itemRef, itemClass, mainClassName,
   } = props;
   } = props;
 
 
@@ -170,6 +170,7 @@ export const SimpleItem: FC<SimpleItemProps> = (props) => {
     isEnableActions,
     isEnableActions,
     isReadOnlyUser,
     isReadOnlyUser,
     isOpen: false,
     isOpen: false,
+    isWipPageShown,
     targetPathOrId,
     targetPathOrId,
     onRenamed,
     onRenamed,
     onClickDuplicateMenuItem,
     onClickDuplicateMenuItem,
@@ -183,6 +184,9 @@ export const SimpleItem: FC<SimpleItemProps> = (props) => {
 
 
   const CustomNextComponents = props.customNextComponents;
   const CustomNextComponents = props.customNextComponents;
 
 
+  if (!isWipPageShown && page.wip) {
+    return <></>;
+  }
 
 
   return (
   return (
     <div
     <div

+ 1 - 0
apps/app/src/components/TreeItem/interfaces/index.ts

@@ -24,6 +24,7 @@ export type TreeItemToolProps = TreeItemBaseProps;
 export type TreeItemProps = TreeItemBaseProps & {
 export type TreeItemProps = TreeItemBaseProps & {
   targetPathOrId?: Nullable<string>,
   targetPathOrId?: Nullable<string>,
   isOpen?: boolean,
   isOpen?: boolean,
+  isWipPageShown?: boolean,
   itemClass?: React.FunctionComponent<TreeItemProps>,
   itemClass?: React.FunctionComponent<TreeItemProps>,
   mainClassName?: string,
   mainClassName?: string,
   customEndComponents?: Array<React.FunctionComponent<TreeItemToolProps>>,
   customEndComponents?: Array<React.FunctionComponent<TreeItemToolProps>>,

+ 17 - 0
apps/app/src/server/models/page.ts

@@ -204,6 +204,18 @@ export class PageQueryBuilder {
     return this;
     return this;
   }
   }
 
 
+  addConditionToExcludeWipPage(): PageQueryBuilder {
+    this.query = this.query
+      .and({
+        $or: [
+          { wip: undefined },
+          { wip: false },
+        ],
+      });
+
+    return this;
+  }
+
   /**
   /**
    * generate the query to find the pages '{path}/*' and '{path}' self.
    * generate the query to find the pages '{path}/*' and '{path}' self.
    * If top page, return without doing anything.
    * If top page, return without doing anything.
@@ -655,10 +667,15 @@ schema.statics.findRecentUpdatedPages = async function(
 
 
   const baseQuery = this.find({});
   const baseQuery = this.find({});
   const queryBuilder = new PageQueryBuilder(baseQuery, includeEmpty);
   const queryBuilder = new PageQueryBuilder(baseQuery, includeEmpty);
+
   if (!options.includeTrashed) {
   if (!options.includeTrashed) {
     queryBuilder.addConditionToExcludeTrashed();
     queryBuilder.addConditionToExcludeTrashed();
   }
   }
 
 
+  if (!options.includeWipPage) {
+    queryBuilder.addConditionToExcludeWipPage();
+  }
+
   queryBuilder.addConditionToListWithDescendants(path, options);
   queryBuilder.addConditionToListWithDescendants(path, options);
   queryBuilder.populateDataToList(User.USER_FIELDS_EXCEPT_CONFIDENTIAL);
   queryBuilder.populateDataToList(User.USER_FIELDS_EXCEPT_CONFIDENTIAL);
   await queryBuilder.addViewerCondition(user);
   await queryBuilder.addViewerCondition(user);

+ 9 - 1
apps/app/src/server/routes/apiv3/pages/index.js

@@ -160,6 +160,11 @@ module.exports = (crowi) => {
   const addActivity = generateAddActivityMiddleware(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);
 
 
   const validator = {
   const validator = {
+    recent: [
+      query('limit').optional().isInt().withMessage('limit must be integer'),
+      query('offset').optional().isInt().withMessage('offset must be integer'),
+      query('includeWipPage').optional().isBoolean().withMessage('includeWipPage must be boolean'),
+    ],
     renamePage: [
     renamePage: [
       body('pageId').isMongoId().withMessage('pageId is required'),
       body('pageId').isMongoId().withMessage('pageId is required'),
       body('revisionId').optional({ nullable: true }).isMongoId().withMessage('revisionId is required'), // required when v4
       body('revisionId').optional({ nullable: true }).isMongoId().withMessage('revisionId is required'), // required when v4
@@ -216,12 +221,15 @@ module.exports = (crowi) => {
    *            description: Return pages recently updated
    *            description: Return pages recently updated
    *
    *
    */
    */
-  router.get('/recent', accessTokenParser, loginRequired, async(req, res) => {
+  router.get('/recent', accessTokenParser, loginRequired, validator.recent, apiV3FormValidator, async(req, res) => {
     const limit = parseInt(req.query.limit) || 20;
     const limit = parseInt(req.query.limit) || 20;
     const offset = parseInt(req.query.offset) || 0;
     const offset = parseInt(req.query.offset) || 0;
+    const includeWipPage = req.query.includeWipPage === 'true'; // Need validation using express-validator
+
     const queryOptions = {
     const queryOptions = {
       offset,
       offset,
       limit,
       limit,
+      includeWipPage,
       includeTrashed: false,
       includeTrashed: false,
       isRegExpEscapedFromPath: true,
       isRegExpEscapedFromPath: true,
       sort: 'updatedAt',
       sort: 'updatedAt',

+ 8 - 7
apps/app/src/stores/page-listing.tsx

@@ -8,12 +8,13 @@ import useSWR, {
   mutate, type SWRConfiguration, type SWRResponse, type Arguments,
   mutate, type SWRConfiguration, type SWRResponse, type Arguments,
 } from 'swr';
 } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
-import useSWRInfinite, { SWRInfiniteResponse } from 'swr/infinite';
+import type { SWRInfiniteResponse } from 'swr/infinite';
+import useSWRInfinite from 'swr/infinite';
 
 
-import { IPagingResult } from '~/interfaces/paging-result';
+import type { IPagingResult } from '~/interfaces/paging-result';
 
 
 import { apiv3Get } from '../client/util/apiv3-client';
 import { apiv3Get } from '../client/util/apiv3-client';
-import {
+import type {
   AncestorsChildrenResult, ChildrenResult, V5MigrationStatus, RootPageResult,
   AncestorsChildrenResult, ChildrenResult, V5MigrationStatus, RootPageResult,
 } from '../interfaces/page-listing-results';
 } from '../interfaces/page-listing-results';
 
 
@@ -32,19 +33,19 @@ type RecentApiResult = {
   totalCount: number,
   totalCount: number,
   offset: number,
   offset: number,
 }
 }
-export const useSWRINFxRecentlyUpdated = (limit: number, config?: SWRConfiguration) : SWRInfiniteResponse<RecentApiResult, Error> => {
+export const useSWRINFxRecentlyUpdated = (limit: number, includeWipPage?: boolean, config?: SWRConfiguration) : SWRInfiniteResponse<RecentApiResult, Error> => {
   return useSWRInfinite(
   return useSWRInfinite(
     (pageIndex, previousPageData) => {
     (pageIndex, previousPageData) => {
       if (previousPageData != null && previousPageData.pages.length === 0) return null;
       if (previousPageData != null && previousPageData.pages.length === 0) return null;
 
 
       if (pageIndex === 0 || previousPageData == null) {
       if (pageIndex === 0 || previousPageData == null) {
-        return ['/pages/recent', undefined, limit];
+        return ['/pages/recent', undefined, limit, includeWipPage];
       }
       }
 
 
       const offset = previousPageData.offset + limit;
       const offset = previousPageData.offset + limit;
-      return ['/pages/recent', offset, limit];
+      return ['/pages/recent', offset, limit, includeWipPage];
     },
     },
-    ([endpoint, offset, limit]) => apiv3Get<RecentApiResult>(endpoint, { offset, limit }).then(response => response.data),
+    ([endpoint, offset, limit, includeWipPage]) => apiv3Get<RecentApiResult>(endpoint, { offset, limit, includeWipPage }).then(response => response.data),
     {
     {
       ...config,
       ...config,
       revalidateFirstPage: false,
       revalidateFirstPage: false,