Răsfoiți Sursa

Merge pull request #5967 from weseek/feat/fire-resume-rename-operation-from-front

feat: Fire resume rename operation from client side
Yohei Shiina 3 ani în urmă
părinte
comite
c664c704ea

+ 4 - 0
packages/app/resource/locales/en_US/translation.json

@@ -1111,5 +1111,9 @@
     "receive_notifications": "Receive Notifications",
     "receive_notifications": "Receive Notifications",
     "stop_notification": "Stop Notification",
     "stop_notification": "Stop Notification",
     "footprints": "Footprints"
     "footprints": "Footprints"
+  },
+  "page_operation":{
+    "paths_recovered": "Paths recovered successfully",
+    "path_recovery_failed":"Path recovery failed"
   }
   }
 }
 }

+ 4 - 0
packages/app/resource/locales/ja_JP/translation.json

@@ -1104,5 +1104,9 @@
     "receive_notifications": "通知を受け取る",
     "receive_notifications": "通知を受け取る",
     "stop_notification": "通知を止める",
     "stop_notification": "通知を止める",
     "footprints": "足跡"
     "footprints": "足跡"
+  },
+  "page_operation":{
+    "paths_recovered": "パスを修復しました",
+    "path_recovery_failed":"パスを修復できませんでした"
   }
   }
 }
 }

+ 4 - 0
packages/app/resource/locales/zh_CN/translation.json

@@ -1114,5 +1114,9 @@
     "receive_notifications": "接收通知",
     "receive_notifications": "接收通知",
     "stop_notification": "停止通知",
     "stop_notification": "停止通知",
     "footprints": "脚印"
     "footprints": "脚印"
+  },
+  "page_operation":{
+    "paths_recovered": "成功恢复了页面路径",
+    "path_recovery_failed":"路径恢复失败"
   }
   }
 }
 }

+ 9 - 1
packages/app/src/client/services/page-operation.ts

@@ -3,7 +3,8 @@ import urljoin from 'url-join';
 import { SubscriptionStatusType } from '~/interfaces/subscription';
 import { SubscriptionStatusType } from '~/interfaces/subscription';
 
 
 import { toastError } from '../util/apiNotification';
 import { toastError } from '../util/apiNotification';
-import { apiv3Put } from '../util/apiv3-client';
+import { apiv3Post, apiv3Put } from '../util/apiv3-client';
+
 
 
 export const toggleSubscribe = async(pageId: string, currentStatus: SubscriptionStatusType | undefined): Promise<void> => {
 export const toggleSubscribe = async(pageId: string, currentStatus: SubscriptionStatusType | undefined): Promise<void> => {
   try {
   try {
@@ -60,3 +61,10 @@ export const exportAsMarkdown = (pageId: string, revisionId: string, format: str
   url.searchParams.append('revisionId', revisionId);
   url.searchParams.append('revisionId', revisionId);
   window.location.href = url.href;
   window.location.href = url.href;
 };
 };
+
+/**
+ * send request to fix broken paths caused by unexpected events such as server shutdown while renaming page paths
+ */
+export const resumeRenameOperation = async(pageId: string): Promise<void> => {
+  await apiv3Post('/pages/resume-rename', { pageId });
+};

+ 14 - 2
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -133,6 +133,10 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
     const showDeviderBeforeAdditionalMenuItems = (forceHideMenuItems?.length ?? 0) < 3;
     const showDeviderBeforeAdditionalMenuItems = (forceHideMenuItems?.length ?? 0) < 3;
     const showDeviderBeforeDelete = AdditionalMenuItems != null || showDeviderBeforeAdditionalMenuItems;
     const showDeviderBeforeDelete = AdditionalMenuItems != null || showDeviderBeforeAdditionalMenuItems;
 
 
+    // PathRecovery
+    // Todo: It is wanted to find a better way to pass operationProcessData to PageItemControl
+    const shouldShowPathRecoveryButton = operationProcessData?.Rename != null ? operationProcessData?.Rename.isProcessable : false;
+
     contents = (
     contents = (
       <>
       <>
         { !isEnableActions && (
         { !isEnableActions && (
@@ -197,7 +201,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
         ) }
         ) }
 
 
         {/* PathRecovery */}
         {/* PathRecovery */}
-        { !forceHideMenuItems?.includes(MenuItemType.PATH_RECOVERY) && isEnableActions && operationProcessData?.Rename != null && (
+        { !forceHideMenuItems?.includes(MenuItemType.PATH_RECOVERY) && isEnableActions && shouldShowPathRecoveryButton && (
           <DropdownItem
           <DropdownItem
             onClick={pathRecoveryItemClickedHandler}
             onClick={pathRecoveryItemClickedHandler}
             className="grw-page-control-dropdown-item"
             className="grw-page-control-dropdown-item"
@@ -247,7 +251,7 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
   const {
   const {
     pageId, pageInfo: presetPageInfo, fetchOnInit,
     pageId, pageInfo: presetPageInfo, fetchOnInit,
     children,
     children,
-    onClickBookmarkMenuItem, onClickRenameMenuItem, onClickDuplicateMenuItem, onClickDeleteMenuItem,
+    onClickBookmarkMenuItem, onClickRenameMenuItem, onClickDuplicateMenuItem, onClickDeleteMenuItem, onClickPathRecoveryMenuItem,
   } = props;
   } = props;
 
 
   const [isOpen, setIsOpen] = useState(false);
   const [isOpen, setIsOpen] = useState(false);
@@ -299,6 +303,13 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
     await onClickDeleteMenuItem(pageId, fetchedPageInfo ?? presetPageInfo);
     await onClickDeleteMenuItem(pageId, fetchedPageInfo ?? presetPageInfo);
   }, [onClickDeleteMenuItem, pageId, fetchedPageInfo, presetPageInfo]);
   }, [onClickDeleteMenuItem, pageId, fetchedPageInfo, presetPageInfo]);
 
 
+  const pathRecoveryMenuItemClickHandler = useCallback(async() => {
+    if (onClickPathRecoveryMenuItem == null) {
+      return;
+    }
+    await onClickPathRecoveryMenuItem(pageId);
+  }, [onClickPathRecoveryMenuItem, pageId]);
+
   return (
   return (
     <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)} data-testid="open-page-item-control-btn">
     <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)} data-testid="open-page-item-control-btn">
       { children ?? (
       { children ?? (
@@ -315,6 +326,7 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
         onClickRenameMenuItem={renameMenuItemClickHandler}
         onClickRenameMenuItem={renameMenuItemClickHandler}
         onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
         onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
         onClickDeleteMenuItem={deleteMenuItemClickHandler}
         onClickDeleteMenuItem={deleteMenuItemClickHandler}
+        onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
       />
       />
     </Dropdown>
     </Dropdown>
   );
   );

+ 21 - 1
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next';
 import { DropdownToggle } from 'reactstrap';
 import { DropdownToggle } from 'reactstrap';
 
 
 
 
-import { bookmark, unbookmark } from '~/client/services/page-operation';
+import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/page-operation';
 import { toastWarning, toastError, toastSuccess } from '~/client/util/apiNotification';
 import { toastWarning, toastError, toastSuccess } from '~/client/util/apiNotification';
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
 import TriangleIcon from '~/components/Icons/TriangleIcon';
 import TriangleIcon from '~/components/Icons/TriangleIcon';
@@ -371,6 +371,25 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
     return null;
     return null;
   };
   };
 
 
+  /**
+   * Users do not need to know if all pages have been renamed.
+   * Make resuming rename operation appears to be working fine to allow users for a seamless operation.
+   */
+  const pathRecoveryMenuItemClickHandler = async(pageId: string): Promise<void> => {
+    try {
+      setRenaming(true);
+      await resumeRenameOperation(pageId);
+
+      if (onRenamed != null) {
+        onRenamed();
+      }
+
+      toastSuccess(t('page_operation.paths_recovered'));
+    }
+    catch {
+      toastError(t('page_operation.path_recovery_failed'));
+    }
+  };
 
 
   // didMount
   // didMount
   useEffect(() => {
   useEffect(() => {
@@ -456,6 +475,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
             onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
             onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
             onClickRenameMenuItem={renameMenuItemClickHandler}
             onClickRenameMenuItem={renameMenuItemClickHandler}
             onClickDeleteMenuItem={deleteMenuItemClickHandler}
             onClickDeleteMenuItem={deleteMenuItemClickHandler}
+            onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
             isInstantRename
             isInstantRename
             operationProcessData={page.processData}
             operationProcessData={page.processData}
           >
           >

+ 4 - 2
packages/app/src/server/routes/apiv3/pages.js

@@ -565,7 +565,9 @@ module.exports = (crowi) => {
     // The user has permission to resume rename operation if page is returned.
     // The user has permission to resume rename operation if page is returned.
     const page = await Page.findByIdAndViewer(pageId, user, null, true);
     const page = await Page.findByIdAndViewer(pageId, user, null, true);
     if (page == null) {
     if (page == null) {
-      return res.apiv3Err(new ErrorV3('The operation is forbidden for this user.'), 403);
+      const msg = 'The operation is forbidden for this user';
+      const code = 'forbidden-user';
+      return res.apiv3Err(new ErrorV3(msg, code), 403);
     }
     }
 
 
     try {
     try {
@@ -573,7 +575,7 @@ module.exports = (crowi) => {
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
-      return res.apiv3Err(new ErrorV3(`Failed to resume rename operation. ${err}`), 500);
+      return res.apiv3Err(err, 500);
     }
     }
     return res.apiv3();
     return res.apiv3();
   });
   });