Taichi Masuyama 3 лет назад
Родитель
Сommit
70534d93fc

+ 16 - 2
packages/app/src/components/Questionnaire/QuestionnaireModal.tsx

@@ -82,17 +82,31 @@ const QuestionnaireModal = ({ questionnaireOrder }: QuestionnaireModalProps): JS
     closeQuestionnaireModal();
   }, [closeQuestionnaireModal, questionnaireOrder._id, t]);
 
+  // No showing toasts since not important
+  const closeBtnClickHandler = useCallback(async() => {
+    closeQuestionnaireModal();
+
+    try {
+      await apiv3Put('/questionnaire/deny', {
+        questionnaireOrderId: questionnaireOrder._id,
+      });
+    }
+    catch (e) {
+      logger.error(e);
+    }
+  }, [closeQuestionnaireModal, questionnaireOrder._id]);
+
   const questionnaireOrderTitle = lang === 'en_US' ? questionnaireOrder.title.en_US : questionnaireOrder.title.ja_JP;
 
   return (<Modal
     size="lg"
     isOpen={isOpened}
-    toggle={() => closeQuestionnaireModal()}
+    toggle={closeBtnClickHandler}
   >
     <form onSubmit={submitHandler}>
       <ModalHeader
         tag="h4"
-        toggle={() => closeQuestionnaireModal()}
+        toggle={closeBtnClickHandler}
         className="bg-primary text-light">
         <span>{t('questionnaire.give_us_feedback')}</span>
       </ModalHeader>

+ 28 - 12
packages/app/src/components/Questionnaire/QuestionnaireToast.tsx

@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useCallback, useState } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
@@ -24,31 +24,47 @@ const QuestionnaireToast = ({ questionnaireOrder }: QuestionnaireToastProps): JS
 
   const { t } = useTranslation();
 
-  const answerBtnClickHandler = () => {
+  const answerBtnClickHandler = useCallback(() => {
     setIsOpen(false);
     openQuestionnaireModal(questionnaireOrder._id);
-  };
+  }, [openQuestionnaireModal, questionnaireOrder._id]);
 
-  const skipBtnClickHandler = async() => {
-    apiv3Put('/questionnaire/skip', {
-      user: currentUser?._id,
-      questionnaireOrderId: questionnaireOrder._id,
-    }).then(() => {
+  const skipBtnClickHandler = useCallback(async() => {
+    // Immediately close
+    setIsOpen(false);
+
+    try {
+      await apiv3Put('/questionnaire/skip', {
+        questionnaireOrderId: questionnaireOrder._id,
+      });
       toastSuccess(t('questionnaire.skipped'));
-    }).catch((e) => {
+    }
+    catch (e) {
       logger.error(e);
       toastError(t('questionnaire.failed_to_skip'));
-    });
+    }
+  }, [questionnaireOrder._id, t]);
 
+  // No showing toasts since not important
+  const closeBtnClickHandler = useCallback(async() => {
     setIsOpen(false);
-  };
+
+    try {
+      await apiv3Put('/questionnaire/deny', {
+        questionnaireOrderId: questionnaireOrder._id,
+      });
+    }
+    catch (e) {
+      logger.error(e);
+    }
+  }, [questionnaireOrder._id]);
 
   const questionnaireOrderTitle = lang === 'en_US' ? questionnaireOrder.title.en_US : questionnaireOrder.title.ja_JP;
 
   return <div className={`toast ${isOpen ? 'show' : 'hide'}`}>
     <div className="toast-header bg-info">
       <strong className="mr-auto text-light">{questionnaireOrderTitle}</strong>
-      <button type="button" className="ml-2 mb-1 close" onClick={() => setIsOpen(false)}>
+      <button type="button" className="ml-2 mb-1 close" onClick={closeBtnClickHandler}>
         <span aria-hidden="true" className="text-light">&times;</span>
       </button>
     </div>

+ 12 - 1
packages/app/src/server/routes/apiv3/questionnaire.ts

@@ -44,7 +44,7 @@ module.exports = (crowi: Crowi): Router => {
 
     // TODO: add condition
     try {
-      const questionnaireOrders = await crowi.questionnaireService.getQuestionnaireOrdersToShow(userInfo, growiInfo);
+      const questionnaireOrders = await crowi.questionnaireService!.getQuestionnaireOrdersToShow(userInfo, growiInfo, req.user?._id ?? null);
 
       return res.apiv3({ questionnaireOrders });
     }
@@ -92,6 +92,17 @@ module.exports = (crowi: Crowi): Router => {
     }
   });
 
+  router.put('/deny', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
+    try {
+      const status = await changeAnswerStatus(req.user, req.body.questionnaireOrderId, StatusType.denied);
+      return res.apiv3({}, status);
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err(err, 500);
+    }
+  });
+
   return router;
 
 };

+ 10 - 0
packages/app/src/server/service/questionnaire-cron.ts

@@ -1,10 +1,12 @@
 import axiosRetry from 'axios-retry';
 
+import { StatusType } from '~/interfaces/questionnaire/questionnaire-answer-status';
 import { IQuestionnaireOrder } from '~/interfaces/questionnaire/questionnaire-order';
 import loggerFactory from '~/utils/logger';
 import { getRandomIntInRange } from '~/utils/rand';
 import { sleep } from '~/utils/sleep';
 
+import QuestionnaireAnswerStatus from '../models/questionnaire/questionnaire-answer-status';
 import QuestionnaireOrder from '../models/questionnaire/questionnaire-order';
 
 const logger = loggerFactory('growi:service:questionnaire-cron');
@@ -56,7 +58,15 @@ class QuestionnaireCronService {
         const response = await axios.get(`${growiQuestionnaireServerOrigin}/questionnaire-order/index`);
         const questionnaireOrders: IQuestionnaireOrder[] = response.data.questionnaireOrders;
 
+        // Reset status (denied => not_answered)
+        await QuestionnaireAnswerStatus.updateMany(
+          { status: StatusType.denied },
+          { status: StatusType.not_answered },
+        );
+
+        // Cleanup
         await QuestionnaireOrder.deleteMany();
+
         await saveOrders(questionnaireOrders);
       }
       catch (e) {

+ 20 - 6
packages/app/src/server/service/questionnaire.ts

@@ -2,11 +2,14 @@ import crypto from 'crypto';
 import * as os from 'node:os';
 
 import { IGrowiInfo } from '~/interfaces/questionnaire/growi-info';
+import { StatusType } from '~/interfaces/questionnaire/questionnaire-answer-status';
 import { IUserInfo, UserType } from '~/interfaces/questionnaire/user-info';
 import { IUserHasId } from '~/interfaces/user';
 import QuestionnaireOrder, { QuestionnaireOrderDocument } from '~/server/models/questionnaire/questionnaire-order';
 
-import { shouldShowQuestionnaire } from '../util/questionnaire/condition';
+import { ObjectIdLike } from '../interfaces/mongoose-utils';
+import QuestionnaireAnswerStatus from '../models/questionnaire/questionnaire-answer-status';
+import { isShowableCondition } from '../util/questionnaire/condition';
 
 class QuestionnaireService {
 
@@ -64,18 +67,29 @@ class QuestionnaireService {
     return { type: UserType.guest };
   }
 
-  async getQuestionnaireOrdersToShow(userInfo, growiInfo): Promise<QuestionnaireOrderDocument[]> {
+  async getQuestionnaireOrdersToShow(userInfo, growiInfo, userId: ObjectIdLike | null): Promise<QuestionnaireOrderDocument[]> {
     const currentDate = new Date();
 
-    const questionnaireOrders = await QuestionnaireOrder.find({
+    let questionnaireOrders = await QuestionnaireOrder.find({
       showUntil: {
         $gte: currentDate,
       },
     });
 
-    return questionnaireOrders.filter((order) => {
-      return shouldShowQuestionnaire(order.condition, userInfo, growiInfo);
-    });
+    if (userId != null) {
+      const statuses = await QuestionnaireAnswerStatus.find({ userId, questionnaireOrderId: { $in: questionnaireOrders.map(d => d._id) } });
+
+      questionnaireOrders = questionnaireOrders.filter((order) => {
+        const status = statuses.find(s => s.questionnaireOrderId.toString() === order._id.toString());
+
+        return status?.status === StatusType.not_answered;
+      });
+    }
+
+    return questionnaireOrders
+      .filter((order) => {
+        return isShowableCondition(order, userInfo, growiInfo);
+      });
   }
 
 }

+ 4 - 1
packages/app/src/server/util/questionnaire/condition.ts

@@ -1,5 +1,6 @@
 import { ICondition } from '~/interfaces/questionnaire/condition';
 import { IGrowiInfo } from '~/interfaces/questionnaire/growi-info';
+import { IQuestionnaireOrder } from '~/interfaces/questionnaire/questionnaire-order';
 import { IUserInfo } from '~/interfaces/questionnaire/user-info';
 
 
@@ -23,7 +24,9 @@ const checkGrowiInfo = (condition: ICondition, growiInfo: IGrowiInfo): boolean =
   return true;
 };
 
-export const shouldShowQuestionnaire = (condition: ICondition, userInfo: IUserInfo, growiInfo: IGrowiInfo): boolean => {
+export const isShowableCondition = (order: IQuestionnaireOrder, userInfo: IUserInfo, growiInfo: IGrowiInfo): boolean => {
+  const { condition } = order;
+
   if (!checkUserInfo(condition, userInfo)) {
     return false;
   }