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

Merge pull request #8792 from weseek/feat/78037-135787-download-bulk-export-from-in-app-notification-FE

Feat/78037 135787 download bulk export from in app notification fe
Yuki Takei 1 год назад
Родитель
Сommit
105adb8356
18 измененных файлов с 214 добавлено и 69 удалено
  1. 2 1
      apps/app/public/static/locales/en_US/translation.json
  2. 8 2
      apps/app/public/static/locales/fr_FR/translation.json
  3. 2 1
      apps/app/public/static/locales/ja_JP/translation.json
  4. 2 1
      apps/app/public/static/locales/zh_CN/translation.json
  5. 30 19
      apps/app/src/components/InAppNotification/InAppNotificationElm.tsx
  6. 13 6
      apps/app/src/components/InAppNotification/ModelNotification/ModelNotification.tsx
  7. 62 0
      apps/app/src/components/InAppNotification/ModelNotification/PageBulkExportJobModelNotification.tsx
  8. 2 8
      apps/app/src/components/InAppNotification/ModelNotification/PageModelNotification.tsx
  9. 2 1
      apps/app/src/components/InAppNotification/ModelNotification/UserModelNotification.tsx
  10. 31 0
      apps/app/src/components/InAppNotification/ModelNotification/index.tsx
  11. 8 0
      apps/app/src/components/InAppNotification/ModelNotification/useActionAndMsg.ts
  12. 0 19
      apps/app/src/components/InAppNotification/PageNotification/index.tsx
  13. 3 0
      apps/app/src/features/page-bulk-export/interfaces/page-bulk-export.ts
  14. 1 1
      apps/app/src/interfaces/in-app-notification.ts
  15. 25 0
      apps/app/src/models/serializers/in-app-notification-snapshot/page-bulk-export-job.ts
  16. 3 4
      apps/app/src/server/models/in-app-notification.ts
  17. 10 5
      apps/app/src/server/service/in-app-notification.ts
  18. 10 1
      apps/app/src/server/service/in-app-notification/in-app-notification-utils.ts

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

@@ -629,7 +629,8 @@
     "bulk_export_notice": "Once a download link is ready, a notification will be sent. If the number of pages is large, it may take a while for preparation.",
     "bulk_export_notice": "Once a download link is ready, a notification will be sent. If the number of pages is large, it may take a while for preparation.",
     "markdown": "Markdown",
     "markdown": "Markdown",
     "choose_export_format": "Select export format",
     "choose_export_format": "Select export format",
-    "bulk_export_started": "Please wait a moment..."
+    "bulk_export_started": "Please wait a moment...",
+    "bulk_export_download_expired": "Download period has expired"
   },
   },
   "message": {
   "message": {
     "successfully_connected": "Successfully Connected!",
     "successfully_connected": "Successfully Connected!",

+ 8 - 2
apps/app/public/static/locales/fr_FR/translation.json

@@ -620,11 +620,17 @@
     "discription_heading": "Créer un compte",
     "discription_heading": "Créer un compte",
     "discription": "Créer un compte avec votre adresse courriel invitée"
     "discription": "Créer un compte avec votre adresse courriel invitée"
   },
   },
-  "export_bulk": {
+  "page_export": {
     "failed_to_export": "Échec de l'export",
     "failed_to_export": "Échec de l'export",
     "failed_to_count_pages": "Échec du compte des pages",
     "failed_to_count_pages": "Échec du compte des pages",
     "export_page_markdown": "Exporter la page en Markdown",
     "export_page_markdown": "Exporter la page en Markdown",
-    "export_page_pdf": "Exporter la page en PDF"
+    "export_page_pdf": "Exporter la page en PDF",
+    "bulk_export": "Exporter la page et toutes les pages enfants",
+    "bulk_export_notice": "Une fois qu'un lien de téléchargement est prêt, une notification sera envoyée. Si le nombre de pages est important, la préparation peut prendre un certain temps.",
+    "markdown": "Markdown",
+    "choose_export_format": "Sélectionnez le format d'exportation",
+    "bulk_export_started": "Patientez s'il-vous-plait...",
+    "bulk_export_download_expired": "La période de téléchargement a expiré"
   },
   },
   "message": {
   "message": {
     "successfully_connected": "Connecté!",
     "successfully_connected": "Connecté!",

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

@@ -662,7 +662,8 @@
     "bulk_export_notice": "ダウンロードの準備が完了すると、通知が届きます。ページ数が多いと、準備に時間がかかる場合があります。",
     "bulk_export_notice": "ダウンロードの準備が完了すると、通知が届きます。ページ数が多いと、準備に時間がかかる場合があります。",
     "markdown": "マークダウン",
     "markdown": "マークダウン",
     "choose_export_format": "エクスポート形式を選択してください",
     "choose_export_format": "エクスポート形式を選択してください",
-    "bulk_export_started": "ただいま準備中です..."
+    "bulk_export_started": "ただいま準備中です...",
+    "bulk_export_download_expired": "ダウンロード期限が切れました"
   },
   },
   "message": {
   "message": {
     "successfully_connected": "接続に成功しました!",
     "successfully_connected": "接続に成功しました!",

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

@@ -632,7 +632,8 @@
     "bulk_export_notice": "下载链接准备好后,将发送通知。如果页数较多,则可能需要一段时间准备。",
     "bulk_export_notice": "下载链接准备好后,将发送通知。如果页数较多,则可能需要一段时间准备。",
     "markdown": "Markdown",
     "markdown": "Markdown",
     "choose_export_format": "选择导出格式",
     "choose_export_format": "选择导出格式",
-    "bulk_export_started": "目前我们正在准备..."
+    "bulk_export_started": "目前我们正在准备...",
+    "bulk_export_download_expired": "下载期限已过"
   },
   },
   "message": {
   "message": {
     "successfully_connected": "连接成功!",
     "successfully_connected": "连接成功!",

+ 30 - 19
apps/app/src/components/InAppNotification/InAppNotificationElm.tsx

@@ -1,12 +1,14 @@
-import React, { FC } from 'react';
+import type { FC } from 'react';
+import React from 'react';
 
 
 import type { HasObjectId } from '@growi/core';
 import type { HasObjectId } from '@growi/core';
 import { UserPicture } from '@growi/ui/dist/components';
 import { UserPicture } from '@growi/ui/dist/components';
 
 
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
-import { IInAppNotification, InAppNotificationStatuses } from '~/interfaces/in-app-notification';
+import type { IInAppNotification } from '~/interfaces/in-app-notification';
+import { InAppNotificationStatuses } from '~/interfaces/in-app-notification';
 
 
-import { useModelNotification } from './PageNotification';
+import { useModelNotification } from './ModelNotification';
 
 
 interface Props {
 interface Props {
   notification: IInAppNotification & HasObjectId
   notification: IInAppNotification & HasObjectId
@@ -21,8 +23,10 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
 
 
   const Notification = modelNotificationUtils?.Notification;
   const Notification = modelNotificationUtils?.Notification;
   const publishOpen = modelNotificationUtils?.publishOpen;
   const publishOpen = modelNotificationUtils?.publishOpen;
+  const clickLink = modelNotificationUtils?.clickLink;
+  const isDisabled = modelNotificationUtils?.isDisabled;
 
 
-  if (Notification == null || publishOpen == null) {
+  if (Notification == null) {
     return <></>;
     return <></>;
   }
   }
 
 
@@ -33,7 +37,9 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
       onUnopenedNotificationOpend?.();
       onUnopenedNotificationOpend?.();
     }
     }
 
 
-    publishOpen();
+    if (isDisabled) return;
+
+    publishOpen?.();
   };
   };
 
 
   const renderActionUserPictures = (): JSX.Element => {
   const renderActionUserPictures = (): JSX.Element => {
@@ -56,21 +62,26 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
   };
   };
 
 
   return (
   return (
-    <div className="list-group-item list-group-item-action" onClick={() => clickHandler(notification)} style={{ cursor: 'pointer' }}>
-      <div className="d-flex align-items-center">
-        <span
-          className={`${notification.status === InAppNotificationStatuses.STATUS_UNOPENED
-            ? 'grw-unopend-notification'
-            : 'ms-2'
-          } rounded-circle me-3`}
-        >
-        </span>
-
-        {renderActionUserPictures()}
-
-        <Notification />
+    <div className="list-group-item list-group-item-action" style={{ cursor: 'pointer' }}>
+      <a
+        href={isDisabled ? undefined : clickLink}
+        onClick={() => clickHandler(notification)}
+      >
+        <div className="d-flex align-items-center">
+          <span
+            className={`${notification.status === InAppNotificationStatuses.STATUS_UNOPENED
+              ? 'grw-unopend-notification'
+              : 'ms-2'
+            } rounded-circle me-3`}
+          >
+          </span>
+
+          {renderActionUserPictures()}
+
+          <Notification />
 
 
-      </div>
+        </div>
+      </a>
     </div>
     </div>
   );
   );
 };
 };

+ 13 - 6
apps/app/src/components/InAppNotification/PageNotification/ModelNotification.tsx → apps/app/src/components/InAppNotification/ModelNotification/ModelNotification.tsx

@@ -13,20 +13,27 @@ type Props = {
   actionMsg: string
   actionMsg: string
   actionIcon: string
   actionIcon: string
   actionUsers: string
   actionUsers: string
+  hideActionUsers?: boolean
+  subMsg?: JSX.Element
 };
 };
 
 
-export const ModelNotification: FC<Props> = (props) => {
-  const {
-    notification, actionMsg, actionIcon, actionUsers,
-  } = props;
+export const ModelNotification: FC<Props> = ({
+  notification,
+  actionMsg,
+  actionIcon,
+  actionUsers,
+  hideActionUsers = false,
+  subMsg,
+}: Props) => {
 
 
   return (
   return (
     <div className="p-2 overflow-hidden">
     <div className="p-2 overflow-hidden">
       <div className="text-truncate">
       <div className="text-truncate">
-        <b>{actionUsers}</b>
-        {actionMsg}
+        {hideActionUsers ? <></> : <b>{actionUsers}</b>}
+        {` ${actionMsg}`}
         <PagePathLabel path={notification.parsedSnapshot?.path ?? ''} />
         <PagePathLabel path={notification.parsedSnapshot?.path ?? ''} />
       </div>
       </div>
+      { subMsg }
       <span className="material-symbols-outlined me-2">{actionIcon}</span>
       <span className="material-symbols-outlined me-2">{actionIcon}</span>
       <FormattedDistanceDate
       <FormattedDistanceDate
         id={notification._id}
         id={notification._id}

+ 62 - 0
apps/app/src/components/InAppNotification/ModelNotification/PageBulkExportJobModelNotification.tsx

@@ -0,0 +1,62 @@
+import React from 'react';
+
+import { isPopulated, type HasObjectId } from '@growi/core';
+import { useTranslation } from 'react-i18next';
+
+import type { IPageBulkExportJobHasId } from '~/features/page-bulk-export/interfaces/page-bulk-export';
+import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
+import type { IInAppNotification } from '~/interfaces/in-app-notification';
+import * as pageBulkExportJobSerializers from '~/models/serializers/in-app-notification-snapshot/page-bulk-export-job';
+
+import { ModelNotification } from './ModelNotification';
+import { useActionMsgAndIconForModelNotification } from './useActionAndMsg';
+
+import type { ModelNotificationUtils } from '.';
+
+
+export const usePageBulkExportJobModelNotification = (notification: IInAppNotification & HasObjectId): ModelNotificationUtils | null => {
+
+  const { t } = useTranslation();
+  const { actionMsg, actionIcon } = useActionMsgAndIconForModelNotification(notification);
+
+  const isPageBulkExportJobModelNotification = (
+      notification: IInAppNotification & HasObjectId,
+  ): notification is IInAppNotification<IPageBulkExportJobHasId> & HasObjectId => {
+    return notification.targetModel === SupportedTargetModel.MODEL_PAGE_BULK_EXPORT_JOB;
+  };
+
+  if (!isPageBulkExportJobModelNotification(notification)) {
+    return null;
+  }
+
+  const actionUsers = notification.user.username;
+
+  notification.parsedSnapshot = pageBulkExportJobSerializers.parseSnapshot(notification.snapshot);
+
+  const subMsg = (notification.action === SupportedAction.ACTION_PAGE_BULK_EXPORT_COMPLETED && notification.target == null)
+    ? <div className="text-danger"><small>{t('page_export.bulk_export_download_expired')}</small></div> : <></>;
+
+  const Notification = () => {
+    return (
+      <ModelNotification
+        notification={notification}
+        actionMsg={actionMsg}
+        actionIcon={actionIcon}
+        actionUsers={actionUsers}
+        hideActionUsers
+        subMsg={subMsg}
+      />
+    );
+  };
+
+  const clickLink = (notification.action === SupportedAction.ACTION_PAGE_BULK_EXPORT_COMPLETED
+    && notification.target?.attachment != null && isPopulated(notification.target?.attachment))
+    ? notification.target.attachment.downloadPathProxied : undefined;
+
+  return {
+    Notification,
+    clickLink,
+    isDisabled: notification.target == null,
+  };
+
+};

+ 2 - 8
apps/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx → apps/app/src/components/InAppNotification/ModelNotification/PageModelNotification.tsx

@@ -1,6 +1,4 @@
-import React, {
-  FC, useCallback,
-} from 'react';
+import React, { useCallback } from 'react';
 
 
 import type { IPage, HasObjectId } from '@growi/core';
 import type { IPage, HasObjectId } from '@growi/core';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
@@ -12,11 +10,7 @@ import * as pageSerializers from '~/models/serializers/in-app-notification-snaps
 import { ModelNotification } from './ModelNotification';
 import { ModelNotification } from './ModelNotification';
 import { useActionMsgAndIconForModelNotification } from './useActionAndMsg';
 import { useActionMsgAndIconForModelNotification } from './useActionAndMsg';
 
 
-
-export interface ModelNotificationUtils {
-  Notification: FC
-  publishOpen: () => void
-}
+import type { ModelNotificationUtils } from '.';
 
 
 export const usePageModelNotification = (notification: IInAppNotification & HasObjectId): ModelNotificationUtils | null => {
 export const usePageModelNotification = (notification: IInAppNotification & HasObjectId): ModelNotificationUtils | null => {
 
 

+ 2 - 1
apps/app/src/components/InAppNotification/PageNotification/UserModelNotification.tsx → apps/app/src/components/InAppNotification/ModelNotification/UserModelNotification.tsx

@@ -7,9 +7,10 @@ import { SupportedTargetModel } from '~/interfaces/activity';
 import type { IInAppNotification } from '~/interfaces/in-app-notification';
 import type { IInAppNotification } from '~/interfaces/in-app-notification';
 
 
 import { ModelNotification } from './ModelNotification';
 import { ModelNotification } from './ModelNotification';
-import { ModelNotificationUtils } from './PageModelNotification';
 import { useActionMsgAndIconForModelNotification } from './useActionAndMsg';
 import { useActionMsgAndIconForModelNotification } from './useActionAndMsg';
 
 
+import type { ModelNotificationUtils } from '.';
+
 
 
 export const useUserModelNotification = (notification: IInAppNotification & HasObjectId): ModelNotificationUtils | null => {
 export const useUserModelNotification = (notification: IInAppNotification & HasObjectId): ModelNotificationUtils | null => {
 
 

+ 31 - 0
apps/app/src/components/InAppNotification/ModelNotification/index.tsx

@@ -0,0 +1,31 @@
+import type { FC } from 'react';
+
+import type { HasObjectId } from '@growi/core';
+
+import type { IInAppNotification } from '~/interfaces/in-app-notification';
+
+
+import { usePageBulkExportJobModelNotification } from './PageBulkExportJobModelNotification';
+import { usePageModelNotification } from './PageModelNotification';
+import { useUserModelNotification } from './UserModelNotification';
+
+export interface ModelNotificationUtils {
+  Notification: FC
+  publishOpen?: () => void
+  clickLink?: string
+  // Whether actions from clicking notification is disabled or not.
+  // User can still open the notification when true.
+  isDisabled?: boolean
+}
+
+export const useModelNotification = (notification: IInAppNotification & HasObjectId): ModelNotificationUtils | null => {
+
+  const pageModelNotificationUtils = usePageModelNotification(notification);
+  const userModelNotificationUtils = useUserModelNotification(notification);
+  const pageBulkExportResultModelNotificationUtils = usePageBulkExportJobModelNotification(notification);
+
+  const modelNotificationUtils = pageModelNotificationUtils ?? userModelNotificationUtils ?? pageBulkExportResultModelNotificationUtils;
+
+
+  return modelNotificationUtils;
+};

+ 8 - 0
apps/app/src/components/InAppNotification/PageNotification/useActionAndMsg.ts → apps/app/src/components/InAppNotification/ModelNotification/useActionAndMsg.ts

@@ -70,6 +70,14 @@ export const useActionMsgAndIconForModelNotification = (notification: IInAppNoti
       actionMsg = 'requested registration approval';
       actionMsg = 'requested registration approval';
       actionIcon = 'add_comment';
       actionIcon = 'add_comment';
       break;
       break;
+    case SupportedAction.ACTION_PAGE_BULK_EXPORT_COMPLETED:
+      actionMsg = 'export completed for';
+      actionIcon = 'download';
+      break;
+    case SupportedAction.ACTION_PAGE_BULK_EXPORT_FAILED:
+      actionMsg = 'export failed for';
+      actionIcon = 'error';
+      break;
     default:
     default:
       actionMsg = '';
       actionMsg = '';
       actionIcon = '';
       actionIcon = '';

+ 0 - 19
apps/app/src/components/InAppNotification/PageNotification/index.tsx

@@ -1,19 +0,0 @@
-import type { HasObjectId } from '@growi/core';
-
-import type { IInAppNotification } from '~/interfaces/in-app-notification';
-
-
-import { usePageModelNotification, type ModelNotificationUtils } from './PageModelNotification';
-import { useUserModelNotification } from './UserModelNotification';
-
-
-export const useModelNotification = (notification: IInAppNotification & HasObjectId): ModelNotificationUtils | null => {
-
-  const pageModelNotificationUtils = usePageModelNotification(notification);
-  const userModelNotificationUtils = useUserModelNotification(notification);
-
-  const modelNotificationUtils = pageModelNotificationUtils ?? userModelNotificationUtils;
-
-
-  return modelNotificationUtils;
-};

+ 3 - 0
apps/app/src/features/page-bulk-export/interfaces/page-bulk-export.ts

@@ -1,4 +1,5 @@
 import type {
 import type {
+  HasObjectId,
   IAttachment, IPage, IRevision, IUser, Ref,
   IAttachment, IPage, IRevision, IUser, Ref,
 } from '@growi/core';
 } from '@growi/core';
 
 
@@ -19,6 +20,8 @@ export interface IPageBulkExportJob {
   attachment?: Ref<IAttachment>,
   attachment?: Ref<IAttachment>,
 }
 }
 
 
+export interface IPageBulkExportJobHasId extends IPageBulkExportJob, HasObjectId {}
+
 // snapshot of page info to upload
 // snapshot of page info to upload
 export interface IPageBulkExportPageInfo {
 export interface IPageBulkExportPageInfo {
   pageBulkExportJob: Ref<IPageBulkExportJob>,
   pageBulkExportJob: Ref<IPageBulkExportJob>,

+ 1 - 1
apps/app/src/interfaces/in-app-notification.ts

@@ -1,6 +1,6 @@
 import type { IUser } from '@growi/core';
 import type { IUser } from '@growi/core';
 
 
-import { SupportedTargetModelType, SupportedActionType } from './activity';
+import type { SupportedTargetModelType, SupportedActionType } from './activity';
 
 
 export enum InAppNotificationStatuses {
 export enum InAppNotificationStatuses {
   STATUS_UNREAD = 'UNREAD',
   STATUS_UNREAD = 'UNREAD',

+ 25 - 0
apps/app/src/models/serializers/in-app-notification-snapshot/page-bulk-export-job.ts

@@ -0,0 +1,25 @@
+import { isPopulated } from '@growi/core';
+import type { IPage } from '@growi/core';
+import mongoose from 'mongoose';
+
+import type { IPageBulkExportJob } from '~/features/page-bulk-export/interfaces/page-bulk-export';
+import type { PageModel } from '~/server/models/page';
+
+export interface IPageBulkExportJobSnapshot {
+  path: string
+}
+
+export const stringifySnapshot = async(exportJob: IPageBulkExportJob): Promise<string | undefined> => {
+  const Page = mongoose.model<IPage, PageModel>('Page');
+  const page = isPopulated(exportJob.page) ? exportJob.page : (await Page.findById(exportJob.page));
+
+  if (page != null) {
+    return JSON.stringify({
+      path: page.path,
+    });
+  }
+};
+
+export const parseSnapshot = (snapshot: string): IPageBulkExportJobSnapshot => {
+  return JSON.parse(snapshot);
+};

+ 3 - 4
apps/app/src/server/models/in-app-notification.ts

@@ -1,6 +1,5 @@
-import {
-  Types, Document, Schema, Model,
-} from 'mongoose';
+import type { Types, Document, Model } from 'mongoose';
+import { Schema } from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 import mongoosePaginate from 'mongoose-paginate-v2';
 
 
 import { AllSupportedTargetModels, AllSupportedActions } from '~/interfaces/activity';
 import { AllSupportedTargetModels, AllSupportedActions } from '~/interfaces/activity';
@@ -8,7 +7,7 @@ import { InAppNotificationStatuses } from '~/interfaces/in-app-notification';
 
 
 import { getOrCreateModel } from '../util/mongoose-utils';
 import { getOrCreateModel } from '../util/mongoose-utils';
 
 
-import { ActivityDocument } from './activity';
+import type { ActivityDocument } from './activity';
 
 
 
 
 const { STATUS_UNREAD, STATUS_UNOPENED, STATUS_OPENED } = InAppNotificationStatuses;
 const { STATUS_UNREAD, STATUS_UNOPENED, STATUS_OPENED } = InAppNotificationStatuses;

+ 10 - 5
apps/app/src/server/service/in-app-notification.ts

@@ -119,21 +119,26 @@ export default class InAppNotificationService {
     const { limit, offset, status } = queryOptions;
     const { limit, offset, status } = queryOptions;
 
 
     try {
     try {
-      const pagenateOptions = { user: userId };
+      const paginateOptions = { user: userId };
       if (status != null) {
       if (status != null) {
-        Object.assign(pagenateOptions, { status });
+        Object.assign(paginateOptions, { status });
       }
       }
       // TODO: import @types/mongoose-paginate-v2 and use PaginateResult as a type after upgrading mongoose v6.0.0
       // TODO: import @types/mongoose-paginate-v2 and use PaginateResult as a type after upgrading mongoose v6.0.0
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       const paginationResult = await (InAppNotification as any).paginate(
       const paginationResult = await (InAppNotification as any).paginate(
-        pagenateOptions,
+        paginateOptions,
         {
         {
           sort: { createdAt: -1 },
           sort: { createdAt: -1 },
           limit,
           limit,
           offset,
           offset,
           populate: [
           populate: [
             { path: 'user' },
             { path: 'user' },
-            { path: 'target' },
+            {
+              path: 'target',
+              populate: [
+                { path: 'attachment', strictPopulate: false },
+              ],
+            },
             { path: 'activities', populate: { path: 'user' } },
             { path: 'activities', populate: { path: 'user' } },
           ],
           ],
         },
         },
@@ -205,7 +210,7 @@ export default class InAppNotificationService {
 
 
     const targetModel = activity.targetModel;
     const targetModel = activity.targetModel;
 
 
-    const snapshot = generateSnapshot(targetModel, target);
+    const snapshot = await generateSnapshot(targetModel, target);
 
 
     if (shouldNotification) {
     if (shouldNotification) {
       const props = preNotifyService.generateInitialPreNotifyProps();
       const props = preNotifyService.generateInitialPreNotifyProps();

+ 10 - 1
apps/app/src/server/service/in-app-notification/in-app-notification-utils.ts

@@ -3,18 +3,27 @@ import type { IUser, IPage } from '@growi/core';
 import type { IPageBulkExportJob } from '~/features/page-bulk-export/interfaces/page-bulk-export';
 import type { IPageBulkExportJob } from '~/features/page-bulk-export/interfaces/page-bulk-export';
 import { SupportedTargetModel } from '~/interfaces/activity';
 import { SupportedTargetModel } from '~/interfaces/activity';
 import * as pageSerializers from '~/models/serializers/in-app-notification-snapshot/page';
 import * as pageSerializers from '~/models/serializers/in-app-notification-snapshot/page';
+import * as pageBulkExportJobSerializers from '~/models/serializers/in-app-notification-snapshot/page-bulk-export-job';
 
 
 const isIPage = (targetModel: string, target: IUser | IPage | IPageBulkExportJob): target is IPage => {
 const isIPage = (targetModel: string, target: IUser | IPage | IPageBulkExportJob): target is IPage => {
   return targetModel === SupportedTargetModel.MODEL_PAGE;
   return targetModel === SupportedTargetModel.MODEL_PAGE;
 };
 };
 
 
-export const generateSnapshot = (targetModel: string, target: IUser | IPage | IPageBulkExportJob): string | undefined => {
+const isIPageBulkExportJob = (targetModel: string, target: IUser | IPage | IPageBulkExportJob): target is IPageBulkExportJob => {
+  return targetModel === SupportedTargetModel.MODEL_PAGE_BULK_EXPORT_JOB;
+};
+
+// snapshots are infos about the target that are displayed in the notification, which should not change on target update/deletion
+export const generateSnapshot = async(targetModel: string, target: IUser | IPage | IPageBulkExportJob): Promise<string | undefined> => {
 
 
   let snapshot: string | undefined;
   let snapshot: string | undefined;
 
 
   if (isIPage(targetModel, target)) {
   if (isIPage(targetModel, target)) {
     snapshot = pageSerializers.stringifySnapshot(target);
     snapshot = pageSerializers.stringifySnapshot(target);
   }
   }
+  else if (isIPageBulkExportJob(targetModel, target)) {
+    snapshot = await pageBulkExportJobSerializers.stringifySnapshot(target);
+  }
 
 
   return snapshot;
   return snapshot;
 };
 };