فهرست منبع

Merge branch 'feat/81841-create-notification-when-page-is-deleted' into feat/81696-81698-create-like-event

Shun Miyazawa 4 سال پیش
والد
کامیت
7031d17c60

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

@@ -259,7 +259,10 @@
   "in_app_notification": {
     "notification_list": "In-App Notification List",
     "see_all": "See All",
-    "no_notification": "You don't have any notificatios."
+    "no_notification": "You don't have any notificatios.",
+    "all": "All",
+    "unopend": "Unread",
+    "mark_all_as_read": "Mark all as read"
   },
   "in_app_notification_settings": {
     "in_app_notification_settings": "In-App Notification Settings",

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

@@ -261,7 +261,10 @@
   "in_app_notification": {
     "notification_list": "アプリ内通知一覧",
     "see_all": "通知一覧を見る",
-    "no_notification": "通知は一つもありません。"
+    "no_notification": "通知はありません",
+    "all": "全て",
+    "unopend": "未読",
+    "mark_all_as_read": "全て既読にする"
   },
   "in_app_notification_settings": {
     "in_app_notification_settings": "アプリ内通知設定",

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

@@ -240,7 +240,10 @@
   "in_app_notification": {
     "notification_list": "应用内通知列表",
     "see_all": "查看通知列表",
-    "no_notification": "您没有任何通知"
+    "no_notification": "您没有任何通知",
+    "all": "全部",
+    "unopend": "未读",
+    "mark_all_as_read" : "标记为已读"
   },
   "in_app_notification_settings": {
     "in_app_notification_settings": "在应用程序通知设置",

+ 2 - 2
packages/app/src/client/app.jsx

@@ -8,7 +8,7 @@ import { SWRConfig } from 'swr';
 import loggerFactory from '~/utils/logger';
 import { swrGlobalConfiguration } from '~/utils/swr-utils';
 
-import AllInAppNotifications from '../components/InAppNotification/AllInAppNotifications';
+import InAppNotificationPage from '../components/InAppNotification/InAppNotificationPage';
 import ErrorBoundary from '../components/ErrorBoudary';
 import Sidebar from '../components/Sidebar';
 import SearchPage from '../components/SearchPage';
@@ -87,7 +87,7 @@ Object.assign(componentMappings, {
   'grw-sidebar-wrapper': <Sidebar />,
 
   'search-page': <SearchPage crowi={appContainer} />,
-  'all-in-app-notifications': <AllInAppNotifications />,
+  'all-in-app-notifications': <InAppNotificationPage />,
 
   // 'revision-history': <PageHistory pageId={pageId} />,
   'tags-page': <TagsList crowi={appContainer} />,

+ 0 - 45
packages/app/src/components/InAppNotification/AllInAppNotifications.tsx

@@ -1,45 +0,0 @@
-import React, { FC, useState } from 'react';
-
-import InAppNotificationList from './InAppNotificationList';
-import { useSWRxInAppNotifications } from '../../stores/in-app-notification';
-import PaginationWrapper from '../PaginationWrapper';
-
-
-const AllInAppNotifications: FC = () => {
-  const [activePage, setActivePage] = useState(1);
-  const [offset, setOffset] = useState(0);
-  const limit = 10;
-  const { data: inAppNotificationData } = useSWRxInAppNotifications(limit, offset);
-
-  if (inAppNotificationData == null) {
-    return (
-      <div className="wiki">
-        <div className="text-muted text-center">
-          <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
-        </div>
-      </div>
-    );
-  }
-
-  const setPageNumber = (selectedPageNumber): void => {
-    setActivePage(selectedPageNumber);
-    const offset = (selectedPageNumber - 1) * limit;
-    setOffset(offset);
-  };
-
-  return (
-    <>
-      <InAppNotificationList inAppNotificationData={inAppNotificationData} />
-      <PaginationWrapper
-        activePage={activePage}
-        changePage={setPageNumber}
-        totalItemsCount={inAppNotificationData.totalDocs}
-        pagingLimit={inAppNotificationData.limit}
-        align="center"
-        size="sm"
-      />
-    </>
-  );
-};
-
-export default AllInAppNotifications;

+ 1 - 1
packages/app/src/components/InAppNotification/InAppNotificationElm.tsx

@@ -108,7 +108,7 @@ const InAppNotificationElm = (props: Props): JSX.Element | null => {
   return (
     <div className="dropdown-item d-flex flex-row mb-3">
       <div className="p-2 mr-2 d-flex align-items-center">
-        <span className={`${notification.status === 'UNOPENED' && 'grw-unopend-notification'} rounded-circle mr-3`}></span>
+        <span className={`${notification.status === 'UNOPENED' ? 'grw-unopend-notification' : 'ml-2'} rounded-circle mr-3`}></span>
         {renderActionUserPictures()}
       </div>
       <div className="p-2">

+ 139 - 0
packages/app/src/components/InAppNotification/InAppNotificationPage.tsx

@@ -0,0 +1,139 @@
+import React, { FC, useState } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import PropTypes from 'prop-types';
+import AppContainer from '~/client/services/AppContainer';
+import { withUnstatedContainers } from '../UnstatedUtils';
+import InAppNotificationList from './InAppNotificationList';
+import { useSWRxInAppNotifications } from '../../stores/in-app-notification';
+import PaginationWrapper from '../PaginationWrapper';
+import CustomNavAndContents from '../CustomNavigation/CustomNavAndContents';
+import { InAppNotificationStatuses } from '~/interfaces/in-app-notification';
+import { apiv3Put } from '~/client/util/apiv3-client';
+
+
+type Props = {
+  appContainer: AppContainer
+}
+
+const InAppNotificationPageBody: FC<Props> = (props) => {
+  const { appContainer } = props;
+  const { t } = useTranslation();
+
+  const limit = appContainer.config.pageLimitationXL;
+  const [activePageOfAllNotificationCat, setActivePage] = useState(1);
+  const [activePageOfUnopenedNotificationCat, setActiveUnopenedNotificationPage] = useState(1);
+
+
+  const setAllNotificationPageNumber = (selectedPageNumber): void => {
+    setActivePage(selectedPageNumber);
+  };
+
+  const setUnopenedPageNumber = (selectedPageNumber): void => {
+    setActiveUnopenedNotificationPage(selectedPageNumber);
+  };
+
+  // commonize notification lists by 81953
+  const AllInAppNotificationList = () => {
+    const offsetOfAllNotificationCat = (activePageOfAllNotificationCat - 1) * limit;
+    const { data: allNotificationData } = useSWRxInAppNotifications(limit, offsetOfAllNotificationCat);
+
+    if (allNotificationData == null) {
+      return (
+        <div className="wiki">
+          <div className="text-muted text-center">
+            <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
+          </div>
+        </div>
+      );
+    }
+
+
+    return (
+      <>
+        <InAppNotificationList inAppNotificationData={allNotificationData} />
+        <PaginationWrapper
+          activePage={activePageOfAllNotificationCat}
+          changePage={setAllNotificationPageNumber}
+          totalItemsCount={allNotificationData.totalDocs}
+          pagingLimit={allNotificationData.limit}
+          align="center"
+          size="sm"
+        />
+      </>
+    );
+  };
+
+
+  // commonize notification lists by 81953
+  const UnopenedInAppNotificationList = () => {
+    const offsetOfUnopenedNotificationCat = (activePageOfUnopenedNotificationCat - 1) * limit;
+    const {
+      data: unopendNotificationData, mutate,
+    } = useSWRxInAppNotifications(limit, offsetOfUnopenedNotificationCat, InAppNotificationStatuses.STATUS_UNOPENED);
+
+    const updateUnopendNotificationStatusesToOpened = async() => {
+      await apiv3Put('/in-app-notification/all-statuses-open');
+      mutate();
+    };
+
+    if (unopendNotificationData == null) {
+      return (
+        <div className="wiki">
+          <div className="text-muted text-center">
+            <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
+          </div>
+        </div>
+      );
+    }
+
+    return (
+      <>
+        <div className="mb-2 d-flex justify-content-end">
+          <button
+            type="button"
+            className="btn btn-outline-primary"
+            onClick={updateUnopendNotificationStatusesToOpened}
+          >
+            {t('in_app_notification.mark_all_as_read')}
+          </button>
+        </div>
+        <InAppNotificationList inAppNotificationData={unopendNotificationData} />
+        <PaginationWrapper
+          activePage={activePageOfUnopenedNotificationCat}
+          changePage={setUnopenedPageNumber}
+          totalItemsCount={unopendNotificationData.totalDocs}
+          pagingLimit={unopendNotificationData.limit}
+          align="center"
+          size="sm"
+        />
+      </>
+    );
+  };
+
+  const navTabMapping = {
+    user_infomation: {
+      Icon: () => <></>,
+      Content: AllInAppNotificationList,
+      i18n: t('in_app_notification.all'),
+      index: 0,
+    },
+    external_accounts: {
+      Icon: () => <></>,
+      Content: UnopenedInAppNotificationList,
+      i18n: t('in_app_notification.unopend'),
+      index: 1,
+    },
+  };
+
+  return (
+    <CustomNavAndContents navTabMapping={navTabMapping} />
+  );
+};
+
+const InAppNotificationPage = withUnstatedContainers(InAppNotificationPageBody, [AppContainer]);
+export default InAppNotificationPage;
+
+InAppNotificationPageBody.propTypes = {
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};

+ 1 - 0
packages/app/src/server/models/config.ts

@@ -235,6 +235,7 @@ schema.statics.getLocalconfig = function(crowi) {
     isSearchServiceReachable: crowi.searchService.isReachable,
     isMailerSetup: crowi.mailService.isMailerSetup,
     globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
+    pageLimitationXL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL'),
   };
 
   return localConfig;

+ 0 - 1
packages/app/src/server/models/page.js

@@ -1022,7 +1022,6 @@ module.exports = function(crowi) {
     }
 
     pageEvent.emit('update', savedPage, user);
-    pageEvent.emit('update:notification', savedPage, user);
 
     return savedPage;
   };

+ 16 - 0
packages/app/src/server/routes/apiv3/in-app-notification.ts

@@ -29,6 +29,10 @@ module.exports = (crowi) => {
       limit,
     };
 
+    // set in-app-notification status to categorize
+    if (req.query.status != null) {
+      Object.assign(queryOptions, { status: req.query.status });
+    }
 
     const paginationResult = await inAppNotificationService.getLatestNotificationsByUser(user._id, queryOptions);
 
@@ -99,5 +103,17 @@ module.exports = (crowi) => {
     }
   });
 
+  router.put('/all-statuses-open', accessTokenParser, loginRequiredStrictly, csrf, async(req, res) => {
+    const user = req.user;
+
+    try {
+      await inAppNotificationService.updateAllNotificationsAsOpened(user);
+      return res.apiv3();
+    }
+    catch (err) {
+      return res.apiv3Err(err);
+    }
+  });
+
   return router;
 };

+ 17 - 4
packages/app/src/server/service/in-app-notification.ts

@@ -1,17 +1,18 @@
 import { Types } from 'mongoose';
 import { subDays } from 'date-fns';
+import { InAppNotificationStatuses, PaginateResult, IInAppNotification } from '~/interfaces/in-app-notification';
 import Crowi from '../crowi';
 import {
   InAppNotification,
   InAppNotificationDocument,
 } from '~/server/models/in-app-notification';
-import { PaginateResult, InAppNotificationStatuses } from '../../interfaces/in-app-notification';
 
 import { ActivityDocument } from '~/server/models/activity';
 import InAppNotificationSettings from '~/server/models/in-app-notification-settings';
 import Subscription, { STATUS_SUBSCRIBE } from '~/server/models/subscription';
 
 import { IUser } from '~/interfaces/user';
+
 import { HasObjectId } from '~/interfaces/has-object-id';
 import loggerFactory from '~/utils/logger';
 import { RoomPrefix, getRoomNameWithId } from '../util/socket-io-helpers';
@@ -88,15 +89,19 @@ export default class InAppNotificationService {
 
   getLatestNotificationsByUser = async(
       userId: Types.ObjectId,
-      queryOptions: {offset: number, limit: number},
+      queryOptions: {offset: number, limit: number, status?: InAppNotificationStatuses},
   ): Promise<PaginateResult<InAppNotificationDocument>> => {
-    const { limit, offset } = queryOptions;
+    const { limit, offset, status } = queryOptions;
 
     try {
+      const pagenateOptions = { user: userId };
+      if (status != null) {
+        Object.assign(pagenateOptions, { status });
+      }
       // 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
       const paginationResult = await (InAppNotification as any).paginate(
-        { user: userId },
+        pagenateOptions,
         {
           sort: { createdAt: -1 },
           limit,
@@ -134,6 +139,14 @@ export default class InAppNotificationService {
     return;
   }
 
+  updateAllNotificationsAsOpened = async function(user: IUser & HasObjectId): Promise<void> {
+    const filter = { user: user._id, status: STATUS_UNOPENED };
+    const options = { status: STATUS_OPENED };
+
+    await InAppNotification.updateMany(filter, options);
+    return;
+  }
+
   getUnreadCountByUser = async function(user: Types.ObjectId): Promise<number| undefined> {
     const query = { user, status: STATUS_UNREAD };
 

+ 4 - 7
packages/app/src/server/service/page.js

@@ -32,7 +32,7 @@ class PageService {
     this.pageEvent.on('create', this.pageEvent.onCreate);
 
     // update
-    this.pageEvent.on('update:notification', async(page, user) => {
+    this.pageEvent.on('update', async(page, user) => {
 
       this.pageEvent.onUpdate();
 
@@ -45,7 +45,7 @@ class PageService {
     });
 
     // rename
-    this.pageEvent.on('rename:notification', async(page, user) => {
+    this.pageEvent.on('rename', async(page, user) => {
       try {
         await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_RENAME);
       }
@@ -55,7 +55,7 @@ class PageService {
     });
 
     // delete
-    this.pageEvent.on('delete:notification', async(page, user) => {
+    this.pageEvent.on('delete', async(page, user) => {
       try {
         await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_DELETE);
       }
@@ -156,9 +156,7 @@ class PageService {
       await Page.create(path, body, user, { redirectTo: newPagePath });
     }
 
-    this.pageEvent.emit('delete', page, user);
-    this.pageEvent.emit('create', renamedPage, user);
-    this.pageEvent.emit('rename:notification', page, user);
+    this.pageEvent.emit('rename', page, user);
 
     return renamedPage;
   }
@@ -487,7 +485,6 @@ class PageService {
 
     this.pageEvent.emit('delete', page, user);
     this.pageEvent.emit('create', deletedPage, user);
-    this.pageEvent.emit('delete:notification', page, user);
 
     return deletedPage;
   }

+ 4 - 0
packages/app/src/server/service/search.js

@@ -68,6 +68,10 @@ class SearchService {
     pageEvent.on('delete', this.delegator.syncPageDeleted.bind(this.delegator));
     pageEvent.on('updateMany', this.delegator.syncPagesUpdated.bind(this.delegator));
     pageEvent.on('syncDescendants', this.delegator.syncDescendantsPagesUpdated.bind(this.delegator));
+    pageEvent.on('rename', () => {
+      this.delegator.syncPageDeleted.bind(this.delegator);
+      this.delegator.syncPageUpdated.bind(this.delegator);
+    });
 
     const bookmarkEvent = this.crowi.event('bookmark');
     bookmarkEvent.on('create', this.delegator.syncBookmarkChanged.bind(this.delegator));

+ 4 - 4
packages/app/src/stores/in-app-notification.ts

@@ -1,15 +1,15 @@
 import useSWR, { SWRResponse } from 'swr';
-
+import { InAppNotificationStatuses, IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
 import { apiv3Get } from '../client/util/apiv3-client';
-import { IInAppNotification, PaginateResult } from '../interfaces/in-app-notification';
 
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 export const useSWRxInAppNotifications = <Data, Error>(
   limit: number,
   offset?: number,
+  status?: InAppNotificationStatuses,
 ): SWRResponse<PaginateResult<IInAppNotification>, Error> => {
   return useSWR(
-    ['/in-app-notification/list', limit, offset],
-    endpoint => apiv3Get(endpoint, { limit, offset }).then(response => response.data),
+    ['/in-app-notification/list', limit, offset, status],
+    endpoint => apiv3Get(endpoint, { limit, offset, status }).then(response => response.data),
   );
 };

+ 6 - 8
packages/app/src/test/integration/service/page.test.js

@@ -351,8 +351,8 @@ describe('PageService', () => {
 
         expect(xssSpy).toHaveBeenCalled();
         expect(renameDescendantsWithStreamSpy).not.toHaveBeenCalled();
-        expect(pageEventSpy).toHaveBeenCalledWith('delete', parentForRename1, testUser2);
-        expect(pageEventSpy).toHaveBeenCalledWith('create', resultPage, testUser2);
+
+        expect(pageEventSpy).toHaveBeenCalledWith('rename', parentForRename1, testUser2);
 
         expect(resultPage.path).toBe('/renamed1');
         expect(resultPage.updatedAt).toEqual(parentForRename1.updatedAt);
@@ -370,8 +370,8 @@ describe('PageService', () => {
 
         expect(xssSpy).toHaveBeenCalled();
         expect(renameDescendantsWithStreamSpy).not.toHaveBeenCalled();
-        expect(pageEventSpy).toHaveBeenCalledWith('delete', parentForRename2, testUser2);
-        expect(pageEventSpy).toHaveBeenCalledWith('create', resultPage, testUser2);
+
+        expect(pageEventSpy).toHaveBeenCalledWith('rename', parentForRename2, testUser2);
 
         expect(resultPage.path).toBe('/renamed2');
         expect(resultPage.updatedAt).toEqual(dateToUse);
@@ -389,8 +389,7 @@ describe('PageService', () => {
 
         expect(xssSpy).toHaveBeenCalled();
         expect(renameDescendantsWithStreamSpy).not.toHaveBeenCalled();
-        expect(pageEventSpy).toHaveBeenCalledWith('delete', parentForRename3, testUser2);
-        expect(pageEventSpy).toHaveBeenCalledWith('create', resultPage, testUser2);
+        expect(pageEventSpy).toHaveBeenCalledWith('rename', parentForRename3, testUser2);
 
         expect(resultPage.path).toBe('/renamed3');
         expect(resultPage.updatedAt).toEqual(parentForRename3.updatedAt);
@@ -413,8 +412,7 @@ describe('PageService', () => {
 
         expect(xssSpy).toHaveBeenCalled();
         expect(renameDescendantsWithStreamSpy).toHaveBeenCalled();
-        expect(pageEventSpy).toHaveBeenCalledWith('delete', parentForRename4, testUser2);
-        expect(pageEventSpy).toHaveBeenCalledWith('create', resultPage, testUser2);
+        expect(pageEventSpy).toHaveBeenCalledWith('rename', parentForRename4, testUser2);
 
         expect(resultPage.path).toBe('/renamed4');
         expect(resultPage.updatedAt).toEqual(parentForRename4.updatedAt);