Explorar o código

Merge pull request #8477 from weseek/feat/140129-turn-existing-pages-into-wip-pages

feat: Turn existing pages into wip pages
Shun Miyazawa %!s(int64=2) %!d(string=hai) anos
pai
achega
b8bd8441d3

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

@@ -825,5 +825,10 @@
   },
   },
   "page_select_modal": {
   "page_select_modal": {
     "select_page_location": "Select page location"
     "select_page_location": "Select page location"
+  },
+  "wip_page": {
+    "save_as_wip": "Save as WIP (Currently drafting)",
+    "success_save_as_wip": "Successfully saved as a WIP page",
+    "fail_save_as_wip": "Failed to save as a WIP page"
   }
   }
 }
 }

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

@@ -858,5 +858,10 @@
   },
   },
   "page_select_modal": {
   "page_select_modal": {
     "select_page_location": "ページの場所を選択"
     "select_page_location": "ページの場所を選択"
+  },
+  "wip_page": {
+    "save_as_wip": "WIP (執筆中) として保存",
+    "success_save_as_wip": "WIP ページとして保存しました",
+    "fail_save_as_wip": "WIP ページとして保存できませんでした"
   }
   }
 }
 }

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

@@ -828,5 +828,10 @@
   },
   },
   "page_select_modal": {
   "page_select_modal": {
     "select_page_location": "选择页面位置"
     "select_page_location": "选择页面位置"
+  },
+  "wip_page": {
+    "save_as_wip": "保存为 WIP(书面)",
+    "success_save_as_wip": "成功保存为 WIP 页面",
+    "fail_save_as_wip": "保存为 WIP 页失败"
   }
   }
 }
 }

+ 6 - 0
apps/app/src/client/services/page-operation.ts

@@ -1,5 +1,6 @@
 import { useCallback } from 'react';
 import { useCallback } from 'react';
 
 
+import type { IPageHasId } from '@growi/core';
 import { SubscriptionStatusType } from '@growi/core';
 import { SubscriptionStatusType } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
@@ -159,3 +160,8 @@ export const exist = async(path: string): Promise<PageExistResponse> => {
   const res = await apiv3Get<PageExistResponse>('/page/exist', { path });
   const res = await apiv3Get<PageExistResponse>('/page/exist', { path });
   return res.data;
   return res.data;
 };
 };
+
+export const unpublish = async(pageId: string): Promise<IPageHasId> => {
+  const res = await apiv3Put(`/page/${pageId}/unpublish`);
+  return res.data;
+};

+ 27 - 1
apps/app/src/components/SavePageControls.tsx

@@ -9,15 +9,18 @@ import {
   DropdownToggle, DropdownMenu, DropdownItem,
   DropdownToggle, DropdownMenu, DropdownItem,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
+import { toastSuccess, toastError } from '~/client/util/toastr';
 import type { IPageGrantData } from '~/interfaces/page';
 import type { IPageGrantData } from '~/interfaces/page';
 import {
 import {
   useIsEditable, useIsAclEnabled,
   useIsEditable, useIsAclEnabled,
 } from '~/stores/context';
 } from '~/stores/context';
 import { useWaitingSaveProcessing } from '~/stores/editor';
 import { useWaitingSaveProcessing } from '~/stores/editor';
-import { useSWRxCurrentPage } from '~/stores/page';
+import { useSWRMUTxCurrentPage, useSWRxCurrentPage } from '~/stores/page';
 import { useSelectedGrant } from '~/stores/ui';
 import { useSelectedGrant } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { unpublish } from '../client/services/page-operation';
+
 import { GrantSelector } from './SavePageControls/GrantSelector';
 import { GrantSelector } from './SavePageControls/GrantSelector';
 
 
 
 
@@ -41,6 +44,7 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
   const { data: isAclEnabled } = useIsAclEnabled();
   const { data: isAclEnabled } = useIsAclEnabled();
   const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
   const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
   const { data: _isWaitingSaveProcessing } = useWaitingSaveProcessing();
   const { data: _isWaitingSaveProcessing } = useWaitingSaveProcessing();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
 
 
   const isWaitingSaveProcessing = _isWaitingSaveProcessing === true; // ignore undefined
   const isWaitingSaveProcessing = _isWaitingSaveProcessing === true; // ignore undefined
 
 
@@ -58,6 +62,24 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
     globalEmitter.emit('saveAndReturnToView', { overwriteScopesOfDescendants: true, slackChannels });
     globalEmitter.emit('saveAndReturnToView', { overwriteScopesOfDescendants: true, slackChannels });
   }, [slackChannels]);
   }, [slackChannels]);
 
 
+  const clickUnpublishButtonHandler = useCallback(async() => {
+    const pageId = currentPage?._id;
+
+    if (pageId == null) {
+      return;
+    }
+
+    try {
+      await unpublish(pageId);
+      await mutateCurrentPage();
+      toastSuccess(t('wip_page.success_save_as_wip'));
+    }
+    catch (err) {
+      logger.error(err);
+      toastError(t('wip_page.fail_save_as_wip'));
+    }
+  }, [currentPage?._id, mutateCurrentPage, t]);
+
 
 
   if (isEditable == null || isAclEnabled == null || grantData == null) {
   if (isEditable == null || isAclEnabled == null || grantData == null) {
     return null;
     return null;
@@ -72,6 +94,7 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
   const isGrantSelectorDisabledPage = isTopPage(currentPage?.path ?? '') || isUsersProtectedPages(currentPage?.path ?? '');
   const isGrantSelectorDisabledPage = isTopPage(currentPage?.path ?? '') || isUsersProtectedPages(currentPage?.path ?? '');
   const labelSubmitButton = t('Update');
   const labelSubmitButton = t('Update');
   const labelOverwriteScopes = t('page_edit.overwrite_scopes', { operation: labelSubmitButton });
   const labelOverwriteScopes = t('page_edit.overwrite_scopes', { operation: labelSubmitButton });
+  const labelUnpublishPage = t('wip_page.save_as_wip');
 
 
   return (
   return (
     <div className="d-flex align-items-center flex-nowrap">
     <div className="d-flex align-items-center flex-nowrap">
@@ -108,6 +131,9 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
           <DropdownItem onClick={saveAndOverwriteScopesOfDescendants}>
           <DropdownItem onClick={saveAndOverwriteScopesOfDescendants}>
             {labelOverwriteScopes}
             {labelOverwriteScopes}
           </DropdownItem>
           </DropdownItem>
+          <DropdownItem onClick={clickUnpublishButtonHandler}>
+            {labelUnpublishPage}
+          </DropdownItem>
         </DropdownMenu>
         </DropdownMenu>
       </UncontrolledButtonDropdown>
       </UncontrolledButtonDropdown>
 
 

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

@@ -1060,6 +1060,7 @@ schema.methods.publish = function() {
 
 
 schema.methods.unpublish = function() {
 schema.methods.unpublish = function() {
   this.wip = true;
   this.wip = true;
+  this.wipExpiredAt = undefined;
 };
 };
 
 
 schema.methods.makeWip = function() {
 schema.methods.makeWip = function() {

+ 6 - 0
apps/app/src/server/routes/apiv3/page/index.js

@@ -22,6 +22,7 @@ import loggerFactory from '~/utils/logger';
 
 
 import { checkPageExistenceHandlersFactory } from './check-page-existence';
 import { checkPageExistenceHandlersFactory } from './check-page-existence';
 import { createPageHandlersFactory } from './create-page';
 import { createPageHandlersFactory } from './create-page';
+import { unpublishPageHandlersFactory } from './unpublish-page';
 import { updatePageHandlersFactory } from './update-page';
 import { updatePageHandlersFactory } from './update-page';
 
 
 
 
@@ -925,5 +926,10 @@ module.exports = (crowi) => {
       }
       }
     });
     });
 
 
+
+  // router.put('/:pageId/publish', publishPageHandlersFactory(crowi));
+
+  router.put('/:pageId/unpublish', unpublishPageHandlersFactory(crowi));
+
   return router;
   return router;
 };
 };

+ 63 - 0
apps/app/src/server/routes/apiv3/page/unpublish-page.ts

@@ -0,0 +1,63 @@
+import type { IPage, IUserHasId } from '@growi/core';
+import { ErrorV3 } from '@growi/core/dist/models';
+import type { Request, RequestHandler } from 'express';
+import type { ValidationChain } from 'express-validator';
+import { param } from 'express-validator';
+import mongoose from 'mongoose';
+
+import type Crowi from '~/server/crowi';
+import type { PageModel } from '~/server/models/page';
+import loggerFactory from '~/utils/logger';
+
+import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
+import type { ApiV3Response } from '../interfaces/apiv3-response';
+
+
+const logger = loggerFactory('growi:routes:apiv3:page:unpublish-page');
+
+
+type ReqParams = {
+  pageId: string,
+}
+
+interface Req extends Request<ReqParams, ApiV3Response> {
+  user: IUserHasId,
+}
+
+type UnpublishPageHandlersFactory = (crowi: Crowi) => RequestHandler[];
+
+export const unpublishPageHandlersFactory: UnpublishPageHandlersFactory = (crowi) => {
+  const Page = mongoose.model<IPage, PageModel>('Page');
+
+  const accessTokenParser = require('../../../middlewares/access-token-parser')(crowi);
+  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
+
+  // define validators for req.body
+  const validator: ValidationChain[] = [
+    param('pageId').isMongoId().withMessage('The param "pageId" must be specified'),
+  ];
+
+  return [
+    accessTokenParser, loginRequiredStrictly,
+    validator, apiV3FormValidator,
+    async(req: Req, res: ApiV3Response) => {
+      const { pageId } = req.params;
+
+      try {
+        const page = await Page.findById(pageId);
+        if (page == null) {
+          return res.apiv3Err(new ErrorV3(`Page ${pageId} is not exist.`), 404);
+        }
+
+        page.unpublish();
+        const updatedPage = await page.save();
+
+        return res.apiv3(updatedPage);
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(err);
+      }
+    },
+  ];
+};

+ 3 - 2
apps/app/src/server/service/page/index.ts

@@ -4108,8 +4108,9 @@ class PageService implements IPageService {
     const clonedPageData = Page.hydrate(pageData.toObject());
     const clonedPageData = Page.hydrate(pageData.toObject());
     const newPageData = pageData;
     const newPageData = pageData;
 
 
-    // Do not consider it for automatic deletion if updated at least once
-    newPageData.wipExpiredAt = undefined;
+    // If updated at least once, publish
+    pageData.publish();
+
 
 
     // use the previous data if absent
     // use the previous data if absent
     const grant = options.grant ?? clonedPageData.grant;
     const grant = options.grant ?? clonedPageData.grant;