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

Merge pull request #10185 from weseek/support/remove-questionnaire

support: Remove questionnaire
mergify[bot] 8 месяцев назад
Родитель
Сommit
d4ace980c4
75 измененных файлов с 297 добавлено и 3645 удалено
  1. 5 0
      .changeset/tough-owls-care.md
  2. 0 1
      apps/app/.env.development
  3. 0 2
      apps/app/bin/openapi/definition-apiv3.js
  4. 0 1
      apps/app/bin/openapi/generate-spec-apiv3.sh
  5. 0 1
      apps/app/config/logger/config.dev.js
  6. 0 2
      apps/app/playwright/40-admin/access-to-admin-page.spec.ts
  7. 2 14
      apps/app/playwright/60-home/home.spec.ts
  8. 32 42
      apps/app/public/static/locales/en_US/admin.json
  9. 0 36
      apps/app/public/static/locales/en_US/commons.json
  10. 0 18
      apps/app/public/static/locales/en_US/translation.json
  11. 1 11
      apps/app/public/static/locales/fr_FR/admin.json
  12. 0 23
      apps/app/public/static/locales/fr_FR/commons.json
  13. 1 19
      apps/app/public/static/locales/fr_FR/translation.json
  14. 1 11
      apps/app/public/static/locales/ja_JP/admin.json
  15. 0 35
      apps/app/public/static/locales/ja_JP/commons.json
  16. 0 17
      apps/app/public/static/locales/ja_JP/translation.json
  17. 227 237
      apps/app/public/static/locales/zh_CN/admin.json
  18. 24 60
      apps/app/public/static/locales/zh_CN/commons.json
  19. 0 17
      apps/app/public/static/locales/zh_CN/translation.json
  20. 0 8
      apps/app/src/client/components/Admin/App/AppSettingsPageContents.tsx
  21. 1 1
      apps/app/src/client/components/Admin/App/PageBulkExportSettings.tsx
  22. 0 130
      apps/app/src/client/components/Admin/App/QuestionnaireSettings.tsx
  23. 0 5
      apps/app/src/client/components/Me/OtherSettings.tsx
  24. 0 109
      apps/app/src/client/components/Me/QuestionnaireSettings.tsx
  25. 1 1
      apps/app/src/client/components/Me/UISettings.tsx
  26. 1 19
      apps/app/src/client/components/Sidebar/SidebarNav/PersonalDropdown.tsx
  27. 0 152
      apps/app/src/features/questionnaire/client/components/ProactiveQuestionnaireModal.tsx
  28. 0 52
      apps/app/src/features/questionnaire/client/components/Question.tsx
  29. 0 167
      apps/app/src/features/questionnaire/client/components/QuestionnaireModal.tsx
  30. 0 9
      apps/app/src/features/questionnaire/client/components/QuestionnaireModalManager.module.scss
  31. 0 51
      apps/app/src/features/questionnaire/client/components/QuestionnaireModalManager.tsx
  32. 0 85
      apps/app/src/features/questionnaire/client/components/QuestionnaireToast.tsx
  33. 0 79
      apps/app/src/features/questionnaire/client/services/guest-questionnaire-answer-status.ts
  34. 0 40
      apps/app/src/features/questionnaire/client/stores/model.tsx
  35. 0 24
      apps/app/src/features/questionnaire/client/stores/questionnaire.tsx
  36. 0 4
      apps/app/src/features/questionnaire/interfaces/answer.ts
  37. 0 25
      apps/app/src/features/questionnaire/interfaces/condition.ts
  38. 0 27
      apps/app/src/features/questionnaire/interfaces/proactive-questionnaire-answer.ts
  39. 0 15
      apps/app/src/features/questionnaire/interfaces/question.ts
  40. 0 16
      apps/app/src/features/questionnaire/interfaces/questionnaire-answer-status.ts
  41. 0 21
      apps/app/src/features/questionnaire/interfaces/questionnaire-answer.ts
  42. 0 21
      apps/app/src/features/questionnaire/interfaces/questionnaire-order.ts
  43. 0 12
      apps/app/src/features/questionnaire/interfaces/user-info.ts
  44. 0 28
      apps/app/src/features/questionnaire/server/models/proactive-questionnaire-answer.ts
  45. 0 19
      apps/app/src/features/questionnaire/server/models/questionnaire-answer-status.ts
  46. 0 25
      apps/app/src/features/questionnaire/server/models/questionnaire-answer.ts
  47. 0 35
      apps/app/src/features/questionnaire/server/models/questionnaire-order.ts
  48. 0 10
      apps/app/src/features/questionnaire/server/models/schema/answer.ts
  49. 0 29
      apps/app/src/features/questionnaire/server/models/schema/condition.ts
  50. 0 42
      apps/app/src/features/questionnaire/server/models/schema/growi-info.ts
  51. 0 16
      apps/app/src/features/questionnaire/server/models/schema/question.ts
  52. 0 10
      apps/app/src/features/questionnaire/server/models/schema/user-info.ts
  53. 0 384
      apps/app/src/features/questionnaire/server/routes/apiv3/questionnaire.ts
  54. 0 515
      apps/app/src/features/questionnaire/server/service/questionnaire-cron.integ.ts
  55. 0 106
      apps/app/src/features/questionnaire/server/service/questionnaire-cron.ts
  56. 0 301
      apps/app/src/features/questionnaire/server/service/questionnaire.integ.ts
  57. 0 74
      apps/app/src/features/questionnaire/server/service/questionnaire.ts
  58. 0 70
      apps/app/src/features/questionnaire/server/util/condition.ts
  59. 0 128
      apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.spec.ts
  60. 0 40
      apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.ts
  61. 0 3
      apps/app/src/interfaces/activity.ts
  62. 0 1
      apps/app/src/interfaces/res/admin/app-settings.ts
  63. 0 2
      apps/app/src/pages/[[...path]].page.tsx
  64. 0 13
      apps/app/src/server/crowi/index.js
  65. 0 4
      apps/app/src/server/crowi/setup-models.ts
  66. 0 6
      apps/app/src/server/models/user.js
  67. 0 3
      apps/app/src/server/routes/apiv3/activity.ts
  68. 0 1
      apps/app/src/server/routes/apiv3/admin-home.ts
  69. 0 78
      apps/app/src/server/routes/apiv3/app-settings.js
  70. 0 1
      apps/app/src/server/routes/apiv3/index.js
  71. 0 46
      apps/app/src/server/routes/apiv3/personal-setting.js
  72. 0 4
      apps/app/src/server/routes/apiv3/users.js
  73. 0 29
      apps/app/src/server/service/config-manager/config-definition.ts
  74. 1 1
      apps/app/src/server/service/import/construct-convert-map.integ.ts
  75. 0 1
      packages/core/src/interfaces/user.ts

+ 5 - 0
.changeset/tough-owls-care.md

@@ -0,0 +1,5 @@
+---
+'@growi/core': minor
+---
+
+Updated the interface to accommodate the removal of the questionnaire feature.

+ 0 - 1
apps/app/.env.development

@@ -14,7 +14,6 @@ ELASTICSEARCH_URI="http://elasticsearch:9200/growi"
 ELASTICSEARCH_REQUEST_TIMEOUT=15000
 ELASTICSEARCH_REJECT_UNAUTHORIZED=true
 OGP_URI="http://ogp:8088"
-QUESTIONNAIRE_SERVER_ORIGIN="http://host.docker.internal:3003"
 # DRAWIO_URI="http://localhost:8080/?offline=1&https=0"
 # S2SMSG_PUBSUB_SERVER_TYPE=nchan
 # PUBLISH_OPEN_API=true

+ 0 - 2
apps/app/bin/openapi/definition-apiv3.js

@@ -98,8 +98,6 @@ module.exports = {
         'MongoDB',
         'NotificationSetting',
         'Plugins',
-        'Questionnaire',
-        'QuestionnaireSetting',
         'SlackIntegration',
         'SlackIntegrationSettings',
         'SlackIntegrationSettings (with proxy)',

+ 0 - 1
apps/app/bin/openapi/generate-spec-apiv3.sh

@@ -11,7 +11,6 @@ swagger-jsdoc \
   -o "${OUT}" \
   -d "${APP_PATH}/bin/openapi/definition-apiv3.js" \
   "${APP_PATH}/src/features/external-user-group/server/routes/apiv3/*.ts" \
-  "${APP_PATH}/src/features/questionnaire/server/routes/apiv3/*.ts" \
   "${APP_PATH}/src/features/templates/server/routes/apiv3/*.ts" \
   "${APP_PATH}/src/features/growi-plugin/server/routes/apiv3/**/*.ts" \
   "${APP_PATH}/src/server/routes/apiv3/**/*.{js,ts}" \

+ 0 - 1
apps/app/config/logger/config.dev.js

@@ -29,7 +29,6 @@ module.exports = {
   'growi-plugin:*': 'debug',
   'growi:service:search-delegator:elasticsearch': 'debug',
   'growi:service:g2g-transfer': 'debug',
-  'growi:service:questionnaire': 'debug',
 
   'growi:migration:add-installed-date-to-config': 'debug',
 

+ 0 - 2
apps/app/playwright/40-admin/access-to-admin-page.spec.ts

@@ -13,8 +13,6 @@ test('admin/app is successfully loaded', async({ page }) => {
   await expect(page.getByTestId('admin-app-settings')).toBeVisible();
   // await expect(page.getByTestId('v5-page-migration')).toBeVisible();
   await expect(page.locator('#cbFileUpload')).toBeChecked();
-  await expect(page.locator('#isQuestionnaireEnabled')).toBeChecked();
-  await expect(page.locator('#isAppSiteUrlHashed')).not.toBeChecked();
 });
 
 test('admin/security is successfully loaded', async({ page }) => {

+ 2 - 14
apps/app/playwright/60-home/home.spec.ts

@@ -25,18 +25,6 @@ test('Vist User settings', async({ page }) => {
   await expect(page.getByTestId('grw-user-settings')).toBeVisible();
 });
 
-test('Open questionnaire modal', async({ page }) => {
-  await page.goto('/dummy');
-
-  // Open PersonalDropdown
-  await page.getByTestId('personal-dropdown-button').click();
-  await expect(page.getByTestId('grw-personal-dropdown-menu-user-home')).toBeVisible();
-
-  // Expect the questionnaire modal to be displayed when the QuestionnaireModalToggleButton is clicked
-  await page.getByTestId('grw-proactive-questionnaire-modal-toggle-btn').click();
-  await expect(page.getByTestId('grw-proactive-questionnaire-modal')).toBeVisible();
-});
-
 test('Access User information', async({ page }) => {
   await page.goto('/me');
 
@@ -116,7 +104,7 @@ test('Acccess Other setting', async({ page }) => {
   await expect(page.getByTestId('grw-user-settings')).toBeVisible();
   await page.getByTestId('other-settings-tab-button').first().click();
 
-  // Expect a success toaster to be displayed when the QuestionnaireSettingsUpdateButton is clicked
-  await page.getByTestId('grw-questionnaire-settings-update-btn').click();
+  // Expect a success toaster to be displayed when the updating UI button is clicked
+  await page.getByTestId('grw-ui-settings-update-btn').click();
   await expect(page.locator('.Toastify__toast')).toBeVisible();
 });

+ 32 - 42
apps/app/public/static/locales/en_US/admin.json

@@ -97,9 +97,9 @@
       "closed": "Closed (Invitation Only)"
     },
     "share_link_management": "Share Link Management",
-    "No_share_links":"No share links",
-    "share_link_notice":"remove all share links",
-    "delete_all_share_links":"Delete all share links",
+    "No_share_links": "No share links",
+    "share_link_notice": "remove all share links",
+    "delete_all_share_links": "Delete all share links",
     "share_link_rights": "Share link rights",
     "enable_link_sharing": "Enable link sharing",
     "all_share_links": "All share links",
@@ -229,7 +229,7 @@
     "prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
     "slack_app_configuration": "Slack app configuration",
     "slack_app_configuration_desc": "This is the way that compatible with Crowi,<br /> but not recommended in GROWI because it is <strong>too complex</strong>.",
-    "use_instead":"Please use Slack Incoming Webhooks Configuration instead.",
+    "use_instead": "Please use Slack Incoming Webhooks Configuration instead.",
     "how_to": {
       "header": "How to configure Incoming Webhooks?",
       "workspace": "(At Workspace) Add a hook",
@@ -296,7 +296,7 @@
     "rebuild_description_1": "Click the button to rebuild index and add all page datas.",
     "rebuild_description_2": "This may take a while."
   },
-  "mailer_setup_required":"<a href='/admin/app'>Email settings</a> are required to send.",
+  "mailer_setup_required": "<a href='/admin/app'>Email settings</a> are required to send.",
   "admin_top": {
     "management_wiki": "Management Wiki",
     "system_information": "System information",
@@ -305,7 +305,7 @@
     "package_name": "Package name",
     "specified_version": "Specified version",
     "installed_version": "Installed version",
-    "list_of_env_vars":"List of environment variables",
+    "list_of_env_vars": "List of environment variables",
     "env_var_priority": "For environment variables other than security, the value of the database is obtained preferentially.",
     "about_security": "Check <a href='/admin/security'>Security Settings</a> for security environment variables.",
     "copy_prefilled_host_information": {
@@ -368,9 +368,9 @@
     "mail_settings": "E-mail Settings",
     "mailer_is_not_set_up": "E-mail setting is not set up.",
     "from_e-mail_address": "From e-mail address",
-    "transmission_method":"Transmission Method",
-    "smtp_label":"SMTP",
-    "ses_label":"SES(AWS)",
+    "transmission_method": "Transmission Method",
+    "smtp_label": "SMTP",
+    "ses_label": "SES(AWS)",
     "send_test_email": "Send a test-email",
     "success_to_send_test_email": "Success to send a test-email",
     "smtp_settings": "SMTP settings",
@@ -380,13 +380,13 @@
     "initialize_mail_settings": "initialize e-mail settings",
     "initialize_mail_modal_header": "Initialize e-mail settings",
     "confirm_to_initialize_mail_settings": "You can't restore to the current settings. Are you sure you want to initialize e-mail settings?",
-    "file_upload_settings":"File Upload Settings",
-    "file_upload_method":"File Upload Method",
-    "file_delivery_method":"File Delivery Method",
-    "file_delivery_method_redirect":"Redirect",
-    "file_delivery_method_relay":"Internal System Relay",
-    "file_delivery_method_redirect_info":"Redirect: It redirects to a signed URL without GROWI server, it gives excellent performance.",
-    "file_delivery_method_relay_info":"Internal System Relay: The GROWI server delivers to clients, it provides complete security.",
+    "file_upload_settings": "File Upload Settings",
+    "file_upload_method": "File Upload Method",
+    "file_delivery_method": "File Delivery Method",
+    "file_delivery_method_redirect": "Redirect",
+    "file_delivery_method_relay": "Internal System Relay",
+    "file_delivery_method_redirect_info": "Redirect: It redirects to a signed URL without GROWI server, it gives excellent performance.",
+    "file_delivery_method_relay_info": "Internal System Relay: The GROWI server delivers to clients, it provides complete security.",
     "fixed_by_env_var": "This is fixed by the env var <code>{{envKey}}={{envVar}}</code>.",
     "gcs_label": "GCP(GCS)",
     "aws_label": "AWS(S3)",
@@ -411,17 +411,7 @@
     "enable": "Enable",
     "disable": "Disable",
     "use_env_var_if_empty": "If the value in the database is empty, the value of the environment variable <code>{{variable}}</code> is used.",
-    "note_for_the_only_env_option": "The GCS Settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
-    "questionnaire_settings": "Questionnaire settings",
-    "questionnaire_settings_explanation": "This will enable/disable questionnaires on the whole system. When enabled, users can also enable/disable questionnaires individually from \"Other Settings\" in the personal settings page.",
-    "about_data_sent": "About information sent",
-    "docs_link": "https://docs.growi.org/en/admin-guide/management-cookbook/app-settings.html#questionnaire-settings",
-    "learn_more": "Learn more",
-    "other_info_will_be_sent": "Along with the questionnaire answer, information necessary to improve GROWI will be sent. Personal user info will not be included in the data sent.",
-    "we_will_use_the_data_to_improve_growi": "We will use the data to improve GROWI experience as much as possible.",
-    "anonymize_app_site_url": "Anonymize app site URL in data sent",
-    "url_anonymization_explanation": "The app site URL included in the questionnaire answer will be anonymized. By enabling this, the GROWI application that sends the questionnaire answer will not be identified.",
-    "enable_questionnaire": "Enable questionnaire"
+    "note_for_the_only_env_option": "The GCS Settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> ."
   },
   "markdown_settings": {
     "markdown_settings": "Markdown Settings",
@@ -502,13 +492,13 @@
       "show_page_side_authors": "Always display creators and updaters above the table of contents",
       "show_page_side_authors_desc": "Displays information about the creator and the last updater above the table of contents in the page sidebar."
     },
-      "presentation": "Presentation",
+    "presentation": "Presentation",
     "presentation_options": {
       "enable_marp": "Enable Marp ",
       "enable_marp_desc": "Marp can be used in presentation preview. This option may make you vulnerable to XSS.",
       "marp_official_site": "The Marp Official Site",
       "marp_official_site_link": "https://marp.app",
-      "marp_in_growi" : "GROWI Docs - Create slide using Marp",
+      "marp_in_growi": "GROWI Docs - Create slide using Marp",
       "marp_in_growi_link": "https://docs.growi.org/en/guide/features/marp.html"
     },
     "custom_title": "Custom title",
@@ -525,7 +515,7 @@
     "custom_presentation": "Custom presentation",
     "write_java": "You can write Javascript that is applied to whole system.",
     "reflect_change": "You need to reload the page to reflect the change.",
-    "custom_logo" : "Custom Logo",
+    "custom_logo": "Custom Logo",
     "default_logo": "Default Logo",
     "upload_logo": "Upload Logo",
     "current_logo": "Current Logo",
@@ -599,9 +589,9 @@
     },
     "import": "Import",
     "skip_username_and_email_when_overlapped": "Skip username and email using same username and email in new environment",
-    "prepare_new_account_for_migration":"Prepare new account for migration",
-    "archive_data_import_detail":"More Details? Ckick here.",
-    "admin_archive_data_import_guide_url":"https://docs.growi.org/en/admin-guide/management-cookbook/import.html",
+    "prepare_new_account_for_migration": "Prepare new account for migration",
+    "archive_data_import_detail": "More Details? Ckick here.",
+    "admin_archive_data_import_guide_url": "https://docs.growi.org/en/admin-guide/management-cookbook/import.html",
     "page_skip": "Pages with a name that already exists on GROWI are not imported",
     "Directory_hierarchy_tag": "Directory hierarchy tag"
   },
@@ -671,7 +661,7 @@
     "delete": "Delete",
     "integration_procedure": "Integration Procedure",
     "custom_bot_without_proxy_settings": "Custom Bot without proxy Settings",
-    "integration_failed":"Integration failed",
+    "integration_failed": "Integration failed",
     "reset": "Reset",
     "reset_all_settings": "Reset all settings",
     "delete_slackbot_settings": "Delete Slack Bot settings",
@@ -718,7 +708,7 @@
       "allow_specified_long": "Allow specified (Allowed from only specified channels)",
       "test_connection": "Test Connection",
       "test_connection_by_pressing_button": "Press the button to test the connection",
-      "test_connection_only_public_channel":"Please test connection in a public channel",
+      "test_connection_only_public_channel": "Please test connection in a public channel",
       "error_check_logs_below": "An error has occurred. Please check the logs below.",
       "send_message_to_slack_work_space": "Send message to Slack work space.",
       "add_slack_workspace": "Add a Slack Workspace"
@@ -753,10 +743,10 @@
     "status": "Status",
     "invite_modal": {
       "emails": "Emails (Possible to issue multiple people with new lines)",
-      "description1":"Temporarily issue new users by email addresses.",
-      "description2":"A temporary password will be generated for the first login.",
+      "description1": "Temporarily issue new users by email addresses.",
+      "description2": "A temporary password will be generated for the first login.",
       "invite_thru_email": "Send invitation email",
-      "mail_setting_link":"<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>Email settings</a>",
+      "mail_setting_link": "<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>Email settings</a>",
       "valid_email": "Valid email address is required",
       "temporary_password": "The created user has a temporary password",
       "send_new_password": "Please send the new password to the user.",
@@ -791,10 +781,10 @@
       "new_password": "New Password"
     },
     "external_account": "External Account Management",
-    "external_accounts":"External accounts",
-    "create_external_account":"Create external account",
+    "external_accounts": "External accounts",
+    "create_external_account": "Create external account",
     "external_account_list": "External Account List",
-    "external_account_none":"No External Account",
+    "external_account_none": "No External Account",
     "invite": "Invite",
     "invited": "User was invited",
     "back_to_user_management": "Back to User Management",
@@ -979,7 +969,7 @@
     "ADMIN_SITE_URL_UPDATE": "Update Site URL Settings",
     "ADMIN_MAIL_SMTP_UPDATE": "Update E-mail(SMTP) Settings",
     "ADMIN_MAIL_SES_UPDATE": "Update E-mail(SES) Settings",
-    "ADMIN_MAIL_TEST_SUBMIT" : "Send test mail",
+    "ADMIN_MAIL_TEST_SUBMIT": "Send test mail",
     "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "Update File Upload Settings",
     "ADMIN_PLUGIN_UPDATE": "Update Plugin Settings",
     "ADMIN_MAINTENANCEMODE_ENABLED": "Enable Maintenance Mode",

+ 0 - 36
apps/app/public/static/locales/en_US/commons.json

@@ -7,7 +7,6 @@
   "Sign out": "Logout",
   "New": "New",
   "Delete": "Delete",
-
   "meta": {
     "display_name": "English"
   },
@@ -30,7 +29,6 @@
   "headers": {
     "app_settings": "App Settings"
   },
-
   "header_search_box": {
     "label": {
       "All pages": "All pages",
@@ -41,20 +39,17 @@
       "This tree": "Only children of this tree"
     }
   },
-
   "search_method_menu_item": {
     "search_in_all": "Search in all",
     "only_children_of_this_tree": "Only children of this tree",
     "exact_mutch": "Exact match"
   },
-
   "share_links": {
     "Share Link": "Share Link",
     "Page Path": "Page Path",
     "expire": "Expiration",
     "description": "Description"
   },
-
   "in_app_notification": {
     "notification_list": "In-App Notification List",
     "see_all": "See All",
@@ -65,7 +60,6 @@
     "no_unread_messages": "no_unread_messages",
     "only_unread": "Only unread"
   },
-
   "personal_dropdown": {
     "home": "Home",
     "settings": "Settings",
@@ -75,7 +69,6 @@
     "use_os_settings": "Use OS settings",
     "feedback": "Feedback"
   },
-
   "create_page_dropdown": {
     "new_page": "Create New Page",
     "open_page_create_modal": "Open new page create modal",
@@ -89,7 +82,6 @@
       "descendants": "Template for descendants"
     }
   },
-
   "copy_to_clipboard": {
     "Copy to clipboard": "Copy to clipboard",
     "Page path": "Page path",
@@ -99,14 +91,12 @@
     "Markdown link": "Markdown link",
     "Append params": "Append params"
   },
-
   "crop_image_modal": {
     "image_crop": "Image Crop",
     "crop": "Crop",
     "save": "Save",
     "cancel": "Cancel"
   },
-
   "handsontable_modal": {
     "title": "Edit Table",
     "data_import": "Data Import",
@@ -122,35 +112,9 @@
       "import": "Import"
     }
   },
-
-  "questionnaire_modal": {
-    "required": "Required",
-    "submit": "Submit",
-    "close": "Close",
-    "title": "GROWI questionnaire for service improvement",
-    "more_satisfied_services": "We hope that GROWI customers will be even more satisfied",
-    "strive_to_improve_services": "once we improve our services based on your feedback.",
-    "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"
-    },
-    "satisfaction_with_growi": "Satisfaction with GROWI",
-    "history_of_growi_usage": "History of GROWI usage",
-    "occupation": "Occupation",
-    "position": "Position",
-    "comment_on_growi": "Comment on GROWI",
-    "successfully_submitted": "Your survey has been submitted.",
-    "thanks_for_answering": "Thank you very much for answering."
-  },
-
   "not_found_page": {
     "page_not_exist": "This page does not exist."
   },
-
   "g2g_data_transfer": {
     "tab": "Data transfer",
     "data_transfer": "Data Transfer",

+ 0 - 18
apps/app/public/static/locales/en_US/translation.json

@@ -142,7 +142,6 @@
   "edited this page": "edited this page.",
   "List Drafts": "Drafts",
   "Deleted Pages": "Deleted Pages",
-  "Questionnaire": "Questionnaire",
   "Disassociate": "Disassociate",
   "No bookmarks yet": "No bookmarks yet",
   "add_bookmark": "Add to Bookmarks",
@@ -979,23 +978,6 @@
     "page_tree_not_avaliable": "Page tree feature is not available yet.",
     "go_to_settings": "Go to settings to enable the feature"
   },
-  "questionnaire": {
-    "give_us_feedback": "Give us feedback for improvements",
-    "thank_you_for_answering": "Thank you for answering",
-    "additional_feedback": "Send us additional feedback from the user icon dropdown.",
-    "dont_show_again": "Don`t show again",
-    "deny": "Don't answer",
-    "agree": "Agree",
-    "disagree": "Disagree",
-    "answer": "Answer",
-    "no_answer": "No answer",
-    "settings": "Questionnaire settings",
-    "failed_to_send": "Failed to send feedback",
-    "denied": "The questionnaire won't be shown again",
-    "personal_settings_explanation": "Questionnaires for improving GROWI will be shown. If you have other feedbacks, you can send them from the user icon dropdown.",
-    "enable_questionnaire": "Enable questionnaire",
-    "disabled_by_admin": "Questionnaire is disabled by admin"
-  },
   "tag_edit_modal": {
     "edit_tags": "Edit Tags",
     "done": "Done",

+ 1 - 11
apps/app/public/static/locales/fr_FR/admin.json

@@ -411,17 +411,7 @@
     "enable": "Activer",
     "disable": "Désactiver",
     "use_env_var_if_empty": "Si la valeur dans la base de données est vide, la valeur de variable d'environnement <code>{{variable}}</code> est utilisé.",
-    "note_for_the_only_env_option": "Les paramètres sont définis par des variables d'environnement.<br>Pour modifier ce paramètre, supprimer la variable d'environnement <code>{{env}}</code> .",
-    "questionnaire_settings": "Sondages anonymes",
-    "questionnaire_settings_explanation": "Paramètres d'activation des données analytiques. L'utilisateur peut choisir ce paramètre individuellement dans \"Autres paramètres\".",
-    "about_data_sent": "À propos",
-    "docs_link": "https://docs.growi.org/en/admin-guide/management-cookbook/app-settings.html#questionnaire-settings",
-    "learn_more": "En savoir plus",
-    "other_info_will_be_sent": "En plus des données analytiques, des données diagnostiques pour améliorer GROWI sont envoyées. Les données personnelles ne sont pas incluses.",
-    "we_will_use_the_data_to_improve_growi": "Les données seront utilisées pour améliorer au mieux GROWI",
-    "anonymize_app_site_url": "Ne pas inclure l'URL du site",
-    "url_anonymization_explanation": "L'URL du site configurée ne sera pas inclue dans les données envoyées.",
-    "enable_questionnaire": "Activer les données analytiques"
+    "note_for_the_only_env_option": "Les paramètres sont définis par des variables d'environnement.<br>Pour modifier ce paramètre, supprimer la variable d'environnement <code>{{env}}</code> ."
   },
   "markdown_settings": {
     "markdown_settings": "Markdown",

+ 0 - 23
apps/app/public/static/locales/fr_FR/commons.json

@@ -111,29 +111,6 @@
       "import": "Importer"
     }
   },
-  "questionnaire_modal": {
-    "required": "Requis",
-    "submit": "Soumettre",
-    "close": "Fermer",
-    "title": "Sondages aléatoires GROWI pour données anonymisées.",
-    "more_satisfied_services": "Nous espérons satisfaire au mieux les utilisateurs de GROWI",
-    "strive_to_improve_services": "et utilisons les retours d'utilisateurs afin d'améliorer l'expérience d'usage GROWI",
-    "length_of_experience": {
-      "more_than_two_years": "Plus de 2 ans",
-      "one_to_two_years": "Plus d'un an, mais moins de 2 ans",
-      "six_months_to_one_year": "Plus de 6 mois, mais moins d'un an",
-      "three_months_to_six_months": "Plus de 3 mois, mais moins de 6 mois",
-      "one_month_to_three_months": "Plus d'un moins, mais moins de 3 mois",
-      "less_than_one_month": "Moins d'un mois"
-    },
-    "satisfaction_with_growi": "Satisfaction avec GROWI",
-    "history_of_growi_usage": "Historique d'usage de GROWI",
-    "occupation": "Occupation",
-    "position": "Position",
-    "comment_on_growi": "Commentaires sur GROWI",
-    "successfully_submitted": "Questionnaire soumis.",
-    "thanks_for_answering": "Merci pour votre avis."
-  },
   "not_found_page": {
     "page_not_exist": "Cette page est introuvable."
   },

+ 1 - 19
apps/app/public/static/locales/fr_FR/translation.json

@@ -143,7 +143,6 @@
   "edited this page": "à modifié cette page.",
   "List Drafts": "Brouillons",
   "Deleted Pages": "Pages supprimées",
-  "Questionnaire": "Questionnaire",
   "Disassociate": "Dissocier",
   "No bookmarks yet": "Aucuns favoris",
   "add_bookmark": "Ajouter aux favoris",
@@ -617,7 +616,7 @@
   "default_ai_assistant": {
     "not_set": "L'assistant par défaut n'est pas configuré"
   },
- "ai_assistant_substance": {
+  "ai_assistant_substance": {
     "add_assistant": "Ajouter un assistant",
     "my_assistants": "Mes assistants",
     "team_assistants": "Assistants d'équipe",
@@ -973,23 +972,6 @@
     "page_tree_not_avaliable": "Cette fonctionnalité n'est pas encore disponible.",
     "go_to_settings": "Activer cette fonctionnalité dans les paramètres"
   },
-  "questionnaire": {
-    "give_us_feedback": "Faites-nous part de votre avis",
-    "thank_you_for_answering": "Merci pour votre réponse",
-    "additional_feedback": "Envoyez-nous votre avis depuis le menu déroulant sur le menu utilisateur.",
-    "dont_show_again": "Ne plus afficher",
-    "deny": "Ne pas répondre",
-    "agree": "En accord",
-    "disagree": "En désaccord",
-    "answer": "Répondre",
-    "no_answer": "Aucune réponse",
-    "settings": "Sondages anonymes",
-    "failed_to_send": "Échec de l'envoi du sondage",
-    "denied": "Les sondages ne seront plus affichés.",
-    "personal_settings_explanation": "Sondages de satisfaction anonymes.",
-    "enable_questionnaire": "Sondages anonymes",
-    "disabled_by_admin": "Sondages anonymes désactivés par l'administrateur"
-  },
   "tag_edit_modal": {
     "edit_tags": "Étiquettes",
     "done": "Mettre à jour",

+ 1 - 11
apps/app/public/static/locales/ja_JP/admin.json

@@ -420,17 +420,7 @@
     "enable": "有効",
     "disable": "無効",
     "use_env_var_if_empty": "データベース側の値が空の場合、環境変数 <code>{{variable}}</code> の値を利用します",
-    "note_for_the_only_env_option": "現在GCS設定は環境変数の値によって制限されています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください",
-    "questionnaire_settings": "アンケート設定",
-    "questionnaire_settings_explanation": "システム全体でアンケート機能を有効/無効にします。有効の場合、各ユーザーはユーザー設定ページの「その他の設定」から個別にアンケート機能を有効/無効にできます。",
-    "about_data_sent": "送信される情報について",
-    "docs_link": "https://docs.growi.org/ja/admin-guide/management-cookbook/app-settings.html#%E3%82%A2%E3%83%B3%E3%82%B1%E3%83%BC%E3%83%88%E8%A8%AD%E5%AE%9A",
-    "learn_more": "詳細",
-    "other_info_will_be_sent": "アンケートの回答と合わせて、GROWI の改善に必要な情報を送信します。送信されるデータにユーザーの個人情報は含まれません。",
-    "we_will_use_the_data_to_improve_growi": "私たちはそれらを活用し、最大限ユーザーの体験を向上させるよう努めます。",
-    "anonymize_app_site_url": "サイト URL を匿名化して送信する",
-    "url_anonymization_explanation": "アンケート回答データに含まれるサイト URL が匿名化されます。この設定を有効にすることで、アンケート回答データの送信元である GROWI アプリケーションが特定されなくなります。",
-    "enable_questionnaire": "アンケートを有効にする"
+    "note_for_the_only_env_option": "現在GCS設定は環境変数の値によって制限されています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください"
   },
   "markdown_settings": {
     "markdown_settings": "マークダウン設定",

+ 0 - 35
apps/app/public/static/locales/ja_JP/commons.json

@@ -32,7 +32,6 @@
   "headers": {
     "app_settings": "アプリ設定"
   },
-
   "header_search_box": {
     "label": {
       "All pages": "全てのページ",
@@ -43,20 +42,17 @@
       "This tree": "この階層下の子ページのみ"
     }
   },
-
   "search_method_menu_item": {
     "search_in_all": "全てのページ",
     "only_children_of_this_tree": "この階層下の子ページのみ",
     "exact_mutch": "キーワードに完全一致した文字を含むページのみ"
   },
-
   "share_links": {
     "Share Link": "共有用リンク",
     "Page Path": "ページパス",
     "expire": "有効期限",
     "description": "概要"
   },
-
   "in_app_notification": {
     "notification_list": "アプリ内通知一覧",
     "see_all": "通知一覧を見る",
@@ -67,7 +63,6 @@
     "no_unread_messages": "未読はありません",
     "only_unread": "未読のみ"
   },
-
   "personal_dropdown": {
     "home": "ホーム",
     "settings": "設定",
@@ -77,7 +72,6 @@
     "use_os_settings": "OS設定を利用する",
     "feedback": "ご意見・ご要望"
   },
-
   "create_page_dropdown": {
     "new_page": "新規ページ作成",
     "open_page_create_modal": "新規ページ作成モーダルを表示",
@@ -91,7 +85,6 @@
       "descendants": "下位層テンプレート"
     }
   },
-
   "copy_to_clipboard": {
     "Copy to clipboard": "クリップボードにコピー",
     "Page path": "ページ名",
@@ -101,14 +94,12 @@
     "Markdown link": "マークダウン形式のリンク",
     "Append params": "パラメータの追加"
   },
-
   "crop_image_modal": {
     "image_crop": "画像の切り抜き",
     "crop": "トリミング",
     "save": "保存",
     "cancel": "キャンセル"
   },
-
   "handsontable_modal": {
     "title": "テーブル編集",
     "data_import": "データインポート",
@@ -124,35 +115,9 @@
       "import": "インポート"
     }
   },
-
-  "questionnaire_modal": {
-    "required": "必須",
-    "submit": "送信",
-    "close": "閉じる",
-    "title": "GROWI サービス改善のためのアンケート",
-    "more_satisfied_services": "GROWI をご利用の皆さまに更にご満足いただけるよう",
-    "strive_to_improve_services": "皆さまからのご意見を参考にサービス改善に努めてまいります。",
-    "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ヶ月未満"
-    },
-    "satisfaction_with_growi": "GROWI の満足度",
-    "history_of_growi_usage": "GROWI の利用歴",
-    "occupation": "職種",
-    "position": "役職",
-    "comment_on_growi": "GROWI へのコメント",
-    "successfully_submitted": "アンケートの送信が完了しました。",
-    "thanks_for_answering": "アンケートのご回答誠にありがとうございました。"
-  },
-
   "not_found_page": {
     "page_not_exist": "このページは存在しません。"
   },
-
   "g2g_data_transfer": {
     "tab": "データ移行",
     "data_transfer": "データ移行",

+ 0 - 17
apps/app/public/static/locales/ja_JP/translation.json

@@ -1011,23 +1011,6 @@
     "page_tree_not_avaliable": "Page Tree 機能は現在使用できません。",
     "go_to_settings": "設定する"
   },
-  "questionnaire": {
-    "give_us_feedback": "GROWI の改善のために、アンケートにご協力ください",
-    "thank_you_for_answering": "ご回答ありがとうございます",
-    "additional_feedback": "その他ご意見ご要望はユーザーアイコンのドロップダウンからお願い致します。",
-    "dont_show_again": "今後このアンケートを表示しない",
-    "deny": "回答しない",
-    "agree": "そう思う",
-    "disagree": "そう思わない",
-    "answer": "回答する",
-    "no_answer": "わからない",
-    "settings": "アンケート設定",
-    "failed_to_send": "回答送信に失敗しました",
-    "denied": "このアンケートは今後表示されません",
-    "personal_settings_explanation": "GROWI 改善のためのアンケートが表示されるようになります。ご意見ご要望はユーザーアイコンのドロップダウンからお願いいたします。",
-    "enable_questionnaire": "アンケートを有効にする",
-    "disabled_by_admin": "管理者によってアンケートは無効化されています"
-  },
   "tag_edit_modal": {
     "edit_tags": "タグの編集",
     "done": "完了",

+ 227 - 237
apps/app/public/static/locales/zh_CN/admin.json

@@ -28,21 +28,21 @@
     "always_hidden": "总是隐藏",
     "Guest Users Access": "来宾用户访问",
     "readonly_users_access": "只浏览用户的访问",
-		"Fixed by env var": "这是由env var<code>%s=%s</code>修复的。",
-		"register_limitation": "注册限制",
-		"register_limitation_desc": "限制新用户注册",
-		"The whitelist of registration permission E-mail address": "注册许可电子邮件地址的白名单",
-		"users_without_account": "无法访问没有帐户的用户",
-		"example": "例子",
-		"restrict_emails": "您可以通过编写电子邮件域(以@开头)将电子邮件注册限制为wiki。",
-		"for_example": " 例如,如果要将注册限制为growi.org网站域,你可以写",
-		"in_this_case": ";在这种情况下,只有growi.org网站域将能够注册,所有其他用户将被拒绝。",
-		"insert_single": "请每行插入一个电子邮件地址。",
+    "Fixed by env var": "这是由env var<code>%s=%s</code>修复的。",
+    "register_limitation": "注册限制",
+    "register_limitation_desc": "限制新用户注册",
+    "The whitelist of registration permission E-mail address": "注册许可电子邮件地址的白名单",
+    "users_without_account": "无法访问没有帐户的用户",
+    "example": "例子",
+    "restrict_emails": "您可以通过编写电子邮件域(以@开头)将电子邮件注册限制为wiki。",
+    "for_example": " 例如,如果要将注册限制为growi.org网站域,你可以写",
+    "in_this_case": ";在这种情况下,只有growi.org网站域将能够注册,所有其他用户将被拒绝。",
+    "insert_single": "请每行插入一个电子邮件地址。",
     "page_list_and_search_results": "页面列表/搜索结果",
-		"page_listing_1": "页面列表/搜索<br>受“仅限我”限制",
-		"page_listing_1_desc": "列出/搜索时显示受“仅限我”选项限制的页面",
-		"page_listing_2": "页面列表/搜索<br>受用户组限制",
-		"page_listing_2_desc": "显示列出/搜索时受用户组限制的页面",
+    "page_listing_1": "页面列表/搜索<br>受“仅限我”限制",
+    "page_listing_1_desc": "列出/搜索时显示受“仅限我”选项限制的页面",
+    "page_listing_2": "页面列表/搜索<br>受用户组限制",
+    "page_listing_2_desc": "显示列出/搜索时受用户组限制的页面",
     "page_access_rights": "页面访问",
     "page_delete_rights": "删除权限",
     "page_delete": "删除",
@@ -56,9 +56,9 @@
     "is_all_group_membership_required_for_page_complete_deletion": "除管理员和页面作者之外的用户必须属于被授予页面访问权限的所有组",
     "is_all_group_membership_required_for_page_complete_deletion_explanation": "如果页面权限设置为\"仅限特定群体\",则会启用此功能。",
     "inherit": "继承(使用与单页相同的设置)。",
-		"admin_only": "仅管理员",
-		"admin_and_author": "管理员|作者",
-		"anyone": "任何人",
+    "admin_only": "仅管理员",
+    "admin_and_author": "管理员|作者",
+    "anyone": "任何人",
     "user_homepage_deletion": {
       "user_homepage_deletion": "删除用户主页",
       "enable_user_homepage_deletion": "启用用户主页删除功能",
@@ -71,15 +71,15 @@
     "max_age_caution": "修改该值后需要重启服务器。",
     "forced_update_desc": "设置已被强行更改。以前的设置: ",
     "page_delete_rights_caution": "\"删除/全部删除\"权限(包括后代页面)被强制强于\"删除/完全删除\"权限。 <br> <br> 仅管理员 > 管理员|作者 > 何人",
-		"Authentication mechanism settings": "身份验证机制设置",
-		"setup_is_not_yet_complete": "安装尚未完成",
-		"xss_prevent_setting": "阻止XSS(跨站点脚本)",
-		"xss_prevent_setting_link": "转到Markdown设置",
-		"callback_URL": "回调URL",
-		"providerName": "提供程序名称",
-		"issuerHost": "发行者主机",
-		"scope": "Scope",
-		"desc_of_callback_URL": "在{{AuthName}}身份提供程序的设置中使用它",
+    "Authentication mechanism settings": "身份验证机制设置",
+    "setup_is_not_yet_complete": "安装尚未完成",
+    "xss_prevent_setting": "阻止XSS(跨站点脚本)",
+    "xss_prevent_setting_link": "转到Markdown设置",
+    "callback_URL": "回调URL",
+    "providerName": "提供程序名称",
+    "issuerHost": "发行者主机",
+    "scope": "Scope",
+    "desc_of_callback_URL": "在{{AuthName}}身份提供程序的设置中使用它",
     "authorization_endpoint": "Authorization Endpoint",
     "token_endpoint": "Token Endpoint",
     "revocation_endpoint": "Revocation Endpoint",
@@ -88,41 +88,41 @@
     "end_session_endpoint": "EndSessioin Endpoint",
     "registration_endpoint": "Registration Endpoint",
     "jwks_uri": "JSON Web Key Set URL",
-		"clientID": "Client ID",
-		"client_secret": "客户机密",
-		"updated_general_security_setting": "更新安全设置成功",
-		"setup_not_completed_yet": "安装尚未完成",
+    "clientID": "Client ID",
+    "client_secret": "客户机密",
+    "updated_general_security_setting": "更新安全设置成功",
+    "setup_not_completed_yet": "安装尚未完成",
     "guest_mode": {
-			"deny": "拒绝(仅限注册用户)",
-			"readonly": "接受(来宾可以只读)"
-		},
+      "deny": "拒绝(仅限注册用户)",
+      "readonly": "接受(来宾可以只读)"
+    },
     "read_only_users_comment": {
       "deny": "拒绝 (禁止只浏览用户操作评论)",
       "accept": "允许 (只浏览用户可以管理评论)"
     },
-		"registration_mode": {
-			"open": "打开(任何人都可以注册)",
-			"restricted": "受限(需要管理员批准)",
-			"closed": "已关闭(仅限邀请)"
-		},
+    "registration_mode": {
+      "open": "打开(任何人都可以注册)",
+      "restricted": "受限(需要管理员批准)",
+      "closed": "已关闭(仅限邀请)"
+    },
     "share_link_management": "Share Link Management",
-    "No_share_links":"No share links",
-    "share_link_notice":"remove all share links",
-    "delete_all_share_links":"Delete all share links",
+    "No_share_links": "No share links",
+    "share_link_notice": "remove all share links",
+    "delete_all_share_links": "Delete all share links",
     "share_link_rights": "分享链接权",
     "enable_link_sharing": "启用链接共享",
     "all_share_links": "所有共享链接",
-		"configuration": " 配置",
-		"Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>username</code> match",
-		"Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>username</code>.",
-		"Treat email matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>email</code> match",
-		"Treat email matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>email</code>.",
-		"Use env var if empty": "Use env var <code>{{env}}</code> if empty",
-		"Use default if both are empty": "If both ​​are empty, the default value <code>{{target}}</code> is used.",
-		"missing mandatory configs": "The following mandatory items are not set in either database nor environment variables.",
-		"Local": {
-			"name": "ID/Password",
-			"note for the only env option": "The LOCAL authentication is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
+    "configuration": " 配置",
+    "Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>username</code> match",
+    "Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>username</code>.",
+    "Treat email matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>email</code> match",
+    "Treat email matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>email</code>.",
+    "Use env var if empty": "Use env var <code>{{env}}</code> if empty",
+    "Use default if both are empty": "If both ​​are empty, the default value <code>{{target}}</code> is used.",
+    "missing mandatory configs": "The following mandatory items are not set in either database nor environment variables.",
+    "Local": {
+      "name": "ID/Password",
+      "note for the only env option": "The LOCAL authentication is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
       "enable_local": "Enable ID/Password",
       "password_reset_by_users": "用户重置密码",
       "enable_password_reset_by_users": "启用用户重置密码",
@@ -130,181 +130,181 @@
       "email_authentication": "用户注册时的电子邮件身份验证",
       "enable_email_authentication": "启用电子邮件身份验证",
       "enable_email_authentication_desc": "用户注册将执行电子邮件身份验证。"
-		},
-		"ldap": {
-			"enable_ldap": "Enable LDAP",
-			"server_url_detail": "The LDAP URL of the directory service in the format <code>ldap://host:port/DN</code> or <code>ldaps://host:port/DN</code>.",
-			"bind_mode": "Binding Mode",
-			"bind_manager": "Manager Bind",
-			"bind_user": "User Bind",
-			"bind_DN_manager_detail": "The DN of the account that authenticates and queries the directory service",
-			"bind_DN_user_detail1": "The query used to bind with the directory service.",
-			"bind_DN_user_detail2": "Use <code>&#123;&#123;username&#125;&#125;</code> to reference the username entered in the login page.",
-			"bind_DN_password": "Bind DN Password",
-			"bind_DN_password_manager_detail": "The password for the Bind DN account.",
-			"bind_DN_password_user_detail": "The password that is entered in the login page will be used to bind.",
-			"search_filter": "Search Filter",
-			"search_filter_detail1": "The query used to locate the authenticated user.",
-			"search_filter_detail2": "Use <code>&#123;&#123;username&#125;&#125;</code> to reference the username entered in the login page.",
-			"search_filter_detail3": "If empty, the filter <code>(uid=&#123;&#123;username&#125;&#125;)</code> is used.",
-			"search_filter_example1": "Match with 'uid' or 'mail'",
-			"search_filter_example2": "Match with 'sAMAccountName' for Active Directory",
-			"username_detail": "Specification of mappings for <code>username</code> when creating new users",
-			"name_detail": "Specification of mappings for full name when creating new users",
-			"mail_detail": "Specification of mappings for mail address when creating new users",
-			"group_search_base_DN": "Group Search Base DN",
-			"group_search_base_DN_detail": "The base DN from which to search for groups. If defined, also <code>Group Search Filter</code> must be defined for the search to work.",
-			"group_search_filter": "Group Search Filter",
-			"group_search_filter_detail1": "The query used to filter for groups.",
-			"group_search_filter_detail2": "Login via LDAP is accepted only when this query hits one or more groups.",
-			"group_search_filter_detail3": "Use <code>&#123;&#123;dn&#125;&#125;</code> to have it replaced of the found user object.",
-			"group_search_filter_detail4": "<code>(&(cn=group1)(memberUid=&#123;&#123;dn&#125;&#125;))</code> hits the groups which has <code>cn=group1</code> and <code>memberUid</code> includes the user's <code>uid</code>(when <code>Group DN Property</code> is not changed from the default value.)",
-			"group_search_user_DN_property": "User DN Property",
-			"group_search_user_DN_property_detail": "The property of user object to use in <code>&#123;&#123;dn&#125;&#125;</code> interpolation of <code>Group Search Filter</code>.",
-			"test_config": "Test Saved Configuration",
-			"updated_ldap": "Succeeded to update LDAP setting"
-		},
-		"SAML": {
-			"name": "SAML",
-			"enable_saml": "Enable SAML",
-			"id_detail": "Specification of the name of attribute which can identify the user in SAML Identity Provider",
-			"username_detail": "Specification of mappings for <code>username</code> when creating new users",
-			"mapping_detail": "Specification of mappings for {{target}} when creating new users",
-			"cert_detail": "PEM-encoded X.509 signing certificate to validate the response from IdP",
-			"Use env var if empty": "If the value in the database is empty, the value of the environment variable <code>{{env}}</code> is used.",
-			"note for the only env option": "The setting item that enables or disables the SAML authentication and the highlighted setting items use only the value of environment variables.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
-			"attr_based_login_control_detail": "Limit who can sign up by using <code>&lt;saml: Attribute&gt;</code> element included in <code>&lt;saml: AttributeStatement&gt;</code> element and its child element <code>&lt;saml: AttributeValue&gt;</code>.",
-			"attr_based_login_control_rule_help": "<h5>Supported Queries:</h5><ul><li>Terms</li><li>Fields</li><li>AND/NOT/OR Operator</li><li>Grouping</li></ul><h5>Unsupported Queries:</h5><ul><li>Wildcard, Fuzzy, Proximity, Range and Boosting</li><li>+/- Operator</li><li>Field Grouping</li></ul><h5>Escaping special characters</h5>It is needed to escape following special characters:<br><code>+ - && || ! ( ) { } [ ] ^ &quot; &tilde; * ? : &#92;</code> and <code>/</code>",
-			"attr_based_login_control_rule_example1": "<h5>Example for conditions</h5>If a rule is <code>(Department: A || Department: B) && Position: Leader</code>, users who have either <code>Department: A</code> or <code>Department: B</code> and have <code>Position: Leader</code> <strong>can</strong> sign in.",
+    },
+    "ldap": {
+      "enable_ldap": "Enable LDAP",
+      "server_url_detail": "The LDAP URL of the directory service in the format <code>ldap://host:port/DN</code> or <code>ldaps://host:port/DN</code>.",
+      "bind_mode": "Binding Mode",
+      "bind_manager": "Manager Bind",
+      "bind_user": "User Bind",
+      "bind_DN_manager_detail": "The DN of the account that authenticates and queries the directory service",
+      "bind_DN_user_detail1": "The query used to bind with the directory service.",
+      "bind_DN_user_detail2": "Use <code>&#123;&#123;username&#125;&#125;</code> to reference the username entered in the login page.",
+      "bind_DN_password": "Bind DN Password",
+      "bind_DN_password_manager_detail": "The password for the Bind DN account.",
+      "bind_DN_password_user_detail": "The password that is entered in the login page will be used to bind.",
+      "search_filter": "Search Filter",
+      "search_filter_detail1": "The query used to locate the authenticated user.",
+      "search_filter_detail2": "Use <code>&#123;&#123;username&#125;&#125;</code> to reference the username entered in the login page.",
+      "search_filter_detail3": "If empty, the filter <code>(uid=&#123;&#123;username&#125;&#125;)</code> is used.",
+      "search_filter_example1": "Match with 'uid' or 'mail'",
+      "search_filter_example2": "Match with 'sAMAccountName' for Active Directory",
+      "username_detail": "Specification of mappings for <code>username</code> when creating new users",
+      "name_detail": "Specification of mappings for full name when creating new users",
+      "mail_detail": "Specification of mappings for mail address when creating new users",
+      "group_search_base_DN": "Group Search Base DN",
+      "group_search_base_DN_detail": "The base DN from which to search for groups. If defined, also <code>Group Search Filter</code> must be defined for the search to work.",
+      "group_search_filter": "Group Search Filter",
+      "group_search_filter_detail1": "The query used to filter for groups.",
+      "group_search_filter_detail2": "Login via LDAP is accepted only when this query hits one or more groups.",
+      "group_search_filter_detail3": "Use <code>&#123;&#123;dn&#125;&#125;</code> to have it replaced of the found user object.",
+      "group_search_filter_detail4": "<code>(&(cn=group1)(memberUid=&#123;&#123;dn&#125;&#125;))</code> hits the groups which has <code>cn=group1</code> and <code>memberUid</code> includes the user's <code>uid</code>(when <code>Group DN Property</code> is not changed from the default value.)",
+      "group_search_user_DN_property": "User DN Property",
+      "group_search_user_DN_property_detail": "The property of user object to use in <code>&#123;&#123;dn&#125;&#125;</code> interpolation of <code>Group Search Filter</code>.",
+      "test_config": "Test Saved Configuration",
+      "updated_ldap": "Succeeded to update LDAP setting"
+    },
+    "SAML": {
+      "name": "SAML",
+      "enable_saml": "Enable SAML",
+      "id_detail": "Specification of the name of attribute which can identify the user in SAML Identity Provider",
+      "username_detail": "Specification of mappings for <code>username</code> when creating new users",
+      "mapping_detail": "Specification of mappings for {{target}} when creating new users",
+      "cert_detail": "PEM-encoded X.509 signing certificate to validate the response from IdP",
+      "Use env var if empty": "If the value in the database is empty, the value of the environment variable <code>{{env}}</code> is used.",
+      "note for the only env option": "The setting item that enables or disables the SAML authentication and the highlighted setting items use only the value of environment variables.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
+      "attr_based_login_control_detail": "Limit who can sign up by using <code>&lt;saml: Attribute&gt;</code> element included in <code>&lt;saml: AttributeStatement&gt;</code> element and its child element <code>&lt;saml: AttributeValue&gt;</code>.",
+      "attr_based_login_control_rule_help": "<h5>Supported Queries:</h5><ul><li>Terms</li><li>Fields</li><li>AND/NOT/OR Operator</li><li>Grouping</li></ul><h5>Unsupported Queries:</h5><ul><li>Wildcard, Fuzzy, Proximity, Range and Boosting</li><li>+/- Operator</li><li>Field Grouping</li></ul><h5>Escaping special characters</h5>It is needed to escape following special characters:<br><code>+ - && || ! ( ) { } [ ] ^ &quot; &tilde; * ? : &#92;</code> and <code>/</code>",
+      "attr_based_login_control_rule_example1": "<h5>Example for conditions</h5>If a rule is <code>(Department: A || Department: B) && Position: Leader</code>, users who have either <code>Department: A</code> or <code>Department: B</code> and have <code>Position: Leader</code> <strong>can</strong> sign in.",
       "attr_based_login_control_rule_example2": "<h5>Example for escaping</h5>If you would like to use URL as a query value, escape the following:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "updated_saml": "Succeeded to update SAML setting"
-		},
-		"OAuth": {
-			"enable_oidc": "Enable OIDC",
-			"register": "Register for %s",
-			"change_redirect_url": "Enter <code>%s</code> <br>(where <code>%s</code> is your host name) for \"Authorized redirect URIs\".",
-			"Google": {
-				"enable_google": "Enable Google OAuth",
-				"name": "Google OAuth",
-				"register_1": "Access {{link}}",
-				"register_2": "Create Project if no projects exist",
-				"register_3": "Create Credentials &rightarrow; OAuth client ID &rightarrow; Select \"Web application\"",
-				"register_4": "Register your OAuth App with one of Authorized redirect URIs as <code>{{url}}</code>",
-				"register_5": "Copy and paste your ClientID and Client Secret above",
-				"updated_google": "Succeeded to update Google OAuth setting"
-			},
-			"GitHub": {
-				"enable_github": "Enable GitHub OAuth",
-				"name": "GitHub OAuth",
-				"register_1": "Access {{link}}",
-				"register_2": "Register your OAuth App with \"Authorization callback URL\" as <code>{{url}}</code>",
-				"register_3": "Copy and paste your ClientID and Client Secret above",
-				"updated_github": "Succeeded to update GitHub OAuth setting"
-			},
-			"OIDC": {
-				"name": "OpenID Connect",
-				"id_detail": "Specification of the name of attribute which can identify the user in OIDC claims",
-				"username_detail": "Specification of mappings for <code>username</code> when creating new users",
-				"name_detail": "Specification of mappings for <code>name</code> when creating new users",
-				"mapping_detail": "Specification of mappings for {{target}} when creating new users",
-				"register_1": "Contact to OIDC IdP Administrator",
-				"register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>{{url}}</code>",
-				"register_3": "Copy and paste your ClientID and Client Secret above",
-				"updated_oidc": "Succeeded to update OpenID Connect",
+    },
+    "OAuth": {
+      "enable_oidc": "Enable OIDC",
+      "register": "Register for %s",
+      "change_redirect_url": "Enter <code>%s</code> <br>(where <code>%s</code> is your host name) for \"Authorized redirect URIs\".",
+      "Google": {
+        "enable_google": "Enable Google OAuth",
+        "name": "Google OAuth",
+        "register_1": "Access {{link}}",
+        "register_2": "Create Project if no projects exist",
+        "register_3": "Create Credentials &rightarrow; OAuth client ID &rightarrow; Select \"Web application\"",
+        "register_4": "Register your OAuth App with one of Authorized redirect URIs as <code>{{url}}</code>",
+        "register_5": "Copy and paste your ClientID and Client Secret above",
+        "updated_google": "Succeeded to update Google OAuth setting"
+      },
+      "GitHub": {
+        "enable_github": "Enable GitHub OAuth",
+        "name": "GitHub OAuth",
+        "register_1": "Access {{link}}",
+        "register_2": "Register your OAuth App with \"Authorization callback URL\" as <code>{{url}}</code>",
+        "register_3": "Copy and paste your ClientID and Client Secret above",
+        "updated_github": "Succeeded to update GitHub OAuth setting"
+      },
+      "OIDC": {
+        "name": "OpenID Connect",
+        "id_detail": "Specification of the name of attribute which can identify the user in OIDC claims",
+        "username_detail": "Specification of mappings for <code>username</code> when creating new users",
+        "name_detail": "Specification of mappings for <code>name</code> when creating new users",
+        "mapping_detail": "Specification of mappings for {{target}} when creating new users",
+        "register_1": "Contact to OIDC IdP Administrator",
+        "register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>{{url}}</code>",
+        "register_3": "Copy and paste your ClientID and Client Secret above",
+        "updated_oidc": "Succeeded to update OpenID Connect",
         "Use discovered URL if empty": "Use discovered URL from \"Issuer Host\" if empty"
-			},
-			"how_to": {
-				"google": "How to configure Google OAuth?",
-				"github": "How to configure GitHub OAuth?",
-				"oidc": "How to configure OIDC?"
-			}
-		},
-		"form_item_name": {
-			"entryPoint": "Entry point",
-			"issuer": "Issuer",
-			"cert": "Certificate",
-			"attrMapId": "ID",
-			"attrMapUsername": "Username",
-			"attrMapMail": "Mail Address",
-			"attrMapFirstName": "First Name",
-			"attrMapLastName": "Last Name",
-			"ABLCRule": "Rule"
-		}
+      },
+      "how_to": {
+        "google": "How to configure Google OAuth?",
+        "github": "How to configure GitHub OAuth?",
+        "oidc": "How to configure OIDC?"
+      }
+    },
+    "form_item_name": {
+      "entryPoint": "Entry point",
+      "issuer": "Issuer",
+      "cert": "Certificate",
+      "attrMapId": "ID",
+      "attrMapUsername": "Username",
+      "attrMapMail": "Mail Address",
+      "attrMapFirstName": "First Name",
+      "attrMapLastName": "Last Name",
+      "ABLCRule": "Rule"
+    }
   },
   "notification_settings": {
     "notification_settings": "通知设置",
-		"slack_incoming_configuration": "Slack Incoming Webhooks configuration",
-		"prioritize_webhook": "Prioritize incoming webhook than Slack App",
-		"prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
-		"slack_app_configuration": "Slack app configuration",
-		"slack_app_configuration_desc": "This is the way that compatible with Crowi,<br /> but not recommended in GROWI because it is <strong>too complex</strong>.",
-		"use_instead": "Please use Slack Incoming Webhooks Configuration instead.",
-		"how_to": {
-			"header": "How to configure Incoming Webhooks?",
-			"workspace": "(At Workspace) Add a hook",
-			"workspace_desc1": "Go to <a href='https://slack.com/services/new/incoming-webhook'>Incoming Webhooks configuration page</a>.",
-			"workspace_desc2": "Choose the default channel to post.",
-			"workspace_desc3": "Add.",
-			"at_growi": "(At GROWI admin page) Set Webhook URL",
-			"at_growi_desc": "Input &rdquo;Webhook URL&rdquo; and submit on this page."
-		},
-		"user_trigger_notification_header": "Default notification settings for patterns",
-		"pattern": "Pattern",
-		"channel": "Channel",
-		"pattern_desc": "Path name of wiki. Pattern expression with <code>*</code> can be used.",
-		"channel_desc": "Slack channel name. Without <code>#</code>.",
-		"valid_page": "启用/禁用通知",
-		"link_notification_help": "<strong>只有那些知道“链接的任何人”链接的人才能查看的页面并不总是得到通知。</strong> ",
-		"just_me_notification_help": "<strong>被“仅限我”限制的页在编辑时被通知。</strong>",
-		"group_notification_help": "<strong>被“用户组”限制的页面在编辑时被通知。</strong>",
-		"notification_list": "List of notification settings",
-		"add_notification": "Add new",
-		"trigger_path": "Trigger path",
-		"trigger_path_help": "(expression with <code>*</code> is supported)",
-		"trigger_events": "Trigger events",
-		"notify_to": "Notify to",
-		"back_to_list": "Go back to list",
-		"notification_detail": "Notification Setting Details",
-		"event_pageCreate": "When new page is \"CREATED\"",
-		"event_pageEdit": "When page is \"EDITED\"",
-		"event_pageDelete": "When page is \"DELETED\"",
-		"event_pageMove": "When page is \"MOVED\" (renamed)",
-		"event_pageLike": "When someone \"LIKES\" page",
-		"event_comment": "When someone \"COMMENTS\" on page",
-		"email": {
-			"ifttt_link": "Create a new IFTTT applet with Email trigger"
-		},
-		"updated_slackApp": "Succeeded to update Slack App Configuration setting",
-		"add_notification_pattern": "Add user trigger notification patterns",
-		"delete_notification_pattern": "Delete notification pattern",
-		"delete_notification_pattern_desc1": "Delete Path: {{path}}",
-		"delete_notification_pattern_desc2": "Once deleted, it cannot be recovered",
-		"toggle_notification": "Updated setting of {{path}}",
+    "slack_incoming_configuration": "Slack Incoming Webhooks configuration",
+    "prioritize_webhook": "Prioritize incoming webhook than Slack App",
+    "prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
+    "slack_app_configuration": "Slack app configuration",
+    "slack_app_configuration_desc": "This is the way that compatible with Crowi,<br /> but not recommended in GROWI because it is <strong>too complex</strong>.",
+    "use_instead": "Please use Slack Incoming Webhooks Configuration instead.",
+    "how_to": {
+      "header": "How to configure Incoming Webhooks?",
+      "workspace": "(At Workspace) Add a hook",
+      "workspace_desc1": "Go to <a href='https://slack.com/services/new/incoming-webhook'>Incoming Webhooks configuration page</a>.",
+      "workspace_desc2": "Choose the default channel to post.",
+      "workspace_desc3": "Add.",
+      "at_growi": "(At GROWI admin page) Set Webhook URL",
+      "at_growi_desc": "Input &rdquo;Webhook URL&rdquo; and submit on this page."
+    },
+    "user_trigger_notification_header": "Default notification settings for patterns",
+    "pattern": "Pattern",
+    "channel": "Channel",
+    "pattern_desc": "Path name of wiki. Pattern expression with <code>*</code> can be used.",
+    "channel_desc": "Slack channel name. Without <code>#</code>.",
+    "valid_page": "启用/禁用通知",
+    "link_notification_help": "<strong>只有那些知道“链接的任何人”链接的人才能查看的页面并不总是得到通知。</strong> ",
+    "just_me_notification_help": "<strong>被“仅限我”限制的页在编辑时被通知。</strong>",
+    "group_notification_help": "<strong>被“用户组”限制的页面在编辑时被通知。</strong>",
+    "notification_list": "List of notification settings",
+    "add_notification": "Add new",
+    "trigger_path": "Trigger path",
+    "trigger_path_help": "(expression with <code>*</code> is supported)",
+    "trigger_events": "Trigger events",
+    "notify_to": "Notify to",
+    "back_to_list": "Go back to list",
+    "notification_detail": "Notification Setting Details",
+    "event_pageCreate": "When new page is \"CREATED\"",
+    "event_pageEdit": "When page is \"EDITED\"",
+    "event_pageDelete": "When page is \"DELETED\"",
+    "event_pageMove": "When page is \"MOVED\" (renamed)",
+    "event_pageLike": "When someone \"LIKES\" page",
+    "event_comment": "When someone \"COMMENTS\" on page",
+    "email": {
+      "ifttt_link": "Create a new IFTTT applet with Email trigger"
+    },
+    "updated_slackApp": "Succeeded to update Slack App Configuration setting",
+    "add_notification_pattern": "Add user trigger notification patterns",
+    "delete_notification_pattern": "Delete notification pattern",
+    "delete_notification_pattern_desc1": "Delete Path: {{path}}",
+    "delete_notification_pattern_desc2": "Once deleted, it cannot be recovered",
+    "toggle_notification": "Updated setting of {{path}}",
     "not_found_global_notification_triggerid": "未找到全局通知 ID"
-	},
+  },
   "full_text_search_management": {
     "full_text_search_management": "全文搜索管理",
-		"elasticsearch_management": "Elasticsearch管理",
-		"connection_status": "连接状态",
-		"connection_status_label_unconfigured": "未配置",
-		"connection_status_label_connected": "已连接",
-		"connection_status_label_disconnected": "断开的",
-		"connection_status_label_erroroccured": "搜索服务出错",
-		"indices_status": "索引状态",
-		"indices_status_label_normalized": "标准化",
-		"indices_status_label_unnormalized": "重建或损坏",
-		"indices_summary": "索引摘要",
-		"reconnect": "重新连接",
-		"reconnect_button": "尝试重新连接",
-		"reconnect_description": "单击按钮尝试重新连接到Elasticsearch。",
-		"normalize": "规范化",
-		"normalize_button": "规范化索引",
-		"normalize_description": "单击按钮修复损坏的索引。",
-		"rebuild": "重建",
-		"rebuild_button": "重建索引",
-		"rebuild_description_1": "单击按钮以重新生成索引并添加所有页面数据。",
-		"rebuild_description_2": "这可能需要一段时间。"
-	},
+    "elasticsearch_management": "Elasticsearch管理",
+    "connection_status": "连接状态",
+    "connection_status_label_unconfigured": "未配置",
+    "connection_status_label_connected": "已连接",
+    "connection_status_label_disconnected": "断开的",
+    "connection_status_label_erroroccured": "搜索服务出错",
+    "indices_status": "索引状态",
+    "indices_status_label_normalized": "标准化",
+    "indices_status_label_unnormalized": "重建或损坏",
+    "indices_summary": "索引摘要",
+    "reconnect": "重新连接",
+    "reconnect_button": "尝试重新连接",
+    "reconnect_description": "单击按钮尝试重新连接到Elasticsearch。",
+    "normalize": "规范化",
+    "normalize_button": "规范化索引",
+    "normalize_description": "单击按钮修复损坏的索引。",
+    "rebuild": "重建",
+    "rebuild_button": "重建索引",
+    "rebuild_description_1": "单击按钮以重新生成索引并添加所有页面数据。",
+    "rebuild_description_2": "这可能需要一段时间。"
+  },
   "mailer_setup_required": "<a href='/admin/app'>Email settings</a> are required to send.",
   "admin_top": {
     "management_wiki": "管理Wiki",
@@ -420,17 +420,7 @@
     "enable": "启用",
     "disable": "停用",
     "use_env_var_if_empty": "如果数据库中的值为空,则环境变量的值 <cod>{{variable}}</code> 启用。",
-    "note_for_the_only_env_option": "The GCS settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
-    "questionnaire_settings": "问卷设置",
-    "questionnaire_settings_explanation": "这将在整个系统上启用/禁用问卷。 启用后,用户还可以在个人设置页面的“其他设置”中单独启用/禁用问卷调查。",
-    "about_data_sent": "关于发送的信息",
-    "docs_link": "https://docs.growi.org/en/admin-guide/management-cookbook/app-settings.html#questionnaire-settings",
-    "learn_more": "细节",
-    "other_info_will_be_sent": "与问卷回答一起,将发送改进 GROWI 所需的信息。个人用户信息将不包含在发送的数据中。",
-    "we_will_use_the_data_to_improve_growi": "我们将使用这些数据尽可能地改善 GROWI 体验。",
-    "anonymize_app_site_url": "在发送的数据中匿名应用程序站点 URL",
-    "url_anonymization_explanation": "问卷答案中包含的应用站点URL将被匿名化,启用后将不会识别发送问卷答案的GROWI应用。",
-    "enable_questionnaire": "启用问卷"
+    "note_for_the_only_env_option": "The GCS settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> ."
   },
   "markdown_settings": {
     "markdown_settings": "Markdown设置",
@@ -511,13 +501,13 @@
       "show_page_side_authors": "在目录上方始终显示创建者和更新者",
       "show_page_side_authors_desc": "在页面侧边栏的目录上方显示创建者和最后更新者的信息。"
     },
-      "presentation": "表达",
-      "presentation_options": {
+    "presentation": "表达",
+    "presentation_options": {
       "enable_marp": "启用 Marp",
       "enable_marp_desc": "Marp 可在演示视图中使用。该选项可能会使您受到 XSS 的攻击。",
       "marp_official_site": "参考资料:Marp 官方网站",
       "marp_official_site_link": "https://marp.app",
-      "marp_in_growi" : "参考资料:GROWI Docs - Create slide using Marp",
+      "marp_in_growi": "参考资料:GROWI Docs - Create slide using Marp",
       "marp_in_growi_link": "https://docs.growi.org/en/guide/features/marp.html"
     },
     "custom_title": "自定义标题",
@@ -531,7 +521,7 @@
     "write_css": "您可以编写应用于整个系统的CSS。",
     "ctrl_space": "Ctrl+Space 自动完成",
     "custom_script": "定制纸条",
-    "custom_presentation":"表达",
+    "custom_presentation": "表达",
     "write_java": "您可以编写应用于整个系统的Javascript。",
     "reflect_change": "您需要重新加载页面以反映更改。",
     "custom_logo": "自定义徽标",
@@ -727,7 +717,7 @@
       "allow_specified_long": "允许指定(只允许来自指定的渠道)",
       "test_connection": "测试连接",
       "test_connection_by_pressing_button": "按下按钮以测试连接",
-      "test_connection_only_public_channel":"请在一个公共频道中测试连接",
+      "test_connection_only_public_channel": "请在一个公共频道中测试连接",
       "error_check_logs_below": "发生了错误。请检查以下日志。",
       "send_message_to_slack_work_space": "发送到 Slack 工作区。",
       "add_slack_workspace": "添加Slack Workspace"
@@ -988,7 +978,7 @@
     "ADMIN_SITE_URL_UPDATE": "更新站点 URL 设置",
     "ADMIN_MAIL_SMTP_UPDATE": "更新电子邮件(SMTP)设置",
     "ADMIN_MAIL_SES_UPDATE": "更新电子邮件(SES)设置",
-    "ADMIN_MAIL_TEST_SUBMIT" : "发送测试邮件",
+    "ADMIN_MAIL_TEST_SUBMIT": "发送测试邮件",
     "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "更新文件上传设置",
     "ADMIN_PLUGIN_UPDATE": "更新插件设置",
     "ADMIN_MAINTENANCEMODE_ENABLED": "启用维护模式",
@@ -1140,8 +1130,8 @@
     "revoke_user_admin": "Succeeded to revoke {{username}} admin",
     "grant_user_read_only": "Succeeded to grant {{username}} read only",
     "revoke_user_read_only": "Succeeded to revoke {{username}} read only",
-		"activate_user_success": "Succeeded to activating {{username}}",
-		"deactivate_user_success": "Succeeded to deactivate {{username}}",
+    "activate_user_success": "Succeeded to activating {{username}}",
+    "deactivate_user_success": "Succeeded to deactivate {{username}}",
     "remove_user_success": "Succeeded to removing {{username}}",
     "remove_external_user_success": "Succeeded to remove {{accountId}}",
     "switch_disable_link_sharing_success": "成功更新分享链接设置",

+ 24 - 60
apps/app/public/static/locales/zh_CN/commons.json

@@ -1,16 +1,15 @@
 {
-	"Show": "显示",
-	"Hide": "隐藏",
+  "Show": "显示",
+  "Hide": "隐藏",
   "Add": "添加",
   "Insert": "插入",
   "Reset": "重启",
-	"Sign out": "退出",
+  "Sign out": "退出",
   "New": "新建",
   "Send": "发送",
   "Close": "关闭",
   "Done": "完成",
   "Delete": "删除",
-
   "meta": {
     "display_name": "简体中文"
   },
@@ -33,52 +32,46 @@
   "headers": {
     "app_settings": "系统设置"
   },
-
   "header_search_box": {
-		"label": {
-			"All pages": "所有页面",
-			"This tree": "当前分支"
-		},
-		"item_label": {
-			"All pages": "所有页面",
-			"This tree": "当前分支以下内容"
-		}
+    "label": {
+      "All pages": "所有页面",
+      "This tree": "当前分支"
+    },
+    "item_label": {
+      "All pages": "所有页面",
+      "This tree": "当前分支以下内容"
+    }
   },
-
   "search_method_menu_item": {
     "search_in_all": "所有页面",
     "only_children_of_this_tree": "当前分支以下内容",
     "exact_mutch": "完全匹配"
   },
-
   "share_links": {
     "Share Link": "Share Link",
     "Page Path": "Page Path",
     "expire": "Expiration",
     "description": "Description"
   },
-
   "in_app_notification": {
     "notification_list": "应用内通知列表",
     "see_all": "查看通知列表",
     "no_notification": "您没有任何通知",
     "all": "全部",
     "unopend": "未读",
-    "mark_all_as_read" : "标记为已读",
+    "mark_all_as_read": "标记为已读",
     "no_unread_messages": "no_unread_messages",
     "only_unread": "Only unread"
   },
-
   "personal_dropdown": {
     "home": "家",
     "settings": "设置",
-		"color_mode": "颜色模式",
-		"sidebar_mode": "边栏模式",
-		"sidebar_mode_editor": "编辑器上的边栏模式",
-		"use_os_settings": "使用操作系统设置",
+    "color_mode": "颜色模式",
+    "sidebar_mode": "边栏模式",
+    "sidebar_mode_editor": "编辑器上的边栏模式",
+    "use_os_settings": "使用操作系统设置",
     "feedback": "意见和要求"
   },
-
   "create_page_dropdown": {
     "new_page": "新页面",
     "open_page_create_modal": "打开新页面创建模式",
@@ -92,24 +85,21 @@
       "descendants": "子代模板"
     }
   },
-
-	"copy_to_clipboard": {
-		"Copy to clipboard": "复制到剪贴板",
-		"Page path": "页面路径",
-		"Page URL": "页面Url",
-		"Parmanent link": "参数化链接",
-		"Page path and parmanent link": "页面路径及参数化链接",
-		"Markdown link": "Markdown链接",
+  "copy_to_clipboard": {
+    "Copy to clipboard": "复制到剪贴板",
+    "Page path": "页面路径",
+    "Page URL": "页面Url",
+    "Parmanent link": "参数化链接",
+    "Page path and parmanent link": "页面路径及参数化链接",
+    "Markdown link": "Markdown链接",
     "Append params": "Append params"
-	},
-
+  },
   "crop_image_modal": {
     "image_crop": "图像裁剪",
     "crop": "修剪",
     "save": "节省",
     "cancel": "取消"
   },
-
   "handsontable_modal": {
     "title": "编辑表格",
     "data_import": "数据导入",
@@ -125,35 +115,9 @@
       "import": "导入"
     }
   },
-
-  "questionnaire_modal": {
-    "required": "必需的",
-    "submit": "发送",
-    "close": "Close",
-    "title": "改善服务的GROWI调查表",
-    "more_satisfied_services": "我们希望让使用GROWI的人更加满意",
-    "strive_to_improve_services": "我们将利用你的反馈来改善我们的服务。",
-    "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个月"
-    },
-    "satisfaction_with_growi": "对GROWI的满意程度",
-    "history_of_growi_usage": "GROWI的使用历史",
-    "occupation": "职位",
-    "position": "职业类型",
-    "comment_on_growi": "关于GROWI的评论",
-    "successfully_submitted": "问卷已经发出。",
-    "thanks_for_answering": "非常感谢您完成问卷调查。"
-  },
-
   "not_found_page": {
     "page_not_exist": "该页面不存在"
   },
-
   "g2g_data_transfer": {
     "tab": "数据迁移",
     "data_transfer": "数据迁移",

+ 0 - 17
apps/app/public/static/locales/zh_CN/translation.json

@@ -978,23 +978,6 @@
     "move_to_root": "移动到根部",
     "root": "root (default)"
   },
-  "questionnaire": {
-    "give_us_feedback": "向我们提供反馈以进行改进",
-    "thank_you_for_answering": "谢谢你的回答",
-    "additional_feedback": "从用户图标下拉菜单向我们发送更多反馈。",
-    "dont_show_again": "不再显示",
-    "deny": "不要回答",
-    "agree": "同意",
-    "disagree": "不同意",
-    "answer": "答案是",
-    "no_answer": "没有答案",
-    "settings": "问卷设置",
-    "failed_to_send": "无法发送反馈",
-    "denied": "问卷不会再显示",
-    "personal_settings_explanation": "将展示改进 GROWI 的问卷。 如果您有其他反馈,可以从用户图标下拉菜单中发送。",
-    "enable_questionnaire": "启用问卷",
-    "disabled_by_admin": "问卷已被管理员禁用"
-  },
   "v5_page_migration": {
     "page_tree_not_avaliable": "Page Tree 功能不可用",
     "go_to_settings": "进入设置,启用该功能"

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

@@ -15,7 +15,6 @@ import FileUploadSetting from './FileUploadSetting';
 import MailSetting from './MailSetting';
 import { MaintenanceMode } from './MaintenanceMode';
 import PageBulkExportSettings from './PageBulkExportSettings';
-import QuestionnaireSettings from './QuestionnaireSettings';
 import SiteUrlSetting from './SiteUrlSetting';
 import V5PageMigration from './V5PageMigration';
 
@@ -119,13 +118,6 @@ const AppSettingsPageContents = (props: Props) => {
         </div>
       )}
 
-      <div className="row mt-5">
-        <div className="col-lg-12">
-          <h2 className="admin-setting-header">{t('admin:app_setting.questionnaire_settings')}</h2>
-          <QuestionnaireSettings />
-        </div>
-      </div>
-
       <div className="row">
         <div className="col-lg-12">
           <h2 className="admin-setting-header" id="maintenance-mode">{t('admin:maintenance_mode.maintenance_mode')}</h2>

+ 1 - 1
apps/app/src/client/components/Admin/App/PageBulkExportSettings.tsx

@@ -30,7 +30,7 @@ const PageBulkExportSettings = (): JSX.Element => {
         isBulkExportPagesEnabled,
         bulkExportDownloadExpirationSeconds,
       });
-      toastSuccess(t('commons:toaster.update_successed', { target: t('app_setting.questionnaire_settings') }));
+      toastSuccess(t('commons:toaster.update_successed', { target: t('app_setting.page_bulk_export_settings') }));
     }
     catch (err) {
       toastError(err);

+ 0 - 130
apps/app/src/client/components/Admin/App/QuestionnaireSettings.tsx

@@ -1,130 +0,0 @@
-import {
-  useState, useCallback, useEffect, type JSX,
-} from 'react';
-
-import { LoadingSpinner } from '@growi/ui/dist/components';
-import { useTranslation } from 'next-i18next';
-
-import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
-import { useSWRxAppSettings } from '~/stores/admin/app-settings';
-
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-const QuestionnaireSettings = (): JSX.Element => {
-  const { t } = useTranslation(['admin', 'commons']);
-
-  const { data, error, mutate } = useSWRxAppSettings();
-
-  const [isQuestionnaireEnabled, setIsQuestionnaireEnabled] = useState(data?.isQuestionnaireEnabled);
-  const onChangeIsQuestionnaireEnabledHandler = useCallback(() => {
-    setIsQuestionnaireEnabled(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', {
-        isQuestionnaireEnabled,
-        isAppSiteUrlHashed,
-      });
-      toastSuccess(t('commons:toaster.update_successed', { target: t('app_setting.questionnaire_settings') }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-    mutate();
-  }, [isAppSiteUrlHashed, isQuestionnaireEnabled, mutate, t]);
-
-  // Sync SWR value and state
-  useEffect(() => {
-    setIsQuestionnaireEnabled(data?.isQuestionnaireEnabled);
-    setIsAppSiteUrlHashed(data?.isAppSiteUrlHashed);
-  }, [data, data?.isAppSiteUrlHashed, data?.isQuestionnaireEnabled]);
-
-  const isLoading = data === undefined && error === undefined;
-
-  return (
-    <div id="questionnaire-settings" className="mb-5">
-      <p className="card custom-card bg-info-subtle">
-        <div className="mb-3">{t('app_setting.questionnaire_settings_explanation')}</div>
-        <span>
-          <div className="mb-2">
-            <span className="text-info me-2"><span className="material-symbols-outlined">info</span>{t('app_setting.about_data_sent')}</span>
-            <a href={t('app_setting.docs_link')} rel="noreferrer" target="_blank" className="d-inline">
-              {t('app_setting.learn_more')} <span className="material-symbols-outlined">share</span>
-            </a>
-          </div>
-          {t('app_setting.other_info_will_be_sent')}<br />
-          {t('app_setting.we_will_use_the_data_to_improve_growi')}
-        </span>
-      </p>
-
-      {isLoading && (
-        <div className="text-muted text-center mb-5">
-          <LoadingSpinner className="me-1 fs-3" />
-        </div>
-      )}
-
-      {!isLoading && (
-        <>
-          <div className="my-4 row">
-            <label
-              className="text-start text-md-end col-md-3 col-form-label"
-            >
-            </label>
-
-            <div className="col-md-6">
-              <div className="form-check form-switch form-check-info">
-                <input
-                  type="checkbox"
-                  className="form-check-input"
-                  id="isQuestionnaireEnabled"
-                  checked={isQuestionnaireEnabled}
-                  onChange={onChangeIsQuestionnaireEnabledHandler}
-                />
-                <label className="form-label form-check-label" htmlFor="isQuestionnaireEnabled">
-                  {t('app_setting.enable_questionnaire')}
-                </label>
-              </div>
-            </div>
-          </div>
-
-          <div className="my-4 row">
-            <label
-              className="text-start text-md-end col-md-3 col-form-label"
-            >
-            </label>
-
-            <div className="col-md-6">
-              <div className="form-check form-check-info">
-                <input
-                  type="checkbox"
-                  className="form-check-input"
-                  id="isAppSiteUrlHashed"
-                  checked={isAppSiteUrlHashed}
-                  onChange={onChangeisAppSiteUrlHashedHandler}
-                  disabled={!isQuestionnaireEnabled}
-                />
-                <label className="form-label form-check-label" htmlFor="isAppSiteUrlHashed">
-                  {t('app_setting.anonymize_app_site_url')}
-                </label>
-                <p className="form-text text-muted small">
-                  {t('app_setting.url_anonymization_explanation')}
-                </p>
-              </div>
-            </div>
-          </div>
-
-          <AdminUpdateButtonRow onClick={onSubmitHandler} />
-        </>
-      )}
-    </div>
-  );
-};
-
-export default QuestionnaireSettings;

+ 0 - 5
apps/app/src/client/components/Me/OtherSettings.tsx

@@ -1,7 +1,6 @@
 import type { JSX } from 'react';
 
 import { ColorModeSettings } from './ColorModeSettings';
-import { QuestionnaireSettings } from './QuestionnaireSettings';
 import { UISettings } from './UISettings';
 
 
@@ -16,10 +15,6 @@ const OtherSettings = (): JSX.Element => {
       <div className="mt-4">
         <UISettings />
       </div>
-
-      <div className="mt-4">
-        <QuestionnaireSettings />
-      </div>
     </>
   );
 };

+ 0 - 109
apps/app/src/client/components/Me/QuestionnaireSettings.tsx

@@ -1,109 +0,0 @@
-import {
-  useCallback, useEffect, useState, type JSX,
-} from 'react';
-
-import { LoadingSpinner } from '@growi/ui/dist/components';
-import { useTranslation } from 'react-i18next';
-import { UncontrolledTooltip } from 'reactstrap';
-
-import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastError, toastSuccess } from '~/client/util/toastr';
-import { useSWRxIsQuestionnaireEnabled } from '~/features/questionnaire/client/stores/questionnaire';
-import { useCurrentUser } from '~/stores-universal/context';
-
-
-export const QuestionnaireSettings = (): JSX.Element => {
-  const { t } = useTranslation();
-  const { data: currentUser, error: errorCurrentUser } = useCurrentUser();
-  const { data: growiIsQuestionnaireEnabled } = useSWRxIsQuestionnaireEnabled();
-
-  const [isQuestionnaireEnabled, setIsQuestionnaireEnabled] = useState(currentUser?.isQuestionnaireEnabled);
-
-  const onChangeIsQuestionnaireEnabledHandler = useCallback(async() => {
-    setIsQuestionnaireEnabled(prev => !prev);
-  }, []);
-
-  const onClickUpdateIsQuestionnaireEnabledHandler = useCallback(async() => {
-    try {
-      await apiv3Put('/personal-setting/questionnaire-settings', {
-        isQuestionnaireEnabled,
-      });
-      toastSuccess(t('toaster.update_successed', { target: t('questionnaire.settings'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [isQuestionnaireEnabled, t]);
-
-  // Sync currentUser and state
-  useEffect(() => {
-    setIsQuestionnaireEnabled(currentUser?.isQuestionnaireEnabled);
-  }, [currentUser?.isQuestionnaireEnabled]);
-
-  const isLoadingCurrentUser = currentUser === undefined && errorCurrentUser === undefined;
-
-  return (
-    <>
-      <h2 className="border-bottom pb-2 mb-4 fs-4">{t('questionnaire.settings')}</h2>
-
-      {isLoadingCurrentUser && (
-        <div className="text-muted text-center mb-5">
-          <LoadingSpinner className="me-1 fs-3" />
-        </div>
-      )}
-
-      <div className="container">
-        {!isLoadingCurrentUser && (
-          <div className="offset-md-3 col-md-6 text-start row">
-            <div className="form-check form-switch">
-              <span id="grw-questionnaire-settings-toggle-wrapper">
-                <input
-                  type="checkbox"
-                  className="form-check-input"
-                  id="isQuestionnaireEnabled"
-                  checked={growiIsQuestionnaireEnabled && isQuestionnaireEnabled}
-                  onChange={onChangeIsQuestionnaireEnabledHandler}
-                  disabled={!growiIsQuestionnaireEnabled}
-                />
-                <label className="form-label form-check-label" htmlFor="isQuestionnaireEnabled">
-                  {t('questionnaire.enable_questionnaire')}
-                </label>
-              </span>
-              {!growiIsQuestionnaireEnabled && (
-                <UncontrolledTooltip placement="bottom" target="grw-questionnaire-settings-toggle-wrapper">
-                  {t('questionnaire.disabled_by_admin')}
-                </UncontrolledTooltip>
-              ) }
-            </div>
-            <p className="form-text text-muted small">
-              {t('questionnaire.personal_settings_explanation')}
-            </p>
-          </div>
-        )}
-      </div>
-
-      <div className="row my-3">
-        <div className="offset-4 col-5">
-          <span className="d-inline-block" id="grw-questionnaire-settings-update-btn-wrapper">
-            <button
-              data-testid="grw-questionnaire-settings-update-btn"
-              type="button"
-              className="btn btn-primary"
-              onClick={onClickUpdateIsQuestionnaireEnabledHandler}
-              disabled={!growiIsQuestionnaireEnabled}
-              style={growiIsQuestionnaireEnabled ? {} : { pointerEvents: 'none' }}
-            >
-              {t('Update')}
-            </button>
-          </span>
-          {!growiIsQuestionnaireEnabled && (
-            <UncontrolledTooltip placement="bottom" target="grw-questionnaire-settings-update-btn-wrapper">
-              {t('questionnaire.disabled_by_admin')}
-            </UncontrolledTooltip>
-          )}
-        </div>
-      </div>
-    </>
-
-  );
-};

+ 1 - 1
apps/app/src/client/components/Me/UISettings.tsx

@@ -101,7 +101,7 @@ export const UISettings = (): JSX.Element => {
 
       <div className="row my-3">
         <div className="offset-4 col-5">
-          <button data-testid="" type="button" className="btn btn-primary" onClick={updateButtonHandler}>
+          <button data-testid="grw-ui-settings-update-btn" type="button" className="btn btn-primary" onClick={updateButtonHandler}>
             {t('Update')}
           </button>
         </div>

+ 1 - 19
apps/app/src/client/components/Sidebar/SidebarNav/PersonalDropdown.tsx

@@ -1,9 +1,8 @@
-import { useState, type JSX } from 'react';
+import { type JSX } from 'react';
 
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { UserPicture } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
-import dynamic from 'next/dynamic';
 import Link from 'next/link';
 import {
   UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
@@ -17,14 +16,10 @@ import { SkeletonItem } from './SkeletonItem';
 
 import styles from './PersonalDropdown.module.scss';
 
-const ProactiveQuestionnaireModal = dynamic(() => import('~/features/questionnaire/client/components/ProactiveQuestionnaireModal'), { ssr: false });
-
 export const PersonalDropdown = (): JSX.Element => {
   const { t } = useTranslation('commons');
   const { data: currentUser } = useCurrentUser();
 
-  const [isQuestionnaireModalOpen, setQuestionnaireModalOpen] = useState(false);
-
   if (currentUser == null) {
     return <SkeletonItem />;
   }
@@ -97,17 +92,6 @@ export const PersonalDropdown = (): JSX.Element => {
             </DropdownItem>
           </Link>
 
-          <DropdownItem
-            data-testid="grw-proactive-questionnaire-modal-toggle-btn"
-            onClick={() => setQuestionnaireModalOpen(true)}
-            className={`my-1 ${styles['personal-dropdown-item']}`}
-          >
-            <span className="d-flex align-items-center">
-              <span className="item-icon material-symbols-outlined me-2 pb-0 fs-6">edit_note</span>
-              <span className="item-text">{t('personal_dropdown.feedback')}</span>
-            </span>
-          </DropdownItem>
-
           <DropdownItem data-testid="logout-button" onClick={logoutHandler} className={`my-1 ${styles['personal-dropdown-item']}`}>
             <span className="d-flex align-items-center">
               <span className="item-icon material-symbols-outlined me-2 pb-0 fs-6">logout</span>
@@ -116,8 +100,6 @@ export const PersonalDropdown = (): JSX.Element => {
           </DropdownItem>
         </DropdownMenu>
       </UncontrolledDropdown>
-
-      <ProactiveQuestionnaireModal isOpen={isQuestionnaireModalOpen} onClose={() => setQuestionnaireModalOpen(false)} />
     </>
   );
 

+ 0 - 152
apps/app/src/features/questionnaire/client/components/ProactiveQuestionnaireModal.tsx

@@ -1,152 +0,0 @@
-import { useState, useCallback, type JSX } from 'react';
-
-import { useTranslation } from 'next-i18next';
-import {
-  Modal, ModalBody,
-} from 'reactstrap';
-
-import { apiv3Post } from '~/client/util/apiv3-client';
-
-type ModalProps = {
-  isOpen: boolean,
-  onClose: () => void,
-};
-
-const QuestionnaireCompletionModal = (props: ModalProps): JSX.Element => {
-  const { t } = useTranslation('commons');
-
-  const { isOpen, onClose } = props;
-
-  return (
-    <Modal
-      size="lg"
-      isOpen={isOpen}
-      toggle={onClose}
-      centered
-    >
-      <ModalBody className="overflow-hidden p-0" style={{ borderRadius: 8 }}>
-        <div className="m-2 p-4" style={{ borderRadius: 8 }}>
-          <div className="text-center">
-            <h2 className="my-4">{t('questionnaire_modal.title')}</h2>
-            <p className="mb-1">{t('questionnaire_modal.successfully_submitted')}</p>
-            <p>{t('questionnaire_modal.thanks_for_answering')}</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('commons');
-
-  const { isOpen, onClose } = props;
-
-  const [isQuestionnaireCompletionModal, setQuestionnaireCompletionModal] = useState(false);
-
-  const submitHandler = useCallback(async(e) => {
-    e.preventDefault();
-
-    const formData = e.target.elements;
-
-    const {
-      satisfaction: { value: satisfaction },
-      lengthOfExperience: { value: lengthOfExperience },
-      occupation: { value: occupation },
-      position: { value: position },
-      commentText: { value: commentText },
-    } = formData;
-
-    const sendValues = {
-      satisfaction: Number(satisfaction),
-      lengthOfExperience,
-      occupation,
-      position,
-      commentText,
-    };
-
-    apiv3Post('/questionnaire/proactive/answer', sendValues);
-
-    onClose();
-    setQuestionnaireCompletionModal(true);
-  }, [onClose]);
-
-  return (
-    <>
-      <Modal
-        data-testid="grw-proactive-questionnaire-modal"
-        size="lg"
-        isOpen={isOpen}
-        toggle={onClose}
-        centered
-      >
-        <ModalBody className="overflow-hidden p-0" style={{ borderRadius: 8 }}>
-          <div className="m-2 p-4" style={{ borderRadius: 8 }}>
-            <div className="text-center">
-              <h2 className="my-4">{t('questionnaire_modal.title')}</h2>
-              <p className="mb-1">{t('questionnaire_modal.more_satisfied_services')}</p>
-              <p>{t('questionnaire_modal.strive_to_improve_services')}</p>
-            </div>
-            <form className="px-5" onSubmit={submitHandler}>
-              <div className="row mt-5">
-                <label className="col-sm-5 col-form-label" htmlFor="satisfaction">
-                  <span className="badge bg-primary me-2">{t('questionnaire_modal.required')}</span>{t('questionnaire_modal.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="row mt-3">
-                <label className="col-sm-5 col-form-label" htmlFor="lengthOfExperience">{t('questionnaire_modal.history_of_growi_usage')}</label>
-                <select
-                  name="lengthOfExperience"
-                  id="lengthOfExperience"
-                  className="col-sm-7 form-control"
-                >
-                  <option value="">▼ {t('Select')}</option>
-                  <option>{t('questionnaire_modal.length_of_experience.more_than_two_years')}</option>
-                  <option>{t('questionnaire_modal.length_of_experience.one_to_two_years')}</option>
-                  <option>{t('questionnaire_modal.length_of_experience.six_months_to_one_year')}</option>
-                  <option>{t('questionnaire_modal.length_of_experience.three_months_to_six_months')}</option>
-                  <option>{t('questionnaire_modal.length_of_experience.one_month_to_three_months')}</option>
-                  <option>{t('questionnaire_modal.length_of_experience.less_than_one_month')}</option>
-                </select>
-              </div>
-              <div className="row mt-3">
-                <label className="col-sm-5 col-form-label" htmlFor="occupation">{t('questionnaire_modal.occupation')}</label>
-                <input className="col-sm-7 form-control" type="text" name="occupation" id="occupation" />
-              </div>
-              <div className="row mt-3">
-                <label className="col-sm-5 col-form-label" htmlFor="position">{t('questionnaire_modal.position')}</label>
-                <input className="col-sm-7 form-control" type="text" name="position" id="position" />
-              </div>
-              <div className="row mt-3">
-                <label className="col-sm-5 col-form-label" htmlFor="commentText">
-                  <span className="badge bg-primary me-2">{t('questionnaire_modal.required')}</span>{t('questionnaire_modal.comment_on_growi')}
-                </label>
-                <textarea className="col-sm-7 form-control" name="commentText" id="commentText" rows={5} required />
-              </div>
-              <div className="text-center mt-5">
-                <button type="submit" className="btn btn-primary">{t('questionnaire_modal.submit')}</button>
-              </div>
-              <div className="text-center my-3">
-                <span style={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={onClose}>{t('questionnaire_modal.close')}</span>
-              </div>
-            </form>
-          </div>
-        </ModalBody>
-      </Modal>
-      <QuestionnaireCompletionModal isOpen={isQuestionnaireCompletionModal} onClose={() => setQuestionnaireCompletionModal(false)} />
-    </>
-  );
-};
-
-export default ProactiveQuestionnaireModal;

+ 0 - 52
apps/app/src/features/questionnaire/client/components/Question.tsx

@@ -1,52 +0,0 @@
-import type { JSX } from 'react';
-
-import { useCurrentUser } from '~/stores-universal/context';
-
-import type { IQuestionHasId } from '../../interfaces/question';
-
-
-type QuestionProps = {
-  question: IQuestionHasId,
-  inputNamePrefix: string,
-}
-
-const Question = ({ question, inputNamePrefix }: QuestionProps): JSX.Element => {
-  const { data: currentUser } = useCurrentUser();
-  const lang = currentUser?.lang;
-
-  const questionText = lang === 'en_US' ? question.text.en_US : question.text.ja_JP;
-
-  return (
-    <div className="row mt-4">
-      <div className="col-6 d-flex align-items-center">
-        <span>
-          {questionText}
-        </span>
-      </div>
-      <div className="col-6 d-flex align-items-center ps-0">
-        <div className="btn-group flex-fill grw-questionnaire-btn-group">
-          <label className="form-label btn btn-outline-primary active me-4 rounded">
-            <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-noAnswer`} value="0" defaultChecked /> -
-          </label>
-          <label className="form-label btn btn-outline-primary rounded-start">
-            <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option1`} value="1" /> 1
-          </label>
-          <label className="form-label btn btn-outline-primary">
-            <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option2`} value="2" /> 2
-          </label>
-          <label className="form-label btn btn-outline-primary">
-            <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option3`} value="3" /> 3
-          </label>
-          <label className="form-label btn btn-outline-primary">
-            <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option4`} value="4" /> 4
-          </label>
-          <label className="form-label btn btn-outline-primary">
-            <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option5`} value="5" /> 5
-          </label>
-        </div>
-      </div>
-    </div>
-  );
-};
-
-export default Question;

+ 0 - 167
apps/app/src/features/questionnaire/client/components/QuestionnaireModal.tsx

@@ -1,167 +0,0 @@
-import { useCallback, type JSX } from 'react';
-
-import { useTranslation } from 'next-i18next';
-import { Modal, ModalBody } from 'reactstrap';
-
-import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
-import { useQuestionnaireModal } from '~/features/questionnaire/client/stores/model';
-import type { IAnswer } from '~/features/questionnaire/interfaces/answer';
-import { StatusType } from '~/features/questionnaire/interfaces/questionnaire-answer-status';
-import type { IQuestionnaireOrderHasId } from '~/features/questionnaire/interfaces/questionnaire-order';
-import { useCurrentUser } from '~/stores-universal/context';
-import loggerFactory from '~/utils/logger';
-
-import { GuestQuestionnaireAnswerStatusService } from '../services/guest-questionnaire-answer-status';
-
-import Question from './Question';
-
-const logger = loggerFactory('growi:QuestionnaireModal');
-
-type QuestionnaireModalProps = {
-  questionnaireOrder: IQuestionnaireOrderHasId
-}
-
-const QuestionnaireModal = ({ questionnaireOrder }: QuestionnaireModalProps): JSX.Element => {
-  const { data: currentUser } = useCurrentUser();
-  const lang = currentUser?.lang;
-
-  const { data: questionnaireModalData, close: closeQuestionnaireModal } = useQuestionnaireModal();
-  const isOpened = questionnaireModalData?.openedQuestionnaireId === questionnaireOrder._id;
-
-  const inputNamePrefix = 'question-';
-
-  const { t } = useTranslation(['translation', 'commons']);
-
-  const sendAnswer = useCallback(async(answers: IAnswer[]) => {
-    try {
-      await apiv3Put('/questionnaire/answer', {
-        questionnaireOrderId: questionnaireOrder._id,
-        answers,
-      });
-      if (currentUser == null) {
-        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.answered);
-      }
-      toastSuccess(
-        <>
-          <div className="fw-bold">{t('questionnaire.thank_you_for_answering')}</div>
-          <div className="pt-2">{t('questionnaire.additional_feedback')}</div>
-        </>,
-        {
-          autoClose: 3000,
-          closeButton: true,
-        },
-      );
-    }
-    catch (e) {
-      logger.error(e);
-      toastError(t('questionnaire.failed_to_send'));
-    }
-  }, [questionnaireOrder._id, t, currentUser]);
-
-  const submitHandler = useCallback(async(event) => {
-    event.preventDefault();
-
-    const answers: IAnswer[] = questionnaireOrder.questions.map((question) => {
-      const answerValue = event.target[`${inputNamePrefix + question._id}`].value;
-      return { question: question._id, value: answerValue };
-    });
-
-    sendAnswer(answers);
-
-    const shouldCloseToast = true;
-    closeQuestionnaireModal(shouldCloseToast);
-  }, [closeQuestionnaireModal, questionnaireOrder.questions, sendAnswer]);
-
-  const denyBtnClickHandler = useCallback(async() => {
-    try {
-      apiv3Put('/questionnaire/deny', {
-        questionnaireOrderId: questionnaireOrder._id,
-      });
-      if (currentUser == null) {
-        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.denied);
-      }
-      toastSuccess(t('questionnaire.denied'));
-    }
-    catch (e) {
-      logger.error(e);
-    }
-    const shouldCloseToast = true;
-    closeQuestionnaireModal(shouldCloseToast);
-  }, [closeQuestionnaireModal, questionnaireOrder._id, t, currentUser]);
-
-  // No showing toasts since not important
-  const closeBtnClickHandler = useCallback(async(shouldCloseToast: boolean) => {
-    closeQuestionnaireModal(shouldCloseToast);
-
-    try {
-      await apiv3Put('/questionnaire/skip', {
-        questionnaireOrderId: questionnaireOrder._id,
-      });
-      if (currentUser == null) {
-        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.skipped);
-      }
-    }
-    catch (e) {
-      logger.error(e);
-    }
-  }, [closeQuestionnaireModal, questionnaireOrder._id, currentUser]);
-
-  const closeBtnClickHandlerClosingToast = useCallback(async() => {
-    closeBtnClickHandler(true);
-  }, [closeBtnClickHandler]);
-
-  const questionnaireOrderTitle = lang === 'en_US' ? questionnaireOrder.title.en_US : questionnaireOrder.title.ja_JP;
-
-  return (
-    <Modal
-      size="lg"
-      isOpen={isOpened}
-      toggle={closeBtnClickHandlerClosingToast}
-      centered
-    >
-      <form onSubmit={submitHandler}>
-        <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 mb-2">
-              <h2 className="my-4">{questionnaireOrderTitle}</h2>
-              <p className="mb-1">{t('commons:questionnaire_modal.more_satisfied_services')}</p>
-              <p>{t('commons:questionnaire_modal.strive_to_improve_services')}</p>
-            </div>
-            <div className="container">
-              <div className="row mt-4">
-                <div className="col-md-2 offset-md-5 fw-bold text-end align-items-center p-0">{t('questionnaire.no_answer')}</div>
-                <div className="col-md-5 d-flex justify-content-between align-items-center">
-                  <span className="fw-bold">{t('questionnaire.disagree')}</span>
-                  <span className="fw-bold">{t('questionnaire.agree')}</span>
-                </div>
-              </div>
-              {questionnaireOrder.questions?.map((question) => {
-                return <Question question={question} inputNamePrefix={inputNamePrefix} key={question._id} />;
-              })}
-            </div>
-            <div className="text-center mt-5">
-              <button type="submit" className="btn btn-primary">{t('questionnaire.answer')}</button>
-            </div>
-            <div className="text-center cursor-pointer text-decoration-underline my-3">
-              <span style={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={denyBtnClickHandler}>{t('questionnaire.dont_show_again')}</span>
-            </div>
-
-            {currentUser?.admin && (
-              <a href="/admin/app#questionnaire-settings">
-                <i className="material-symbols-outlined me-1">admin_panel_settings</i>
-              </a>
-            )}
-            {currentUser != null && (
-              <a href="/me#other_settings">
-                <i className="material-symbols-outlined">settings</i>
-              </a>
-            )}
-          </div>
-        </ModalBody>
-      </form>
-    </Modal>
-  );
-};
-
-export default QuestionnaireModal;

+ 0 - 9
apps/app/src/features/questionnaire/client/components/QuestionnaireModalManager.module.scss

@@ -1,9 +0,0 @@
-@use '@growi/core-styles/scss/bootstrap/init' as bs;
-
-.grw-questionnaire-toasts :global {
-  position: fixed;
-  right: 20px;
-  bottom: 52px;
-  z-index: bs.$zindex-fixed + 1;
-  width: 230px;
-}

+ 0 - 51
apps/app/src/features/questionnaire/client/components/QuestionnaireModalManager.tsx

@@ -1,51 +0,0 @@
-import { useCallback, type JSX } from 'react';
-
-import { useCurrentUser } from '~/stores-universal/context';
-
-
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import type { IQuestionnaireOrderHasId } from '../../interfaces/questionnaire-order';
-import { GuestQuestionnaireAnswerStatusService } from '../services/guest-questionnaire-answer-status';
-import { useSWRxQuestionnaireOrders } from '../stores/questionnaire';
-
-import QuestionnaireModal from './QuestionnaireModal';
-import QuestionnaireToast from './QuestionnaireToast';
-
-import styles from './QuestionnaireModalManager.module.scss';
-
-const QuestionnaireModalManager = ():JSX.Element => {
-  const { data: questionnaireOrders } = useSWRxQuestionnaireOrders();
-  const { data: currentUser } = useCurrentUser();
-
-  const questionnaireOrdersToShow = useCallback((questionnaireOrders: IQuestionnaireOrderHasId[] | undefined) => {
-    const guestQuestionnaireAnswerStorage = GuestQuestionnaireAnswerStatusService.getStorage();
-    if (currentUser != null || guestQuestionnaireAnswerStorage == null) {
-      return questionnaireOrders;
-    }
-
-    return questionnaireOrders?.filter((questionnaireOrder) => {
-      const localAnswerStatus = guestQuestionnaireAnswerStorage[questionnaireOrder._id];
-      return localAnswerStatus == null || localAnswerStatus.status === StatusType.not_answered;
-    });
-  }, [currentUser]);
-
-  return (
-    <>
-      {questionnaireOrders?.map((questionnaireOrder) => {
-        return (
-          <QuestionnaireModal
-            questionnaireOrder={questionnaireOrder}
-            key={questionnaireOrder._id}
-          />
-        );
-      })}
-      <div className={styles['grw-questionnaire-toasts']}>
-        {questionnaireOrdersToShow(questionnaireOrders)?.map((questionnaireOrder) => {
-          return <QuestionnaireToast questionnaireOrder={questionnaireOrder} key={questionnaireOrder._id} />;
-        })}
-      </div>
-    </>
-  );
-};
-
-export default QuestionnaireModalManager;

+ 0 - 85
apps/app/src/features/questionnaire/client/components/QuestionnaireToast.tsx

@@ -1,85 +0,0 @@
-import { useCallback, useState, type JSX } from 'react';
-
-import { useTranslation } from 'next-i18next';
-
-import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastSuccess } from '~/client/util/toastr';
-import { useQuestionnaireModal } from '~/features/questionnaire/client/stores/model';
-import { useCurrentUser } from '~/stores-universal/context';
-import loggerFactory from '~/utils/logger';
-
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import type { IQuestionnaireOrderHasId } from '../../interfaces/questionnaire-order';
-import { GuestQuestionnaireAnswerStatusService } from '../services/guest-questionnaire-answer-status';
-
-const logger = loggerFactory('growi:QuestionnaireToast');
-
-type QuestionnaireToastProps = {
-  questionnaireOrder: IQuestionnaireOrderHasId,
-}
-
-const QuestionnaireToast = ({ questionnaireOrder }: QuestionnaireToastProps): JSX.Element => {
-  const { open: openQuestionnaireModal } = useQuestionnaireModal();
-  const { data: currentUser } = useCurrentUser();
-  const lang = currentUser?.lang;
-
-  const [isOpen, setIsOpen] = useState(true);
-
-  const { t } = useTranslation();
-
-  const answerBtnClickHandler = useCallback(() => {
-    openQuestionnaireModal(questionnaireOrder._id, () => setIsOpen(false));
-  }, [openQuestionnaireModal, questionnaireOrder._id]);
-
-  const denyBtnClickHandler = useCallback(async() => {
-    // Immediately close
-    setIsOpen(false);
-
-    try {
-      await apiv3Put('/questionnaire/deny', {
-        questionnaireOrderId: questionnaireOrder._id,
-      });
-      if (currentUser == null) {
-        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.denied);
-      }
-      toastSuccess(t('questionnaire.denied'));
-    }
-    catch (e) {
-      logger.error(e);
-    }
-  }, [questionnaireOrder._id, t, currentUser]);
-
-  // No showing toasts since not important
-  const closeBtnClickHandler = useCallback(async() => {
-    setIsOpen(false);
-
-    try {
-      await apiv3Put('/questionnaire/skip', {
-        questionnaireOrderId: questionnaireOrder._id,
-      });
-      if (currentUser == null) {
-        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.skipped);
-      }
-    }
-    catch (e) {
-      logger.error(e);
-    }
-  }, [questionnaireOrder._id, currentUser]);
-
-  const questionnaireOrderShortTitle = lang === 'en_US' ? questionnaireOrder.shortTitle.en_US : questionnaireOrder.shortTitle.ja_JP;
-
-  return (
-    <div className={`toast ${isOpen ? 'show' : 'hide'}`}>
-      <div className="toast-header bg-primary">
-        <strong className="me-auto text-light">{questionnaireOrderShortTitle}</strong>
-        <button type="button" className="ms-2 mb-1 btn-close" onClick={closeBtnClickHandler} aria-label="Close"></button>
-      </div>
-      <div className="toast-body bg-light text-dark d-flex justify-content-end">
-        <button type="button" className="btn btn-secondary me-3" onClick={answerBtnClickHandler}>{t('questionnaire.answer')}</button>
-        <button type="button" className="btn btn-secondary" onClick={denyBtnClickHandler}>{t('questionnaire.deny')}</button>
-      </div>
-    </div>
-  );
-};
-
-export default QuestionnaireToast;

+ 0 - 79
apps/app/src/features/questionnaire/client/services/guest-questionnaire-answer-status.ts

@@ -1,79 +0,0 @@
-// A service to manage questionnaire answer statuses for guest user.
-// Saves statuses in localStorage.
-
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-
-interface GuestQuestionnaireAnswerStatus {
-  status: StatusType
-  updatedDate: string
-}
-
-interface GuestQuestionnaireAnswerStatusStorage {
-  [key: string]: GuestQuestionnaireAnswerStatus
-}
-
-const storageKey = 'guestQuestionnaireAnswerStatuses';
-const DAYS_UNTIL_EXPIRATION = 30;
-
-/**
- * Get all answer statuses stored in localStorage as GuestQuestionnaireAnswerStatusStorage,
- * and update outdated information.
- */
-const getStorage = (): GuestQuestionnaireAnswerStatusStorage | null => {
-  if (typeof window === 'undefined') { return null }
-
-  const currentStorage = localStorage.getItem(storageKey);
-
-  if (currentStorage == null) { return null }
-
-  const storageJson: GuestQuestionnaireAnswerStatusStorage = JSON.parse(currentStorage);
-  // delete status if outdated to prevent localStorage overflow
-  // change skipped to not_answered if different date than when skipped
-  Object.keys(storageJson).forEach((key) => {
-    const answerStatus = storageJson[key];
-    const updatedDate = new Date(answerStatus.updatedDate);
-    const expirationDate = new Date(updatedDate.setDate(updatedDate.getDate() + DAYS_UNTIL_EXPIRATION));
-    if (expirationDate < new Date()) {
-      delete storageJson[key];
-    }
-    else if (answerStatus.status === StatusType.skipped
-          && new Date().toDateString() !== answerStatus.updatedDate) {
-      storageJson[key] = {
-        status: StatusType.not_answered,
-        updatedDate: new Date().toDateString(),
-      };
-    }
-  });
-
-  return storageJson;
-};
-
-/**
- * Set answer status for questionnaire order in GuestQuestionnaireAnswerStatusStorage,
- * and save it in localStorage.
- */
-const setStatus = (questionnaireOrderId: string, status: StatusType): void => {
-  if (typeof window === 'undefined') { return }
-
-  const guestQuestionnaireAnswerStatus: GuestQuestionnaireAnswerStatus = {
-    status,
-    updatedDate: new Date().toDateString(),
-  };
-
-  const storage = getStorage();
-
-  if (storage != null) {
-    storage[questionnaireOrderId] = guestQuestionnaireAnswerStatus;
-    localStorage.setItem(storageKey, JSON.stringify(storage));
-    return;
-  }
-
-  const initialStorage: GuestQuestionnaireAnswerStatusStorage = { [questionnaireOrderId]: guestQuestionnaireAnswerStatus };
-  localStorage.setItem(storageKey, JSON.stringify(initialStorage));
-
-};
-
-export const GuestQuestionnaireAnswerStatusService = {
-  setStatus,
-  getStorage,
-};

+ 0 - 40
apps/app/src/features/questionnaire/client/stores/model.tsx

@@ -1,40 +0,0 @@
-import type { SWRResponse } from 'swr';
-
-import { useStaticSWR } from '~/stores/use-static-swr';
-import loggerFactory from '~/utils/logger';
-
-const logger = loggerFactory('growi:stores:modal');
-
-/*
-* QuestionnaireModals
-*/
-type QuestionnaireModalStatuses = {
-  openedQuestionnaireId: string | null,
-  closeToast?: () => void | Promise<void>,
-}
-
-type QuestionnaireModalStatusUtils = {
-  open(string: string, closeToast: () => void | Promise<void>): Promise<QuestionnaireModalStatuses | undefined>
-  close(shouldCloseToast?: boolean): Promise<QuestionnaireModalStatuses | undefined>
-}
-
-export const useQuestionnaireModal = (status?: QuestionnaireModalStatuses): SWRResponse<QuestionnaireModalStatuses, Error> & QuestionnaireModalStatusUtils => {
-  const initialData: QuestionnaireModalStatuses = { openedQuestionnaireId: null };
-  const swrResponse = useStaticSWR<QuestionnaireModalStatuses, Error>('questionnaireModalStatus', status, { fallbackData: initialData });
-
-  return {
-    ...swrResponse,
-    open: (questionnaireOrderId: string, closeToast: () => void | Promise<void>) => swrResponse.mutate({
-      openedQuestionnaireId: questionnaireOrderId,
-      closeToast,
-    }),
-    close: (shouldCloseToast?: boolean) => {
-      if (shouldCloseToast) {
-        swrResponse.data?.closeToast?.();
-        if (swrResponse.data?.closeToast === undefined) logger.debug('Tried to run `swrResponse.data?.closeToast` but it was `undefined`');
-      }
-
-      return swrResponse.mutate({ openedQuestionnaireId: null });
-    },
-  };
-};

+ 0 - 24
apps/app/src/features/questionnaire/client/stores/questionnaire.tsx

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

+ 0 - 4
apps/app/src/features/questionnaire/interfaces/answer.ts

@@ -1,4 +0,0 @@
-export interface IAnswer<ID = string> {
-  question: ID
-  value: string
-}

+ 0 - 25
apps/app/src/features/questionnaire/interfaces/condition.ts

@@ -1,25 +0,0 @@
-import type { HasObjectId } from '@growi/core';
-import type { GrowiServiceType } from '@growi/core/dist/consts';
-
-import type { UserType } from './user-info';
-
-
-interface UserCondition {
-  types: UserType[] // user types to show questionnaire
-  daysSinceCreation?: {
-    moreThanOrEqualTo?: number
-    lessThanOrEqualTo?: number
-  }
-}
-
-interface GrowiCondition {
-  types: GrowiServiceType[] // GROWI types to show questionnaire in
-  versionRegExps: string[] // GROWI versions to show questionnaire in
-}
-
-export interface ICondition {
-  user: UserCondition
-  growi: GrowiCondition
-}
-
-export type IConditionHasId = ICondition & HasObjectId;

+ 0 - 27
apps/app/src/features/questionnaire/interfaces/proactive-questionnaire-answer.ts

@@ -1,27 +0,0 @@
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-
-import type { IGrowiAppAdditionalInfo, IGrowiAppInfoLegacy } from './growi-app-info';
-import type { IUserInfo } from './user-info';
-
-
-export interface IProactiveQuestionnaireAnswer {
-  satisfaction: number,
-  commentText: string,
-  growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>,
-  userInfo: IUserInfo,
-  answeredAt: Date,
-  lengthOfExperience?: string,
-  position?: string,
-  occupation?: string,
-}
-
-export interface IProactiveQuestionnaireAnswerLegacy {
-  satisfaction: number,
-  commentText: string,
-  growiInfo: IGrowiAppInfoLegacy,
-  userInfo: IUserInfo,
-  answeredAt: Date,
-  lengthOfExperience?: string,
-  position?: string,
-  occupation?: string,
-}

+ 0 - 15
apps/app/src/features/questionnaire/interfaces/question.ts

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

+ 0 - 16
apps/app/src/features/questionnaire/interfaces/questionnaire-answer-status.ts

@@ -1,16 +0,0 @@
-// eslint-disable-next-line max-len
-// see: https://dev.growi.org/6385911e1632aa30f4dae6a4#mdcont-%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%94%E3%81%A8%E3%81%AB%E5%9B%9E%E7%AD%94%E6%B8%88%E3%81%BF%E3%81%8B%E3%81%A9%E3%81%86%E3%81%8B%E3%82%92%E4%BF%9D%E5%AD%98%E3%81%99%E3%82%8B
-
-import type { Types } from 'mongoose';
-
-export const StatusType = {
-  not_answered: 'not_answered', answered: 'answered', skipped: 'skipped', denied: 'denied',
-} as const;
-
-export type StatusType = typeof StatusType[keyof typeof StatusType];
-
-export interface IQuestionnaireAnswerStatus {
-  user: Types.ObjectId | string // user that answered questionnaire
-  questionnaireOrderId: string
-  status: StatusType
-}

+ 0 - 21
apps/app/src/features/questionnaire/interfaces/questionnaire-answer.ts

@@ -1,21 +0,0 @@
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-
-import type { IAnswer } from './answer';
-import type { IGrowiAppAdditionalInfo, IGrowiAppInfoLegacy } from './growi-app-info';
-import type { IUserInfo } from './user-info';
-
-export interface IQuestionnaireAnswer<ID = string> {
-  answers: IAnswer[]
-  answeredAt: Date
-  growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>
-  userInfo: IUserInfo
-  questionnaireOrder: ID
-}
-
-export interface IQuestionnaireAnswerLegacy<ID = string> {
-  answers: IAnswer[]
-  answeredAt: Date
-  growiInfo: IGrowiAppInfoLegacy,
-  userInfo: IUserInfo
-  questionnaireOrder: ID
-}

+ 0 - 21
apps/app/src/features/questionnaire/interfaces/questionnaire-order.ts

@@ -1,21 +0,0 @@
-import type { HasObjectId } from '@growi/core';
-
-import type { ICondition, IConditionHasId } from './condition';
-import type { IQuestion, IQuestionHasId } from './question';
-
-export interface IQuestionnaireOrder<TQUESTION = IQuestion, TCONDITION = ICondition> {
-  shortTitle: {
-    ja_JP: string
-    en_US: string
-  }
-  title: {
-    ja_JP: string
-    en_US: string
-  }
-  showFrom: Date
-  showUntil: Date
-  questions: TQUESTION[]
-  condition: TCONDITION
-}
-
-export type IQuestionnaireOrderHasId = IQuestionnaireOrder<IQuestionHasId, IConditionHasId> & HasObjectId;

+ 0 - 12
apps/app/src/features/questionnaire/interfaces/user-info.ts

@@ -1,12 +0,0 @@
-export const UserType = { admin: 'admin', general: 'general', guest: 'guest' } as const;
-
-type guestType = typeof UserType.guest
-export type UserType = typeof UserType[keyof typeof UserType];
-
-export type IUserInfo = {
-  userIdHash: string // userId hash generated by using appSiteUrl as salt
-  type: Exclude<UserType, guestType>
-  userCreatedAt: Date // createdAt of user that answered the questionnaire
-} | {
-  type: guestType
-}

+ 0 - 28
apps/app/src/features/questionnaire/server/models/proactive-questionnaire-answer.ts

@@ -1,28 +0,0 @@
-import type { Model } from 'mongoose';
-import { Schema } from 'mongoose';
-
-import { getOrCreateModel } from '~/server/util/mongoose-utils';
-
-import type { IProactiveQuestionnaireAnswer } from '../../interfaces/proactive-questionnaire-answer';
-
-import { growiInfoSchema } from './schema/growi-info';
-import { userInfoSchema } from './schema/user-info';
-
-interface ProactiveQuestionnaireAnswerDocument extends IProactiveQuestionnaireAnswer, Document {}
-
-type ProactiveQuestionnaireAnswerModel = Model<ProactiveQuestionnaireAnswerDocument>
-
-export const proactiveQuestionnaireAnswerSchema = new Schema<ProactiveQuestionnaireAnswerDocument>({
-  satisfaction: { type: Number, required: true },
-  lengthOfExperience: { type: String },
-  position: { type: String },
-  occupation: { type: String },
-  commentText: { type: String, required: true },
-  growiInfo: { type: growiInfoSchema, required: true },
-  userInfo: { type: userInfoSchema, required: true },
-  answeredAt: { type: Date },
-}, { timestamps: true });
-
-export default getOrCreateModel<ProactiveQuestionnaireAnswerDocument, ProactiveQuestionnaireAnswerModel>(
-  'ProactiveQuestionnaireAnswer', proactiveQuestionnaireAnswerSchema,
-);

+ 0 - 19
apps/app/src/features/questionnaire/server/models/questionnaire-answer-status.ts

@@ -1,19 +0,0 @@
-import type { Model, Document } from 'mongoose';
-import { Schema } from 'mongoose';
-
-import { getOrCreateModel } from '~/server/util/mongoose-utils';
-
-import type { IQuestionnaireAnswerStatus } from '../../interfaces/questionnaire-answer-status';
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-
-export interface QuestionnaireAnswerStatusDocument extends IQuestionnaireAnswerStatus, Document {}
-
-export type QuestionnaireAnswerStatusModel = Model<QuestionnaireAnswerStatusDocument>
-
-const questionnaireOrderSchema = new Schema<QuestionnaireAnswerStatusDocument>({
-  user: { type: Schema.Types.ObjectId, required: true },
-  questionnaireOrderId: { type: String, required: true },
-  status: { type: String, enum: Object.values(StatusType), default: StatusType.not_answered },
-}, { timestamps: true });
-
-export default getOrCreateModel<QuestionnaireAnswerStatusDocument, QuestionnaireAnswerStatusModel>('QuestionnaireAnswerStatus', questionnaireOrderSchema);

+ 0 - 25
apps/app/src/features/questionnaire/server/models/questionnaire-answer.ts

@@ -1,25 +0,0 @@
-import type { Document, Model } from 'mongoose';
-import { Schema } from 'mongoose';
-
-import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
-import { getOrCreateModel } from '~/server/util/mongoose-utils';
-
-import type { IQuestionnaireAnswer } from '../../interfaces/questionnaire-answer';
-
-import { answerSchema } from './schema/answer';
-import { growiInfoSchema } from './schema/growi-info';
-import { userInfoSchema } from './schema/user-info';
-
-interface QuestionnaireAnswerDocument extends IQuestionnaireAnswer<ObjectIdLike>, Document {}
-
-type QuestionnaireAnswerModel = Model<QuestionnaireAnswerDocument>
-
-const questionnaireAnswerSchema = new Schema<QuestionnaireAnswerDocument>({
-  answers: [answerSchema],
-  answeredAt: { type: Date, required: true },
-  growiInfo: { type: growiInfoSchema, required: true },
-  userInfo: { type: userInfoSchema, required: true },
-  questionnaireOrder: { type: Schema.Types.ObjectId, ref: 'QuestionnaireOrder' },
-}, { timestamps: true });
-
-export default getOrCreateModel<QuestionnaireAnswerDocument, QuestionnaireAnswerModel>('QuestionnaireAnswer', questionnaireAnswerSchema);

+ 0 - 35
apps/app/src/features/questionnaire/server/models/questionnaire-order.ts

@@ -1,35 +0,0 @@
-import type { Model, Document } from 'mongoose';
-import { Schema } from 'mongoose';
-
-import { getOrCreateModel } from '~/server/util/mongoose-utils';
-
-import type { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
-
-import conditionSchema from './schema/condition';
-import questionSchema from './schema/question';
-
-export interface QuestionnaireOrderDocument extends IQuestionnaireOrder, Document {}
-
-export type QuestionnaireOrderModel = Model<QuestionnaireOrderDocument>
-
-const questionnaireOrderTitleSchema = new Schema<IQuestionnaireOrder['title']>({
-  ja_JP: { type: String, required: true },
-  en_US: { type: String, required: true },
-}, { _id: false });
-
-const questionnaireOrderSchema = new Schema<QuestionnaireOrderDocument>({
-  shortTitle: { type: questionnaireOrderTitleSchema, required: true },
-  title: { type: questionnaireOrderTitleSchema, required: true },
-  showFrom: { type: Date, required: true },
-  showUntil: {
-    type: Date,
-    required: true,
-    validate: [function(value) {
-      return this.showFrom <= value;
-    }, 'showFrom must be before showUntil'],
-  },
-  questions: [questionSchema],
-  condition: { type: conditionSchema, required: true },
-}, { timestamps: true });
-
-export default getOrCreateModel<QuestionnaireOrderDocument, QuestionnaireOrderModel>('QuestionnaireOrder', questionnaireOrderSchema);

+ 0 - 10
apps/app/src/features/questionnaire/server/models/schema/answer.ts

@@ -1,10 +0,0 @@
-import { Schema } from 'mongoose';
-
-import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
-
-import type { IAnswer } from '../../../interfaces/answer';
-
-export const answerSchema = new Schema<IAnswer<ObjectIdLike>>({
-  question: { type: Schema.Types.ObjectId, ref: 'Question', required: true },
-  value: { type: String, required: true },
-});

+ 0 - 29
apps/app/src/features/questionnaire/server/models/schema/condition.ts

@@ -1,29 +0,0 @@
-import { GrowiServiceType } from '@growi/core/dist/consts';
-import { Schema } from 'mongoose';
-
-import type { ICondition } from '../../../interfaces/condition';
-import { UserType } from '../../../interfaces/user-info';
-
-const conditionSchema = new Schema<ICondition>({
-  user: {
-    types: [{ type: String, enum: Object.values(UserType) }],
-    daysSinceCreation: {
-      moreThanOrEqualTo: { type: Number, min: 0 },
-      lessThanOrEqualTo: {
-        type: Number,
-        min: 0,
-        validate: [
-          function(value) {
-            return this.user.daysSinceCreation.moreThanOrEqualTo == null || this.user.daysSinceCreation.moreThanOrEqualTo <= value;
-          }, 'daysSinceCreation.lessThanOrEqualTo must be greater than moreThanOrEqualTo',
-        ],
-      },
-    },
-  },
-  growi: {
-    types: [{ type: String, enum: Object.values(GrowiServiceType) }],
-    versionRegExps: [String],
-  },
-});
-
-export default conditionSchema;

+ 0 - 42
apps/app/src/features/questionnaire/server/models/schema/growi-info.ts

@@ -1,42 +0,0 @@
-import { GrowiDeploymentType, GrowiServiceType } from '@growi/core/dist/consts';
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-import { GrowiWikiType } from '@growi/core/dist/interfaces';
-import { Schema } from 'mongoose';
-
-import type { IGrowiAppAdditionalInfo } from '~/features/questionnaire/interfaces/growi-app-info';
-import { AttachmentMethodType } from '~/interfaces/attachment';
-import { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
-
-const growiAdditionalInfoSchema = new Schema<IGrowiAppAdditionalInfo>({
-  installedAt: { type: Date, required: true },
-  installedAtByOldestUser: { type: Date, required: true },
-  currentUsersCount: { type: Number, required: true },
-  currentActiveUsersCount: { type: Number, required: true },
-  attachmentType: { type: String, required: true, enum: Object.values(AttachmentMethodType) },
-  activeExternalAccountTypes: [{ type: String, enum: Object.values(IExternalAuthProviderType) }],
-});
-
-export const growiInfoSchema = new Schema<IGrowiInfo<IGrowiAppAdditionalInfo> & IGrowiAppAdditionalInfo>({
-  version: { type: String, required: true },
-  appSiteUrl: { type: String },
-  serviceInstanceId: { type: String, required: true },
-  type: { type: String, required: true, enum: Object.values(GrowiServiceType) },
-  wikiType: { type: String, required: true, enum: Object.values(GrowiWikiType) },
-  osInfo: {
-    type: { type: String },
-    platform: String,
-    arch: String,
-    totalmem: Number,
-  },
-  deploymentType: { type: String, enum: (<(string | null)[]>Object.values(GrowiDeploymentType)).concat([null]) },
-  additionalInfo: growiAdditionalInfoSchema,
-
-  // legacy properties (extracted from additionalInfo for growi-questionnaire)
-  // see: https://gitlab.weseek.co.jp/tech/growi/growi-questionnaire
-  installedAt: { type: Date },
-  installedAtByOldestUser: { type: Date },
-  currentUsersCount: { type: Number },
-  currentActiveUsersCount: { type: Number },
-  attachmentType: { type: String, enum: Object.values(AttachmentMethodType) },
-  activeExternalAccountTypes: [{ type: String, enum: Object.values(IExternalAuthProviderType) }],
-});

+ 0 - 16
apps/app/src/features/questionnaire/server/models/schema/question.ts

@@ -1,16 +0,0 @@
-import { Schema } from 'mongoose';
-
-import type { IQuestion } from '../../../interfaces/question';
-import { QuestionType } from '../../../interfaces/question';
-
-const questionTextSchema = new Schema<IQuestion['text']>({
-  ja_JP: { type: String, required: true },
-  en_US: { type: String, required: true },
-}, { _id: false });
-
-const questionSchema = new Schema<IQuestion>({
-  type: { type: String, required: true, enum: Object.values(QuestionType) },
-  text: { type: questionTextSchema, required: true },
-}, { timestamps: true });
-
-export default questionSchema;

+ 0 - 10
apps/app/src/features/questionnaire/server/models/schema/user-info.ts

@@ -1,10 +0,0 @@
-import { Schema } from 'mongoose';
-
-import type { IUserInfo } from '../../../interfaces/user-info';
-import { UserType } from '../../../interfaces/user-info';
-
-export const userInfoSchema = new Schema<IUserInfo>({
-  userIdHash: { type: String },
-  type: { type: String, required: true, enum: Object.values(UserType) },
-  userCreatedAt: { type: Date },
-});

+ 0 - 384
apps/app/src/features/questionnaire/server/routes/apiv3/questionnaire.ts

@@ -1,384 +0,0 @@
-import type { IUserHasId } from '@growi/core';
-import type { Request } from 'express';
-import { Router } from 'express';
-import { body, validationResult } from 'express-validator';
-
-import type Crowi from '~/server/crowi';
-import { accessTokenParser } from '~/server/middlewares/access-token-parser';
-import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
-import { configManager } from '~/server/service/config-manager';
-import { growiInfoService } from '~/server/service/growi-info';
-import axios from '~/utils/axios';
-import loggerFactory from '~/utils/logger';
-
-import type { IAnswer } from '../../../interfaces/answer';
-import type { IProactiveQuestionnaireAnswer } from '../../../interfaces/proactive-questionnaire-answer';
-import type { IQuestionnaireAnswer } from '../../../interfaces/questionnaire-answer';
-import { StatusType } from '../../../interfaces/questionnaire-answer-status';
-import ProactiveQuestionnaireAnswer from '../../models/proactive-questionnaire-answer';
-import QuestionnaireAnswer from '../../models/questionnaire-answer';
-import QuestionnaireAnswerStatus from '../../models/questionnaire-answer-status';
-import { convertToLegacyFormat, getSiteUrlHashed } from '../../util/convert-to-legacy-format';
-
-
-const logger = loggerFactory('growi:routes:apiv3:questionnaire');
-
-const router = Router();
-
-interface AuthorizedRequest extends Request {
-  user?: any
-}
-
-module.exports = (crowi: Crowi): Router => {
-  const loginRequired = require('~/server/middlewares/login-required')(crowi, true);
-
-  const validators = {
-    proactiveAnswer: [
-      body('satisfaction').exists().isNumeric(),
-      body('lengthOfExperience').isString(),
-      body('position').isString(),
-      body('occupation').isString(),
-      body('commentText').exists().isString(),
-    ],
-    answer: [body('questionnaireOrderId').exists().isString(), body('answers').exists().isArray({ min: 1 })],
-    skipDeny: [body('questionnaireOrderId').exists().isString()],
-  };
-
-  const changeAnswerStatus = async(user, questionnaireOrderId, status) => {
-    const result = await QuestionnaireAnswerStatus.updateOne({
-      user: { $eq: user },
-      questionnaireOrderId: { $eq: questionnaireOrderId },
-    }, {
-      status,
-    }, { upsert: true });
-
-    if (result.modifiedCount === 1) {
-      return 204;
-    }
-    if (result.upsertedCount === 1) {
-      return 201;
-    }
-    return 404;
-  };
-
-  /**
-   * @swagger
-   *
-   * /questionnaire/orders:
-   *   get:
-   *     tags: [Questionnaire]
-   *     security:
-   *       - bearer: []
-   *       - accessTokenInQuery: []
-   *     summary: /questionnaire/orders
-   *     description: Get questionnaire orders
-   *     responses:
-   *       200:
-   *         description: OK
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *               properties:
-   *                 questionnaireOrders:
-   *                   type: array
-   *                   items:
-   *                     type: object
-   */
-  router.get('/orders', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const growiInfo = await growiInfoService.getGrowiInfo(true);
-    const userInfo = crowi.questionnaireService.getUserInfo(req.user ?? null, getSiteUrlHashed(growiInfo.appSiteUrl));
-
-    try {
-      const questionnaireOrders = await crowi.questionnaireService!.getQuestionnaireOrdersToShow(userInfo, growiInfo, req.user?._id ?? null);
-
-      return res.apiv3({ questionnaireOrders });
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
-
-  /**
-   * @swagger
-   *
-   * /questionnaire/is-enabled:
-   *   get:
-   *     tags: [Questionnaire]
-   *     security:
-   *       - bearer: []
-   *       - accessTokenInQuery: []
-   *     summary: /questionnaire/is-enabled
-   *     description: Get questionnaire is enabled
-   *     responses:
-   *       200:
-   *         description: OK
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *               properties:
-   *                 isEnabled:
-   *                   type: boolean
-   */
-  router.get('/is-enabled', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const isEnabled = configManager.getConfig('questionnaire:isQuestionnaireEnabled');
-    return res.apiv3({ isEnabled });
-  });
-
-  /**
-   * @swagger
-   *
-   * /questionnaire/proactive/answer:
-   *   post:
-   *     tags: [Questionnaire]
-   *     security:
-   *       - bearer: []
-   *       - accessTokenInQuery: []
-   *     summary: /questionnaire/proactive/answer
-   *     description: Post proactive questionnaire answer
-   *     requestBody:
-   *       required: true
-   *       content:
-   *         application/json:
-   *           schema:
-   *             type: object
-   *     responses:
-   *       200:
-   *         description: Success
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   */
-  router.post('/proactive/answer', accessTokenParser, loginRequired, validators.proactiveAnswer, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const sendQuestionnaireAnswer = async() => {
-      const questionnaireServerOrigin = configManager.getConfig('app:questionnaireServerOrigin');
-      const isAppSiteUrlHashed = configManager.getConfig('questionnaire:isAppSiteUrlHashed');
-      const growiInfo = await growiInfoService.getGrowiInfo(true);
-      const userInfo = crowi.questionnaireService.getUserInfo(req.user ?? null, getSiteUrlHashed(growiInfo.appSiteUrl));
-
-      const proactiveQuestionnaireAnswer: IProactiveQuestionnaireAnswer = {
-        satisfaction: req.body.satisfaction,
-        lengthOfExperience: req.body.lengthOfExperience,
-        position: req.body.position,
-        occupation: req.body.occupation,
-        commentText: req.body.commentText,
-        growiInfo,
-        userInfo,
-        answeredAt: new Date(),
-      };
-
-      const proactiveQuestionnaireAnswerLegacy = convertToLegacyFormat(proactiveQuestionnaireAnswer, isAppSiteUrlHashed);
-
-      try {
-        await axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive`, proactiveQuestionnaireAnswerLegacy);
-      }
-      catch (err) {
-        if (err.request != null) {
-          // when failed to send, save to resend in cronjob
-          await ProactiveQuestionnaireAnswer.create(proactiveQuestionnaireAnswer);
-        }
-        else {
-          throw err;
-        }
-      }
-    };
-
-    const errors = validationResult(req);
-    if (!errors.isEmpty()) {
-      return res.status(400).json({ errors: errors.array() });
-    }
-
-    try {
-      await sendQuestionnaireAnswer();
-      return res.apiv3({});
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
-
-  /**
-   * @swagger
-   *
-   * /questionnaire/answer:
-   *   put:
-   *     tags: [Questionnaire]
-   *     security:
-   *       - bearer: []
-   *       - accessTokenInQuery: []
-   *     summary: /questionnaire/answer
-   *     description: Post questionnaire answer
-   *     requestBody:
-   *       required: true
-   *       content:
-   *         application/json:
-   *           schema:
-   *             type: object
-   *     responses:
-   *       201:
-   *         description: Created
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *       204:
-   *         description: No Content
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *       404:
-   *         description: Not Found
-   */
-  router.put('/answer', accessTokenParser, loginRequired, validators.answer, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const sendQuestionnaireAnswer = async(user: IUserHasId, answers: IAnswer[]) => {
-      const questionnaireServerOrigin = crowi.configManager.getConfig('app:questionnaireServerOrigin');
-      const isAppSiteUrlHashed = configManager.getConfig('questionnaire:isAppSiteUrlHashed');
-      const growiInfo = await growiInfoService.getGrowiInfo(true);
-      const userInfo = crowi.questionnaireService.getUserInfo(user, getSiteUrlHashed(growiInfo.appSiteUrl));
-
-      const questionnaireAnswer: IQuestionnaireAnswer = {
-        growiInfo,
-        userInfo,
-        answers,
-        answeredAt: new Date(),
-        questionnaireOrder: req.body.questionnaireOrderId,
-      };
-
-      const questionnaireAnswerLegacy = convertToLegacyFormat(questionnaireAnswer, isAppSiteUrlHashed);
-
-      try {
-        await axios.post(`${questionnaireServerOrigin}/questionnaire-answer`, questionnaireAnswerLegacy);
-      }
-      catch (err) {
-        if (err.request != null) {
-          // when failed to send, save to resend in cronjob
-          await QuestionnaireAnswer.create(questionnaireAnswer);
-        }
-        else {
-          throw err;
-        }
-      }
-    };
-
-    const errors = validationResult(req);
-    if (!errors.isEmpty()) {
-      return res.status(400).json({ errors: errors.array() });
-    }
-
-    try {
-      await sendQuestionnaireAnswer(req.user ?? null, req.body.answers);
-      const status = await changeAnswerStatus(req.user, req.body.questionnaireOrderId, StatusType.answered);
-      return res.apiv3({}, status);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
-
-  /**
-   * @swagger
-   *
-   * /questionnaire/skip:
-   *   put:
-   *     tags: [Questionnaire]
-   *     security:
-   *       - bearer: []
-   *       - accessTokenInQuery: []
-   *     summary: /questionnaire/skip
-   *     description: Skip questionnaire
-   *     requestBody:
-   *       required: true
-   *       content:
-   *         application/json:
-   *           schema:
-   *             type: object
-   *     responses:
-   *       201:
-   *         description: Created
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *       204:
-   *         description: No Content
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *       404:
-   *         description: Not Found
-   */
-  router.put('/skip', accessTokenParser, loginRequired, validators.skipDeny, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const errors = validationResult(req);
-    if (!errors.isEmpty()) {
-      return res.status(400).json({ errors: errors.array() });
-    }
-
-    try {
-      const status = await changeAnswerStatus(req.user, req.body.questionnaireOrderId, StatusType.skipped);
-      return res.apiv3({}, status);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
-
-  /**
-   * @swagger
-   *
-   * /questionnaire/deny:
-   *   put:
-   *     tags: [Questionnaire]
-   *     security:
-   *       - bearer: []
-   *       - accessTokenInQuery: []
-   *     summary: /questionnaire/deny
-   *     description: Deny questionnaire
-   *     requestBody:
-   *       required: true
-   *       content:
-   *         application/json:
-   *           schema:
-   *             type: object
-   *     responses:
-   *       201:
-   *         description: Created
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *       204:
-   *         description: No Content
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *       404:
-   *         description: Not Found
-   */
-  router.put('/deny', accessTokenParser, loginRequired, validators.skipDeny, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const errors = validationResult(req);
-    if (!errors.isEmpty()) {
-      return res.status(400).json({ errors: errors.array() });
-    }
-
-    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;
-
-};

+ 0 - 515
apps/app/src/features/questionnaire/server/service/questionnaire-cron.integ.ts

@@ -1,515 +0,0 @@
-import { GrowiDeploymentType, GrowiServiceType, GrowiWikiType } from '@growi/core';
-// eslint-disable-next-line no-restricted-imports
-import axios from 'axios';
-import mongoose from 'mongoose';
-
-import { configManager } from '~/server/service/config-manager';
-
-import { AttachmentMethodType } from '../../../../interfaces/attachment';
-import type {
-  IProactiveQuestionnaireAnswer, IProactiveQuestionnaireAnswerLegacy,
-} from '../../interfaces/proactive-questionnaire-answer';
-import type { IQuestionnaireAnswer, IQuestionnaireAnswerLegacy } from '../../interfaces/questionnaire-answer';
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import ProactiveQuestionnaireAnswer from '../models/proactive-questionnaire-answer';
-import QuestionnaireAnswer from '../models/questionnaire-answer';
-import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
-import QuestionnaireOrder from '../models/questionnaire-order';
-
-import questionnaireCronService from './questionnaire-cron';
-
-// TODO: use actual user model after ~/server/models/user.js becomes importable in vitest
-// ref: https://github.com/vitest-dev/vitest/issues/846
-const userSchema = new mongoose.Schema({
-  name: { type: String },
-  username: { type: String, required: true, unique: true },
-  email: { type: String, unique: true, sparse: true },
-}, {
-  timestamps: true,
-});
-const User = mongoose.model('User', userSchema);
-
-describe('QuestionnaireCronService', () => {
-  const mockResponse = {
-    data: {
-      questionnaireOrders: [
-        // saved in db、not finished (user.types is updated from the time it was saved)
-        {
-          _id: '63a8354837e7aa378e16f0b1',
-          shortTitle: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          title: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          showFrom: '2022-12-11',
-          showUntil: '2100-12-12',
-          questions: [
-            {
-              type: 'points',
-              text: {
-                ja_JP: 'GROWI は使いやすいですか?',
-                en_US: 'Is GROWI easy to use?',
-              },
-            },
-          ],
-          condition: {
-            user: {
-              types: ['admin', 'general'],
-            },
-            growi: {
-              types: ['cloud', 'private-cloud'],
-              versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-            },
-          },
-          createdAt: '2022-12-01',
-          updatedAt: '2022-12-01',
-          __v: 0,
-        },
-        // not saved, not finished
-        {
-          _id: '63a8354837e7aa378e16f0b2',
-          shortTitle: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          title: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          showFrom: '2021-12-11',
-          showUntil: '2100-12-12',
-          questions: [
-            {
-              type: 'points',
-              text: {
-                ja_JP: 'アンケート機能は正常動作していますか?',
-                en_US: 'Is this questionnaire functioning properly?',
-              },
-            },
-          ],
-          condition: {
-            user: {
-              types: ['general'],
-            },
-            growi: {
-              types: ['cloud'],
-              versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-            },
-          },
-          createdAt: '2022-12-02',
-          updatedAt: '2022-12-02',
-          __v: 0,
-        },
-        // not saved, finished
-        {
-          _id: '63a8354837e7aa378e16f0b3',
-          shortTitle: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          title: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          showFrom: '2021-12-11',
-          showUntil: '2021-12-12',
-          questions: [
-            {
-              type: 'points',
-              text: {
-                ja_JP: 'これはいい質問ですか?',
-                en_US: 'Is this a good question?',
-              },
-            },
-          ],
-          condition: {
-            user: {
-              types: ['general'],
-            },
-            growi: {
-              types: ['cloud'],
-              versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-            },
-          },
-          createdAt: '2022-12-03',
-          updatedAt: '2022-12-03',
-          __v: 0,
-        },
-      ],
-    },
-  };
-
-  beforeAll(async() => {
-    await configManager.loadConfigs();
-    await configManager.updateConfig('app:questionnaireCronMaxHoursUntilRequest', 0);
-    await User.create({
-      name: 'Example for Questionnaire Service Test',
-      username: 'questionnaire cron test user',
-      email: 'questionnaireCronTestUser@example.com',
-      createdAt: '2020-01-01',
-    });
-  });
-
-  beforeEach(async() => {
-    // insert initial db data
-    await QuestionnaireOrder.insertMany([
-      {
-        _id: '63a8354837e7aa378e16f0b1',
-        shortTitle: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        title: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        showFrom: '2022-12-11',
-        showUntil: '2100-12-12',
-        questions: [
-          {
-            type: 'points',
-            text: {
-              ja_JP: 'GROWI は使いやすいですか?',
-              en_US: 'Is GROWI easy to use?',
-            },
-          },
-        ],
-        condition: {
-          user: {
-            types: ['general'],
-          },
-          growi: {
-            types: ['cloud', 'private-cloud'],
-            versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-          },
-        },
-      },
-      // finished
-      {
-        _id: '63a8354837e7aa378e16f0b4',
-        shortTitle: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        title: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        showFrom: '2020-12-11',
-        showUntil: '2021-12-12',
-        questions: [
-          {
-            type: 'points',
-            text: {
-              ja_JP: 'ver 2.0 は 1.0 より良いですか?',
-              en_US: 'Is ver 2.0 better than 1.0?',
-            },
-          },
-        ],
-        condition: {
-          user: {
-            types: ['general'],
-          },
-          growi: {
-            types: ['cloud'],
-            versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-          },
-        },
-      },
-      // questionnaire that doesn't exist in questionnaire server
-      {
-        _id: '63a8354837e7aa378e16f0b5',
-        shortTitle: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        title: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        showFrom: '2020-12-11',
-        showUntil: '2100-12-12',
-        questions: [
-          {
-            type: 'points',
-            text: {
-              ja_JP: '新しいデザインは良いですか?',
-              en_US: 'How would you rate the latest design?',
-            },
-          },
-        ],
-        condition: {
-          user: {
-            types: ['general'],
-          },
-          growi: {
-            types: ['cloud'],
-            versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-          },
-        },
-      },
-    ]);
-
-    await QuestionnaireAnswerStatus.insertMany([
-      {
-        user: new mongoose.Types.ObjectId(),
-        questionnaireOrderId: '63a8354837e7aa378e16f0b1',
-        status: StatusType.skipped,
-      },
-      {
-        user: new mongoose.Types.ObjectId(),
-        questionnaireOrderId: '63a8354837e7aa378e16f0b1',
-        status: StatusType.answered,
-      },
-      {
-        user: new mongoose.Types.ObjectId(),
-        questionnaireOrderId: '63a8354837e7aa378e16f0b1',
-        status: StatusType.not_answered,
-      },
-    ]);
-
-    const validQuestionnaireAnswer: IQuestionnaireAnswer = {
-      answers: [{
-        question: '63c6da88143e531d95346188',
-        value: '1',
-      }],
-      answeredAt: new Date(),
-      growiInfo: {
-        version: '1.0',
-        appSiteUrl: 'https://example.com',
-        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
-        type: GrowiServiceType.cloud,
-        wikiType: GrowiWikiType.open,
-        deploymentType: GrowiDeploymentType.others,
-        osInfo: {
-          type: 'Linux',
-          platform: 'linux',
-          arch: 'x64',
-          totalmem: 8589934592,
-        },
-        additionalInfo: {
-          installedAt: new Date('2000-01-01'),
-          installedAtByOldestUser: new Date('2020-01-01'),
-          currentUsersCount: 100,
-          currentActiveUsersCount: 50,
-          attachmentType: AttachmentMethodType.aws,
-        },
-      },
-      userInfo: {
-        userIdHash: '542bcc3bc5bc61b840017a18',
-        type: 'general',
-        userCreatedAt: new Date(),
-      },
-      questionnaireOrder: '63a8354837e7aa378e16f0b1',
-    };
-
-    const validQuestionnaireAnswerLegacy: IQuestionnaireAnswerLegacy = {
-      answers: [{
-        question: '63c6da88143e531d95346188',
-        value: '1',
-      }],
-      answeredAt: new Date(),
-      growiInfo: {
-        version: '1.0',
-        appSiteUrl: 'https://example.com',
-        appSiteUrlHashed: 'hashed',
-        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
-        type: GrowiServiceType.cloud,
-        wikiType: GrowiWikiType.open,
-        deploymentType: GrowiDeploymentType.others,
-        installedAt: new Date('2000-01-01'),
-        installedAtByOldestUser: new Date('2020-01-01'),
-        currentUsersCount: 100,
-        currentActiveUsersCount: 50,
-        osInfo: {
-          type: 'Linux',
-          platform: 'linux',
-          arch: 'x64',
-          totalmem: 8589934592,
-        },
-        attachmentType: AttachmentMethodType.aws,
-      },
-      userInfo: {
-        userIdHash: '542bcc3bc5bc61b840017a18',
-        type: 'general',
-        userCreatedAt: new Date(),
-      },
-      questionnaireOrder: '63a8354837e7aa378e16f0b1',
-    };
-
-    await QuestionnaireAnswer.insertMany([
-      validQuestionnaireAnswer,
-      validQuestionnaireAnswer,
-      validQuestionnaireAnswer,
-      validQuestionnaireAnswerLegacy,
-      validQuestionnaireAnswerLegacy,
-    ]);
-
-    const validProactiveQuestionnaireAnswer: IProactiveQuestionnaireAnswer = {
-      satisfaction: 1,
-      commentText: 'answer text',
-      growiInfo: {
-        version: '1.0',
-        appSiteUrl: 'https://example.com',
-        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
-        type: GrowiServiceType.cloud,
-        wikiType: GrowiWikiType.open,
-        deploymentType: GrowiDeploymentType.others,
-        osInfo: {
-          type: 'Linux',
-          platform: 'linux',
-          arch: 'x64',
-          totalmem: 8589934592,
-        },
-        additionalInfo: {
-          installedAt: new Date('2000-01-01'),
-          installedAtByOldestUser: new Date('2020-01-01'),
-          currentUsersCount: 100,
-          currentActiveUsersCount: 50,
-          attachmentType: AttachmentMethodType.aws,
-        },
-      },
-      userInfo: {
-        userIdHash: '542bcc3bc5bc61b840017a18',
-        type: 'general',
-        userCreatedAt: new Date(),
-      },
-      answeredAt: new Date(),
-    };
-    const validProactiveQuestionnaireAnswerLegacy: IProactiveQuestionnaireAnswerLegacy = {
-      satisfaction: 1,
-      commentText: 'answer text',
-      growiInfo: {
-        version: '1.0',
-        appSiteUrl: 'https://example.com',
-        appSiteUrlHashed: 'hashed',
-        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
-        type: GrowiServiceType.cloud,
-        wikiType: GrowiWikiType.open,
-        deploymentType: GrowiDeploymentType.others,
-        osInfo: {
-          type: 'Linux',
-          platform: 'linux',
-          arch: 'x64',
-          totalmem: 8589934592,
-        },
-        // legacy properties
-        installedAt: new Date('2000-01-01'),
-        installedAtByOldestUser: new Date('2020-01-01'),
-        currentUsersCount: 100,
-        currentActiveUsersCount: 50,
-        attachmentType: AttachmentMethodType.aws,
-      },
-      userInfo: {
-        userIdHash: '542bcc3bc5bc61b840017a18',
-        type: 'general',
-        userCreatedAt: new Date(),
-      },
-      answeredAt: new Date(),
-    };
-
-    await ProactiveQuestionnaireAnswer.insertMany([
-      validProactiveQuestionnaireAnswer,
-      validProactiveQuestionnaireAnswer,
-      validProactiveQuestionnaireAnswer,
-      validProactiveQuestionnaireAnswerLegacy,
-      validProactiveQuestionnaireAnswerLegacy,
-    ]);
-
-    questionnaireCronService.startCron();
-
-    vi.spyOn(axios, 'get').mockResolvedValue(mockResponse);
-    vi.spyOn(axios, 'post').mockResolvedValue({ data: { result: 'success' } });
-  });
-
-  afterAll(() => {
-    questionnaireCronService.stopCron(); // vitest will not finish until cronjob stops
-  });
-
-  test('Job execution should save(update) quesionnaire orders, delete outdated ones, update skipped answer statuses, and delete resent answers', async() => {
-    // testing the cronjob from schedule has untrivial overhead, so test job execution in place
-    await questionnaireCronService.executeJob();
-
-    const savedOrders = await QuestionnaireOrder.find()
-      .select('-condition._id -questions._id -questions.createdAt -questions.updatedAt')
-      .sort({ _id: 1 });
-
-    expect(JSON.parse(JSON.stringify(savedOrders))).toEqual([
-      {
-        _id: '63a8354837e7aa378e16f0b1',
-        shortTitle: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        title: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        showFrom: '2022-12-11T00:00:00.000Z',
-        showUntil: '2100-12-12T00:00:00.000Z',
-        questions: [
-          {
-            type: 'points',
-            text: {
-              ja_JP: 'GROWI は使いやすいですか?',
-              en_US: 'Is GROWI easy to use?',
-            },
-          },
-        ],
-        condition: {
-          user: {
-            types: ['admin', 'general'],
-          },
-          growi: {
-            types: ['cloud', 'private-cloud'],
-            versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-          },
-        },
-        createdAt: '2022-12-01T00:00:00.000Z',
-        updatedAt: '2022-12-01T00:00:00.000Z',
-        __v: 0,
-      },
-      {
-        _id: '63a8354837e7aa378e16f0b2',
-        shortTitle: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        title: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        showFrom: '2021-12-11T00:00:00.000Z',
-        showUntil: '2100-12-12T00:00:00.000Z',
-        questions: [
-          {
-            type: 'points',
-            text: {
-              ja_JP: 'アンケート機能は正常動作していますか?',
-              en_US: 'Is this questionnaire functioning properly?',
-            },
-          },
-        ],
-        condition: {
-          user: {
-            types: ['general'],
-          },
-          growi: {
-            types: ['cloud'],
-            versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-          },
-        },
-        createdAt: '2022-12-02T00:00:00.000Z',
-        updatedAt: '2022-12-02T00:00:00.000Z',
-        __v: 0,
-      },
-    ]);
-
-    expect((await QuestionnaireAnswerStatus.find({ status: StatusType.not_answered })).length).toEqual(2);
-    expect((await QuestionnaireAnswer.find()).length).toEqual(0);
-    expect((await ProactiveQuestionnaireAnswer.find()).length).toEqual(0);
-  });
-});

+ 0 - 106
apps/app/src/features/questionnaire/server/service/questionnaire-cron.ts

@@ -1,106 +0,0 @@
-import axiosRetry from 'axios-retry';
-
-import { configManager } from '~/server/service/config-manager';
-import CronService from '~/server/service/cron';
-import { getRandomIntInRange } from '~/utils/rand';
-
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import type { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
-import ProactiveQuestionnaireAnswer from '../models/proactive-questionnaire-answer';
-import QuestionnaireAnswer from '../models/questionnaire-answer';
-import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
-import QuestionnaireOrder from '../models/questionnaire-order';
-import { convertToLegacyFormat } from '../util/convert-to-legacy-format';
-
-const axios = require('axios').default;
-
-axiosRetry(axios, { retries: 3 });
-
-/**
- * Manages cronjob which
- *  1. fetches QuestionnaireOrders from questionnaire server
- *  2. updates QuestionnaireOrder collection to contain only the ones that exist in the fetched list and is not finished (doesn't have to be started)
- *  3. changes QuestionnaireAnswerStatuses which are 'skipped' to 'not_answered'
- *  4. resend QuestionnaireAnswers & ProactiveQuestionnaireAnswers which failed to reach questionnaire server
- */
-class QuestionnaireCronService extends CronService {
-
-  sleep = (msec: number): Promise<void> => new Promise(resolve => setTimeout(resolve, msec));
-
-  override getCronSchedule(): string {
-    return configManager.getConfig('app:questionnaireCronSchedule');
-  }
-
-  override async executeJob(): Promise<void> {
-    // sleep for a random amount to scatter request time from GROWI apps to questionnaire server
-    await this.sleepBeforeJob();
-
-    const questionnaireServerOrigin = configManager.getConfig('app:questionnaireServerOrigin');
-    const isAppSiteUrlHashed = configManager.getConfig('questionnaire:isAppSiteUrlHashed');
-
-    const fetchQuestionnaireOrders = async(): Promise<IQuestionnaireOrder[]> => {
-      const response = await axios.get(`${questionnaireServerOrigin}/questionnaire-order/index`);
-      return response.data.questionnaireOrders;
-    };
-
-    const saveUnfinishedOrders = async(questionnaireOrders: IQuestionnaireOrder[]) => {
-      const currentDate = new Date(Date.now());
-      const unfinishedOrders = questionnaireOrders.filter(order => new Date(order.showUntil) > currentDate);
-      await QuestionnaireOrder.insertMany(unfinishedOrders);
-    };
-
-    const changeSkippedAnswerStatusToNotAnswered = async() => {
-      await QuestionnaireAnswerStatus.updateMany(
-        { status: StatusType.skipped },
-        { status: StatusType.not_answered },
-      );
-    };
-
-    const resendQuestionnaireAnswers = async() => {
-      const questionnaireAnswers = await QuestionnaireAnswer.find()
-        .select('-_id -answers._id  -growiInfo._id -userInfo._id')
-        .lean();
-      const proactiveQuestionnaireAnswers = await ProactiveQuestionnaireAnswer.find()
-        .select('-_id -growiInfo._id -userInfo._id')
-        .lean();
-
-      axios.post(`${questionnaireServerOrigin}/questionnaire-answer/batch`, {
-        // convert to legacy format
-        questionnaireAnswers: questionnaireAnswers.map(answer => convertToLegacyFormat(answer, isAppSiteUrlHashed)),
-      })
-        .then(async() => {
-          await QuestionnaireAnswer.deleteMany();
-        });
-      axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive/batch`, {
-        // convert to legacy format
-        proactiveQuestionnaireAnswers: proactiveQuestionnaireAnswers.map(answer => convertToLegacyFormat(answer, isAppSiteUrlHashed)),
-      })
-        .then(async() => {
-          await ProactiveQuestionnaireAnswer.deleteMany();
-        });
-    };
-
-    const questionnaireOrders: IQuestionnaireOrder[] = await fetchQuestionnaireOrders();
-
-    resendQuestionnaireAnswers();
-
-    // reset QuestionnaireOrder collection and save unfinished ones that exist on questionnaire server
-    await QuestionnaireOrder.deleteMany();
-    await saveUnfinishedOrders(questionnaireOrders);
-
-    await changeSkippedAnswerStatusToNotAnswered();
-  }
-
-  private async sleepBeforeJob() {
-    const maxHoursUntilRequest = configManager.getConfig('app:questionnaireCronMaxHoursUntilRequest');
-    const maxSecondsUntilRequest = maxHoursUntilRequest * 60 * 60;
-
-    const secToSleep = getRandomIntInRange(0, maxSecondsUntilRequest);
-    await this.sleep(secToSleep * 1000);
-  }
-
-}
-
-const questionnaireCronService = new QuestionnaireCronService();
-
-export default questionnaireCronService;

+ 0 - 301
apps/app/src/features/questionnaire/server/service/questionnaire.integ.ts

@@ -1,301 +0,0 @@
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-import { mock } from 'vitest-mock-extended';
-
-import pkg from '^/package.json';
-
-
-import type UserEvent from '~/server/events/user';
-import { configManager } from '~/server/service/config-manager';
-
-import type Crowi from '../../../../server/crowi';
-import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import { UserType } from '../../interfaces/user-info';
-import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
-import QuestionnaireOrder from '../models/questionnaire-order';
-
-import QuestionnaireService from './questionnaire';
-
-
-describe('QuestionnaireService', () => {
-  const appVersion = pkg.version;
-
-  let questionnaireService: QuestionnaireService;
-
-  let User;
-  let user;
-
-  beforeAll(async() => {
-
-    await configManager.loadConfigs();
-
-    const crowiMock = mock<Crowi>({
-      version: appVersion,
-      event: vi.fn().mockImplementation((eventName) => {
-        if (eventName === 'user') {
-          return mock<UserEvent>({
-            on: vi.fn(),
-          });
-        }
-      }),
-    });
-    const userModelFactory = (await import('~/server/models/user')).default;
-    User = userModelFactory(crowiMock);
-
-    await User.deleteMany({}); // clear users
-    user = await User.create({
-      name: 'Example for Questionnaire Service Test',
-      username: 'questionnaire test user',
-      email: 'questionnaireTestUser@example.com',
-      password: 'usertestpass',
-      createdAt: '2000-01-01',
-    });
-
-    questionnaireService = new QuestionnaireService(crowiMock);
-  });
-
-  describe('getUserInfo', () => {
-    test('Should get correct user info when user given', () => {
-      const userInfo = questionnaireService.getUserInfo(user, 'growiurlhashfortest');
-      expect(userInfo).not.toBeNull();
-      assert(userInfo != null);
-
-      expect(userInfo.type).equal(UserType.general);
-      assert(userInfo.type === UserType.general);
-
-      expect(userInfo.userIdHash).toBeTruthy();
-      expect(userInfo.userIdHash).not.toBe(user._id);
-
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      delete (userInfo as any).userIdHash;
-
-      expect(userInfo).toEqual({ type: 'general', userCreatedAt: new Date('2000-01-01') });
-    });
-
-    test('Should get correct user info when user is null', () => {
-      const userInfo = questionnaireService.getUserInfo(null, '');
-      expect(userInfo).toEqual({ type: 'guest' });
-    });
-  });
-
-  describe('getQuestionnaireOrdersToShow', () => {
-    let doc1;
-    let doc2;
-    let doc3;
-    let doc4;
-    let doc5;
-    let doc6;
-    let doc7;
-    let doc8;
-    let doc9;
-    let doc10;
-    let doc11;
-    let doc12;
-
-    beforeAll(async() => {
-      const questionnaireToBeShown = {
-        shortTitle: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        title: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        showFrom: '2022-12-11',
-        showUntil: '2100-12-12',
-        condition: {
-          user: {
-            types: ['general'],
-            daysSinceCreation: {
-              moreThanOrEqualTo: 365,
-              lessThanOrEqualTo: 365 * 1000,
-            },
-          },
-          growi: {
-            types: ['on-premise'],
-            versionRegExps: [appVersion],
-          },
-        },
-        createdAt: '2023-01-01',
-        updatedAt: '2023-01-01',
-      };
-
-      // insert initial db data
-      doc1 = await QuestionnaireOrder.create(questionnaireToBeShown);
-      // insert finished data
-      doc2 = await QuestionnaireOrder.create({
-        ...questionnaireToBeShown,
-        showFrom: '2020-12-11',
-        showUntil: '2021-12-12',
-      });
-      // insert data for admin or guest
-      doc3 = await QuestionnaireOrder.create({
-        ...questionnaireToBeShown,
-        condition: {
-          user: {
-            types: ['admin', 'guest'],
-          },
-          growi: {
-            types: ['on-premise'],
-            versionRegExps: [appVersion],
-          },
-        },
-      });
-      // insert answered data
-      doc4 = await QuestionnaireOrder.create(questionnaireToBeShown);
-      // insert skipped data
-      doc5 = await QuestionnaireOrder.create(questionnaireToBeShown);
-      // insert denied data
-      doc6 = await QuestionnaireOrder.create(questionnaireToBeShown);
-      // insert data for different growi type
-      doc7 = await QuestionnaireOrder.create(
-        {
-          ...questionnaireToBeShown,
-          condition: {
-            user: {
-              types: ['general'],
-            },
-            growi: {
-              types: ['cloud'],
-              versionRegExps: [appVersion],
-            },
-          },
-        },
-      );
-      // insert data for different growi version
-      doc8 = await QuestionnaireOrder.create(
-        {
-          ...questionnaireToBeShown,
-          condition: {
-            user: {
-              types: ['general'],
-            },
-            growi: {
-              types: ['on-premise'],
-              versionRegExps: ['1.0.0-alpha'],
-            },
-          },
-        },
-      );
-      // insert data for users that used GROWI for less than or equal to a year
-      doc9 = await QuestionnaireOrder.create(
-        {
-          ...questionnaireToBeShown,
-          condition: {
-            user: {
-              types: ['general'],
-              daysSinceCreation: {
-                lessThanOrEqualTo: 365,
-              },
-            },
-            growi: {
-              types: ['on-premise'],
-              versionRegExps: [appVersion],
-            },
-          },
-        },
-      );
-      // insert data for users that used GROWI for more than or equal to 1000 years
-      doc10 = await QuestionnaireOrder.create(
-        {
-          ...questionnaireToBeShown,
-          condition: {
-            user: {
-              types: ['general'],
-              daysSinceCreation: {
-                moreThanOrEqualTo: 365 * 1000,
-              },
-            },
-            growi: {
-              types: ['on-premise'],
-              versionRegExps: [appVersion],
-            },
-          },
-        },
-      );
-      // insert data for users that used GROWI for more than a month and less than 6 months
-      doc11 = await QuestionnaireOrder.create(
-        {
-          ...questionnaireToBeShown,
-          condition: {
-            user: {
-              types: ['general'],
-              daysSinceCreation: {
-                moreThanOrEqualTo: 30,
-                lessThanOrEqualTo: 30 * 6,
-              },
-            },
-            growi: {
-              types: ['on-premise'],
-              versionRegExps: [appVersion],
-            },
-          },
-        },
-      );
-
-      await QuestionnaireAnswerStatus.insertMany([
-        {
-          user: user._id,
-          questionnaireOrderId: doc4._id,
-          status: StatusType.answered,
-        },
-        {
-          user: user._id,
-          questionnaireOrderId: doc5._id,
-          status: StatusType.skipped,
-        },
-        {
-          user: user._id,
-          questionnaireOrderId: doc6._id,
-          status: StatusType.skipped,
-        },
-      ]);
-    });
-
-    test('Should get questionnaire orders to show', async() => {
-      const growiInfo = mock<IGrowiInfo<IGrowiAppAdditionalInfo>>({
-        type: 'on-premise',
-        version: appVersion,
-      });
-      const userInfo = questionnaireService.getUserInfo(user, 'appSiteUrlHashed');
-
-      const questionnaireOrderDocuments = await questionnaireService.getQuestionnaireOrdersToShow(userInfo, growiInfo, user._id);
-
-      expect(questionnaireOrderDocuments[0].toObject()).toMatchObject(
-        {
-          __v: 0,
-          shortTitle: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          title: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          showFrom: new Date('2022-12-11'),
-          showUntil: new Date('2100-12-12'),
-          questions: [],
-          condition: {
-            user: {
-              types: ['general'],
-              daysSinceCreation: {
-                moreThanOrEqualTo: 365,
-                lessThanOrEqualTo: 365 * 1000,
-              },
-            },
-            growi: {
-              types: ['on-premise'],
-              versionRegExps: [appVersion],
-            },
-          },
-          createdAt: new Date('2023-01-01'),
-          updatedAt: new Date('2023-01-01'),
-        },
-      );
-
-    });
-
-  });
-
-});

+ 0 - 74
apps/app/src/features/questionnaire/server/service/questionnaire.ts

@@ -1,74 +0,0 @@
-import crypto from 'crypto';
-
-import type { IUserHasId } from '@growi/core';
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-
-import type Crowi from '~/server/crowi';
-import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
-import loggerFactory from '~/utils/logger';
-
-import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import { type IUserInfo, UserType } from '../../interfaces/user-info';
-import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
-import type { QuestionnaireOrderDocument } from '../models/questionnaire-order';
-import QuestionnaireOrder from '../models/questionnaire-order';
-import { isShowableCondition } from '../util/condition';
-
-
-const logger = loggerFactory('growi:service:questionnaire');
-
-class QuestionnaireService {
-
-  crowi: Crowi;
-
-  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-  constructor(crowi: Crowi) {
-    this.crowi = crowi;
-  }
-
-  getUserInfo(user: IUserHasId | null, appSiteUrlHashed: string): IUserInfo {
-    if (user != null) {
-      const hasher = crypto.createHmac('sha256', appSiteUrlHashed);
-      hasher.update(user._id.toString());
-
-      return {
-        userIdHash: hasher.digest('hex'),
-        type: user.admin ? UserType.admin : UserType.general,
-        userCreatedAt: user.createdAt,
-      };
-    }
-
-    return { type: UserType.guest };
-  }
-
-  async getQuestionnaireOrdersToShow(
-      userInfo: IUserInfo, growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>, userId: ObjectIdLike | null,
-  ): Promise<QuestionnaireOrderDocument[]> {
-    const currentDate = new Date();
-
-    let questionnaireOrders = await QuestionnaireOrder.find({
-      showUntil: {
-        $gte: currentDate,
-      },
-    });
-
-    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?.status === StatusType.not_answered;
-      });
-    }
-
-    return questionnaireOrders
-      .filter((order) => {
-        return isShowableCondition(order, userInfo, growiInfo);
-      });
-  }
-
-}
-
-export default QuestionnaireService;

+ 0 - 70
apps/app/src/features/questionnaire/server/util/condition.ts

@@ -1,70 +0,0 @@
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-
-import type { ICondition } from '../../interfaces/condition';
-import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
-import type { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
-import type { IUserInfo } from '../../interfaces/user-info';
-import { UserType } from '../../interfaces/user-info';
-
-
-const checkUserInfo = (condition: ICondition, userInfo: IUserInfo): boolean => {
-  const { user: { types, daysSinceCreation } } = condition;
-
-  if (!types.includes(userInfo.type)) {
-    return false;
-  }
-
-  // Check if "time passed since user creation" is between specified range
-  if (userInfo.type !== UserType.guest) {
-    const createdAt = userInfo.userCreatedAt;
-    const moreThanOrEqualTo = daysSinceCreation?.moreThanOrEqualTo;
-    const lessThanOrEqualTo = daysSinceCreation?.lessThanOrEqualTo;
-    const currentDate = new Date();
-
-    const isValidLeftThreshold = (() => {
-      if (moreThanOrEqualTo == null) {
-        return true;
-      }
-      const leftThreshold = new Date(createdAt.getTime() + 60 * 1000 * 60 * 24 * moreThanOrEqualTo);
-      return leftThreshold <= currentDate;
-    })();
-    const isValidRightThreshold = (() => {
-      if (lessThanOrEqualTo == null) {
-        return true;
-      }
-      const rightThreshold = new Date(createdAt.getTime() + 60 * 1000 * 60 * 24 * lessThanOrEqualTo);
-      return currentDate <= rightThreshold;
-    })();
-
-    return isValidLeftThreshold && isValidRightThreshold;
-  }
-
-  return true;
-};
-
-const checkGrowiInfo = (condition: ICondition, growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>): boolean => {
-  const { growi: { types, versionRegExps } } = condition;
-
-  if (!types.includes(growiInfo.type)) {
-    return false;
-  }
-
-  if (!versionRegExps.some(rs => new RegExp(rs).test(growiInfo.version))) {
-    return false;
-  }
-
-  return true;
-};
-
-export const isShowableCondition = (order: IQuestionnaireOrder, userInfo: IUserInfo, growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>): boolean => {
-  const { condition } = order;
-
-  if (!checkUserInfo(condition, userInfo)) {
-    return false;
-  }
-  if (!checkGrowiInfo(condition, growiInfo)) {
-    return false;
-  }
-
-  return true;
-};

+ 0 - 128
apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.spec.ts

@@ -1,128 +0,0 @@
-import { GrowiDeploymentType, GrowiServiceType } from '@growi/core/dist/consts';
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-import { GrowiWikiType } from '@growi/core/dist/interfaces';
-import {
-  describe, test, expect,
-} from 'vitest';
-import { mock } from 'vitest-mock-extended';
-
-import { AttachmentMethodType } from '../../../../interfaces/attachment';
-import type { IGrowiAppAdditionalInfo, IGrowiAppInfoLegacy } from '../../interfaces/growi-app-info';
-
-import { convertToLegacyFormat } from './convert-to-legacy-format';
-
-describe('convertToLegacyFormat', () => {
-  test('should return same object when input is already in legacy format', () => {
-    const growiInfoLegacy: IGrowiAppInfoLegacy = {
-      version: '1.0.0',
-      appSiteUrl: 'https://example.com',
-      appSiteUrlHashed: '100680ad546ce6a577f42f52df33b4cfdca756859e664b8d7de329b150d09ce9',
-      serviceInstanceId: 'service-instance-id',
-      type: GrowiServiceType.cloud,
-      wikiType: GrowiWikiType.open,
-      deploymentType: GrowiDeploymentType.others,
-      osInfo: {
-        type: 'Linux',
-        platform: 'linux',
-        arch: 'x64',
-        totalmem: 8589934592,
-      },
-
-      // legacy properties
-      installedAt: new Date(),
-      installedAtByOldestUser: new Date(),
-      currentUsersCount: 1,
-      currentActiveUsersCount: 1,
-      attachmentType: AttachmentMethodType.local,
-    };
-
-    const legacyData = {
-      someData: 'test',
-      growiInfo: growiInfoLegacy,
-    };
-
-    const result = convertToLegacyFormat(legacyData);
-    expect(result).toStrictEqual(legacyData);
-  });
-
-  test('should convert new format to legacy format', () => {
-    const installedAt = new Date();
-    const installedAtByOldestUser = new Date();
-
-    const growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo> = {
-      version: '1.0.0',
-      appSiteUrl: 'https://example.com',
-      serviceInstanceId: 'service-instance-id',
-      type: GrowiServiceType.cloud,
-      wikiType: GrowiWikiType.open,
-      deploymentType: GrowiDeploymentType.others,
-      osInfo: {
-        type: 'Linux',
-        platform: 'linux',
-        arch: 'x64',
-        totalmem: 8589934592,
-      },
-      additionalInfo: {
-        installedAt,
-        installedAtByOldestUser,
-        currentUsersCount: 1,
-        currentActiveUsersCount: 1,
-        attachmentType: AttachmentMethodType.local,
-      },
-    };
-    const newFormatData = {
-      someData: 'test',
-      growiInfo,
-    };
-
-    const growiInfoLegacy: IGrowiAppInfoLegacy = {
-      version: '1.0.0',
-      appSiteUrl: 'https://example.com',
-      appSiteUrlHashed: '100680ad546ce6a577f42f52df33b4cfdca756859e664b8d7de329b150d09ce9',
-      serviceInstanceId: 'service-instance-id',
-      type: GrowiServiceType.cloud,
-      wikiType: GrowiWikiType.open,
-      deploymentType: GrowiDeploymentType.others,
-      osInfo: {
-        type: 'Linux',
-        platform: 'linux',
-        arch: 'x64',
-        totalmem: 8589934592,
-      },
-
-      // legacy properties
-      installedAt,
-      installedAtByOldestUser,
-      currentUsersCount: 1,
-      currentActiveUsersCount: 1,
-      attachmentType: AttachmentMethodType.local,
-    };
-    const expected = {
-      someData: 'test',
-      growiInfo: growiInfoLegacy,
-    };
-
-    const result = convertToLegacyFormat(newFormatData);
-    expect(result).toStrictEqual(expected);
-  });
-
-  test('should convert new format and omit appSiteUrl', () => {
-    // arrange
-    const growiInfo = mock<IGrowiInfo<IGrowiAppAdditionalInfo>>({
-      appSiteUrl: 'https://example.com',
-      additionalInfo: {
-        installedAt: new Date(),
-        installedAtByOldestUser: new Date(),
-        currentUsersCount: 1,
-        currentActiveUsersCount: 1,
-        attachmentType: AttachmentMethodType.local,
-      },
-    });
-
-    // act
-    const result = convertToLegacyFormat({ growiInfo }, true);
-
-    // assert
-    expect(result.growiInfo.appSiteUrl).toBeUndefined();
-  });
-});

+ 0 - 40
apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.ts

@@ -1,40 +0,0 @@
-import assert from 'assert';
-import crypto from 'crypto';
-
-import type { IGrowiAppInfoLegacy } from '../../interfaces/growi-app-info';
-
-
-type IHasGrowiAppInfoLegacy<T> = T & {
-  growiInfo: IGrowiAppInfoLegacy;
-};
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-function isLegacy<T extends { growiInfo: any }>(data: T): data is IHasGrowiAppInfoLegacy<T> {
-  return !('additionalInfo' in data.growiInfo);
-}
-
-export function getSiteUrlHashed(siteUrl: string): string {
-  const hasher = crypto.createHash('sha256');
-  hasher.update(siteUrl);
-  return hasher.digest('hex');
-}
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export function convertToLegacyFormat<T extends { growiInfo: any }>(questionnaireAnswer: T, isAppSiteUrlHashed = false): IHasGrowiAppInfoLegacy<T> {
-  if (isLegacy(questionnaireAnswer)) {
-    return questionnaireAnswer;
-  }
-
-  const { additionalInfo, appSiteUrl, ...rest } = questionnaireAnswer.growiInfo;
-  assert(additionalInfo != null);
-
-  return {
-    ...questionnaireAnswer,
-    growiInfo: {
-      appSiteUrl: isAppSiteUrlHashed ? undefined : appSiteUrl,
-      appSiteUrlHashed: getSiteUrlHashed(appSiteUrl),
-      ...rest,
-      ...additionalInfo,
-    },
-  };
-}

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

@@ -78,7 +78,6 @@ const ACTION_ADMIN_MAIL_SMTP_UPDATE = 'ADMIN_MAIL_SMTP_UPDATE';
 const ACTION_ADMIN_MAIL_SES_UPDATE = 'ADMIN_MAIL_SES_UPDATE';
 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_QUESTIONNAIRE_SETTINGS_UPDATE = 'ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE';
 const ACTION_ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE = 'ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE';
 const ACTION_ADMIN_MAINTENANCEMODE_ENABLED = 'ADMIN_MAINTENANCEMODE_ENABLED';
 const ACTION_ADMIN_MAINTENANCEMODE_DISABLED = 'ADMIN_MAINTENANCEMODE_DISABLED';
@@ -259,7 +258,6 @@ export const SupportedAction = {
   ACTION_ADMIN_MAIL_SES_UPDATE,
   ACTION_ADMIN_MAIL_TEST_SUBMIT,
   ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE,
-  ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE,
   ACTION_ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE,
   ACTION_ADMIN_MAINTENANCEMODE_ENABLED,
   ACTION_ADMIN_MAINTENANCEMODE_DISABLED,
@@ -454,7 +452,6 @@ export const LargeActionGroup = {
   ACTION_ADMIN_MAIL_SES_UPDATE,
   ACTION_ADMIN_MAIL_TEST_SUBMIT,
   ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE,
-  ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE,
   ACTION_ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE,
   ACTION_ADMIN_MAINTENANCEMODE_ENABLED,
   ACTION_ADMIN_MAINTENANCEMODE_DISABLED,

+ 0 - 1
apps/app/src/interfaces/res/admin/app-settings.ts

@@ -55,7 +55,6 @@ export type IResAppSettings = {
 
   isEnabledPlugins: boolean,
 
-  isQuestionnaireEnabled: boolean,
   isAppSiteUrlHashed: boolean,
 
   isMaintenanceMode: boolean,

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

@@ -91,7 +91,6 @@ const TemplateModal = dynamic(() => import('~/client/components/TemplateModal').
 const LinkEditModal = dynamic(() => import('~/client/components/PageEditor/LinkEditModal').then(mod => mod.LinkEditModal), { ssr: false });
 const TagEditModal = dynamic(() => import('~/client/components/PageTags/TagEditModal').then(mod => mod.TagEditModal), { ssr: false });
 const ConflictDiffModal = dynamic(() => import('~/client/components/PageEditor/ConflictDiffModal').then(mod => mod.ConflictDiffModal), { ssr: false });
-const QuestionnaireModalManager = dynamic(() => import('~/features/questionnaire/client/components/QuestionnaireModalManager'), { ssr: false });
 
 const EditablePageEffects = dynamic(() => import('~/client/components/Page/EditablePageEffects').then(mod => mod.EditablePageEffects), { ssr: false });
 
@@ -426,7 +425,6 @@ Page.getLayout = function getLayout(page: React.ReactElement<Props>) {
       <DescendantsPageListModal />
       <DrawioModal />
       <HandsontableModal />
-      <QuestionnaireModalManager />
       <TemplateModal />
       <LinkEditModal />
       <TagEditModal />

+ 0 - 13
apps/app/src/server/crowi/index.js

@@ -17,8 +17,6 @@ import instanciatePageBulkExportJobCleanUpCronService, {
   pageBulkExportJobCleanUpCronService,
 } from '~/features/page-bulk-export/server/service/page-bulk-export-job-clean-up-cron';
 import instanciatePageBulkExportJobCronService from '~/features/page-bulk-export/server/service/page-bulk-export-job-cron';
-import QuestionnaireService from '~/features/questionnaire/server/service/questionnaire';
-import questionnaireCronService from '~/features/questionnaire/server/service/questionnaire-cron';
 import { getGrowiVersion } from '~/utils/growi-version';
 import loggerFactory from '~/utils/logger';
 import { projectRoot } from '~/utils/project-dir-utils';
@@ -95,9 +93,6 @@ class Crowi {
   /** @type {PassportService} */
   passportService;
 
-  /** @type {QuestionnaireService} */
-  questionnaireService;
-
   /** @type {import('../service/rest-qiita-API')} */
   restQiitaAPIService;
 
@@ -147,7 +142,6 @@ class Crowi {
     this.inAppNotificationService = null;
     this.activityService = null;
     this.commentService = null;
-    this.questionnaireService = null;
     this.openaiThreadDeletionCronService = null;
     this.openaiVectorStoreFileDeletionCronService = null;
 
@@ -212,7 +206,6 @@ Crowi.prototype.init = async function() {
     this.setupActivityService(),
     this.setupCommentService(),
     this.setupSyncPageStatusService(),
-    this.setupQuestionnaireService(),
     this.setUpCustomize(), // depends on pluginService
   ]);
 
@@ -362,8 +355,6 @@ Crowi.prototype.setupSocketIoService = async function() {
 };
 
 Crowi.prototype.setupCron = function() {
-  questionnaireCronService.startCron();
-
   instanciatePageBulkExportJobCronService(this);
   checkPageBulkExportJobInProgressCronService.startCron();
 
@@ -373,10 +364,6 @@ Crowi.prototype.setupCron = function() {
   startOpenaiCronIfEnabled();
 };
 
-Crowi.prototype.setupQuestionnaireService = function() {
-  this.questionnaireService = new QuestionnaireService(this);
-};
-
 Crowi.prototype.getSlack = function() {
   return this.slack;
 };

+ 0 - 4
apps/app/src/server/crowi/setup-models.ts

@@ -44,10 +44,6 @@ export const setupIndependentModels = async(): Promise<void> => {
     import('~/features/external-user-group/server/models/external-user-group-relation'),
     import('~/features/external-user-group/server/models/external-user-group'),
     import('~/features/growi-plugin/server/models'),
-    import('~/features/questionnaire/server/models/proactive-questionnaire-answer'),
-    import('~/features/questionnaire/server/models/questionnaire-answer-status'),
-    import('~/features/questionnaire/server/models/questionnaire-answer'),
-    import('~/features/questionnaire/server/models/questionnaire-order'),
     import('../models/activity'),
     import('../models/attachment'),
     import('../models/bookmark-folder'),

+ 0 - 6
apps/app/src/server/models/user.js

@@ -81,7 +81,6 @@ const factory = (crowi) => {
     admin: { type: Boolean, default: 0, index: true },
     readOnly: { type: Boolean, default: 0 },
     isInvitationEmailSended: { type: Boolean, default: false },
-    isQuestionnaireEnabled: { type: Boolean, default: true },
   }, {
     timestamps: true,
     toObject: {
@@ -763,11 +762,6 @@ const factory = (crowi) => {
     return { users, totalCount };
   };
 
-  userSchema.methods.updateIsQuestionnaireEnabled = async function(value) {
-    this.isQuestionnaireEnabled = value;
-    return this.save();
-  };
-
   class UserUpperLimitException {
 
     constructor() {

+ 0 - 3
apps/app/src/server/routes/apiv3/activity.ts

@@ -107,9 +107,6 @@ const validator = {
  *                       isInvitationEmailSended:
  *                         type: boolean
  *                         example: false
- *                       isQuestionnaireEnabled:
- *                         type: boolean
- *                         example: true
  *                       name:
  *                         type: string
  *                         example: "Taro"

+ 0 - 1
apps/app/src/server/routes/apiv3/admin-home.ts

@@ -41,7 +41,6 @@ const router = express.Router();
  *              "ELASTICSEARCH_REQUEST_TIMEOUT": 15000
  *              "ELASTICSEARCH_REJECT_UNAUTHORIZED": true
  *              "OGP_URI": "http://ogp:8088"
- *              "QUESTIONNAIRE_SERVER_ORIGIN": "http://host.docker.internal:3003"
  *          isV5Compatible:
  *            type: boolean
  *            description: This value is true if this GROWI is compatible v5.

+ 0 - 78
apps/app/src/server/routes/apiv3/app-settings.js

@@ -106,9 +106,6 @@ const router = express.Router();
  *          isMaintenanceMode:
  *            type: boolean
  *            example: false
- *          isQuestionnaireEnabled:
- *            type: boolean
- *            example: true
  *          isV5Compatible:
  *            type: boolean
  *            example: true
@@ -317,17 +314,6 @@ const router = express.Router();
  *          azureReferenceFileWithRelayMode:
  *            type: boolean
  *            description: is enable internal stream system for azure file request
- *      QuestionnaireSettingParams:
- *        description: QuestionnaireSettingParams
- *        type: object
- *        properties:
- *          isQuestionnaireEnabled:
- *            type: boolean
- *            description: is questionnaire enabled, or not
- *            example: true
- *          isAppSiteUrlHashed:
- *            type: boolean
- *            description: is app site url hashed, or not
  */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
@@ -401,10 +387,6 @@ module.exports = (crowi) => {
       body('azureReferenceFileWithRelayMode').if(value => value != null).isBoolean(),
 
     ],
-    questionnaireSettings: [
-      body('isQuestionnaireEnabled').isBoolean(),
-      body('isAppSiteUrlHashed').isBoolean(),
-    ],
     pageBulkExportSettings: [
       body('isBulkExportPagesEnabled').isBoolean(),
       body('bulkExportDownloadExpirationSeconds').isInt(),
@@ -495,9 +477,6 @@ module.exports = (crowi) => {
 
       isEnabledPlugins: configManager.getConfig('plugin:isEnabledPlugins'),
 
-      isQuestionnaireEnabled: configManager.getConfig('questionnaire:isQuestionnaireEnabled'),
-      isAppSiteUrlHashed: configManager.getConfig('questionnaire:isAppSiteUrlHashed'),
-
       isMaintenanceMode: configManager.getConfig('app:isMaintenanceMode'),
 
       isBulkExportPagesEnabled: configManager.getConfig('app:isBulkExportPagesEnabled'),
@@ -1027,63 +1006,6 @@ module.exports = (crowi) => {
 
   });
 
-  /**
-   * @swagger
-   *
-   *    /app-settings/questionnaire-settings:
-   *      put:
-   *        tags: [AppSettings]
-   *        security:
-   *          - cookieAuth: []
-   *        summary: /app-settings/questionnaire-settings
-   *        description: Update QuestionnaireSetting
-   *        requestBody:
-   *          required: true
-   *          content:
-   *            application/json:
-   *              schema:
-   *                $ref: '#/components/schemas/QuestionnaireSettingParams'
-   *        responses:
-   *          200:
-   *            description: Succeeded to update QuestionnaireSetting
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  type: object
-   *                  properties:
-   *                    responseParams:
-   *                      type: object
-   *                      $ref: '#/components/schemas/QuestionnaireSettingParams'
-   */
-  // eslint-disable-next-line max-len
-  router.put('/questionnaire-settings', loginRequiredStrictly, adminRequired, addActivity, validator.questionnaireSettings, apiV3FormValidator, async(req, res) => {
-    const { isQuestionnaireEnabled, isAppSiteUrlHashed } = req.body;
-
-    const requestParams = {
-      'questionnaire:isQuestionnaireEnabled': isQuestionnaireEnabled,
-      'questionnaire:isAppSiteUrlHashed': isAppSiteUrlHashed,
-    };
-
-    try {
-      await configManager.updateConfigs(requestParams, { skipPubsub: true });
-
-      const responseParams = {
-        isQuestionnaireEnabled: configManager.getConfig('questionnaire:isQuestionnaireEnabled'),
-        isAppSiteUrlHashed: configManager.getConfig('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.put('/page-bulk-export-settings', loginRequiredStrictly, adminRequired, addActivity, validator.pageBulkExportSettings, apiV3FormValidator,
     async(req, res) => {
       const requestParams = {

+ 0 - 1
apps/app/src/server/routes/apiv3/index.js

@@ -123,7 +123,6 @@ module.exports = (crowi, app) => {
   router.use('/user-ui-settings', require('./user-ui-settings')(crowi));
 
   router.use('/bookmark-folder', require('./bookmark-folder')(crowi));
-  router.use('/questionnaire', require('~/features/questionnaire/server/routes/apiv3/questionnaire')(crowi));
   router.use('/templates', require('~/features/templates/server/routes/apiv3')(crowi));
   router.use('/page-bulk-export', require('~/features/page-bulk-export/server/routes/apiv3/page-bulk-export')(crowi));
 

+ 0 - 46
apps/app/src/server/routes/apiv3/personal-setting.js

@@ -123,9 +123,6 @@ module.exports = (crowi) => {
       body('defaultSubscribeRules.*.name').isString(),
       body('defaultSubscribeRules.*.isEnabled').optional().isBoolean(),
     ],
-    questionnaireSettings: [
-      body('isQuestionnaireEnabled').isBoolean(),
-    ],
   };
 
   /**
@@ -713,48 +710,5 @@ module.exports = (crowi) => {
     }
   });
 
-  /**
-   * @swagger
-   *   /personal-setting/questionnaire-settings:
-   *     put:
-   *       tags: [QuestionnaireSetting]
-   *       summary: /personal-setting/questionnaire-settings
-   *       description: Update the questionnaire settings for the current user
-   *       requestBody:
-   *         required: true
-   *         content:
-   *           application/json:
-   *             schema:
-   *               properties:
-   *                 isQuestionnaireEnabled:
-   *                   type: boolean
-   *       responses:
-   *         200:
-   *           description: Successfully updated questionnaire settings
-   *           content:
-   *             application/json:
-   *               schema:
-   *                 properties:
-   *                   message:
-   *                     type: string
-   *                   isQuestionnaireEnabled:
-   *                     type: boolean
-   */
-  // eslint-disable-next-line max-len
-  router.put('/questionnaire-settings', accessTokenParser, loginRequiredStrictly, validator.questionnaireSettings, apiV3FormValidator, async(req, res) => {
-    const { isQuestionnaireEnabled } = req.body;
-    const { user } = req;
-    try {
-      await user.updateIsQuestionnaireEnabled(isQuestionnaireEnabled);
-
-      return res.apiv3({ message: 'Successfully updated questionnaire settings.', isQuestionnaireEnabled });
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err({ error: 'Failed to update questionnaire settings.' });
-    }
-  });
-
-
   return router;
 };

+ 0 - 4
apps/app/src/server/routes/apiv3/users.js

@@ -89,10 +89,6 @@ const validator = {};
  *            type: boolean
  *            description: whether the invitation email is sent
  *            example: false
- *          isQuestionnaireEnabled:
- *            type: boolean
- *            description: whether the questionnaire is enabled
- *            example: false
  *          lastLoginAt:
  *            type: string
  *            description: datetime last login at

+ 0 - 29
apps/app/src/server/service/config-manager/config-definition.ts

@@ -68,9 +68,6 @@ export const CONFIG_KEYS = [
   'app:auditLogActionGroupSize',
   'app:auditLogAdditionalActions',
   'app:auditLogExcludeActions',
-  'app:questionnaireServerOrigin',
-  'app:questionnaireCronSchedule',
-  'app:questionnaireCronMaxHoursUntilRequest',
   'app:serviceType',
   'app:deploymentType',
   'app:ssrMaxRevisionBodyLength',
@@ -282,10 +279,6 @@ export const CONFIG_KEYS = [
   's2cMessagingPubsub:connectionsLimitForAdmin',
   's2cMessagingPubsub:connectionsLimitForGuest',
 
-  // Questionnaire Settings
-  'questionnaire:isQuestionnaireEnabled',
-  'questionnaire:isAppSiteUrlHashed',
-
   // Notification Settings
   'notification:owner-page:isEnabled',
   'notification:group-page:isEnabled',
@@ -502,18 +495,6 @@ export const CONFIG_DEFINITIONS = {
     envVarName: 'AUDIT_LOG_EXCLUDE_ACTIONS',
     defaultValue: undefined,
   }),
-  'app:questionnaireServerOrigin': defineConfig<string>({
-    envVarName: 'QUESTIONNAIRE_SERVER_ORIGIN',
-    defaultValue: 'https://q.growi.org',
-  }),
-  'app:questionnaireCronSchedule': defineConfig<string>({
-    envVarName: 'QUESTIONNAIRE_CRON_SCHEDULE',
-    defaultValue: '0 22 * * *',
-  }),
-  'app:questionnaireCronMaxHoursUntilRequest': defineConfig<number>({
-    envVarName: 'QUESTIONNAIRE_CRON_MAX_HOURS_UNTIL_REQUEST',
-    defaultValue: 4,
-  }),
   'app:serviceType': defineConfig<GrowiServiceType>({
     envVarName: 'SERVICE_TYPE',
     defaultValue: GrowiServiceType.onPremise,
@@ -1175,16 +1156,6 @@ export const CONFIG_DEFINITIONS = {
     defaultValue: 2000,
   }),
 
-  // Questionnaire Settings
-  'questionnaire:isQuestionnaireEnabled': defineConfig<boolean>({
-    envVarName: 'QUESTIONNAIRE_IS_ENABLE_QUESTIONNAIRE',
-    defaultValue: true,
-  }),
-  'questionnaire:isAppSiteUrlHashed': defineConfig<boolean>({
-    envVarName: 'QUESTIONNAIRE_IS_APP_SITE_URL_HASHED',
-    defaultValue: false,
-  }),
-
   // Notification Settings
   'notification:owner-page:isEnabled': defineConfig<boolean>({
     defaultValue: false,

+ 1 - 1
apps/app/src/server/service/import/construct-convert-map.integ.ts

@@ -30,6 +30,6 @@ describe('constructConvertMap', () => {
 
     // assert
     expect(result).not.toBeNull();
-    expect(Object.keys(result).length).toEqual(36);
+    expect(Object.keys(result).length).toEqual(32);
   });
 });

+ 0 - 1
packages/core/src/interfaces/user.ts

@@ -23,7 +23,6 @@ export type IUser = {
   lastLoginAt?: Date;
   introduction: string;
   status: IUserStatus;
-  isQuestionnaireEnabled: boolean;
 };
 
 export type IUserGroupRelation = {