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

Merge pull request #7299 from weseek/feat/proactive-questionnaire

feat: Proactive questionnaire
Haku Mizuki 3 лет назад
Родитель
Сommit
5626d5a729

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

@@ -50,6 +50,8 @@
   "Sign up with Google Account": "Sign up with Google Account",
   "Sign up with Google Account": "Sign up with Google Account",
   "Sign in with Google Account": "Sign in with Google Account",
   "Sign in with Google Account": "Sign in with Google Account",
   "Sign up with this Google Account": "Sign up with this Google Account",
   "Sign up with this Google Account": "Sign up with this Google Account",
+  "Select": "Select",
+  "Required": "Required",
   "Example": "Example",
   "Example": "Example",
   "Taro Yamada": "John Doe",
   "Taro Yamada": "John Doe",
   "List View": "List",
   "List View": "List",
@@ -136,6 +138,7 @@
   "edited this page": "edited this page.",
   "edited this page": "edited this page.",
   "List Drafts": "Drafts",
   "List Drafts": "Drafts",
   "Deleted Pages": "Deleted Pages",
   "Deleted Pages": "Deleted Pages",
+  "Questionnaire": "Questionnaire",
   "Disassociate": "Disassociate",
   "Disassociate": "Disassociate",
   "No bookmarks yet": "No bookmarks yet",
   "No bookmarks yet": "No bookmarks yet",
   "add_bookmark": "Add to Bookmarks",
   "add_bookmark": "Add to Bookmarks",
@@ -802,6 +805,7 @@
     "bookmarks": "Bookmarks",
     "bookmarks": "Bookmarks",
     "recently_created": "Recently Created"
     "recently_created": "Recently Created"
   },
   },
+
   "v5_page_migration": {
   "v5_page_migration": {
     "page_tree_not_avaliable" : "Page tree feature is not available yet.",
     "page_tree_not_avaliable" : "Page tree feature is not available yet.",
     "go_to_settings": "Go to settings to enable the feature"
     "go_to_settings": "Go to settings to enable the feature"
@@ -816,7 +820,25 @@
     "disagree": "Disagree",
     "disagree": "Disagree",
     "answer": "Answer",
     "answer": "Answer",
     "no_answer": "No answer",
     "no_answer": "No answer",
-    "settings": "Questionnaire settings"
+    "settings": "Questionnaire settings",
+    "title": "GROWI questionnaire for service improvement",
+    "successfully_submit": "Your survey has been submitted.",
+    "thanks_for_answer": "Thank you very much for taking the time to complete the survey.",
+    "more_satisfied_services": "We hope that GROWI customers will be even more satisfied with our services,",
+    "strive_to_improve_services": "we will strive to improve our services based on your feedback.",
+    "satisfaction_with_growi": "Satisfaction with GROWI",
+    "history_of_growi_use": "History of GROWI use",
+    "position": "Position",
+    "occupation": "Occupation",
+    "comment_on_growi": "Comment on GROWI",
+    "length_of_experience": {
+      "more_than_two_years": "More than 2 years",
+      "one_to_two_years": "More than 1 year but less than 2 years",
+      "six_months_to_one_year": "More than 6 months but less than 1 year",
+      "three_months_to_six_months": "More than 3 months but less than 6 months",
+      "one_month_to_three_months": "More than 1 month but less than 3 months",
+      "less_than_one_month": "Less than 1 month"
+    }
   },
   },
   "tag_edit_modal": {
   "tag_edit_modal": {
     "edit_tags": "Edit Tags",
     "edit_tags": "Edit Tags",

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

@@ -47,6 +47,8 @@
   "Sign up with Google Account": "Google で登録",
   "Sign up with Google Account": "Google で登録",
   "Sign in with Google Account": "Google でログイン",
   "Sign in with Google Account": "Google でログイン",
   "Sign up with this Google Account": "この Google アカウントで登録します",
   "Sign up with this Google Account": "この Google アカウントで登録します",
+  "Select": "選択してください",
+  "Required": "必須",
   "Example": "例",
   "Example": "例",
   "Taro Yamada": "山田 太郎",
   "Taro Yamada": "山田 太郎",
   "List View": "リスト表示",
   "List View": "リスト表示",
@@ -135,6 +137,7 @@
   "edited this page": "さんがこのページを編集しました。",
   "edited this page": "さんがこのページを編集しました。",
   "List Drafts": "下書き一覧",
   "List Drafts": "下書き一覧",
   "Deleted Pages": "削除済みページ",
   "Deleted Pages": "削除済みページ",
+  "Questionnaire": "アンケート",
   "Disassociate": "連携解除",
   "Disassociate": "連携解除",
   "Color mode": "カラーモード",
   "Color mode": "カラーモード",
   "Sidebar mode": "サイドバーモード",
   "Sidebar mode": "サイドバーモード",
@@ -816,7 +819,25 @@
     "disagree": "そう思わない",
     "disagree": "そう思わない",
     "answer": "回答する",
     "answer": "回答する",
     "no_answer": "無回答",
     "no_answer": "無回答",
-    "settings": "アンケート設定"
+    "settings": "アンケート設定",
+    "title": "GROWI サービス改善のためのアンケート",
+    "successfully_submit": "アンケートの送信が完了しました。",
+    "thanks_for_answer": "アンケートのご回答誠にありがとうございました。",
+    "more_satisfied_services": "GROWI をご利用の皆さまに更にご満足いただけるよう",
+    "strive_to_improve_services": "皆さまからのご意見を参考にサービス改善に務めてまいります。",
+    "satisfaction_with_growi": "GROWI の満足度",
+    "history_of_growi_use": "GROWI の利用歴",
+    "position": "職種",
+    "occupation": "役職",
+    "comment_on_growi": "GROWI へのコメント",
+    "length_of_experience": {
+      "more_than_two_years": "2年以上",
+      "one_to_two_years": "1年以上2年未満",
+      "six_months_to_one_year": "6ヶ月以上1年未満",
+      "three_months_to_six_months": "3ヶ月以上6ヶ月未満",
+      "one_month_to_three_months": "1ヶ月以上3ヶ月未満",
+      "less_than_one_month": "1ヶ月未満"
+    }
   },
   },
   "tag_edit_modal": {
   "tag_edit_modal": {
     "edit_tags": "タグの編集",
     "edit_tags": "タグの編集",

+ 24 - 0
packages/app/public/static/locales/zh_CN/translation.json

@@ -49,6 +49,8 @@
 	"Sign up with this Google Account": "Sign up with this Google Account",
 	"Sign up with this Google Account": "Sign up with this Google Account",
 	"Example": "例如",
 	"Example": "例如",
 	"Taro Yamada": "John Doe",
 	"Taro Yamada": "John Doe",
+  "Select": "请选择",
+  "Required": "必需的",
 	"List View": "列表",
 	"List View": "列表",
 	"Timeline View": "时间线",
 	"Timeline View": "时间线",
   "History": "历史",
   "History": "历史",
@@ -143,6 +145,7 @@
 	"edited this page": "edited this page.",
 	"edited this page": "edited this page.",
 	"List Drafts": "草稿",
 	"List Drafts": "草稿",
 	"Deleted Pages": "已删除页",
 	"Deleted Pages": "已删除页",
+  "Questionnaire": "调查",
   "Disassociate": "解除关联",
   "Disassociate": "解除关联",
   "No bookmarks yet": "暂无书签",
   "No bookmarks yet": "暂无书签",
   "add_bookmark": "添加到书签",
   "add_bookmark": "添加到书签",
@@ -807,6 +810,27 @@
     "bookmarks": "书签",
     "bookmarks": "书签",
     "recently_created": "最近创建页面"
     "recently_created": "最近创建页面"
   },
   },
+  "questionnaire": {
+    "title": "改善服务的GROWI调查表",
+    "successfully_submit": "问卷已经发出。",
+    "thanks_for_answer": "非常感谢您完成问卷调查。",
+    "more_satisfied_services": "我们希望让使用GROWI的人更加满意",
+    "strive_to_improve_services": "我们将利用你的反馈来改善我们的服务。",
+    "satisfaction_with_growi": "对GROWI的满意程度",
+    "history_of_growi_use": "GROWI的使用历史",
+    "position": "职业类型",
+    "occupation": "职位",
+    "comment_on_growi": "关于GROWI的评论",
+    "answer": "答案是",
+    "length_of_experience": {
+      "more_than_two_years": "2年以上",
+      "one_to_two_years": "超过1年但少于2年",
+      "six_months_to_one_year": "超过6个月但少于1年",
+      "three_months_to_six_months": "超过3个月但少于6个月",
+      "one_month_to_three_months": "超过1个月但少于3个月",
+      "less_than_one_month": "不到1个月"
+    }
+  },
   "v5_page_migration": {
   "v5_page_migration": {
     "page_tree_not_avaliable": "Page Tree 功能不可用",
     "page_tree_not_avaliable": "Page Tree 功能不可用",
     "go_to_settings": "进入设置,启用该功能"
     "go_to_settings": "进入设置,启用该功能"

+ 25 - 9
packages/app/src/components/Navbar/PersonalDropdown.jsx → packages/app/src/components/Navbar/PersonalDropdown.tsx

@@ -1,4 +1,4 @@
-import React, { useRef } from 'react';
+import { useRef, useState } from 'react';
 
 
 import { UserPicture } from '@growi/ui';
 import { UserPicture } from '@growi/ui';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
@@ -9,15 +9,23 @@ import { toastError } from '~/client/util/apiNotification';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
 
 
-const PersonalDropdown = () => {
+import ProactiveQuestionnaireModal from '../Questionnaire/ProactiveQuestionnaireModal';
+
+const PersonalDropdown = (): JSX.Element => {
   const { t } = useTranslation('commons');
   const { t } = useTranslation('commons');
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
 
 
+  const [isQuestionnaireModalOpen, setQuestionnaireModalOpen] = useState(false);
+
   // ripple
   // ripple
   const buttonRef = useRef(null);
   const buttonRef = useRef(null);
   useRipple(buttonRef, { rippleColor: 'rgba(255, 255, 255, 0.3)' });
   useRipple(buttonRef, { rippleColor: 'rgba(255, 255, 255, 0.3)' });
 
 
-  const user = currentUser || {};
+  if (currentUser == null) {
+    return <div className="text-muted text-center mb-5">
+      <i className="fa fa-2x fa-spinner fa-pulse mr-1" />
+    </div>;
+  }
 
 
   const logoutHandler = async() => {
   const logoutHandler = async() => {
     try {
     try {
@@ -35,26 +43,26 @@ const PersonalDropdown = () => {
       {/* remove .dropdown-toggle for hide caret */}
       {/* remove .dropdown-toggle for hide caret */}
       {/* See https://stackoverflow.com/a/44577512/13183572 */}
       {/* See https://stackoverflow.com/a/44577512/13183572 */}
       <button className="bg-transparent border-0 nav-link" type="button" ref={buttonRef} data-toggle="dropdown" data-testid="personal-dropdown-button">
       <button className="bg-transparent border-0 nav-link" type="button" ref={buttonRef} data-toggle="dropdown" data-testid="personal-dropdown-button">
-        <UserPicture user={user} noLink noTooltip /><span className="ml-1 d-none d-lg-inline-block">&nbsp;{user.name}</span>
+        <UserPicture user={currentUser} noLink noTooltip /><span className="ml-1 d-none d-lg-inline-block">&nbsp;{currentUser.name}</span>
       </button>
       </button>
 
 
       {/* Menu */}
       {/* Menu */}
       <div className="dropdown-menu dropdown-menu-right" data-testid="personal-dropdown-menu">
       <div className="dropdown-menu dropdown-menu-right" data-testid="personal-dropdown-menu">
 
 
         <div className="px-4 pt-3 pb-2 text-center">
         <div className="px-4 pt-3 pb-2 text-center">
-          <UserPicture user={user} size="lg" noLink noTooltip />
+          <UserPicture user={currentUser} size="lg" noLink noTooltip />
 
 
           <h5 className="mt-2">
           <h5 className="mt-2">
-            {user.name}
+            {currentUser.name}
           </h5>
           </h5>
 
 
           <div className="my-2">
           <div className="my-2">
-            <i className="icon-user icon-fw"></i>{user.username}<br />
-            <i className="icon-envelope icon-fw"></i><span className="grw-email-sm">{user.email}</span>
+            <i className="icon-user icon-fw"></i>{currentUser.username}<br />
+            <i className="icon-envelope icon-fw"></i><span className="grw-email-sm">{currentUser.email}</span>
           </div>
           </div>
 
 
           <div className="btn-group btn-block mt-2" role="group">
           <div className="btn-group btn-block mt-2" role="group">
-            <Link href={`/user/${user.username}`}>
+            <Link href={`/user/${currentUser.username}`}>
               <a className="btn btn-sm btn-outline-secondary col" data-testid="grw-personal-dropdown-menu-user-home">
               <a className="btn btn-sm btn-outline-secondary col" data-testid="grw-personal-dropdown-menu-user-home">
                 <i className="icon-fw icon-home"></i>{t('personal_dropdown.home')}
                 <i className="icon-fw icon-home"></i>{t('personal_dropdown.home')}
               </a>
               </a>
@@ -69,9 +77,17 @@ const PersonalDropdown = () => {
 
 
         <div className="dropdown-divider"></div>
         <div className="dropdown-divider"></div>
 
 
+        <button type="button" className="dropdown-item" onClick={() => setQuestionnaireModalOpen(true)}>
+          <i className="icon-fw icon-pencil"></i>{ t('Questionnaire') }
+        </button>
+
+        <div className="dropdown-divider"></div>
+
         <button type="button" className="dropdown-item" onClick={logoutHandler}><i className="icon-fw icon-power"></i>{t('Sign out')}</button>
         <button type="button" className="dropdown-item" onClick={logoutHandler}><i className="icon-fw icon-power"></i>{t('Sign out')}</button>
       </div>
       </div>
 
 
+      <ProactiveQuestionnaireModal isOpen={isQuestionnaireModalOpen} onClose={() => setQuestionnaireModalOpen(false)} />
+
     </>
     </>
   );
   );
 
 

+ 153 - 0
packages/app/src/components/Questionnaire/ProactiveQuestionnaireModal.tsx

@@ -0,0 +1,153 @@
+import { useState, useCallback } from 'react';
+
+import { useTranslation } from 'next-i18next';
+import {
+  Modal, ModalBody,
+} from 'reactstrap';
+
+import { useSiteUrl, useGrowiVersion } from '~/stores/context';
+
+type ModalProps = {
+  isOpen: boolean,
+  onClose: () => void,
+};
+
+const QuestionnaireCompletionModal = (props: ModalProps): JSX.Element => {
+  const { t } = useTranslation();
+
+  const { isOpen, onClose } = props;
+
+  return (
+    <Modal
+      size="lg"
+      isOpen={isOpen}
+      toggle={onClose}
+      centered
+    >
+      <ModalBody className="bg-primary overflow-hidden p-0" style={{ borderRadius: 8 }}>
+        <div className="bg-white m-2 p-4" style={{ borderRadius: 8 }}>
+          <div className="text-center">
+            <h2 className="my-4">{t('questionnaire.title')}</h2>
+            <p className="mb-1">{t('questionnaire.successfully_submit')}</p>
+            <p>{t('questionnaire.thanks_for_answer')}</p>
+          </div>
+          <div className="text-center my-3">
+            <span style={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={onClose}>{t('Close')}</span>
+          </div>
+        </div>
+      </ModalBody>
+    </Modal>
+  );
+};
+
+const ProactiveQuestionnaireModal = (props: ModalProps): JSX.Element => {
+  const { t } = useTranslation();
+
+  const { isOpen, onClose } = props;
+  const { data: siteUrl } = useSiteUrl();
+  const { data: growiVersion } = useGrowiVersion();
+
+  const [isQuestionnaireCompletionModal, setQuestionnaireCompletionModal] = useState(false);
+
+  const submitHandler = useCallback(async(e) => {
+    e.preventDefault();
+
+    const formData = e.target.elements;
+
+    const {
+      satisfaction: { value: satisfaction },
+      lengthOfExperience: { value: lengthOfExperience },
+      position: { value: position },
+      occupation: { value: occupation },
+      commentText: { value: commentText },
+    } = formData;
+
+    const sendValues = {
+      satisfaction: Number(satisfaction),
+      lengthOfExperience,
+      position,
+      occupation,
+      commentText,
+      growiUri: siteUrl,
+      growiVersion,
+    };
+
+    // TODO: send qestionnaire data
+
+    onClose();
+    setQuestionnaireCompletionModal(true);
+  }, [growiVersion, onClose, siteUrl]);
+
+  return (
+    <>
+      <Modal
+        size="lg"
+        isOpen={isOpen}
+        toggle={onClose}
+        centered
+      >
+        <ModalBody className="bg-primary overflow-hidden p-0" style={{ borderRadius: 8 }}>
+          <div className="bg-white m-2 p-4" style={{ borderRadius: 8 }}>
+            <div className="text-center">
+              <h2 className="my-4">{t('questionnaire.title')}</h2>
+              <p className="mb-1">{t('questionnaire.more_satisfied_services')}</p>
+              <p>{t('questionnaire.strive_to_improve_services')}</p>
+            </div>
+            <form className="px-5" onSubmit={submitHandler}>
+              <div className="form-group row mt-5">
+                <label className="col-sm-5 col-form-label" htmlFor="satisfaction">
+                  <span className="badge badge-primary mr-2">{t('Required')}</span>{t('questionnaire.satisfaction_with_growi')}
+                </label>
+                <select className="col-sm-7 form-control" name="satisfaction" id="satisfaction" required>
+                  <option value="">▼ {t('Select')}</option>
+                  <option>1</option>
+                  <option>2</option>
+                  <option>3</option>
+                  <option>4</option>
+                  <option>5</option>
+                </select>
+              </div>
+              <div className="form-group row mt-3">
+                <label className="col-sm-5 col-form-label" htmlFor="lengthOfExperience">{t('questionnaire.history_of_growi_use')}</label>
+                <select
+                  name="lengthOfExperience"
+                  id="lengthOfExperience"
+                  className="col-sm-7 form-control"
+                >
+                  <option value="">▼ {t('Select')}</option>
+                  <option>{t('questionnaire.length_of_experience.more_than_two_years')}</option>
+                  <option>{t('questionnaire.length_of_experience.one_to_two_years')}</option>
+                  <option>{t('questionnaire.length_of_experience.six_months_to_one_year')}</option>
+                  <option>{t('questionnaire.length_of_experience.three_months_to_six_months')}</option>
+                  <option>{t('questionnaire.length_of_experience.one_month_to_three_months')}</option>
+                  <option>{t('questionnaire.length_of_experience.less_than_one_month')}</option>
+                </select>
+              </div>
+              <div className="form-group row mt-3">
+                <label className="col-sm-5 col-form-label" htmlFor="position">{t('questionnaire.position')}</label>
+                <input className="col-sm-7 form-control" type="text" name="position" id="position" />
+              </div>
+              <div className="form-group row mt-3">
+                <label className="col-sm-5 col-form-label" htmlFor="occupation">{t('questionnaire.occupation')}</label>
+                <input className="col-sm-7 form-control" type="text" name="occupation" id="occupation" />
+              </div>
+              <div className="form-group row mt-3">
+                <label className="col-sm-5 col-form-label" htmlFor="commentText">{t('questionnaire.comment_on_growi')}</label>
+                <textarea className="col-sm-7 form-control" name="commentText" id="commentText" rows={5} />
+              </div>
+              <div className="text-center mt-5">
+                <button type="submit" className="btn btn-primary">{t('questionnaire.answer')}</button>
+              </div>
+              <div className="text-center my-3">
+                <span style={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={onClose}>{t('Close')}</span>
+              </div>
+            </form>
+          </div>
+        </ModalBody>
+      </Modal>
+      <QuestionnaireCompletionModal isOpen={isQuestionnaireCompletionModal} onClose={() => setQuestionnaireCompletionModal(false)} />
+    </>
+  );
+};
+
+export default ProactiveQuestionnaireModal;