Browse Source

Merge pull request #6161 from weseek/feat/98981-trash-page-alert

create TrashPageAlert
Yuki Takei 3 years ago
parent
commit
94b15bccd1

+ 15 - 7
packages/app/src/components/PageAlert/PageAlerts.tsx

@@ -1,6 +1,13 @@
-import { FixPageGrantAlert } from "./FixPageGrantAlert";
-import { PageGrantAlert } from "./PageGrantAlert";
-import { PageStaleAlert } from "./PageStaleAlert";
+import React from 'react';
+
+import dynamic from 'next/dynamic';
+
+import { FixPageGrantAlert } from './FixPageGrantAlert';
+import { PageGrantAlert } from './PageGrantAlert';
+import { PageStaleAlert } from './PageStaleAlert';
+
+// dynamic import because TrashPageAlert uses localStorageMiddleware
+const TrashPageAlert = dynamic(() => import('./TrashPageAlert').then(mod => mod.TrashPageAlert), { ssr: false });
 
 
 export const PageAlerts = (): JSX.Element => {
 export const PageAlerts = (): JSX.Element => {
 
 
@@ -9,10 +16,11 @@ export const PageAlerts = (): JSX.Element => {
     <div className="row d-edit-none">
     <div className="row d-edit-none">
       <div className="col-sm-12">
       <div className="col-sm-12">
         {/* alerts */}
         {/* alerts */}
-        <FixPageGrantAlert/>
-        <PageGrantAlert/>
-        <PageStaleAlert/>
+        <FixPageGrantAlert />
+        <PageGrantAlert />
+        <TrashPageAlert />
+        <PageStaleAlert />
       </div>
       </div>
     </div>
     </div>
   );
   );
-}
+};

+ 111 - 0
packages/app/src/components/PageAlert/TrashPageAlert.tsx

@@ -0,0 +1,111 @@
+import React from 'react';
+
+import { UserPicture } from '@growi/ui';
+import { format } from 'date-fns';
+import { useTranslation } from 'react-i18next';
+
+import {
+  useCurrentUpdatedAt, useIsTrashPage, useShareLinkId,
+} from '~/stores/context';
+import { usePageDeleteModal, usePutBackPageModal } from '~/stores/modal';
+import { useSWRxPageInfo, useSWRxCurrentPage } from '~/stores/page';
+import { useIsAbleToShowTrashPageManagementButtons } from '~/stores/ui';
+
+const onDeletedHandler = (pathOrPathsToDelete) => {
+  if (typeof pathOrPathsToDelete !== 'string') {
+    return;
+  }
+
+  window.location.href = '/';
+};
+
+export const TrashPageAlert = (): JSX.Element => {
+  const { t } = useTranslation();
+
+  const { data: isAbleToShowTrashPageManagementButtons } = useIsAbleToShowTrashPageManagementButtons();
+  const { data: shareLinkId } = useShareLinkId();
+  const { data: pageData } = useSWRxCurrentPage();
+  const { data: isTrashPage } = useIsTrashPage();
+  const pageId = pageData?._id;
+  const pagePath = pageData?.path;
+  const { data: pageInfo } = useSWRxPageInfo(pageId ?? null, shareLinkId);
+
+  const { data: updatedAt } = useCurrentUpdatedAt();
+
+  const { open: openDeleteModal } = usePageDeleteModal();
+  const { open: openPutBackPageModal } = usePutBackPageModal();
+
+  const lastUpdateUserName = pageData?.lastUpdateUser.name;
+  const deletedAt = pageData?.deletedAt ? format(new Date(pageData?.deletedAt), 'yyyy/MM/dd HH:mm') : '';
+  const revisionId = pageData?.revision._id;
+
+  if (!isTrashPage) {
+    return <></>;
+  }
+
+  function openPutbackPageModalHandler() {
+    if (pageId === undefined || pagePath === undefined) {
+      return;
+    }
+    const putBackedHandler = () => {
+      window.location.reload();
+    };
+    openPutBackPageModal({ pageId, path: pagePath }, { onPutBacked: putBackedHandler });
+  }
+
+  function openPageDeleteModalHandler() {
+    if (pageId === undefined || revisionId === undefined || pagePath === undefined) {
+      return;
+    }
+    const pageToDelete = {
+      data: {
+        _id: pageId,
+        revision: revisionId,
+        path: pagePath,
+      },
+      meta: pageInfo,
+    };
+    openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
+  }
+
+  function renderTrashPageManagementButtons() {
+    return (
+      <>
+        <button
+          type="button"
+          className="btn btn-info rounded-pill btn-sm ml-auto mr-2"
+          onClick={openPutbackPageModalHandler}
+          data-toggle="modal"
+        >
+          <i className="icon-action-undo" aria-hidden="true"></i> { t('Put Back') }
+        </button>
+        <button
+          type="button"
+          className="btn btn-danger rounded-pill btn-sm"
+          disabled={!(pageInfo?.isAbleToDeleteCompletely ?? false)}
+          onClick={openPageDeleteModalHandler}
+        >
+          <i className="icon-fire" aria-hidden="true"></i> { t('Delete Completely') }
+        </button>
+      </>
+    );
+  }
+
+  return (
+    <>
+      <div className="alert alert-warning py-3 pl-4 d-flex flex-column flex-lg-row">
+        <div className="flex-grow-1">
+          This page is in the trash <i className="icon-trash" aria-hidden="true"></i>.
+          <br />
+          <UserPicture user={{ username: lastUpdateUserName }} />
+          <span className="ml-2">
+            Deleted by { lastUpdateUserName } at {deletedAt || updatedAt}
+          </span>
+        </div>
+        <div className="pt-1 d-flex align-items-end align-items-lg-center">
+          { isAbleToShowTrashPageManagementButtons && renderTrashPageManagementButtons()}
+        </div>
+      </div>
+    </>
+  );
+};

+ 15 - 13
packages/app/src/pages/[[...path]].page.tsx

@@ -1,6 +1,7 @@
 import React, { useEffect } from 'react';
 import React, { useEffect } from 'react';
 
 
 import { pagePathUtils } from '@growi/core';
 import { pagePathUtils } from '@growi/core';
+import { isValidObjectId } from 'mongoose';
 import {
 import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 } from 'next';
@@ -16,10 +17,11 @@ import { CrowiRequest } from '~/interfaces/crowi-request';
 // import { useRendererSettings } from '~/stores/renderer';
 // import { useRendererSettings } from '~/stores/renderer';
 // import { EditorMode, useEditorMode, useIsMobile } from '~/stores/ui';
 // import { EditorMode, useEditorMode, useIsMobile } from '~/stores/ui';
 import { IPageWithMeta } from '~/interfaces/page';
 import { IPageWithMeta } from '~/interfaces/page';
+import { PageModel } from '~/server/models/page';
 import { serializeUserSecurely } from '~/server/models/serializers/user-serializer';
 import { serializeUserSecurely } from '~/server/models/serializers/user-serializer';
-import { useSWRxCurrentPage, useSWRxPageInfo } from '~/stores/page';
-import loggerFactory from '~/utils/logger';
 import Xss from '~/services/xss';
 import Xss from '~/services/xss';
+import { useSWRxCurrentPage, useSWRxPage, useSWRxPageInfo } from '~/stores/page';
+import loggerFactory from '~/utils/logger';
 
 
 // import { isUserPage, isTrashPage, isSharedPage } from '~/utils/path-utils';
 // import { isUserPage, isTrashPage, isSharedPage } from '~/utils/path-utils';
 
 
@@ -39,14 +41,13 @@ import {
   useAppTitle, useSiteUrl, useConfidential, useIsEnabledStaleNotification,
   useAppTitle, useSiteUrl, useConfidential, useIsEnabledStaleNotification,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsMailerSetup,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsMailerSetup,
   useAclEnabled, useHasSlackConfig, useDrawioUri, useHackmdUri, useMathJax, useNoCdn, useEditorConfig, useCsrfToken,
   useAclEnabled, useHasSlackConfig, useDrawioUri, useHackmdUri, useMathJax, useNoCdn, useEditorConfig, useCsrfToken,
-  useCurrentPageId
+  useCurrentPageId,
 } from '../stores/context';
 } from '../stores/context';
-
-import { useXss } from '../stores/xss'
+import { useXss } from '../stores/xss';
 
 
 import { CommonProps, getServerSideCommonProps, useCustomTitle } from './commons';
 import { CommonProps, getServerSideCommonProps, useCustomTitle } from './commons';
-import { PageModel } from '~/server/models/page';
-import { isValidObjectId } from 'mongoose';
+
+
 // import { useCurrentPageSWR } from '../stores/page';
 // import { useCurrentPageSWR } from '../stores/page';
 
 
 
 
@@ -95,7 +96,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   // commons
   // commons
   useAppTitle(props.appTitle);
   useAppTitle(props.appTitle);
   useSiteUrl(props.siteUrl);
   useSiteUrl(props.siteUrl);
-  useXss(new Xss())
+  useXss(new Xss());
   // useEditorConfig(props.editorConfig);
   // useEditorConfig(props.editorConfig);
   useConfidential(props.confidential);
   useConfidential(props.confidential);
   useCsrfToken(props.csrfToken);
   useCsrfToken(props.csrfToken);
@@ -105,7 +106,6 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   // useOwnerOfCurrentPage(props.pageUser != null ? JSON.parse(props.pageUser) : null);
   // useOwnerOfCurrentPage(props.pageUser != null ? JSON.parse(props.pageUser) : null);
   // useIsForbidden(props.isForbidden);
   // useIsForbidden(props.isForbidden);
   // useNotFound(props.isNotFound);
   // useNotFound(props.isNotFound);
-  // useIsTrashPage(_isTrashPage(props.currentPagePath));
   // useShared(isSharedPage(props.currentPagePath));
   // useShared(isSharedPage(props.currentPagePath));
   // useShareLinkId(props.shareLinkId);
   // useShareLinkId(props.shareLinkId);
   // useIsAbleToDeleteCompletely(props.isAbleToDeleteCompletely);
   // useIsAbleToDeleteCompletely(props.isAbleToDeleteCompletely);
@@ -136,9 +136,11 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   if (props.pageWithMetaStr != null) {
   if (props.pageWithMetaStr != null) {
     pageWithMeta = JSON.parse(props.pageWithMetaStr) as IPageWithMeta;
     pageWithMeta = JSON.parse(props.pageWithMetaStr) as IPageWithMeta;
   }
   }
-  useCurrentPageId(pageWithMeta?.data._id)
+  useCurrentPageId(pageWithMeta?.data._id);
   useSWRxCurrentPage(undefined, pageWithMeta?.data); // store initial data
   useSWRxCurrentPage(undefined, pageWithMeta?.data); // store initial data
+  // useSWRxPage(pageWithMeta?.data._id);
   useSWRxPageInfo(pageWithMeta?.data._id, undefined, pageWithMeta?.meta); // store initial data
   useSWRxPageInfo(pageWithMeta?.data._id, undefined, pageWithMeta?.meta); // store initial data
+  useIsTrashPage(_isTrashPage(pageWithMeta?.data.path ?? ''));
 
 
   const classNames: string[] = [];
   const classNames: string[] = [];
   // switch (editorMode) {
   // switch (editorMode) {
@@ -232,15 +234,15 @@ async function injectPageInformation(context: GetServerSidePropsContext, props:
   const { currentPathname } = props;
   const { currentPathname } = props;
 
 
   // determine pageId
   // determine pageId
-  let pageId;
   const pageIdStr = currentPathname.substring(1);
   const pageIdStr = currentPathname.substring(1);
-  pageId = isValidObjectId(pageIdStr) ? pageIdStr : null;
+  const pageId = isValidObjectId(pageIdStr) ? pageIdStr : null;
 
 
   const result: IPageWithMeta = await pageService.findPageAndMetaDataByViewer(pageId, currentPathname, user, true); // includeEmpty = true, isSharedPage = false
   const result: IPageWithMeta = await pageService.findPageAndMetaDataByViewer(pageId, currentPathname, user, true); // includeEmpty = true, isSharedPage = false
   const page = result.data;
   const page = result.data;
 
 
+
   if (page == null) {
   if (page == null) {
-    const count = pageId != null ?  await Page.count({ _id: pageId }) : await Page.count({ path: currentPathname }) ;
+    const count = pageId != null ? await Page.count({ _id: pageId }) : await Page.count({ path: currentPathname });
     // check the page is forbidden or just does not exist.
     // check the page is forbidden or just does not exist.
     props.isForbidden = count > 0;
     props.isForbidden = count > 0;
     props.isNotFound = true;
     props.isNotFound = true;

+ 3 - 3
packages/app/src/stores/ui.tsx

@@ -19,7 +19,7 @@ import loggerFactory from '~/utils/logger';
 
 
 import {
 import {
   useCurrentPageId, useCurrentPagePath, useIsEditable, useIsTrashPage, useIsUserPage, useIsGuestUser, useEmptyPageId,
   useCurrentPageId, useCurrentPagePath, useIsEditable, useIsTrashPage, useIsUserPage, useIsGuestUser, useEmptyPageId,
-  useIsNotCreatable, useIsSharedUser, useNotFoundTargetPathOrId, useIsForbidden, useIsIdenticalPath, useIsNotFoundPermalink, useCurrentUser, useIsDeleted,
+  useIsNotCreatable, useIsSharedUser, useNotFoundTargetPathOrId, useIsForbidden, useIsIdenticalPath, useIsNotFoundPermalink, useCurrentUser,
 } from './context';
 } from './context';
 import { localStorageMiddleware } from './middlewares/sync-to-storage';
 import { localStorageMiddleware } from './middlewares/sync-to-storage';
 import { useStaticSWR } from './use-static-swr';
 import { useStaticSWR } from './use-static-swr';
@@ -396,9 +396,9 @@ export const usePageTreeDescCountMap = (initialData?: UpdateDescCountData): SWRR
 
 
 export const useIsAbleToShowTrashPageManagementButtons = (): SWRResponse<boolean, Error> => {
 export const useIsAbleToShowTrashPageManagementButtons = (): SWRResponse<boolean, Error> => {
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
-  const { data: isDeleted } = useIsDeleted();
+  const { data: isTrashPage } = useIsTrashPage();
 
 
-  return useStaticSWR('isAbleToShowTrashPageManagementButtons', isDeleted && currentUser != null);
+  return useStaticSWR('isAbleToShowTrashPageManagementButtons', isTrashPage && currentUser != null);
 };
 };
 
 
 export const useIsAbleToShowPageManagement = (): SWRResponse<boolean, Error> => {
 export const useIsAbleToShowPageManagement = (): SWRResponse<boolean, Error> => {