Bläddra i källkod

Merge pull request #7294 from weseek/feat/admin-questionnaire-toggle-enable-disable

feat: Admin questionnaire toggle enable disable
Haku Mizuki 3 år sedan
förälder
incheckning
1b723a218a

+ 8 - 0
packages/app/src/components/Admin/App/AppSettingsPageContents.tsx

@@ -16,6 +16,7 @@ import MailSetting from './MailSetting';
 import { MaintenanceMode } from './MaintenanceMode';
 import { MaintenanceMode } from './MaintenanceMode';
 import SiteUrlSetting from './SiteUrlSetting';
 import SiteUrlSetting from './SiteUrlSetting';
 import V5PageMigration from './V5PageMigration';
 import V5PageMigration from './V5PageMigration';
+import QuestionnaireSettings from './QuestionnaireSettings';
 
 
 
 
 const logger = loggerFactory('growi:appSettings');
 const logger = loggerFactory('growi:appSettings');
@@ -107,6 +108,13 @@ const AppSettingsPageContents = (props: Props) => {
         </div>
         </div>
       </div>
       </div>
 
 
+      <div className="row mt-5">
+        <div className="col-lg-12">
+          <h2 className="admin-setting-header">アンケート設定</h2>
+          <QuestionnaireSettings />
+        </div>
+      </div>
+
       <div className="row">
       <div className="row">
         <div className="col-lg-12">
         <div className="col-lg-12">
           <h2 className="admin-setting-header" id="maintenance-mode">{t('admin:maintenance_mode.maintenance_mode')}</h2>
           <h2 className="admin-setting-header" id="maintenance-mode">{t('admin:maintenance_mode.maintenance_mode')}</h2>

+ 109 - 0
packages/app/src/components/Admin/App/QuestionnaireSettings.tsx

@@ -0,0 +1,109 @@
+import {
+  useState, useCallback, useEffect,
+} from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Put } from '~/client/util/apiv3-client';
+import { useSWRxAppSettings } from '~/stores/admin/app-settings';
+import loggerFactory from '~/utils/logger';
+
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
+const logger = loggerFactory('growi:questionnaireSettings');
+
+const QuestionnaireSettings = (): JSX.Element => {
+  // TODO: i18n
+  const { t } = useTranslation();
+
+  const { data, error, mutate } = useSWRxAppSettings();
+
+  const [isEnableQuestionnaire, setIsEnableQuestionnaire] = useState(data?.isEnableQuestionnaire);
+  const onChangeIsEnableQuestionnaireHandler = useCallback(() => {
+    setIsEnableQuestionnaire(prev => !prev);
+  }, []);
+
+  const [isAppSiteUrlHashed, setIsAppSiteUrlHashed] = useState(data?.isAppSiteUrlHashed);
+  const onChangeisAppSiteUrlHashedHandler = useCallback(() => {
+    setIsAppSiteUrlHashed(prev => !prev);
+  }, []);
+
+  const onSubmitHandler = useCallback(async() => {
+    try {
+      await apiv3Put('/app-settings/questionnaire-settings', {
+        isEnableQuestionnaire,
+        isAppSiteUrlHashed,
+      });
+      toastSuccess(t('toaster.update_successed', { target: 'アンケート設定', ns: 'commons' }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+    mutate();
+  }, [isAppSiteUrlHashed, isEnableQuestionnaire, mutate, t]);
+
+  // Sync SWR value and state
+  useEffect(() => {
+    setIsEnableQuestionnaire(data?.isEnableQuestionnaire);
+    setIsAppSiteUrlHashed(data?.isAppSiteUrlHashed);
+  }, [data, data?.isAppSiteUrlHashed, data?.isEnableQuestionnaire]);
+
+  const isLoading = data === undefined && error === undefined;
+
+  return (
+    <div className="mb-5">
+      <p className="card well">
+        送信されるデータにユーザーの個人情報は一切含まれません。
+        <br />
+        ユーザー設定画面から個別にアンケート機能を有効無効に設定できます。
+        <br />
+        <br />
+        GROWI の改善にご協力お願いします。
+      </p>
+
+      {isLoading && <div className="text-muted text-center mb-5">
+        <i className="fa fa-2x fa-spinner fa-pulse mr-1" />
+      </div>}
+
+      {!isLoading && (
+        <>
+          <div className="row my-3">
+            <div className="custom-control custom-switch custom-checkbox-primary col-md-5 offset-md-5">
+              <input
+                type="checkbox"
+                className="custom-control-input"
+                id="isEnableQuestionnaire"
+                checked={isEnableQuestionnaire}
+                onChange={onChangeIsEnableQuestionnaireHandler}
+              />
+              <label className="custom-control-label" htmlFor="isEnableQuestionnaire">
+                アンケートを有効にする
+              </label>
+            </div>
+          </div>
+
+          <div className="row my-4">
+            <div className="custom-control custom-checkbox custom-checkbox-primary col-md-5 offset-md-5">
+              <input
+                type="checkbox"
+                className="custom-control-input"
+                id="isAppSiteUrlHashed"
+                checked={isAppSiteUrlHashed}
+                onChange={onChangeisAppSiteUrlHashedHandler}
+                disabled={!isEnableQuestionnaire}
+              />
+              <label className="custom-control-label" htmlFor="isAppSiteUrlHashed">
+                アプリ URL を暗号化して送信する
+              </label>
+            </div>
+          </div>
+
+          <AdminUpdateButtonRow onClick={onSubmitHandler}/>
+        </>
+      )}
+    </div>
+  );
+};
+
+export default QuestionnaireSettings;

+ 2 - 3
packages/app/src/components/Admin/Common/AdminUpdateButtonRow.tsx

@@ -4,8 +4,7 @@ import { useTranslation } from 'next-i18next';
 
 
 type Props = {
 type Props = {
   onClick: () => void,
   onClick: () => void,
-  disabled: boolean,
-
+  disabled?: boolean,
 }
 }
 
 
 const AdminUpdateButtonRow = (props: Props): JSX.Element => {
 const AdminUpdateButtonRow = (props: Props): JSX.Element => {
@@ -14,7 +13,7 @@ const AdminUpdateButtonRow = (props: Props): JSX.Element => {
   return (
   return (
     <div className="row my-3">
     <div className="row my-3">
       <div className="mx-auto">
       <div className="mx-auto">
-        <button type="button" className="btn btn-primary" onClick={props.onClick} disabled={props.disabled}>{ t('Update') }</button>
+        <button type="button" className="btn btn-primary" onClick={props.onClick} disabled={props.disabled ?? false}>{ t('Update') }</button>
       </div>
       </div>
     </div>
     </div>
   );
   );

+ 3 - 0
packages/app/src/interfaces/activity.ts

@@ -74,6 +74,7 @@ const ACTION_ADMIN_MAIL_SMTP_UPDATE = 'ADMIN_MAIL_SMTP_UPDATE';
 const ACTION_ADMIN_MAIL_SES_UPDATE = 'ADMIN_MAIL_SES_UPDATE';
 const ACTION_ADMIN_MAIL_SES_UPDATE = 'ADMIN_MAIL_SES_UPDATE';
 const ACTION_ADMIN_MAIL_TEST_SUBMIT = 'ADMIN_MAIL_TEST_SUBMIT';
 const ACTION_ADMIN_MAIL_TEST_SUBMIT = 'ADMIN_MAIL_TEST_SUBMIT';
 const ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE = 'ADMIN_FILE_UPLOAD_CONFIG_UPDATE';
 const ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE = 'ADMIN_FILE_UPLOAD_CONFIG_UPDATE';
+const ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE = 'ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE';
 const ACTION_ADMIN_MAINTENANCEMODE_ENABLED = 'ADMIN_MAINTENANCEMODE_ENABLED';
 const ACTION_ADMIN_MAINTENANCEMODE_ENABLED = 'ADMIN_MAINTENANCEMODE_ENABLED';
 const ACTION_ADMIN_MAINTENANCEMODE_DISABLED = 'ADMIN_MAINTENANCEMODE_DISABLED';
 const ACTION_ADMIN_MAINTENANCEMODE_DISABLED = 'ADMIN_MAINTENANCEMODE_DISABLED';
 const ACTION_ADMIN_SECURITY_SETTINGS_UPDATE = 'ADMIN_SECURITY_SETTINGS_UPDATE';
 const ACTION_ADMIN_SECURITY_SETTINGS_UPDATE = 'ADMIN_SECURITY_SETTINGS_UPDATE';
@@ -248,6 +249,7 @@ export const SupportedAction = {
   ACTION_ADMIN_MAIL_SES_UPDATE,
   ACTION_ADMIN_MAIL_SES_UPDATE,
   ACTION_ADMIN_MAIL_TEST_SUBMIT,
   ACTION_ADMIN_MAIL_TEST_SUBMIT,
   ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE,
   ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE,
+  ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE,
   ACTION_ADMIN_MAINTENANCEMODE_ENABLED,
   ACTION_ADMIN_MAINTENANCEMODE_ENABLED,
   ACTION_ADMIN_MAINTENANCEMODE_DISABLED,
   ACTION_ADMIN_MAINTENANCEMODE_DISABLED,
   ACTION_ADMIN_SECURITY_SETTINGS_UPDATE,
   ACTION_ADMIN_SECURITY_SETTINGS_UPDATE,
@@ -430,6 +432,7 @@ export const LargeActionGroup = {
   ACTION_ADMIN_MAIL_SES_UPDATE,
   ACTION_ADMIN_MAIL_SES_UPDATE,
   ACTION_ADMIN_MAIL_TEST_SUBMIT,
   ACTION_ADMIN_MAIL_TEST_SUBMIT,
   ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE,
   ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE,
+  ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE,
   ACTION_ADMIN_MAINTENANCEMODE_ENABLED,
   ACTION_ADMIN_MAINTENANCEMODE_ENABLED,
   ACTION_ADMIN_MAINTENANCEMODE_DISABLED,
   ACTION_ADMIN_MAINTENANCEMODE_DISABLED,
   ACTION_ADMIN_SECURITY_SETTINGS_UPDATE,
   ACTION_ADMIN_SECURITY_SETTINGS_UPDATE,

+ 48 - 0
packages/app/src/interfaces/res/admin/app-settings.ts

@@ -0,0 +1,48 @@
+export type IResAppSettings = {
+  title: string,
+  confidential: string,
+  globalLang: string,
+  isEmailPublishedForNewUser: boolean,
+  fileUpload: string,
+  isV5Compatible: boolean,
+  siteUrl: string,
+  envSiteUrl: string,
+  isMailerSetup: boolean,
+  fromAddress: string,
+
+  transmissionMethod: string,
+  smtpHost: string,
+  smtpPort: string | number, // TODO: check
+  smtpUser: string,
+  smtpPassword: string,
+  sesAccessKeyId: string,
+  sesSecretAccessKey: string,
+
+  fileUploadType: string,
+  envFileUploadType: string,
+  useOnlyEnvVarForFileUploadType: boolean,
+
+  s3Region: string,
+  s3CustomEndpoint: string,
+  s3Bucket:string,
+  s3AccessKeyId: string,
+  s3SecretAccessKey: string,
+  s3ReferenceFileWithRelayMode: string,
+
+  gcsUseOnlyEnvVars: boolean,
+  gcsApiKeyJsonPath: string,
+  gcsBucket: string,
+  gcsUploadNamespace: string,
+  gcsReferenceFileWithRelayMode: string,
+
+  envGcsApiKeyJsonPath: string,
+  envGcsBucket: string,
+  envGcsUploadNamespace: string,
+
+  isEnabledPlugins: boolean,
+
+  isEnableQuestionnaire: boolean,
+  isAppSiteUrlHashed: boolean,
+
+  isMaintenanceMode: boolean,
+}

+ 37 - 0
packages/app/src/server/routes/apiv3/app-settings.js

@@ -198,6 +198,10 @@ module.exports = (crowi) => {
       body('s3SecretAccessKey').trim(),
       body('s3SecretAccessKey').trim(),
       body('s3ReferenceFileWithRelayMode').if(value => value != null).isBoolean(),
       body('s3ReferenceFileWithRelayMode').if(value => value != null).isBoolean(),
     ],
     ],
+    questionnaireSettings: [
+      body('isEnableQuestionnaire').isBoolean(),
+      body('isAppSiteUrlHashed').isBoolean(),
+    ],
     maintenanceMode: [
     maintenanceMode: [
       body('flag').isBoolean(),
       body('flag').isBoolean(),
     ],
     ],
@@ -266,6 +270,10 @@ module.exports = (crowi) => {
       envGcsUploadNamespace: crowi.configManager.getConfigFromEnvVars('crowi', 'gcs:uploadNamespace'),
       envGcsUploadNamespace: crowi.configManager.getConfigFromEnvVars('crowi', 'gcs:uploadNamespace'),
 
 
       isEnabledPlugins: crowi.configManager.getConfig('crowi', 'plugin:isEnabledPlugins'),
       isEnabledPlugins: crowi.configManager.getConfig('crowi', 'plugin:isEnabledPlugins'),
+
+      isEnableQuestionnaire: crowi.configManager.getConfig('crowi', 'questionnaire:isEnableQuestionnaire'),
+      isAppSiteUrlHashed: crowi.configManager.getConfig('crowi', 'questionnaire:isAppSiteUrlHashed'),
+
       isMaintenanceMode: crowi.configManager.getConfig('crowi', 'app:isMaintenanceMode'),
       isMaintenanceMode: crowi.configManager.getConfig('crowi', 'app:isMaintenanceMode'),
     };
     };
     return res.apiv3({ appSettingsParams });
     return res.apiv3({ appSettingsParams });
@@ -670,6 +678,35 @@ module.exports = (crowi) => {
 
 
   });
   });
 
 
+  // eslint-disable-next-line max-len
+  router.put('/questionnaire-settings', loginRequiredStrictly, adminRequired, addActivity, validator.questionnaireSettings, apiV3FormValidator, async(req, res) => {
+    const { isEnableQuestionnaire, isAppSiteUrlHashed } = req.body;
+
+    const requestParams = {
+      'questionnaire:isEnableQuestionnaire': isEnableQuestionnaire,
+      'questionnaire:isAppSiteUrlHashed': isAppSiteUrlHashed,
+    };
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams, true);
+
+      const responseParams = {
+        isEnableQuestionnaire: crowi.configManager.getConfig('crowi', 'questionnaire:isEnableQuestionnaire'),
+        isAppSiteUrlHashed: crowi.configManager.getConfig('crowi', 'questionnaire:isAppSiteUrlHashed'),
+      };
+
+      const parameters = { action: SupportedAction.ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE };
+      activityEvent.emit('update', res.locals.activity._id, parameters);
+      return res.apiv3({ responseParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating questionnaire settings';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-questionnaire-settings-failed'));
+    }
+
+  });
+
   router.post('/v5-schema-migration', accessTokenParser, loginRequiredStrictly, adminRequired, async(req, res) => {
   router.post('/v5-schema-migration', accessTokenParser, loginRequiredStrictly, adminRequired, async(req, res) => {
     const isMaintenanceMode = crowi.appService.isMaintenanceMode();
     const isMaintenanceMode = crowi.appService.isMaintenanceMode();
     if (!isMaintenanceMode) {
     if (!isMaintenanceMode) {

+ 12 - 0
packages/app/src/server/service/config-loader.ts

@@ -658,6 +658,18 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type: ValueType.NUMBER,
     type: ValueType.NUMBER,
     default: 4,
     default: 4,
   },
   },
+  QUESTIONNAIRE_IS_ENABLE_QUESTIONNAIRE: {
+    ns: 'crowi',
+    key: 'questionnaire:isEnableQuestionnaire',
+    type: ValueType.BOOLEAN,
+    default: true,
+  },
+  QUESTIONNAIRE_IS_APP_SITE_URL_HASHED: {
+    ns: 'crowi',
+    key: 'questionnaire:isAppSiteUrlHashed',
+    type: ValueType.BOOLEAN,
+    default: true,
+  },
 };
 };
 
 
 
 

+ 13 - 0
packages/app/src/stores/admin/app-settings.tsx

@@ -0,0 +1,13 @@
+import useSWR, { SWRResponse } from 'swr';
+
+import { apiv3Get } from '~/client/util/apiv3-client';
+import { IResAppSettings } from '~/interfaces/res/admin/app-settings';
+
+export const useSWRxAppSettings = (): SWRResponse<IResAppSettings, Error> => {
+  return useSWR(
+    '/app-settings/',
+    endpoint => apiv3Get(endpoint).then((response) => {
+      return response.data.appSettingsParams;
+    }),
+  );
+};