Просмотр исходного кода

Merge pull request #8480 from weseek/feat/140683-publish-wip-pages-from-the-view-screen

feat: Publish WIP pages from the view screen
Shun Miyazawa 2 лет назад
Родитель
Сommit
09cbec41ea

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

@@ -829,6 +829,10 @@
   "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"
+    "fail_save_as_wip": "Failed to save as a WIP page",
+    "alert": "This page is a work in progress",
+    "publish_page": "Publish page",
+    "success_publish_page": "Page has been published",
+    "fail_publish_page": "Failed to publish the Page"
   }
 }

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

@@ -862,6 +862,10 @@
   "wip_page": {
     "save_as_wip": "WIP (執筆中) として保存",
     "success_save_as_wip": "WIP ページとして保存しました",
-    "fail_save_as_wip": "WIP ページとして保存できませんでした"
+    "fail_save_as_wip": "WIP ページとして保存できませんでした",
+    "alert": "このページは作業途中です",
+    "publish_page": "WIP を解除",
+    "success_publish_page": "WIP を解除しました",
+    "fail_publish_page": "WIP を解除できませんでした"
   }
 }

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

@@ -832,6 +832,10 @@
   "wip_page": {
     "save_as_wip": "保存为 WIP(书面)",
     "success_save_as_wip": "成功保存为 WIP 页面",
-    "fail_save_as_wip": "保存为 WIP 页失败"
+    "fail_save_as_wip": "保存为 WIP 页失败",
+    "alert": "本页面正在制作中",
+    "publish_page": "发布 WIP",
+    "success_publish_page": "WIP 已停用",
+    "fail_publish_page": "无法停用 WIP"
   }
 }

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

@@ -161,6 +161,11 @@ export const exist = async(path: string): Promise<PageExistResponse> => {
   return res.data;
 };
 
+export const publish = async(pageId: string): Promise<IPageHasId> => {
+  const res = await apiv3Put(`/page/${pageId}/publish`);
+  return res.data;
+};
+
 export const unpublish = async(pageId: string): Promise<IPageHasId> => {
   const res = await apiv3Put(`/page/${pageId}/unpublish`);
   return res.data;

+ 2 - 0
apps/app/src/components/PageAlert/PageAlerts.tsx

@@ -8,6 +8,7 @@ import { OldRevisionAlert } from './OldRevisionAlert';
 import { PageGrantAlert } from './PageGrantAlert';
 import { PageRedirectedAlert } from './PageRedirectedAlert';
 import { PageStaleAlert } from './PageStaleAlert';
+import { WipPageAlert } from './WipPageAlert';
 
 const FixPageGrantAlert = dynamic(() => import('./FixPageGrantAlert').then(mod => mod.FixPageGrantAlert), { ssr: false });
 // dynamic import because TrashPageAlert uses localStorageMiddleware
@@ -22,6 +23,7 @@ export const PageAlerts = (): JSX.Element => {
       <div className="col-sm-12">
         {/* alerts */}
         { !isNotFound && <FixPageGrantAlert /> }
+        <WipPageAlert />
         <PageGrantAlert />
         <TrashPageAlert />
         <PageStaleAlert />

+ 51 - 0
apps/app/src/components/PageAlert/WipPageAlert.tsx

@@ -0,0 +1,51 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { toastSuccess, toastError } from '~/client/util/toastr';
+import { useSWRMUTxCurrentPage, useSWRxCurrentPage } from '~/stores/page';
+
+import { publish } from '../../client/services/page-operation';
+
+
+export const WipPageAlert = (): JSX.Element => {
+  const { t } = useTranslation();
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
+
+  const clickPagePublishButton = useCallback(async() => {
+    const pageId = currentPage?._id;
+
+    if (pageId == null) {
+      return;
+    }
+
+    try {
+      await publish(pageId);
+      await mutateCurrentPage();
+      toastSuccess(t('wip_page.success_publish_page'));
+    }
+    catch {
+      toastError(t('wip_page.fail_publish_page'));
+    }
+  }, [currentPage?._id, mutateCurrentPage, t]);
+
+
+  if (!currentPage?.wip) {
+    return <></>;
+  }
+
+  return (
+    <p className="d-flex align-items-center alert alert-secondary py-3 px-4">
+      <span className="material-symbols-outlined me-1 fs-5">info</span>
+      <span>{t('wip_page.alert')}</span>
+      <button
+        type="button"
+        className="btn btn-outline-secondary ms-auto"
+        onClick={clickPagePublishButton}
+      >
+        {t('wip_page.publish_page') }
+      </button>
+    </p>
+  );
+};

+ 2 - 1
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 { createPageHandlersFactory } from './create-page';
+import { publishPageHandlersFactory } from './publish-page';
 import { unpublishPageHandlersFactory } from './unpublish-page';
 import { updatePageHandlersFactory } from './update-page';
 
@@ -927,7 +928,7 @@ module.exports = (crowi) => {
     });
 
 
-  // router.put('/:pageId/publish', publishPageHandlersFactory(crowi));
+  router.put('/:pageId/publish', publishPageHandlersFactory(crowi));
 
   router.put('/:pageId/unpublish', unpublishPageHandlersFactory(crowi));
 

+ 62 - 0
apps/app/src/server/routes/apiv3/page/publish-page.ts

@@ -0,0 +1,62 @@
+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 PublishPageHandlersFactory = (crowi: Crowi) => RequestHandler[];
+
+export const publishPageHandlersFactory: PublishPageHandlersFactory = (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.publish();
+        const updatedPage = await page.save();
+        return res.apiv3(updatedPage);
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(err);
+      }
+    },
+  ];
+};