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

refs 112702: questionnaire modal

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

+ 8 - 0
packages/app/public/static/locales/ja_JP/translation.json

@@ -797,5 +797,13 @@
   "v5_page_migration": {
     "page_tree_not_avaliable" : "Page Tree 機能は現在使用できません。",
     "go_to_settings": "設定する"
+  },
+  "questionnaire": {
+    "Questionnaire": "アンケート",
+    "don't show again": "今後このアンケートを表示しない",
+    "answer later": "今は回答しない",
+    "strongly agree": "非常にそう思う",
+    "strongly disagree": "全くそう思わない",
+    "send answer": "回答する"
   }
 }

+ 36 - 0
packages/app/src/components/Questionnaire/Question.tsx

@@ -0,0 +1,36 @@
+import { IQuestionHasId } from '~/interfaces/questionnaire/question';
+
+type QuestionProps = {
+  question: IQuestionHasId,
+}
+
+const Question = ({ question }: QuestionProps): JSX.Element => {
+  return <div className="row mt-4">
+    <div className="col-5 d-flex align-items-center">
+      <span>
+        {question.text}
+      </span>
+    </div>
+    <div className="col-7 d-flex align-items-center">
+      <div className="btn-group btn-group-toggle flex-fill" data-toggle="buttons">
+        <label className="btn btn-outline-primary">
+          <input type="radio" name="options" id="option1"/> 1
+        </label>
+        <label className="btn btn-outline-primary">
+          <input type="radio" name="options" id="option2" /> 2
+        </label>
+        <label className="btn btn-outline-primary">
+          <input type="radio" name="options" id="option3" /> 3
+        </label>
+        <label className="btn btn-outline-primary">
+          <input type="radio" name="options" id="option3" /> 4
+        </label>
+        <label className="btn btn-outline-primary">
+          <input type="radio" name="options" id="option3" /> 5
+        </label>
+      </div>
+    </div>
+  </div>;
+};
+
+export default Question;

+ 57 - 0
packages/app/src/components/Questionnaire/QuestionnaireModal.tsx

@@ -0,0 +1,57 @@
+import { useTranslation } from 'next-i18next';
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+
+import { useQuestionnaireModal } from '~/stores/modal';
+import { useSWRxQuestionnaireOrders } from '~/stores/questionnaire';
+
+import Question from './Question';
+
+const QuestionnaireModal = (): JSX.Element => {
+  const { data: questionnaireModalData, close: closeQuestionnaireModal } = useQuestionnaireModal();
+  const isOpened = questionnaireModalData?.isOpened;
+
+  const { data: questionnaireOrders } = useSWRxQuestionnaireOrders();
+
+  const questions = questionnaireOrders?.flatMap(questionnaireOrder => questionnaireOrder.questions);
+
+  const { t } = useTranslation();
+
+  return (<Modal
+    size="lg"
+    isOpen={isOpened}
+    toggle={() => closeQuestionnaireModal()}
+  >
+    <ModalHeader
+      tag="h4"
+      toggle={() => closeQuestionnaireModal()}
+      className="bg-primary text-light"
+      cssModule={{ 'modal-title': 'modal-title flex-fill' }}>
+      <div className="d-flex">
+        <span className="mr-auto">{t('questionnaire.Questionnaire')}</span>
+        <button type="button" className="btn btn-secondary mr-2">{t('questionnaire.don\'t show again')}</button>
+        <button type="button" className="btn btn-secondary">{t('questionnaire.answer later')}</button>
+      </div>
+    </ModalHeader>
+    <ModalBody className="my-4">
+      <div className="container">
+        <div className="row">
+          <div className="col-5"> </div>
+          <div className="col-7 d-flex justify-content-between">
+            <span>{t('questionnaire.strongly disagree')}</span>
+            <span>{t('questionnaire.strongly agree')}</span>
+          </div>
+        </div>
+        {questions?.map((question) => {
+          return <Question question={question} key={question._id.toString()} />;
+        })}
+      </div>
+    </ModalBody>
+    <ModalFooter>
+      <button type="button" className="btn btn-primary">{t('questionnaire.send answer')}</button>
+    </ModalFooter>
+  </Modal>);
+};
+
+export default QuestionnaireModal;

+ 4 - 0
packages/app/src/interfaces/questionnaire/condition.ts

@@ -1,3 +1,5 @@
+import { HasObjectId } from '@growi/core';
+
 import { GrowiServiceType } from './growi-info';
 import { UserType } from './user-info';
 
@@ -14,3 +16,5 @@ export interface ICondition {
   user: UserCondition
   growi: GrowiCondition
 }
+
+export type IConditionHasId = ICondition & HasObjectId;

+ 4 - 0
packages/app/src/interfaces/questionnaire/question.ts

@@ -1,3 +1,5 @@
+import { HasObjectId } from '@growi/core';
+
 export const QuestionType = { points: 'points', text: 'text' } as const;
 
 type QuestionType = typeof QuestionType[keyof typeof QuestionType];
@@ -6,3 +8,5 @@ export interface IQuestion {
   type: QuestionType
   text: string
 }
+
+export type IQuestionHasId = IQuestion & HasObjectId;

+ 11 - 2
packages/app/src/interfaces/questionnaire/questionnaire-order.ts

@@ -1,5 +1,7 @@
-import { ICondition } from './condition';
-import { IQuestion } from './question';
+import { HasObjectId } from '@growi/core';
+
+import { ICondition, IConditionHasId } from './condition';
+import { IQuestion, IQuestionHasId } from './question';
 
 export interface IQuestionnaireOrder {
   showFrom: Date
@@ -7,3 +9,10 @@ export interface IQuestionnaireOrder {
   questions: IQuestion[]
   condition: ICondition
 }
+
+export type IQuestionnaireOrderHasId = {
+  showFrom: Date
+  showUntil: Date
+  questions: IQuestionHasId[]
+  condition: IConditionHasId
+} & HasObjectId;

+ 2 - 0
packages/app/src/pages/[[...path]].page.tsx

@@ -25,6 +25,7 @@ import { MainPane } from '~/components/Layout/MainPane';
 import { PageAlerts } from '~/components/PageAlert/PageAlerts';
 // import { useTranslation } from '~/i18n';
 import { PageContentFooter } from '~/components/PageContentFooter';
+import QuestionnaireModal from '~/components/Questionnaire/QuestionnaireModal';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import { UsersHomePageFooterProps } from '~/components/UsersHomePageFooter';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
@@ -372,6 +373,7 @@ Page.getLayout = function getLayout(page) {
       <DescendantsPageListModal />
       <DrawioModal />
       <HandsontableModal />
+      <QuestionnaireModal />
     </>
   );
 };

+ 2 - 0
packages/app/src/server/routes/apiv3/index.js

@@ -108,6 +108,8 @@ module.exports = (crowi, app) => {
 
   router.use('/user-ui-settings', require('./user-ui-settings')(crowi));
 
+  router.use('/questionnaire-orders', require('./questionnaire-orders')());
+
 
   return [router, routerForAdmin, routerForAuth];
 };

+ 33 - 0
packages/app/src/server/routes/apiv3/questionnaire-orders.ts

@@ -0,0 +1,33 @@
+import express, { Request } from 'express';
+
+import QuestionnaireOrder from '~/server/models/questionnaire/questionnaire-order';
+import loggerFactory from '~/utils/logger';
+
+import { ApiV3Response } from './interfaces/apiv3-response';
+
+const logger = loggerFactory('growi:routes:apiv3:questionnaire');
+
+const router = express.Router();
+
+module.exports = () => {
+
+  router.get('/', async(req: Request, res: ApiV3Response) => {
+    const currentDate = new Date();
+    try {
+      const questionnaireOrders = await QuestionnaireOrder.find({
+        showUntil: {
+          $gte: currentDate,
+        },
+      });
+
+      return res.apiv3({ questionnaireOrders });
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err(err, 500);
+    }
+  });
+
+  return router;
+
+};

+ 23 - 0
packages/app/src/stores/modal.tsx

@@ -580,3 +580,26 @@ export const useConflictDiffModal = (): SWRResponse<ConflictDiffModalStatus, Err
     },
   });
 };
+
+/*
+* QuestionnaireModal
+*/
+type QuestionnaireModalStatus = {
+  isOpened: boolean,
+}
+
+type QuestionnaireModalStatusUtils = {
+  open(): Promise<QuestionnaireModalStatus | undefined>
+  close(): Promise<QuestionnaireModalStatus | undefined>
+}
+
+export const useQuestionnaireModal = (status?: QuestionnaireModalStatus): SWRResponse<QuestionnaireModalStatus, Error> & QuestionnaireModalStatusUtils => {
+  const initialData: QuestionnaireModalStatus = { isOpened: true };
+  const swrResponse = useStaticSWR<QuestionnaireModalStatus, Error>('questionnaireModalStatus', status, { fallbackData: initialData });
+
+  return {
+    ...swrResponse,
+    open: () => swrResponse.mutate({ isOpened: true }),
+    close: () => swrResponse.mutate({ isOpened: false }),
+  };
+};

+ 13 - 0
packages/app/src/stores/questionnaire.tsx

@@ -0,0 +1,13 @@
+import useSWR, { SWRResponse } from 'swr';
+
+import { apiv3Get } from '~/client/util/apiv3-client';
+import { IQuestionnaireOrderHasId } from '~/interfaces/questionnaire/questionnaire-order';
+
+export const useSWRxQuestionnaireOrders = (): SWRResponse<IQuestionnaireOrderHasId[], Error> => {
+  return useSWR(
+    '/questionnaire-orders',
+    endpoint => apiv3Get(endpoint).then((response) => {
+      return response.data.questionnaireOrders;
+    }),
+  );
+};