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

refs 113224: send questionnaire answer

Futa Arai 3 лет назад
Родитель
Сommit
617ec88913

+ 3 - 1
packages/app/public/static/locales/en_US/translation.json

@@ -814,7 +814,9 @@
     "disagree": "Disagree",
     "answer": "Answer",
     "no_answer": "No answer",
-    "settings": "Questionnaire settings"
+    "settings": "Questionnaire settings",
+    "failed_to_send": "Failed to send feedback",
+    "failed_to_get_user_info": "Feedback couldn't be sent because user info couldn't be achieved"
   },
   "tag_edit_modal": {
     "edit_tags": "Edit Tags",

+ 3 - 1
packages/app/public/static/locales/ja_JP/translation.json

@@ -814,7 +814,9 @@
     "disagree": "そう思わない",
     "answer": "回答する",
     "no_answer": "無回答",
-    "settings": "アンケート設定"
+    "settings": "アンケート設定",
+    "failed_to_send": "回答送信に失敗しました",
+    "failed_to_get_user_info": "ユーザ情報の取得に失敗したため、回答を送信できませんでした"
   },
   "tag_edit_modal": {
     "edit_tags": "タグの編集",

+ 8 - 7
packages/app/src/components/Questionnaire/Question.tsx

@@ -3,9 +3,10 @@ import { useCurrentUser } from '~/stores/context';
 
 type QuestionProps = {
   question: IQuestionHasId,
+  inputNamePrefix: string,
 }
 
-const Question = ({ question }: QuestionProps): JSX.Element => {
+const Question = ({ question, inputNamePrefix }: QuestionProps): JSX.Element => {
   const { data: currentUser } = useCurrentUser();
   const lang = currentUser?.lang;
 
@@ -20,26 +21,26 @@ const Question = ({ question }: QuestionProps): JSX.Element => {
     <div className="col-1 d-flex align-items-center p-0">
       <div className="btn-group btn-group-toggle flex-fill grw-questionnaire-btn-group" data-toggle="buttons">
         <label className="btn btn-outline-primary active">
-          <input type="radio" name={`question-${question._id}`} id={`${question._id}-noAnswer`}/> -
+          <input type="radio" name={`${inputNamePrefix}${question._id}`} id={`${question._id}-noAnswer`} value='0'/> -
         </label>
       </div>
     </div>
     <div className="col-5 d-flex align-items-center">
       <div className="btn-group btn-group-toggle flex-fill grw-questionnaire-btn-group" data-toggle="buttons">
         <label className="btn btn-outline-primary">
-          <input type="radio" name={`question-${question._id}`} id={`${question._id}-option1`}/> 1
+          <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option1`} value='1'/> 1
         </label>
         <label className="btn btn-outline-primary">
-          <input type="radio" name={`question-${question._id}`} id={`${question._id}-option2`}/> 2
+          <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option2`} value='2'/> 2
         </label>
         <label className="btn btn-outline-primary">
-          <input type="radio" name={`question-${question._id}`} id={`${question._id}-option3`}/> 3
+          <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option3`} value='3'/> 3
         </label>
         <label className="btn btn-outline-primary">
-          <input type="radio" name={`question-${question._id}`} id={`${question._id}-option4`}/> 4
+          <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option4`} value='4'/> 4
         </label>
         <label className="btn btn-outline-primary">
-          <input type="radio" name={`question-${question._id}`} id={`${question._id}-option5`}/> 5
+          <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option5`} value='5'/> 5
         </label>
       </div>
     </div>

+ 119 - 41
packages/app/src/components/Questionnaire/QuestionnaireModal.tsx

@@ -3,38 +3,114 @@ import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
-import { toastSuccess } from '~/client/util/toastr';
+import { toastSuccess, toastError } from '~/client/util/toastr';
+import { IAnswer } from '~/interfaces/questionnaire/answer';
+import { IGrowiInfo } from '~/interfaces/questionnaire/growi-info';
+import { IQuestionnaireAnswer } from '~/interfaces/questionnaire/questionnaire-answer';
 import { IQuestionnaireOrderHasId } from '~/interfaces/questionnaire/questionnaire-order';
-import { useCurrentUser } from '~/stores/context';
+import { IUserInfo } from '~/interfaces/questionnaire/user-info';
+import { useCurrentUser, useGrowiVersion } from '~/stores/context';
 import { useQuestionnaireModal } from '~/stores/modal';
-
+import axios from '~/utils/axios';
+import loggerFactory from '~/utils/logger';
 
 import Question from './Question';
 
+const logger = loggerFactory('growi:QuestionnaireModal');
+
 type QuestionnaireModalProps = {
   questionnaireOrder: IQuestionnaireOrderHasId
+  growiQuestionnaireServerOrigin: string
 }
 
-const QuestionnaireModal = ({ questionnaireOrder }: QuestionnaireModalProps): JSX.Element => {
+const QuestionnaireModal = ({ questionnaireOrder, growiQuestionnaireServerOrigin }: QuestionnaireModalProps): JSX.Element => {
   const { data: currentUser } = useCurrentUser();
   const lang = currentUser?.lang;
 
   const { data: questionnaireModalData, close: closeQuestionnaireModal } = useQuestionnaireModal();
   const isOpened = questionnaireModalData?.openedQuestionnaireId === questionnaireOrder._id;
 
+  const { data: growiVersion } = useGrowiVersion();
+
+  const inputNamePrefix = 'question-';
+
   const { t } = useTranslation();
 
-  const answerBtnClickHandler = () => {
-    toastSuccess(
-      <>
-        <div className="font-weight-bold">{t('questionnaire.thank_you_for_answering')}</div>
-        <div className="pt-2">{t('questionnaire.additional_feedback')}</div>
-      </>,
-      {
-        autoClose: 3000,
-        closeButton: true,
+  // TODO: モック化されている箇所を実装
+  const getGrowiInfo = (): IGrowiInfo => {
+    return {
+      version: growiVersion || '',
+      osInfo: {
+        type: 'Linux',
+        platform: 'linux',
+        arch: 'arm',
+        totalmem: 8,
       },
-    );
+      appSiteUrlHashed: 'c83e8d2a1aa87b2a3f90561be372ca523bb931e2d00013c1d204879621a25b90',
+      type: 'cloud',
+      currentUsersCount: 100,
+      currentActiveUsersCount: 50,
+      wikiType: 'open',
+      attachmentType: 'aws',
+      activeExternalAccountTypes: 'sample account type',
+      deploymentType: 'official-helm-chart',
+    };
+  };
+
+  // TODO: モック化されている箇所を実装
+  const getUserInfo = (): IUserInfo | null => {
+    if (currentUser) {
+      return {
+        userIdHash: '542bcc3bc5bc61b840017a18',
+        type: currentUser.admin ? 'admin' : 'general',
+        userCreatedAt: currentUser.createdAt,
+      };
+    }
+    return null;
+  };
+
+  const submitHandler = (event) => {
+    event.preventDefault();
+
+    const toastOptions = {
+      autoClose: 3000,
+      closeButton: true,
+    };
+
+    const growiInfo = getGrowiInfo();
+    const userInfo = getUserInfo();
+    const answers: IAnswer[] = questionnaireOrder.questions.map((question) => {
+      const answerValue = event.target[`${inputNamePrefix + question._id}`].value;
+      return { question: question._id, value: answerValue };
+    });
+
+    if (userInfo) {
+      const questionnaireAnswer: IQuestionnaireAnswer = {
+        growiInfo,
+        userInfo,
+        answers,
+        answeredAt: new Date(),
+      };
+
+      axios.post('http://localhost:3003/questionnaire-answer', questionnaireAnswer)
+        .then(() => {
+          toastSuccess(
+            <>
+              <div className="font-weight-bold">{t('questionnaire.thank_you_for_answering')}</div>
+              <div className="pt-2">{t('questionnaire.additional_feedback')}</div>
+            </>,
+            toastOptions,
+          );
+        })
+        .catch((e) => {
+          logger.error(e);
+          toastError(t('questionnaire.failed_to_send'), toastOptions);
+        });
+    }
+    else {
+      toastError(t('questionnaire.failed_to_get_user_info'), toastOptions);
+    }
+
     closeQuestionnaireModal();
   };
 
@@ -45,36 +121,38 @@ const QuestionnaireModal = ({ questionnaireOrder }: QuestionnaireModalProps): JS
     isOpen={isOpened}
     toggle={() => closeQuestionnaireModal()}
   >
-    <ModalHeader
-      tag="h4"
-      toggle={() => closeQuestionnaireModal()}
-      className="bg-primary text-light">
-      <span>{t('questionnaire.give_us_feedback')}</span>
-    </ModalHeader>
-    <ModalBody className="my-4">
-      <div className="container">
-        <h3 className="grw-modal-head">{questionnaireOrderTitle}</h3>
-        <div className="row mt-4">
-          <div className="col-6"></div>
-          <div className="col-1 p-0 font-weight-bold text-center align-items-center">{t('questionnaire.no_answer')}</div>
-          <div className="col-5 d-flex justify-content-between align-items-center">
-            <span className="font-weight-bold">{t('questionnaire.disagree')}</span>
-            <span className="font-weight-bold">{t('questionnaire.agree')}</span>
+    <form onSubmit={submitHandler}>
+      <ModalHeader
+        tag="h4"
+        toggle={() => closeQuestionnaireModal()}
+        className="bg-primary text-light">
+        <span>{t('questionnaire.give_us_feedback')}</span>
+      </ModalHeader>
+      <ModalBody className="my-4">
+        <div className="container">
+          <h3 className="grw-modal-head">{questionnaireOrderTitle}</h3>
+          <div className="row mt-4">
+            <div className="col-6"></div>
+            <div className="col-1 p-0 font-weight-bold text-center align-items-center">{t('questionnaire.no_answer')}</div>
+            <div className="col-5 d-flex justify-content-between align-items-center">
+              <span className="font-weight-bold">{t('questionnaire.disagree')}</span>
+              <span className="font-weight-bold">{t('questionnaire.agree')}</span>
+            </div>
           </div>
+          {questionnaireOrder.questions?.map((question) => {
+            return <Question question={question} inputNamePrefix={inputNamePrefix} key={question._id.toString()}/>;
+          })}
         </div>
-        {questionnaireOrder.questions?.map((question) => {
-          return <Question question={question} key={question._id.toString()}/>;
-        })}
-      </div>
-    </ModalBody>
-    <ModalFooter>
-      {currentUser?.admin
+      </ModalBody>
+      <ModalFooter>
+        {currentUser?.admin
         && <a href="" className="mr-auto d-flex align-items-center"><i className="material-icons mr-1">settings</i>{t('questionnaire.settings')}</a>}
-      <>
-        <button type="button" className="btn btn-outline-secondary mr-3">{t('questionnaire.dont_show_again')}</button>
-        <button type="button" className="btn btn-primary" onClick={answerBtnClickHandler}>{t('questionnaire.answer')}</button>
-      </>
-    </ModalFooter>
+        <>
+          <button type="button" className="btn btn-outline-secondary mr-3">{t('questionnaire.dont_show_again')}</button>
+          <button type="submit" className="btn btn-primary">{t('questionnaire.answer')}</button>
+        </>
+      </ModalFooter>
+    </form>
   </Modal>);
 };
 

+ 9 - 2
packages/app/src/components/Questionnaire/QuestionnaireModalManager.tsx

@@ -5,12 +5,19 @@ import QuestionnaireToast from './QuestionnaireToast';
 
 import styles from './QuestionnaireModalManager.module.scss';
 
-const QuestionnaireModalManager = ():JSX.Element => {
+type QuestionnaireModalManagerProps = {
+  growiQuestionnaireServerOrigin: string;
+}
+
+const QuestionnaireModalManager = ({ growiQuestionnaireServerOrigin }: QuestionnaireModalManagerProps):JSX.Element => {
   const { data: questionnaireOrders } = useSWRxQuestionnaireOrders();
 
   return <>
     {questionnaireOrders?.map((questionnaireOrder) => {
-      return <QuestionnaireModal questionnaireOrder={questionnaireOrder} key={questionnaireOrder._id} />;
+      return <QuestionnaireModal
+        questionnaireOrder={questionnaireOrder}
+        growiQuestionnaireServerOrigin = {growiQuestionnaireServerOrigin}
+        key={questionnaireOrder._id} />;
     })}
     <div className={styles['grw-questionnaire-toasts']}>
       {questionnaireOrders?.map((questionnaireOrder) => {

+ 6 - 0
packages/app/src/interfaces/questionnaire/answer.ts

@@ -0,0 +1,6 @@
+import { Types } from 'mongoose';
+
+export interface IAnswer {
+  question: Types.ObjectId | string
+  value: string
+}

+ 10 - 0
packages/app/src/interfaces/questionnaire/questionnaire-answer.ts

@@ -0,0 +1,10 @@
+import { IAnswer } from './answer';
+import { IGrowiInfo } from './growi-info';
+import { IUserInfo } from './user-info';
+
+export interface IQuestionnaireAnswer {
+  answers: IAnswer[]
+  answeredAt: Date
+  growiInfo: IGrowiInfo
+  userInfo: IUserInfo
+}

+ 5 - 1
packages/app/src/pages/[[...path]].page.tsx

@@ -209,6 +209,8 @@ type Props = CommonProps & {
   userUISettings?: IUserUISettings
   // Sidebar
   sidebarConfig: ISidebarConfig,
+
+  growiQuestionnaireServerOrigin: string,
 };
 
 const Page: NextPageWithLayout<Props> = (props: Props) => {
@@ -392,7 +394,7 @@ Page.getLayout = function getLayout(page) {
       <DescendantsPageListModal />
       <DrawioModal />
       <HandsontableModal />
-      <QuestionnaireModalManager />
+      <QuestionnaireModalManager growiQuestionnaireServerOrigin={page.props.growiQuestionnaireServerOrigin}/>
     </>
   );
 };
@@ -618,6 +620,8 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
     isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
     isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
   };
+
+  props.growiQuestionnaireServerOrigin = configManager.getConfig('crowi', 'app:growiQuestionnaireServerOrigin');
 }
 
 /**