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

Merge branch 'master' into feat/rich-attachment

ryoji-s 2 лет назад
Родитель
Сommit
7d3d1f66ec
73 измененных файлов с 1576 добавлено и 1230 удалено
  1. 3 3
      .devcontainer/docker-compose.yml
  2. 2 1
      .github/workflows/release-slackbot-proxy.yml
  3. 7 5
      .github/workflows/release.yml
  4. 2 2
      .github/workflows/reusable-app-build-image.yml
  5. 58 1
      CHANGELOG.md
  6. 13 1
      THIRD-PARTY-NOTICES.md
  7. 1 0
      apps/app/docker/Dockerfile
  8. 6 0
      apps/app/docker/codebuild/buildspec.yml
  9. 1 1
      apps/app/docker/codebuild/codebuild.tf
  10. 12 12
      apps/app/package.json
  11. 3 0
      apps/app/resource/fonts/SourceHanCodeJP-Regular-subset-jis2.woff2
  12. 3 0
      apps/app/resource/fonts/SourceHanCodeJP-Regular-subset-main.woff2
  13. 0 3
      apps/app/resource/fonts/SourceHanCodeJP-Regular.woff2
  14. 65 132
      apps/app/resource/locales/en_US/sandbox-diagrams.md
  15. 65 133
      apps/app/resource/locales/ja_JP/sandbox-diagrams.md
  16. 65 132
      apps/app/resource/locales/zh_CN/sandbox-diagrams.md
  17. 8 8
      apps/app/src/client/services/AdminLocalSecurityContainer.js
  18. 10 10
      apps/app/src/client/services/AdminMarkDownContainer.js
  19. 1 1
      apps/app/src/components/Admin/AdminHome/AdminHome.jsx
  20. 6 10
      apps/app/src/components/Admin/AdminHome/EnvVarsTable.tsx
  21. 17 17
      apps/app/src/components/Admin/MarkdownSetting/WhitelistInput.jsx
  22. 2 2
      apps/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  23. 3 3
      apps/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx
  24. 7 9
      apps/app/src/components/DuplicatedPathsTable.tsx
  25. 1 1
      apps/app/src/components/Layout/SearchResultLayout.module.scss
  26. 5 5
      apps/app/src/components/LoginForm.tsx
  27. 4 4
      apps/app/src/components/Me/BasicInfoSettings.tsx
  28. 21 13
      apps/app/src/components/PageEditor/CodeMirrorEditor.jsx
  29. 1 0
      apps/app/src/components/PageEditor/CodeMirrorEditor.module.scss
  30. 0 471
      apps/app/src/components/PageEditor/LinkEditModal.jsx
  31. 372 0
      apps/app/src/components/PageEditor/LinkEditModal.tsx
  32. 4 2
      apps/app/src/pages/[[...path]].page.tsx
  33. 9 2
      apps/app/src/pages/_app.page.tsx
  34. 2 2
      apps/app/src/pages/_private-legacy-pages.page.tsx
  35. 2 2
      apps/app/src/pages/_search.page.tsx
  36. 3 3
      apps/app/src/pages/login/index.page.tsx
  37. 6 6
      apps/app/src/pages/me/[[...path]].page.tsx
  38. 2 2
      apps/app/src/pages/share/[[...path]].page.tsx
  39. 2 2
      apps/app/src/server/models/activity.ts
  40. 1 0
      apps/app/src/server/models/bookmark-folder.ts
  41. 3 3
      apps/app/src/server/models/config.ts
  42. 1 1
      apps/app/src/server/models/user.js
  43. 13 13
      apps/app/src/server/routes/apiv3/markdown-setting.js
  44. 6 6
      apps/app/src/server/routes/apiv3/security-setting.js
  45. 2 2
      apps/app/src/server/routes/page.js
  46. 1 1
      apps/app/src/server/service/config-loader.ts
  47. 3 3
      apps/app/src/server/service/in-app-notification.ts
  48. 7 7
      apps/app/src/server/service/page.ts
  49. 1 0
      apps/app/src/server/service/search.ts
  50. 6 6
      apps/app/src/server/service/xss.js
  51. 2 1
      apps/app/src/server/util/mongoose-utils.ts
  52. 2 2
      apps/app/src/services/renderer/renderer.tsx
  53. 6 6
      apps/app/src/services/xss/index.js
  54. 9 8
      apps/app/src/services/xss/xssOption.ts
  55. 2 2
      apps/app/src/stores/context.tsx
  56. 29 0
      apps/app/src/stores/modal.tsx
  57. 0 137
      apps/app/src/styles/_search.scss
  58. 5 1
      apps/app/src/styles/organisms/_wiki.scss
  59. 0 1
      apps/app/src/styles/style-app.scss
  60. 1 1
      apps/app/test/integration/service/questionnaire-cron.test.ts
  61. 2 2
      apps/slackbot-proxy/package.json
  62. 1 1
      package.json
  63. 1 1
      packages/core/package.json
  64. 1 1
      packages/hackmd/package.json
  65. 2 2
      packages/presentation/package.json
  66. 1 1
      packages/preset-themes/package.json
  67. 6 11
      packages/remark-attachment-refs/package.json
  68. 1 1
      packages/remark-drawio/package.json
  69. 1 1
      packages/remark-growi-directive/package.json
  70. 4 4
      packages/remark-lsx/package.json
  71. 1 1
      packages/slack/package.json
  72. 2 2
      packages/ui/package.json
  73. 659 0
      yarn.lock

+ 3 - 3
.devcontainer/docker-compose.yml

@@ -28,7 +28,7 @@ services:
     tty: true
 
   mongo:
-    image: mongo:4.4
+    image: mongo:6.0
     restart: unless-stopped
     ports:
       - 27017:27017
@@ -46,7 +46,7 @@ services:
   #   cloned from https://github.com/weseek/growi-docker-compose.git
   elasticsearch:
     build:
-      context: ../../growi-docker-compose/elasticsearch
+      context: ../../growi-docker-compose/elasticsearch/v8
       dockerfile: ./Dockerfile
       args:
         - version=8.7.0
@@ -63,7 +63,7 @@ services:
         hard: -1
     volumes:
       - /usr/share/elasticsearch/data
-      - ../../growi-docker-compose/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
+      - ../../growi-docker-compose/elasticsearch/v8/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
 
   #need to adjust kibana version based on elasticsearch version (use same version as elasticsearch version)
   kibana:

+ 2 - 1
.github/workflows/release-slackbot-proxy.yml

@@ -108,7 +108,8 @@ jobs:
 
     - name: Install dependencies
       run: |
-        npx lerna bootstrap
+        yarn global add turbo
+        yarn --frozen-lockfile
 
     - name: Bump versions for next RC
       run: |

+ 7 - 5
.github/workflows/release.yml

@@ -24,13 +24,14 @@ jobs:
 
     - uses: actions/setup-node@v3
       with:
-        node-version: '16'
+        node-version: '18'
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
 
     - name: Install dependencies
       run: |
-        npx lerna bootstrap
+        yarn global add turbo
+        yarn --frozen-lockfile
 
     - name: Bump versions
       run: |
@@ -68,7 +69,7 @@ jobs:
         token: ${{ secrets.GITHUB_TOKEN }}
 
     - name: Delete drafts
-      uses: hugo19941994/delete-draft-releases@v1.0.0
+      uses: hugo19941994/delete-draft-releases@v1.0.1
       env:
         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
@@ -85,13 +86,14 @@ jobs:
 
     - uses: actions/setup-node@v3
       with:
-        node-version: '16'
+        node-version: '18'
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
 
     - name: Install dependencies
       run: |
-        npx lerna bootstrap
+        yarn global add turbo
+        yarn --frozen-lockfile
 
     - name: Bump versions for next RC
       run: |

+ 2 - 2
.github/workflows/reusable-app-build-image.yml

@@ -36,7 +36,7 @@ jobs:
     - uses: actions/checkout@v3
 
     - name: Configure AWS Credentials
-      uses: aws-actions/configure-aws-credentials@v1
+      uses: aws-actions/configure-aws-credentials@v2
       with:
         aws-region: ap-northeast-1
         role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME_FOR_OIDC }}
@@ -48,7 +48,7 @@ jobs:
         projectName: growi-official-image-builder
       env:
         CODEBUILD__sourceVersion: ${{ inputs.source-version }}
-        CODEBUILD__imageOverride: ${{ (matrix.platform == 'amd64' && 'aws/codebuild/standard:6.0') || 'aws/codebuild/amazonlinux2-aarch64-standard:2.0' }}
+        CODEBUILD__imageOverride: ${{ (matrix.platform == 'amd64' && 'aws/codebuild/amazonlinux2-x86_64-standard:4.0') || 'aws/codebuild/amazonlinux2-aarch64-standard:2.0' }}
         CODEBUILD__environmentTypeOverride: ${{ (matrix.platform == 'amd64' && 'LINUX_CONTAINER') || 'ARM_CONTAINER' }}
         CODEBUILD__environmentVariablesOverride: '[
           { "name": "IMAGE_TAG", "type": "PLAINTEXT", "value": "docker.io/${{ inputs.image-name }}:${{ inputs.tag-temporary }}-${{ matrix.platform }}" },

+ 58 - 1
CHANGELOG.md

@@ -1,9 +1,66 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.0.15...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.1.0...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v6.1.0](https://github.com/weseek/growi/compare/v6.0.15...v6.1.0) - 2023-05-17
+
+### 💎 Features
+
+- feat: Add read-only user feature (#7648) @jam411
+- feat: Support Mermaid (move into the feature dierctory) (#7647) @miya
+- feat: Fix APP_SITE_URL with an environment variable (#7646) @yuki-takei
+- feat: Support Mermaid (#7645) @miya
+- feat: Support Elasticsearch v8 (#7623) @miya
+- feat: Elasticsearchv8 module (#7623) @miya
+- feat: Bookmarks folder and sidebar menu (#7450) @mudana-grune
+- feat: GROWI Questionnaire (#7316) @hakumizuki
+- feat: Revive attachment-refs with remark (#7597) @arafubeatbox
+
+### 🚀 Improvement
+
+- imprv: Font size (#7663) @yuki-takei
+- imprv: Admin user can use `reset-password` without email settings (#7650) @jam411
+- imprv: Optimize fonts with next/font (#7633) @yuki-takei
+- imprv: GFM table performance 2 (#7640) @yuki-takei
+- imprv: GFM footnote styles (#7628) @yuki-takei
+- imprv: Omit clobber prefix (#7627) @yuki-takei
+- imprv: GFM table performance (#7619) @yuki-takei
+- imprv: Show unsaved warning when comment not posted (#7603) @arafubeatbox
+- imprv: Suppress UI Flickering for dropdowns (#7608) @jam411
+- imprv: Allow registering without GROWI email settings for ID/Password authentication's restricted registration (#7591) @jam411
+- imprv: Enable browsing video (for v6.1.0) (#7589) @yuki-takei
+- imprv: Show a spinner into the save button while the saving process (#7579) @yuki-takei
+- imprv: Inject PlantUML URI with config-loader (#7577) @yuki-takei
+- imprv: Loading draw.io (diagrams.net) resources (#7575) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: The environment variable for disabling link sharing (#7652) @yuki-takei
+- fix: Cursor resetting occurs after updating with the built-in editor (#7644) @yuki-takei
+- fix: Revision schema migration for v5 to v6 (#7637) @yuki-takei
+- fix: Editor not resetting when the same markdown (#7625) @arafubeatbox
+- support: Replaced by IAttachmentHasId (#7629) @reiji-h
+- fix: AlignRight DropdownMenu flickering (#7606) @mudana-grune
+- fix: Not display page list count badge in trash page (#7600) @yukendev
+- fix: Reverted descendant pages do not appear in search results (#7587) @miya
+- fix: Deleted descendant pages do not appear in search results (#7583) @miya
+- fix: Show lsx page list in trash page correctly (#7582) @yukendev
+- fix: Uncaught type error by `sticky-event` (#7566) @mudana-grune
+
+### 🧰 Maintenance
+
+- support: mongoose update (#7659) @jam411
+- support: Elasticsearch8 (#7592) @miya
+- support: Replaced by IAttachmentHasId (#7629) @reiji-h
+- support: Dedupe packages (#7590) @yuki-takei
+- support: Omit textlint (#7578) @yuki-takei
+- support: Typescriptize CustomNav (#7584) @yuki-takei
+- support: Replaced by IAttachmentHasId (#7629) @reiji-h
+- support: Remove Blockdiag codes (#7576) @yuki-takei
+- support: Migrate to Turborepo (#7417) @yuki-takei
+
 ## [v6.0.15](https://github.com/weseek/growi/compare/v6.0.14...v6.0.15) - 2023-04-10
 
 ### 🐛 Bug Fixes

+ 13 - 1
THIRD-PARTY-NOTICES.md

@@ -19,6 +19,8 @@ https://github.com/weseek/growi.
 1. Lato (https://fonts.google.com/specimen/Lato)
 1. Press Start 2P (https://fonts.google.com/specimen/Press+Start+2P)
 1. stephenhutchings/typicons.font (https://github.com/stephenhutchings/typicons.font)
+1. Source Han Code JP (https://github.com/adobe-fonts/source-han-code-jp)
+
 
 
 License Notice for Apache License, Version 2.0 Derivative Works
@@ -115,7 +117,7 @@ Designed by Łukasz Dziedzic
 License Notice for Press Start 2P
 ------------------------------
 
-https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL
+http://scripts.sil.org/OFL
 
 ```
 Designed by CodeMan38
@@ -129,4 +131,14 @@ https://creativecommons.org/licenses/by-sa/3.0/
 
 ```
 Copyright (c) 2018 Stephen Hutchings
+```
+
+
+License Notice for Source Han Code JP
+----------------------------------
+
+http://scripts.sil.org/OFL
+
+```
+Copyright (c) 2014-2020 Adobe Systems Incorporated
 ```

+ 1 - 0
apps/app/docker/Dockerfile

@@ -96,6 +96,7 @@ RUN tar -cf packages.tar \
   apps/app/tmp \
   apps/app/.env.production* \
   apps/app/next.config.js \
+  apps/app/package.json \
   packages/*/package.json \
   packages/*/dist
 

+ 6 - 0
apps/app/docker/codebuild/buildspec.yml

@@ -12,6 +12,11 @@ env:
 phases:
   pre_build:
     commands:
+      # install Git LFS
+      - curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.rpm.sh | bash
+      - yum install -y git-lfs
+      # fetch LFS files
+      - git-lfs pull
       # login to docker.io
       - echo ${DOCKER_REGISTRY_PASSWORD} | docker login --username wsmoogle --password-stdin
       # login to ghcr.io
@@ -29,4 +34,5 @@ phases:
 cache:
   paths:
     - node_modules/**/*
+    - apps/*/node_modules/**/*
     - packages/*/node_modules/**/*

+ 1 - 1
apps/app/docker/codebuild/codebuild.tf

@@ -14,7 +14,7 @@ module "codebuild" {
   buildspec           = "apps/app/docker/codebuild/buildspec.yml"
 
   # https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-available.html
-  build_image         = "aws/codebuild/amazonlinux2-x86_64-standard:3.0"
+  build_image         = "aws/codebuild/amazonlinux2-x86_64-standard:4.0"
   build_compute_type  = "BUILD_GENERAL1_LARGE"
 
   privileged_mode     = true

+ 12 - 12
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "6.1.0-RC.0",
+  "version": "6.1.1-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -59,14 +59,14 @@
     "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.7.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/core": "^6.1.0-RC.0",
-    "@growi/hackmd": "^6.1.0-RC.0",
-    "@growi/preset-themes": "^6.1.0-RC.0",
-    "@growi/remark-attachment-refs": "^6.1.0-RC.0",
-    "@growi/remark-drawio": "^6.1.0-RC.0",
-    "@growi/remark-growi-directive": "^6.1.0-RC.0",
-    "@growi/remark-lsx": "^6.1.0-RC.0",
-    "@growi/slack": "^6.1.0-RC.0",
+    "@growi/core": "^6.1.1-RC.0",
+    "@growi/hackmd": "^6.1.1-RC.0",
+    "@growi/preset-themes": "^6.1.1-RC.0",
+    "@growi/remark-attachment-refs": "^6.1.1-RC.0",
+    "@growi/remark-drawio": "^6.1.1-RC.0",
+    "@growi/remark-growi-directive": "^6.1.1-RC.0",
+    "@growi/remark-lsx": "^6.1.1-RC.0",
+    "@growi/slack": "^6.1.1-RC.0",
     "@promster/express": "^7.0.6",
     "@promster/server": "^7.0.8",
     "@slack/web-api": "^6.2.4",
@@ -123,7 +123,7 @@
     "method-override": "^3.0.0",
     "migrate-mongo": "^8.2.3",
     "mkdirp": "^1.0.3",
-    "mongoose": "^6.0.13",
+    "mongoose": "^6.5.0",
     "mongoose-gridfs": "^1.2.42",
     "mongoose-paginate-v2": "^1.3.9",
     "mongoose-unique-validator": "^2.0.3",
@@ -202,8 +202,8 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "devDependencies": {
-    "@growi/presentation": "^6.1.0-RC.0",
-    "@growi/ui": "^6.1.0-RC.0",
+    "@growi/presentation": "^6.1.1-RC.0",
+    "@growi/ui": "^6.1.1-RC.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^13.2.3",

+ 3 - 0
apps/app/resource/fonts/SourceHanCodeJP-Regular-subset-jis2.woff2

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aab6592478ac9a7a4caa2085c61c637850fc3158c5e1362d35582c03101e128d
+size 603008

+ 3 - 0
apps/app/resource/fonts/SourceHanCodeJP-Regular-subset-main.woff2

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4813d38c434c84f37fc0eaf51160eccea51e1e437391d07795485faaebdcc7be
+size 512260

+ 0 - 3
apps/app/resource/fonts/SourceHanCodeJP-Regular.woff2

@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:d4b582d043b48510614205db1f1bc1519a70acbe0d89f53843e9131fe7f655b8
-size 3333108

+ 65 - 132
apps/app/resource/locales/en_US/sandbox-diagrams.md

@@ -152,141 +152,74 @@ State3 --> [*] : Aborted
 ```
 
 
-# :pencil: blockdiag
 
-See [blockdiag](http://blockdiag.com/).
+# :pencil: Mermaid
 
-## blockdiag
+## Pie chart diagram
 
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: blockdiag
-blockdiag {
-   A -> B -> C -> D;
-   A -> E -> F -> G;
-}
-:::
-
-</div>
-
-## seqdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: seqdiag
-seqdiag {
-  browser  -> webserver [label = "GET /index.html"];
-  browser <-- webserver;
-  browser  -> webserver [label = "POST /blog/comment"];
-              webserver  -> database [label = "INSERT comment"];
-              webserver <-- database;
-  browser <-- webserver;
-}
-:::
-
-</div>
-
-## actdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: actdiag
-actdiag {
-  write -> convert -> image
-
-  lane user {
-     label = "User"
-     write [label = "Writing reST"];
-     image [label = "Get diagram IMAGE"];
-  }
-  lane actdiag {
-     convert [label = "Convert reST to Image"];
-  }
-}
-:::
-
-</div>
-
-## nwdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: nwdiag
-nwdiag {
-  network dmz {
-      address = "210.x.x.x/24"
-
-      web01 [address = "210.x.x.1"];
-      web02 [address = "210.x.x.2"];
-  }
-  network internal {
-      address = "172.x.x.x/24";
-
-      web01 [address = "172.x.x.1"];
-      web02 [address = "172.x.x.2"];
-      db01;
-      db02;
-  }
-}
-:::
-
-</div>
-
-## rackdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
+```mermaid
+%%{init: {"pie": {"textPosition": 0.5}, "themeVariables": {"pieOuterStrokeWidth": "5px"}} }%%
+pie showData
+    title Key elements in Product X
+    "Calcium" : 42.96
+    "Potassium" : 50.05
+    "Magnesium" : 10.01
+    "Iron" :  5
+```
 
-::: rackdiag
-rackdiag {
-  // define height of rack
-  8U;
+## Gantt diagram
+
+```mermaid
+gantt
+    title A Gantt Diagram
+    dateFormat  YYYY-MM-DD
+    section Section
+    A task           :a1, 2014-01-01, 30d
+    Another task     :after a1  , 20d
+    section Another
+    Task in sec      :2014-01-12  , 12d
+    another task      : 24d
+```
 
-  // define rack items
-  1: UPS [2U];
-  3: DB Server
-  4: Web Server
-  5: Web Server
-  6: Web Server
-  7: Load Balancer
-  8: L3 Switch
-}
-:::
-
-</div>
-
-## packetdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: packetdiag
-packetdiag {
-  colwidth = 32
-  node_height = 72
-
-  0-15: Source Port
-  16-31: Destination Port
-  32-63: Sequence Number
-  64-95: Acknowledgment Number
-  96-99: Data Offset
-  100-105: Reserved
-  106: URG [rotate = 270]
-  107: ACK [rotate = 270]
-  108: PSH [rotate = 270]
-  109: RST [rotate = 270]
-  110: SYN [rotate = 270]
-  111: FIN [rotate = 270]
-  112-127: Window
-  128-143: Checksum
-  144-159: Urgent Pointer
-  160-191: (Options and Padding)
-  192-223: data [colheight = 3]
-}
-:::
+## Gitgraph diagram
+
+```mermaid
+%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'rotateCommitLabel': true}} }%%
+gitGraph
+  commit id: "feat(api): ..."
+  commit id: "a"
+  commit id: "b"
+  commit id: "fix(client): .extra long label.."
+  branch c2
+  commit id: "feat(modules): ..."
+  commit id: "test(client): ..."
+  checkout main
+  commit id: "fix(api): ..."
+  commit id: "ci: ..."
+  branch b1
+  commit
+  branch b2
+  commit
+```
 
-</div>
+## Mindmap diagram
+
+```mermaid
+mindmap
+  root((mindmap))
+    Origins
+      Long history
+      ::icon(fa fa-book)
+      Popularisation
+        British popular psychology author Tony Buzan
+    Research
+      On effectiveness<br/>and features
+      On Automatic creation
+        Uses
+            Creative techniques
+            Strategic planning
+            Argument mapping
+    Tools
+      Pen and paper
+      Mermaid
+```

+ 65 - 133
apps/app/resource/locales/ja_JP/sandbox-diagrams.md

@@ -150,142 +150,74 @@ State3 --> [*] : Aborted
 ```
 
 
-# :pencil: blockdiag
 
-See [blockdiag](http://blockdiag.com/).
+# :pencil: Mermaid
 
+## 円グラフ
 
-## blockdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: blockdiag
-blockdiag {
-   A -> B -> C -> D;
-   A -> E -> F -> G;
-}
-:::
-
-</div>
-
-## seqdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: seqdiag
-seqdiag {
-  browser  -> webserver [label = "GET /index.html"];
-  browser <-- webserver;
-  browser  -> webserver [label = "POST /blog/comment"];
-              webserver  -> database [label = "INSERT comment"];
-              webserver <-- database;
-  browser <-- webserver;
-}
-:::
-
-</div>
-
-## actdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: actdiag
-actdiag {
-  write -> convert -> image
-
-  lane user {
-     label = "User"
-     write [label = "Writing reST"];
-     image [label = "Get diagram IMAGE"];
-  }
-  lane actdiag {
-     convert [label = "Convert reST to Image"];
-  }
-}
-:::
-
-</div>
-
-## nwdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: nwdiag
-nwdiag {
-  network dmz {
-      address = "210.x.x.x/24"
-
-      web01 [address = "210.x.x.1"];
-      web02 [address = "210.x.x.2"];
-  }
-  network internal {
-      address = "172.x.x.x/24";
-
-      web01 [address = "172.x.x.1"];
-      web02 [address = "172.x.x.2"];
-      db01;
-      db02;
-  }
-}
-:::
-
-</div>
-
-## rackdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
+```mermaid
+%%{init: {"pie": {"textPosition": 0.5}, "themeVariables": {"pieOuterStrokeWidth": "5px"}} }%%
+pie showData
+    title Key elements in Product X
+    "Calcium" : 42.96
+    "Potassium" : 50.05
+    "Magnesium" : 10.01
+    "Iron" :  5
+```
 
-::: rackdiag
-rackdiag {
-  // define height of rack
-  8U;
+## ガントチャート
+
+```mermaid
+gantt
+    title A Gantt Diagram
+    dateFormat  YYYY-MM-DD
+    section Section
+    A task           :a1, 2014-01-01, 30d
+    Another task     :after a1  , 20d
+    section Another
+    Task in sec      :2014-01-12  , 12d
+    another task      : 24d
+```
 
-  // define rack items
-  1: UPS [2U];
-  3: DB Server
-  4: Web Server
-  5: Web Server
-  6: Web Server
-  7: Load Balancer
-  8: L3 Switch
-}
-:::
-
-</div>
-
-## packetdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: packetdiag
-packetdiag {
-  colwidth = 32
-  node_height = 72
-
-  0-15: Source Port
-  16-31: Destination Port
-  32-63: Sequence Number
-  64-95: Acknowledgment Number
-  96-99: Data Offset
-  100-105: Reserved
-  106: URG [rotate = 270]
-  107: ACK [rotate = 270]
-  108: PSH [rotate = 270]
-  109: RST [rotate = 270]
-  110: SYN [rotate = 270]
-  111: FIN [rotate = 270]
-  112-127: Window
-  128-143: Checksum
-  144-159: Urgent Pointer
-  160-191: (Options and Padding)
-  192-223: data [colheight = 3]
-}
-:::
+## Git 樹形図
+
+```mermaid
+%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'rotateCommitLabel': true}} }%%
+gitGraph
+  commit id: "feat(api): ..."
+  commit id: "a"
+  commit id: "b"
+  commit id: "fix(client): .extra long label.."
+  branch c2
+  commit id: "feat(modules): ..."
+  commit id: "test(client): ..."
+  checkout main
+  commit id: "fix(api): ..."
+  commit id: "ci: ..."
+  branch b1
+  commit
+  branch b2
+  commit
+```
 
-</div>
+## マインドマップ
+
+```mermaid
+mindmap
+  root((mindmap))
+    Origins
+      Long history
+      ::icon(fa fa-book)
+      Popularisation
+        British popular psychology author Tony Buzan
+    Research
+      On effectiveness<br/>and features
+      On Automatic creation
+        Uses
+            Creative techniques
+            Strategic planning
+            Argument mapping
+    Tools
+      Pen and paper
+      Mermaid
+```

+ 65 - 132
apps/app/resource/locales/zh_CN/sandbox-diagrams.md

@@ -152,141 +152,74 @@ State3 --> [*] : Aborted
 ```
 
 
-# :pencil: blockdiag
 
-See [blockdiag](http://blockdiag.com/).
+# :pencil: Mermaid
 
-## blockdiag
+## Pie chart diagram
 
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: blockdiag
-blockdiag {
-   A -> B -> C -> D;
-   A -> E -> F -> G;
-}
-:::
-
-</div>
-
-## seqdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: seqdiag
-seqdiag {
-  browser  -> webserver [label = "GET /index.html"];
-  browser <-- webserver;
-  browser  -> webserver [label = "POST /blog/comment"];
-              webserver  -> database [label = "INSERT comment"];
-              webserver <-- database;
-  browser <-- webserver;
-}
-:::
-
-</div>
-
-## actdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: actdiag
-actdiag {
-  write -> convert -> image
-
-  lane user {
-     label = "User"
-     write [label = "Writing reST"];
-     image [label = "Get diagram IMAGE"];
-  }
-  lane actdiag {
-     convert [label = "Convert reST to Image"];
-  }
-}
-:::
-
-</div>
-
-## nwdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: nwdiag
-nwdiag {
-  network dmz {
-      address = "210.x.x.x/24"
-
-      web01 [address = "210.x.x.1"];
-      web02 [address = "210.x.x.2"];
-  }
-  network internal {
-      address = "172.x.x.x/24";
-
-      web01 [address = "172.x.x.1"];
-      web02 [address = "172.x.x.2"];
-      db01;
-      db02;
-  }
-}
-:::
-
-</div>
-
-## rackdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
+```mermaid
+%%{init: {"pie": {"textPosition": 0.5}, "themeVariables": {"pieOuterStrokeWidth": "5px"}} }%%
+pie showData
+    title Key elements in Product X
+    "Calcium" : 42.96
+    "Potassium" : 50.05
+    "Magnesium" : 10.01
+    "Iron" :  5
+```
 
-::: rackdiag
-rackdiag {
-  // define height of rack
-  8U;
+## Gantt diagram
+
+```mermaid
+gantt
+    title A Gantt Diagram
+    dateFormat  YYYY-MM-DD
+    section Section
+    A task           :a1, 2014-01-01, 30d
+    Another task     :after a1  , 20d
+    section Another
+    Task in sec      :2014-01-12  , 12d
+    another task      : 24d
+```
 
-  // define rack items
-  1: UPS [2U];
-  3: DB Server
-  4: Web Server
-  5: Web Server
-  6: Web Server
-  7: Load Balancer
-  8: L3 Switch
-}
-:::
-
-</div>
-
-## packetdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: packetdiag
-packetdiag {
-  colwidth = 32
-  node_height = 72
-
-  0-15: Source Port
-  16-31: Destination Port
-  32-63: Sequence Number
-  64-95: Acknowledgment Number
-  96-99: Data Offset
-  100-105: Reserved
-  106: URG [rotate = 270]
-  107: ACK [rotate = 270]
-  108: PSH [rotate = 270]
-  109: RST [rotate = 270]
-  110: SYN [rotate = 270]
-  111: FIN [rotate = 270]
-  112-127: Window
-  128-143: Checksum
-  144-159: Urgent Pointer
-  160-191: (Options and Padding)
-  192-223: data [colheight = 3]
-}
-:::
+## Gitgraph diagram
+
+```mermaid
+%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'rotateCommitLabel': true}} }%%
+gitGraph
+  commit id: "feat(api): ..."
+  commit id: "a"
+  commit id: "b"
+  commit id: "fix(client): .extra long label.."
+  branch c2
+  commit id: "feat(modules): ..."
+  commit id: "test(client): ..."
+  checkout main
+  commit id: "fix(api): ..."
+  commit id: "ci: ..."
+  branch b1
+  commit
+  branch b2
+  commit
+```
 
-</div>
+## Mindmap diagram
+
+```mermaid
+mindmap
+  root((mindmap))
+    Origins
+      Long history
+      ::icon(fa fa-book)
+      Popularisation
+        British popular psychology author Tony Buzan
+    Research
+      On effectiveness<br/>and features
+      On Automatic creation
+        Uses
+            Creative techniques
+            Strategic planning
+            Argument mapping
+    Tools
+      Pen and paper
+      Mermaid
+```

+ 8 - 8
apps/app/src/client/services/AdminLocalSecurityContainer.js

@@ -28,7 +28,7 @@ export default class AdminLocalSecurityContainer extends Container {
       retrieveError: null,
       // set dummy value tile for using suspense
       registrationMode: this.dummyRegistrationMode,
-      registrationWhiteList: [],
+      registrationWhitelist: [],
       useOnlyEnvVars: false,
       isPasswordResetEnabled: false,
       isEmailAuthenticationEnabled: false,
@@ -43,7 +43,7 @@ export default class AdminLocalSecurityContainer extends Container {
       this.setState({
         useOnlyEnvVars: localSetting.useOnlyEnvVarsForSomeOptions,
         registrationMode: localSetting.registrationMode,
-        registrationWhiteList: localSetting.registrationWhiteList,
+        registrationWhitelist: localSetting.registrationWhitelist,
         isPasswordResetEnabled: localSetting.isPasswordResetEnabled,
         isEmailAuthenticationEnabled: localSetting.isEmailAuthenticationEnabled,
       });
@@ -72,10 +72,10 @@ export default class AdminLocalSecurityContainer extends Container {
   }
 
   /**
-   * Change registration white list
+   * Change registration whitelist
    */
-  changeRegistrationWhiteList(value) {
-    this.setState({ registrationWhiteList: value.split('\n') });
+  changeRegistrationWhitelist(value) {
+    this.setState({ registrationWhitelist: value.split('\n') });
   }
 
   /**
@@ -96,10 +96,10 @@ export default class AdminLocalSecurityContainer extends Container {
    * update local security setting
    */
   async updateLocalSecuritySetting() {
-    const { registrationWhiteList, isPasswordResetEnabled, isEmailAuthenticationEnabled } = this.state;
+    const { registrationWhitelist, isPasswordResetEnabled, isEmailAuthenticationEnabled } = this.state;
     const response = await apiv3Put('/security-setting/local-setting', {
       registrationMode: this.state.registrationMode,
-      registrationWhiteList,
+      registrationWhitelist,
       isPasswordResetEnabled,
       isEmailAuthenticationEnabled,
     });
@@ -108,7 +108,7 @@ export default class AdminLocalSecurityContainer extends Container {
 
     this.setState({
       registrationMode: localSettingParams.registrationMode,
-      registrationWhiteList: localSettingParams.registrationWhiteList,
+      registrationWhitelist: localSettingParams.registrationWhitelist,
       isPasswordResetEnabled: localSettingParams.isPasswordResetEnabled,
       isEmailAuthenticationEnabled: localSettingParams.isEmailAuthenticationEnabled,
     });

+ 10 - 10
apps/app/src/client/services/AdminMarkDownContainer.js

@@ -26,8 +26,8 @@ export default class AdminMarkDownContainer extends Container {
       isIndentSizeForced: false,
       isEnabledXss: false,
       xssOption: '',
-      tagWhiteList: '',
-      attrWhiteList: '{}',
+      tagWhitelist: '',
+      attrWhitelist: '{}',
     };
 
     this.switchEnableXss = this.switchEnableXss.bind(this);
@@ -55,8 +55,8 @@ export default class AdminMarkDownContainer extends Container {
       isIndentSizeForced: markdownParams.isIndentSizeForced,
       isEnabledXss: markdownParams.isEnabledXss,
       xssOption: markdownParams.xssOption,
-      tagWhiteList: markdownParams.tagWhiteList || '',
-      attrWhiteList: markdownParams.attrWhiteList || '',
+      tagWhitelist: markdownParams.tagWhitelist || '',
+      attrWhitelist: markdownParams.attrWhitelist || '',
     });
   }
 
@@ -101,14 +101,14 @@ export default class AdminMarkDownContainer extends Container {
    * Update Xss Setting
    */
   async updateXssSetting() {
-    let { tagWhiteList } = this.state;
-    const { attrWhiteList } = this.state;
+    let { tagWhitelist } = this.state;
+    const { attrWhitelist } = this.state;
 
-    tagWhiteList = Array.isArray(tagWhiteList) ? tagWhiteList : tagWhiteList.split(',');
+    tagWhitelist = Array.isArray(tagWhitelist) ? tagWhitelist : tagWhitelist.split(',');
 
     try {
       // Check if parsing is possible
-      JSON.parse(attrWhiteList);
+      JSON.parse(attrWhitelist);
     }
     catch (err) {
       throw Error(err);
@@ -117,8 +117,8 @@ export default class AdminMarkDownContainer extends Container {
     await apiv3Put('/markdown-setting/xss', {
       isEnabledXss: this.state.isEnabledXss,
       xssOption: this.state.xssOption,
-      tagWhiteList,
-      attrWhiteList: attrWhiteList ?? '{}',
+      tagWhitelist,
+      attrWhitelist: attrWhitelist ?? '{}',
     });
   }
 

+ 1 - 1
apps/app/src/components/Admin/AdminHome/AdminHome.jsx

@@ -14,7 +14,7 @@ import loggerFactory from '~/utils/logger';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
-import EnvVarsTable from './EnvVarsTable';
+import { EnvVarsTable } from './EnvVarsTable';
 import SystemInfomationTable from './SystemInfomationTable';
 
 const logger = loggerFactory('growi:admin');

+ 6 - 10
apps/app/src/components/Admin/AdminHome/EnvVarsTable.jsx → apps/app/src/components/Admin/AdminHome/EnvVarsTable.tsx

@@ -1,8 +1,11 @@
 import React from 'react';
-import PropTypes from 'prop-types';
 
-const EnvVarsTable = (props) => {
-  const envVarRows = [];
+type EnvVarsTableProps = {
+  envVars: Record<string, string | number | boolean>,
+}
+
+export const EnvVarsTable: React.FC<EnvVarsTableProps> = (props: EnvVarsTableProps) => {
+  const envVarRows: JSX.Element[] = [];
 
   for (const [key, value] of Object.entries(props.envVars)) {
     if (value != null) {
@@ -22,11 +25,4 @@ const EnvVarsTable = (props) => {
       </tbody>
     </table>
   );
-
 };
-
-EnvVarsTable.propTypes = {
-  envVars: PropTypes.object.isRequired,
-};
-
-export default EnvVarsTable;

+ 17 - 17
apps/app/src/components/Admin/MarkdownSetting/WhiteListInput.jsx → apps/app/src/components/Admin/MarkdownSetting/WhitelistInput.jsx

@@ -8,13 +8,13 @@ import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
-class WhiteListInput extends React.Component {
+class WhitelistInput extends React.Component {
 
   constructor(props) {
     super(props);
 
-    this.tagWhiteList = React.createRef();
-    this.attrWhiteList = React.createRef();
+    this.tagWhitelist = React.createRef();
+    this.attrWhitelist = React.createRef();
 
     this.tags = sanitizeDefaultSchema.tagNames;
     this.attrs = JSON.stringify(sanitizeDefaultSchema.attributes);
@@ -24,13 +24,13 @@ class WhiteListInput extends React.Component {
   }
 
   onClickRecommendTagButton() {
-    this.tagWhiteList.current.value = this.tags;
-    this.props.adminMarkDownContainer.setState({ tagWhiteList: this.tags });
+    this.tagWhitelist.current.value = this.tags;
+    this.props.adminMarkDownContainer.setState({ tagWhitelist: this.tags });
   }
 
   onClickRecommendAttrButton() {
-    this.attrWhiteList.current.value = this.attrs;
-    this.props.adminMarkDownContainer.setState({ attrWhiteList: this.attrs });
+    this.attrWhitelist.current.value = this.attrs;
+    this.props.adminMarkDownContainer.setState({ attrWhitelist: this.attrs });
   }
 
   render() {
@@ -50,9 +50,9 @@ class WhiteListInput extends React.Component {
             name="recommendedTags"
             rows="6"
             cols="40"
-            ref={this.tagWhiteList}
-            defaultValue={adminMarkDownContainer.state.tagWhiteList}
-            onChange={(e) => { adminMarkDownContainer.setState({ tagWhiteList: e.target.value }) }}
+            ref={this.tagWhitelist}
+            defaultValue={adminMarkDownContainer.state.tagWhitelist}
+            onChange={(e) => { adminMarkDownContainer.setState({ tagWhitelist: e.target.value }) }}
           />
         </div>
         <div className="mt-4">
@@ -67,9 +67,9 @@ class WhiteListInput extends React.Component {
             name="recommendedAttrs"
             rows="6"
             cols="40"
-            ref={this.attrWhiteList}
-            defaultValue={adminMarkDownContainer.state.attrWhiteList}
-            onChange={(e) => { adminMarkDownContainer.setState({ attrWhiteList: e.target.value }) }}
+            ref={this.attrWhitelist}
+            defaultValue={adminMarkDownContainer.state.attrWhitelist}
+            onChange={(e) => { adminMarkDownContainer.setState({ attrWhitelist: e.target.value }) }}
           />
         </div>
       </>
@@ -79,7 +79,7 @@ class WhiteListInput extends React.Component {
 }
 
 
-WhiteListInput.propTypes = {
+WhitelistInput.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 
@@ -88,9 +88,9 @@ WhiteListInput.propTypes = {
 const PresentationFormWrapperFC = (props) => {
   const { t } = useTranslation('admin');
 
-  return <WhiteListInput t={t} {...props} />;
+  return <WhitelistInput t={t} {...props} />;
 };
 
-const WhiteListWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AdminMarkDownContainer]);
+const WhitelistWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AdminMarkDownContainer]);
 
-export default WhiteListWrapper;
+export default WhitelistWrapper;

+ 2 - 2
apps/app/src/components/Admin/MarkdownSetting/XssForm.jsx

@@ -12,7 +12,7 @@ import loggerFactory from '~/utils/logger';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
-import WhiteListInput from './WhiteListInput';
+import WhitelistInput from './WhitelistInput';
 
 const logger = loggerFactory('growi:importer');
 
@@ -102,7 +102,7 @@ class XssForm extends React.Component {
               />
               <label className="custom-control-label w-100" htmlFor="xssOption2">
                 <p className="font-weight-bold">{t('markdown_settings.xss_options.custom_whitelist')}</p>
-                <WhiteListInput customizable />
+                <WhitelistInput customizable />
               </label>
             </div>
           </div>

+ 3 - 3
apps/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx

@@ -146,9 +146,9 @@ class LocalSecuritySettingContents extends React.Component {
                 <textarea
                   className="form-control"
                   type="textarea"
-                  name="registrationWhiteList"
-                  defaultValue={adminLocalSecurityContainer.state.registrationWhiteList.join('\n')}
-                  onChange={e => adminLocalSecurityContainer.changeRegistrationWhiteList(e.target.value)}
+                  name="registrationWhitelist"
+                  defaultValue={adminLocalSecurityContainer.state.registrationWhitelist.join('\n')}
+                  onChange={e => adminLocalSecurityContainer.changeRegistrationWhitelist(e.target.value)}
                 />
                 <p className="form-text text-muted small">
                   {t('security_settings.restrict_emails')}

+ 7 - 9
apps/app/src/components/DuplicatedPathsTable.jsx → apps/app/src/components/DuplicatedPathsTable.tsx

@@ -2,12 +2,17 @@ import React from 'react';
 
 import { pagePathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
 
 
 const { convertToNewAffiliationPath } = pagePathUtils;
 
-function DuplicatedPathsTable(props) {
+type DuplicatedPathsTableProps = {
+  existingPaths: string[],
+  fromPath: string,
+  toPath: string
+}
+
+const DuplicatedPathsTable: React.FC<DuplicatedPathsTableProps> = (props: DuplicatedPathsTableProps) => {
   const { t } = useTranslation();
 
   const {
@@ -41,13 +46,6 @@ function DuplicatedPathsTable(props) {
       </tbody>
     </table>
   );
-}
-
-
-DuplicatedPathsTable.propTypes = {
-  existingPaths: PropTypes.array.isRequired,
-  fromPath: PropTypes.string.isRequired,
-  toPath: PropTypes.string.isRequired,
 };
 
 

+ 1 - 1
apps/app/src/components/Layout/SearchResultLayout.module.scss

@@ -75,7 +75,7 @@
 
         .wiki {
           padding: 16px;
-          font-size: 13px;
+          font-size: 14px;
         }
       }
     }

+ 5 - 5
apps/app/src/components/LoginForm.tsx

@@ -25,7 +25,7 @@ type LoginFormProps = {
   email?: string,
   isEmailAuthenticationEnabled: boolean,
   registrationMode: RegistrationMode,
-  registrationWhiteList: string[],
+  registrationWhitelist: string[],
   isPasswordResetEnabled: boolean,
   isLocalStrategySetup: boolean,
   isLdapStrategySetup: boolean,
@@ -41,7 +41,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
   const {
     isLocalStrategySetup, isLdapStrategySetup, isLdapSetupFailed, isPasswordResetEnabled,
-    isEmailAuthenticationEnabled, registrationMode, registrationWhiteList, isMailerSetup, objOfIsExternalAuthEnableds,
+    isEmailAuthenticationEnabled, registrationMode, registrationWhitelist, isMailerSetup, objOfIsExternalAuthEnableds,
   } = props;
   const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
   const isSomeExternalAuthEnabled = Object.values(objOfIsExternalAuthEnableds).some(elem => elem);
@@ -439,11 +439,11 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
             />
           </div>
 
-          {registrationWhiteList.length > 0 && (
+          {registrationWhitelist.length > 0 && (
             <>
               <p className="form-text">{t('page_register.form_help.email')}</p>
               <ul>
-                {registrationWhiteList.map((elem) => {
+                {registrationWhitelist.map((elem) => {
                   return (
                     <li key={elem}>
                       <code>{elem}</code>
@@ -503,7 +503,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     );
   }, [
     t, isEmailAuthenticationEnabled, registrationMode, isMailerSetup, registerErrors, isSuccessToRagistration,
-    emailForRegistrationOrder, props.username, props.name, props.email, registrationWhiteList, switchForm, handleRegisterFormSubmit,
+    emailForRegistrationOrder, props.username, props.name, props.email, registrationWhitelist, switchForm, handleRegisterFormSubmit,
   ]);
 
   if (registrationMode === RegistrationMode.RESTRICTED && isSuccessToRagistration && !isEmailAuthenticationEnabled) {

+ 4 - 4
apps/app/src/components/Me/BasicInfoSettings.tsx

@@ -5,12 +5,12 @@ import { useTranslation, i18n } from 'next-i18next';
 import { i18n as i18nConfig } from '^/config/next-i18next.config';
 
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { useRegistrationWhiteList } from '~/stores/context';
+import { useRegistrationWhitelist } from '~/stores/context';
 import { usePersonalSettings } from '~/stores/personal-settings';
 
 export const BasicInfoSettings = (): JSX.Element => {
   const { t } = useTranslation();
-  const { data: registrationWhiteList } = useRegistrationWhiteList();
+  const { data: registrationWhitelist } = useRegistrationWhitelist();
 
   const {
     data: personalSettingsInfo, mutate: mutatePersonalSettings, sync, updateBasicInfo, error,
@@ -63,11 +63,11 @@ export const BasicInfoSettings = (): JSX.Element => {
             defaultValue={personalSettingsInfo?.email || ''}
             onChange={e => changePersonalSettingsHandler({ email: e.target.value })}
           />
-          {registrationWhiteList != null && registrationWhiteList.length !== 0 && (
+          {registrationWhitelist != null && registrationWhitelist.length !== 0 && (
             <div className="form-text text-muted">
               {t('page_register.form_help.email')}
               <ul>
-                {registrationWhiteList.map(data => <li key={data}><code>{data}</code></li>)}
+                {registrationWhitelist.map(data => <li key={data}><code>{data}</code></li>)}
               </ul>
             </div>
           )}

+ 21 - 13
apps/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -9,7 +9,9 @@ import { throttle, debounce } from 'throttle-debounce';
 import urljoin from 'url-join';
 
 import InterceptorManager from '~/services/interceptor-manager';
-import { useHandsontableModal, useDrawioModal, useTemplateModal } from '~/stores/modal';
+import {
+  useHandsontableModal, useDrawioModal, useTemplateModal, useLinkEditModal,
+} from '~/stores/modal';
 import loggerFactory from '~/utils/logger';
 
 import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
@@ -22,7 +24,6 @@ import EmojiPickerHelper from './EmojiPickerHelper';
 import GridEditModal from './GridEditModal';
 // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
 // import geu from './GridEditorUtil';
-import LinkEditModal from './LinkEditModal';
 import mdu from './MarkdownDrawioUtil';
 import markdownLinkUtil from './MarkdownLinkUtil';
 import markdownListUtil from './MarkdownListUtil';
@@ -149,13 +150,13 @@ class CodeMirrorEditor extends AbstractEditor {
     this.makeHeaderHandler = this.makeHeaderHandler.bind(this);
     // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
     // this.showGridEditorHandler = this.showGridEditorHandler.bind(this);
-    this.showLinkEditHandler = this.showLinkEditHandler.bind(this);
 
     this.foldDrawioSection = this.foldDrawioSection.bind(this);
     this.clickDrawioIconHandler = this.clickDrawioIconHandler.bind(this);
     this.clickTableIconHandler = this.clickTableIconHandler.bind(this);
 
     this.showTemplateModal = this.showTemplateModal.bind(this);
+    this.showLinkEditModal = this.showLinkEditModal.bind(this);
 
   }
 
@@ -846,15 +847,21 @@ class CodeMirrorEditor extends AbstractEditor {
   //   this.gridEditModal.current.show(geu.getGridHtml(this.getCodeMirror()));
   // }
 
-  showLinkEditHandler() {
-    this.linkEditModal.current.show(markdownLinkUtil.getMarkdownLink(this.getCodeMirror()));
-  }
-
   showTemplateModal() {
     const onSubmit = templateText => this.setValue(templateText);
     this.props.onClickTemplateBtn(onSubmit);
   }
 
+  showLinkEditModal() {
+    const onSubmit = (linkText) => {
+      return markdownLinkUtil.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText);
+    };
+
+    const defaultMarkdownLink = markdownLinkUtil.getMarkdownLink(this.getCodeMirror());
+
+    this.props.onClickLinkEditBtn(defaultMarkdownLink, onSubmit);
+  }
+
   // fold draw.io section (``` drawio ~ ```)
   foldDrawioSection() {
     const editor = this.getCodeMirror();
@@ -985,7 +992,7 @@ class CodeMirrorEditor extends AbstractEditor {
         color={null}
         size="sm"
         title="Link"
-        onClick={this.showLinkEditHandler}
+        onClick={this.showLinkEditModal}
       >
         <EditorIcon icon="Link" />
       </Button>,
@@ -1125,11 +1132,6 @@ class CodeMirrorEditor extends AbstractEditor {
           onSave={(grid) => { return geu.replaceGridWithHtmlWithEditor(this.getCodeMirror(), grid) }}
         />
          */}
-
-        <LinkEditModal
-          ref={this.linkEditModal}
-          onSave={(linkText) => { return markdownLinkUtil.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText) }}
-        />
       </div>
     );
   }
@@ -1154,6 +1156,7 @@ const CodeMirrorEditorFc = React.forwardRef((props, ref) => {
   const { open: openDrawioModal } = useDrawioModal();
   const { open: openHandsontableModal } = useHandsontableModal();
   const { open: openTemplateModal } = useTemplateModal();
+  const { open: openLinkEditModal } = useLinkEditModal();
 
   const openDrawioModalHandler = useCallback((drawioMxFile, onSave) => {
     openDrawioModal(drawioMxFile, onSave);
@@ -1167,12 +1170,17 @@ const CodeMirrorEditorFc = React.forwardRef((props, ref) => {
     openTemplateModal(onSubmit);
   }, [openTemplateModal]);
 
+  const openLinkEditModalHandler = useCallback((defaultMarkdownLink, onSubmit) => {
+    openLinkEditModal(defaultMarkdownLink, onSubmit);
+  }, [openLinkEditModal]);
+
   return (
     <CodeMirrorEditorMemoized
       ref={ref}
       onClickDrawioBtn={openDrawioModalHandler}
       onClickTableBtn={openTableModalHandler}
       onClickTemplateBtn={openTemplateModalHandler}
+      onClickLinkEditBtn={openLinkEditModalHandler}
       {...props}
     />
   );

+ 1 - 0
apps/app/src/components/PageEditor/CodeMirrorEditor.module.scss

@@ -13,6 +13,7 @@
 
   .CodeMirror {
     font-family: var(--font-family-monospace);
+    font-size: 15px;
 
     pre.CodeMirror-line.grw-cm-header-line {
       padding-top: 0.16em;

+ 0 - 471
apps/app/src/components/PageEditor/LinkEditModal.jsx

@@ -1,471 +0,0 @@
-import React from 'react';
-
-import path from 'path';
-
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-import {
-  Modal,
-  ModalHeader,
-  ModalBody,
-  ModalFooter,
-  Popover,
-  PopoverBody,
-} from 'reactstrap';
-import validator from 'validator';
-
-
-import Linker from '~/client/models/Linker';
-import { apiv3Get } from '~/client/util/apiv3-client';
-import { useCurrentPagePath } from '~/stores/page';
-import loggerFactory from '~/utils/logger';
-
-import PagePreviewIcon from '../Icons/PagePreviewIcon';
-import SearchTypeahead from '../SearchTypeahead';
-
-import Preview from './Preview';
-
-
-import styles from './LinkEditPreview.module.scss';
-
-
-const logger = loggerFactory('growi:components:LinkEditModal');
-
-class LinkEditModal extends React.PureComponent {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      show: false,
-      isUseRelativePath: false,
-      isUsePermanentLink: false,
-      linkInputValue: '',
-      labelInputValue: '',
-      linkerType: Linker.types.markdownLink,
-      markdown: null,
-      pagePath: null,
-      previewError: '',
-      permalink: '',
-      isPreviewOpen: false,
-    };
-
-    // this.isApplyPukiwikiLikeLinkerPlugin = window.growiRenderer.preProcessors.some(process => process.constructor.name === 'PukiwikiLikeLinker');
-
-    this.show = this.show.bind(this);
-    this.hide = this.hide.bind(this);
-    this.cancel = this.cancel.bind(this);
-    this.handleChangeTypeahead = this.handleChangeTypeahead.bind(this);
-    this.handleChangeLabelInput = this.handleChangeLabelInput.bind(this);
-    this.handleChangeLinkInput = this.handleChangeLinkInput.bind(this);
-    this.handleSelecteLinkerType = this.handleSelecteLinkerType.bind(this);
-    this.toggleIsUseRelativePath = this.toggleIsUseRelativePath.bind(this);
-    this.toggleIsUsePamanentLink = this.toggleIsUsePamanentLink.bind(this);
-    this.save = this.save.bind(this);
-    this.generateLink = this.generateLink.bind(this);
-    this.getRootPath = this.getRootPath.bind(this);
-    this.toggleIsPreviewOpen = this.toggleIsPreviewOpen.bind(this);
-    this.setMarkdown = this.setMarkdown.bind(this);
-  }
-
-  // defaultMarkdownLink is an instance of Linker
-  show(defaultMarkdownLink = null) {
-    // if defaultMarkdownLink is null, set default value in inputs.
-    const { label = '', link = '' } = defaultMarkdownLink;
-    let { type = Linker.types.markdownLink } = defaultMarkdownLink;
-
-    // if type of defaultMarkdownLink is pukiwikiLink when pukiwikiLikeLinker plugin is disable, change type(not change label and link)
-    if (type === Linker.types.pukiwikiLink && !this.isApplyPukiwikiLikeLinkerPlugin) {
-      type = Linker.types.markdownLink;
-    }
-
-    this.parseLinkAndSetState(link, type);
-
-    this.setState({
-      show: true,
-      labelInputValue: label,
-      isUsePermanentLink: false,
-      permalink: '',
-      linkerType: type,
-    });
-  }
-
-  // parse link, link is ...
-  // case-1. url of this growi's page (ex. 'http://localhost:3000/hoge/fuga')
-  // case-2. absolute path of this growi's page (ex. '/hoge/fuga')
-  // case-3. relative path of this growi's page (ex. '../fuga', 'hoge')
-  // case-4. external link (ex. 'https://growi.org')
-  // case-5. the others (ex. '')
-  parseLinkAndSetState(link, type) {
-    // create url from link, add dummy origin if link is not valid url.
-    // ex-1. link = 'https://growi.org/' -> url = 'https://growi.org/' (case-1,4)
-    // ex-2. link = 'hoge' -> url = 'http://example.com/hoge' (case-2,3,5)
-    let isFqcn = false;
-    let isUseRelativePath = false;
-    let url;
-    try {
-      const url = new URL(link, 'http://example.com');
-      isFqcn = url.origin !== 'http://example.com';
-    }
-    catch (err) {
-      logger.debug(err);
-    }
-
-    // case-1: when link is this growi's page url, return pathname only
-    let reshapedLink = url != null && url.origin === window.location.origin
-      ? decodeURIComponent(url.pathname)
-      : link;
-
-    // case-3
-    if (!isFqcn && !reshapedLink.startsWith('/') && reshapedLink !== '') {
-      isUseRelativePath = true;
-      const rootPath = this.getRootPath(type);
-      reshapedLink = path.resolve(rootPath, reshapedLink);
-    }
-
-    this.setState({
-      linkInputValue: reshapedLink,
-      isUseRelativePath,
-    });
-  }
-
-  cancel() {
-    this.hide();
-  }
-
-  hide() {
-    this.setState({
-      show: false,
-    });
-  }
-
-  toggleIsUseRelativePath() {
-    if (!this.state.linkInputValue.startsWith('/') || this.state.linkerType === Linker.types.growiLink) {
-      return;
-    }
-
-    // User can't use both relativePath and permalink at the same time
-    this.setState({ isUseRelativePath: !this.state.isUseRelativePath, isUsePermanentLink: false });
-  }
-
-  toggleIsUsePamanentLink() {
-    if (this.state.permalink === '' || this.state.linkerType === Linker.types.growiLink) {
-      return;
-    }
-
-    // User can't use both relativePath and permalink at the same time
-    this.setState({ isUsePermanentLink: !this.state.isUsePermanentLink, isUseRelativePath: false });
-  }
-
-  async setMarkdown() {
-    const { t } = this.props;
-    const path = this.state.linkInputValue;
-    let markdown = null;
-    let pagePath = null;
-    let permalink = '';
-    let previewError = '';
-
-    if (path.startsWith('/')) {
-      try {
-        const pathWithoutFragment = new URL(path, 'http://dummy').pathname;
-        const isPermanentLink = validator.isMongoId(pathWithoutFragment.slice(1));
-        const pageId = isPermanentLink ? pathWithoutFragment.slice(1) : null;
-
-        const { data } = await apiv3Get('/page', { path: pathWithoutFragment, page_id: pageId });
-        const { page } = data;
-        markdown = page.revision.body;
-        pagePath = page.path;
-        permalink = page.id;
-      }
-      catch (err) {
-        previewError = err.message;
-      }
-    }
-    else {
-      previewError = t('link_edit.page_not_found_in_preview', { path });
-    }
-    this.setState({
-      markdown, pagePath, previewError, permalink,
-    });
-  }
-
-  renderLinkPreview() {
-    const linker = this.generateLink();
-    return (
-      <div className="d-flex justify-content-between mb-3 flex-column flex-sm-row">
-        <div className="card card-disabled w-100 p-1 mb-0">
-          <p className="text-left text-muted mb-1 small">Markdown</p>
-          <p className="text-center text-truncate text-muted">{linker.generateMarkdownText()}</p>
-        </div>
-        <div className="d-flex align-items-center justify-content-center">
-          <span className="lead mx-3">
-            <i className="d-none d-sm-block fa fa-caret-right"></i>
-            <i className="d-sm-none fa fa-caret-down"></i>
-          </span>
-        </div>
-        <div className="card w-100 p-1 mb-0">
-          <p className="text-left text-muted mb-1 small">HTML</p>
-          <p className="text-center text-truncate">
-            <a href={linker.link}>{linker.label}</a>
-          </p>
-        </div>
-      </div>
-    );
-  }
-
-  handleChangeTypeahead(selected) {
-    const pageWithMeta = selected[0];
-    if (pageWithMeta != null) {
-      const page = pageWithMeta.data;
-      const permalink = `${window.location.origin}/${page.id}`;
-      this.setState({ linkInputValue: page.path, permalink });
-    }
-  }
-
-  handleChangeLabelInput(label) {
-    this.setState({ labelInputValue: label });
-  }
-
-  handleChangeLinkInput(link) {
-    let isUseRelativePath = this.state.isUseRelativePath;
-    if (!this.state.linkInputValue.startsWith('/') || this.state.linkerType === Linker.types.growiLink) {
-      isUseRelativePath = false;
-    }
-    this.setState({
-      linkInputValue: link, isUseRelativePath, isUsePermanentLink: false, permalink: '',
-    });
-  }
-
-  handleSelecteLinkerType(linkerType) {
-    let { isUseRelativePath, isUsePermanentLink } = this.state;
-    if (linkerType === Linker.types.growiLink) {
-      isUseRelativePath = false;
-      isUsePermanentLink = false;
-    }
-    this.setState({ linkerType, isUseRelativePath, isUsePermanentLink });
-  }
-
-  save() {
-    const linker = this.generateLink();
-
-    if (this.props.onSave != null) {
-      this.props.onSave(linker.generateMarkdownText());
-    }
-
-    this.hide();
-  }
-
-  generateLink() {
-    const {
-      linkInputValue, labelInputValue, linkerType, isUseRelativePath, isUsePermanentLink, permalink,
-    } = this.state;
-
-    let reshapedLink = linkInputValue;
-    if (isUseRelativePath) {
-      const rootPath = this.getRootPath(linkerType);
-      reshapedLink = rootPath === linkInputValue ? '.' : path.relative(rootPath, linkInputValue);
-    }
-
-    if (isUsePermanentLink && permalink != null) {
-      reshapedLink = permalink;
-    }
-
-    return new Linker(linkerType, labelInputValue, reshapedLink);
-  }
-
-  getRootPath(type) {
-    const { pagePath } = this.props;
-    // rootPaths of md link and pukiwiki link are different
-    return type === Linker.types.markdownLink ? path.dirname(pagePath) : pagePath;
-  }
-
-  async toggleIsPreviewOpen() {
-    // open popover
-    if (this.state.isPreviewOpen === false) {
-      this.setMarkdown();
-    }
-    this.setState({ isPreviewOpen: !this.state.isPreviewOpen });
-  }
-
-  renderLinkAndLabelForm() {
-    const { t } = this.props;
-    const { pagePath } = this.state;
-
-    return (
-      <>
-        <h3 className="grw-modal-head">{t('link_edit.set_link_and_label')}</h3>
-        <form className="form-group">
-          <div className="form-gorup my-3">
-            <div className="input-group flex-nowrap">
-              <div className="input-group-prepend">
-                <span className="input-group-text">{t('link_edit.link')}</span>
-              </div>
-              <SearchTypeahead
-                onChange={this.handleChangeTypeahead}
-                onInputChange={this.handleChangeLinkInput}
-                inputName="link"
-                placeholder={t('link_edit.placeholder_of_link_input')}
-                keywordOnInit={this.state.linkInputValue}
-                autoFocus
-              />
-              <div className="d-none d-sm-block input-group-append">
-                <button type="button" id="preview-btn" className={`btn btn-info btn-page-preview ${styles['btn-page-preview']}`}>
-                  <PagePreviewIcon />
-                </button>
-                <Popover trigger="focus" placement="right" isOpen={this.state.isPreviewOpen} target="preview-btn" toggle={this.toggleIsPreviewOpen}>
-                  <PopoverBody>
-                    {this.state.markdown != null && pagePath != null
-                    && <div className={`linkedit-preview ${styles['linkedit-preview']}`}>
-                      <Preview markdown={this.state.markdown} pagePath={pagePath} />
-                    </div>
-                    }
-                  </PopoverBody>
-                </Popover>
-              </div>
-            </div>
-          </div>
-          <div className="form-gorup my-3">
-            <div className="input-group flex-nowrap">
-              <div className="input-group-prepend">
-                <span className="input-group-text">{t('link_edit.label')}</span>
-              </div>
-              <input
-                type="text"
-                className="form-control"
-                id="label"
-                value={this.state.labelInputValue}
-                onChange={e => this.handleChangeLabelInput(e.target.value)}
-                disabled={this.state.linkerType === Linker.types.growiLink}
-                placeholder={this.state.linkInputValue}
-              />
-            </div>
-          </div>
-        </form>
-      </>
-    );
-  }
-
-  renderPathFormatForm() {
-    const { t } = this.props;
-    return (
-      <div className="card well pt-3">
-        <form className="form-group mb-0">
-          <div className="form-group mb-0 row">
-            <label className="col-sm-3">{t('link_edit.path_format')}</label>
-            <div className="col-sm-9">
-              <div className="custom-control custom-checkbox custom-checkbox-info custom-control-inline">
-                <input
-                  className="custom-control-input"
-                  id="relativePath"
-                  type="checkbox"
-                  checked={this.state.isUseRelativePath}
-                  onChange={this.toggleIsUseRelativePath}
-                  disabled={!this.state.linkInputValue.startsWith('/') || this.state.linkerType === Linker.types.growiLink}
-                />
-                <label className="custom-control-label" htmlFor="relativePath">
-                  {t('link_edit.use_relative_path')}
-                </label>
-              </div>
-              <div className="custom-control custom-checkbox custom-checkbox-info custom-control-inline">
-                <input
-                  className="custom-control-input"
-                  id="permanentLink"
-                  type="checkbox"
-                  checked={this.state.isUsePermanentLink}
-                  onChange={this.toggleIsUsePamanentLink}
-                  disabled={this.state.permalink === '' || this.state.linkerType === Linker.types.growiLink}
-                />
-                <label className="custom-control-label" htmlFor="permanentLink">
-                  {t('link_edit.use_permanent_link')}
-                </label>
-              </div>
-            </div>
-          </div>
-          {this.isApplyPukiwikiLikeLinkerPlugin && (
-            <div className="form-group row mb-0 mt-1">
-              <label className="col-sm-3">{t('link_edit.notation')}</label>
-              <div className="col-sm-9">
-                <div className="custom-control custom-radio custom-control-inline">
-                  <input
-                    type="radio"
-                    className="custom-control-input"
-                    id="markdownType"
-                    value={Linker.types.markdownLink}
-                    checked={this.state.linkerType === Linker.types.markdownLink}
-                    onChange={e => this.handleSelecteLinkerType(e.target.value)}
-                  />
-                  <label className="custom-control-label" htmlFor="markdownType">
-                    {t('link_edit.markdown')}
-                  </label>
-                </div>
-                <div className="custom-control custom-radio custom-control-inline">
-                  <input
-                    type="radio"
-                    className="custom-control-input"
-                    id="pukiwikiType"
-                    value={Linker.types.pukiwikiLink}
-                    checked={this.state.linkerType === Linker.types.pukiwikiLink}
-                    onChange={e => this.handleSelecteLinkerType(e.target.value)}
-                  />
-                  <label className="custom-control-label" htmlFor="pukiwikiType">
-                    {t('link_edit.pukiwiki')}
-                  </label>
-                </div>
-              </div>
-            </div>
-          )}
-        </form>
-      </div>
-    );
-  }
-
-  render() {
-    const { t } = this.props;
-    return (
-      <Modal className="link-edit-modal" isOpen={this.state.show} toggle={this.cancel} size="lg" autoFocus={false}>
-        <ModalHeader tag="h4" toggle={this.cancel} className="bg-primary text-light">
-          {t('link_edit.edit_link')}
-        </ModalHeader>
-
-        <ModalBody className="container">
-          <div className="row">
-            <div className="col-12">
-              {this.renderLinkAndLabelForm()}
-              {this.renderPathFormatForm()}
-            </div>
-          </div>
-          <div className="row">
-            <div className="col-12">
-              <h3 className="grw-modal-head">{t('link_edit.preview')}</h3>
-              {this.renderLinkPreview()}
-            </div>
-          </div>
-        </ModalBody>
-        <ModalFooter>
-          <button type="button" className="btn btn-sm btn-outline-secondary mx-1" onClick={this.hide}>
-            {t('Cancel')}
-          </button>
-          <button type="submit" className="btn btn-sm btn-primary mx-1" onClick={this.save}>
-            {t('Done')}
-          </button>
-        </ModalFooter>
-      </Modal>
-    );
-  }
-
-}
-
-const LinkEditModalFc = React.memo(React.forwardRef((props, ref) => {
-  const { t } = useTranslation();
-  const { data: currentPath } = useCurrentPagePath();
-  return <LinkEditModal t={t} ref={ref} pagePath={currentPath} {...props} />;
-}));
-
-LinkEditModal.propTypes = {
-  t: PropTypes.func.isRequired,
-  pagePath: PropTypes.string,
-  onSave: PropTypes.func,
-};
-
-
-export default LinkEditModalFc;

+ 372 - 0
apps/app/src/components/PageEditor/LinkEditModal.tsx

@@ -0,0 +1,372 @@
+import React, { useEffect, useState, useCallback } from 'react';
+
+import path from 'path';
+
+import { useTranslation } from 'next-i18next';
+import {
+  Modal,
+  ModalHeader,
+  ModalBody,
+  ModalFooter,
+  Popover,
+  PopoverBody,
+} from 'reactstrap';
+import validator from 'validator';
+
+
+import Linker from '~/client/models/Linker';
+import { apiv3Get } from '~/client/util/apiv3-client';
+import { useLinkEditModal } from '~/stores/modal';
+import { useCurrentPagePath } from '~/stores/page';
+import { usePreviewOptions } from '~/stores/renderer';
+import loggerFactory from '~/utils/logger';
+
+import PagePreviewIcon from '../Icons/PagePreviewIcon';
+import SearchTypeahead from '../SearchTypeahead';
+
+import Preview from './Preview';
+
+
+import styles from './LinkEditPreview.module.scss';
+
+
+const logger = loggerFactory('growi:components:LinkEditModal');
+
+export const LinkEditModal = (): JSX.Element => {
+  const { t } = useTranslation();
+  const { data: currentPath } = useCurrentPagePath();
+  const { data: rendererOptions } = usePreviewOptions();
+  const { data: linkEditModalStatus, close } = useLinkEditModal();
+
+  const [isUseRelativePath, setIsUseRelativePath] = useState<boolean>(false);
+  const [isUsePermanentLink, setIsUsePermanentLink] = useState<boolean>(false);
+  const [linkInputValue, setLinkInputValue] = useState<string>('');
+  const [labelInputValue, setLabelInputValue] = useState<string>('');
+  const [linkerType, setLinkerType] = useState<string>('');
+  const [markdown, setMarkdown] = useState<string>('');
+  const [pagePath, setPagePath] = useState<string>('');
+  const [previewError, setPreviewError] = useState<string>();
+  const [permalink, setPermalink] = useState<string>('');
+  const [isPreviewOpen, setIsPreviewOpen] = useState<boolean>(false);
+
+  const getRootPath = useCallback((type: string) => {
+    // rootPaths of md link and pukiwiki link are different
+    if (currentPath == null) return '';
+    return type === Linker.types.markdownLink ? path.dirname(currentPath) : currentPath;
+  }, [currentPath]);
+
+  // parse link, link is ...
+  // case-1. url of this growi's page (ex. 'http://localhost:3000/hoge/fuga')
+  // case-2. absolute path of this growi's page (ex. '/hoge/fuga')
+  // case-3. relative path of this growi's page (ex. '../fuga', 'hoge')
+  // case-4. external link (ex. 'https://growi.org')
+  // case-5. the others (ex. '')
+  const parseLinkAndSetState = useCallback((link: string, type: string) => {
+    // create url from link, add dummy origin if link is not valid url.
+    // ex-1. link = 'https://growi.org/' -> url = 'https://growi.org/' (case-1,4)
+    // ex-2. link = 'hoge' -> url = 'http://example.com/hoge' (case-2,3,5)
+    let isFqcn = false;
+    let isUseRelativePath = false;
+    let url;
+    try {
+      const url = new URL(link, 'http://example.com');
+      isFqcn = url.origin !== 'http://example.com';
+    }
+    catch (err) {
+      logger.debug(err);
+    }
+
+    // case-1: when link is this growi's page url, return pathname only
+    let reshapedLink = url != null && url.origin === window.location.origin
+      ? decodeURIComponent(url.pathname)
+      : link;
+
+    // case-3
+    if (!isFqcn && !reshapedLink.startsWith('/') && reshapedLink !== '') {
+      isUseRelativePath = true;
+      const rootPath = getRootPath(type);
+      reshapedLink = path.resolve(rootPath, reshapedLink);
+    }
+
+    setLinkInputValue(reshapedLink);
+    setIsUseRelativePath(isUseRelativePath);
+  }, [getRootPath]);
+
+  useEffect(() => {
+    if (linkEditModalStatus == null) { return }
+    const { label = '', link = '' } = linkEditModalStatus.defaultMarkdownLink ?? {};
+    const { type = Linker.types.markdownLink } = linkEditModalStatus.defaultMarkdownLink ?? {};
+
+    parseLinkAndSetState(link, type);
+    setLabelInputValue(label);
+    setIsUsePermanentLink(false);
+    setPermalink('');
+    setLinkerType(type);
+
+  }, [linkEditModalStatus, parseLinkAndSetState]);
+
+  const toggleIsUseRelativePath = () => {
+    if (!linkInputValue.startsWith('/') || linkerType === Linker.types.growiLink) {
+      return;
+    }
+
+    // User can't use both relativePath and permalink at the same time
+    setIsUseRelativePath(!isUseRelativePath);
+    setIsUsePermanentLink(false);
+  };
+
+  const toggleIsUsePamanentLink = () => {
+    if (permalink === '' || linkerType === Linker.types.growiLink) {
+      return;
+    }
+
+    // User can't use both relativePath and permalink at the same time
+    setIsUsePermanentLink(!isUsePermanentLink);
+    setIsUseRelativePath(false);
+  };
+
+  const setMarkdownHandler = async() => {
+    const path = linkInputValue;
+    let markdown = '';
+    let pagePath = '';
+    let permalink = '';
+
+    if (path.startsWith('/')) {
+      try {
+        const pathWithoutFragment = new URL(path, 'http://dummy').pathname;
+        const isPermanentLink = validator.isMongoId(pathWithoutFragment.slice(1));
+        const pageId = isPermanentLink ? pathWithoutFragment.slice(1) : null;
+
+        const { data } = await apiv3Get('/page', { path: pathWithoutFragment, page_id: pageId });
+        const { page } = data;
+        markdown = page.revision.body;
+        pagePath = page.path;
+        permalink = page.id;
+      }
+      catch (err) {
+        setPreviewError(err.message);
+      }
+    }
+    else {
+      setPreviewError(t('link_edit.page_not_found_in_preview', { path }));
+    }
+
+    setMarkdown(markdown);
+    setPagePath(pagePath);
+    setPermalink(permalink);
+  };
+
+  const generateLink = () => {
+
+    let reshapedLink = linkInputValue;
+    if (isUseRelativePath) {
+      const rootPath = getRootPath(linkerType);
+      reshapedLink = rootPath === linkInputValue ? '.' : path.relative(rootPath, linkInputValue);
+    }
+
+    if (isUsePermanentLink && permalink != null) {
+      reshapedLink = permalink;
+    }
+
+    return new Linker(linkerType, labelInputValue, reshapedLink);
+  };
+
+  const renderLinkPreview = (): JSX.Element => {
+    const linker = generateLink();
+    return (
+      <div className="d-flex justify-content-between mb-3 flex-column flex-sm-row">
+        <div className="card card-disabled w-100 p-1 mb-0">
+          <p className="text-left text-muted mb-1 small">Markdown</p>
+          <p className="text-center text-truncate text-muted">{linker.generateMarkdownText()}</p>
+        </div>
+        <div className="d-flex align-items-center justify-content-center">
+          <span className="lead mx-3">
+            <i className="d-none d-sm-block fa fa-caret-right"></i>
+            <i className="d-sm-none fa fa-caret-down"></i>
+          </span>
+        </div>
+        <div className="card w-100 p-1 mb-0">
+          <p className="text-left text-muted mb-1 small">HTML</p>
+          <p className="text-center text-truncate">
+            <a href={linker.link}>{linker.label}</a>
+          </p>
+        </div>
+      </div>
+    );
+  };
+
+  const handleChangeTypeahead = (selected) => {
+    const pageWithMeta = selected[0];
+    if (pageWithMeta != null) {
+      const page = pageWithMeta.data;
+      const permalink = `${window.location.origin}/${page.id}`;
+      setLinkInputValue(page.path);
+      setPermalink(permalink);
+    }
+  };
+
+  const handleChangeLabelInput = (label: string) => {
+    setLabelInputValue(label);
+  };
+
+  const handleChangeLinkInput = (link) => {
+    let useRelativePath = isUseRelativePath;
+    if (!linkInputValue.startsWith('/') || linkerType === Linker.types.growiLink) {
+      useRelativePath = false;
+    }
+    setLinkInputValue(link);
+    setIsUseRelativePath(useRelativePath);
+    setIsUsePermanentLink(false);
+    setPermalink('');
+  };
+
+  const save = () => {
+    const linker = generateLink();
+
+    if (linkEditModalStatus?.onSave != null) {
+      linkEditModalStatus.onSave(linker.generateMarkdownText() ?? '');
+    }
+
+    close();
+  };
+
+  const toggleIsPreviewOpen = async() => {
+    // open popover
+    if (!isPreviewOpen) {
+      setMarkdownHandler();
+    }
+    setIsPreviewOpen(!isPreviewOpen);
+  };
+
+  const renderLinkAndLabelForm = (): JSX.Element => {
+    return (
+      <>
+        <h3 className="grw-modal-head">{t('link_edit.set_link_and_label')}</h3>
+        <form className="form-group">
+          <div className="form-gorup my-3">
+            <div className="input-group flex-nowrap">
+              <div className="input-group-prepend">
+                <span className="input-group-text">{t('link_edit.link')}</span>
+              </div>
+              <SearchTypeahead
+                onChange={handleChangeTypeahead}
+                onInputChange={handleChangeLinkInput}
+                placeholder={t('link_edit.placeholder_of_link_input')}
+                keywordOnInit={linkInputValue}
+                autoFocus
+              />
+              <div className="d-none d-sm-block input-group-append">
+                <button type="button" id="preview-btn" className={`btn btn-info btn-page-preview ${styles['btn-page-preview']}`}>
+                  <PagePreviewIcon />
+                </button>
+                <Popover trigger="focus" placement="right" isOpen={isPreviewOpen} target="preview-btn" toggle={toggleIsPreviewOpen}>
+                  <PopoverBody>
+                    {markdown != null && pagePath != null && rendererOptions != null
+                    && <div className={`linkedit-preview ${styles['linkedit-preview']}`}>
+                      <Preview markdown={markdown} pagePath={pagePath} rendererOptions={rendererOptions} />
+                    </div>
+                    }
+                  </PopoverBody>
+                </Popover>
+              </div>
+            </div>
+          </div>
+          <div className="form-gorup my-3">
+            <div className="input-group flex-nowrap">
+              <div className="input-group-prepend">
+                <span className="input-group-text">{t('link_edit.label')}</span>
+              </div>
+              <input
+                type="text"
+                className="form-control"
+                id="label"
+                value={labelInputValue}
+                onChange={e => handleChangeLabelInput(e.target.value)}
+                disabled={linkerType === Linker.types.growiLink}
+                placeholder={linkInputValue}
+              />
+            </div>
+          </div>
+        </form>
+      </>
+    );
+  };
+
+  const renderPathFormatForm = (): JSX.Element => {
+    return (
+      <div className="card well pt-3">
+        <form className="form-group mb-0">
+          <div className="form-group mb-0 row">
+            <label className="col-sm-3">{t('link_edit.path_format')}</label>
+            <div className="col-sm-9">
+              <div className="custom-control custom-checkbox custom-checkbox-info custom-control-inline">
+                <input
+                  className="custom-control-input"
+                  id="relativePath"
+                  type="checkbox"
+                  checked={isUseRelativePath}
+                  onChange={toggleIsUseRelativePath}
+                  disabled={!linkInputValue.startsWith('/') || linkerType === Linker.types.growiLink}
+                />
+                <label className="custom-control-label" htmlFor="relativePath">
+                  {t('link_edit.use_relative_path')}
+                </label>
+              </div>
+              <div className="custom-control custom-checkbox custom-checkbox-info custom-control-inline">
+                <input
+                  className="custom-control-input"
+                  id="permanentLink"
+                  type="checkbox"
+                  checked={isUsePermanentLink}
+                  onChange={toggleIsUsePamanentLink}
+                  disabled={permalink === '' || linkerType === Linker.types.growiLink}
+                />
+                <label className="custom-control-label" htmlFor="permanentLink">
+                  {t('link_edit.use_permanent_link')}
+                </label>
+              </div>
+            </div>
+          </div>
+        </form>
+      </div>
+    );
+  };
+
+  if (linkEditModalStatus == null) {
+    return <></>;
+  }
+
+  return (
+    <Modal className="link-edit-modal" isOpen={linkEditModalStatus.isOpened} toggle={close} size="lg" autoFocus={false}>
+      <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
+        {t('link_edit.edit_link')}
+      </ModalHeader>
+
+      <ModalBody className="container">
+        <div className="row">
+          <div className="col-12">
+            {renderLinkAndLabelForm()}
+            {renderPathFormatForm()}
+          </div>
+        </div>
+        <div className="row">
+          <div className="col-12">
+            <h3 className="grw-modal-head">{t('link_edit.preview')}</h3>
+            {renderLinkPreview()}
+          </div>
+        </div>
+      </ModalBody>
+      <ModalFooter>
+        { previewError && <span className='text-danger'>{previewError}</span>}
+        <button type="button" className="btn btn-sm btn-outline-secondary mx-1" onClick={close}>
+          {t('Cancel')}
+        </button>
+        <button type="submit" className="btn btn-sm btn-primary mx-1" onClick={save}>
+          {t('Done')}
+        </button>
+      </ModalFooter>
+    </Modal>
+  );
+};
+
+LinkEditModal.displayName = 'LinkEditModal';

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

@@ -75,6 +75,7 @@ const GrowiSubNavigationSwitcher = dynamic<GrowiSubNavigationSwitcherProps>(() =
 const DrawioModal = dynamic(() => import('../components/PageEditor/DrawioModal').then(mod => mod.DrawioModal), { ssr: false });
 const HandsontableModal = dynamic(() => import('../components/PageEditor/HandsontableModal').then(mod => mod.HandsontableModal), { ssr: false });
 const TemplateModal = dynamic(() => import('../components/TemplateModal').then(mod => mod.TemplateModal), { ssr: false });
+const LinkEditModal = dynamic(() => import('../components/PageEditor/LinkEditModal').then(mod => mod.LinkEditModal), { ssr: false });
 const PageStatusAlert = dynamic(() => import('../components/PageStatusAlert').then(mod => mod.PageStatusAlert), { ssr: false });
 const QuestionnaireModalManager = dynamic(() => import('~/features/questionnaire/client/components/QuestionnaireModalManager'), { ssr: false });
 
@@ -380,6 +381,7 @@ Page.getLayout = function getLayout(page: React.ReactElement<Props>) {
       <HandsontableModal />
       <QuestionnaireModalManager />
       <TemplateModal />
+      <LinkEditModal />
     </>
   );
 };
@@ -583,8 +585,8 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
     // XSS Options
     isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
     xssOption: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    attrWhiteList: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    tagWhiteList: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
+    attrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
+    tagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
 

+ 9 - 2
apps/app/src/pages/_app.page.tsx

@@ -33,7 +33,14 @@ const lato = Lato({
   subsets: ['latin'],
   display: 'swap',
 });
-const sourceHanCodeJP = localFont({ src: '../../resource/fonts/SourceHanCodeJP-Regular.woff2' });
+const sourceHanCodeJPSubsetMain = localFont({
+  src: '../../resource/fonts/SourceHanCodeJP-Regular-subset-main.woff2',
+  display: 'optional',
+});
+const sourceHanCodeJPSubsetJis2 = localFont({
+  src: '../../resource/fonts/SourceHanCodeJP-Regular-subset-jis2.woff2',
+  display: 'optional',
+});
 
 // eslint-disable-next-line @typescript-eslint/ban-types
 export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
@@ -72,7 +79,7 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
         :root {
           --font-family-sans-serif: ${lato.style.fontFamily}, -apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif;
           --font-family-serif: Georgia, 'Times New Roman', Times, serif;
-          --font-family-monospace: monospace, ${sourceHanCodeJP.style.fontFamily};
+          --font-family-monospace: monospace, ${sourceHanCodeJPSubsetMain.style.fontFamily}, ${sourceHanCodeJPSubsetJis2.style.fontFamily};
         }
       `}</style>
       <SWRConfig value={swrGlobalConfiguration}>

+ 2 - 2
apps/app/src/pages/_private-legacy-pages.page.tsx

@@ -101,8 +101,8 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
     // XSS Options
     isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
     xssOption: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    attrWhiteList: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    tagWhiteList: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
+    attrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
+    tagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
 }

+ 2 - 2
apps/app/src/pages/_search.page.tsx

@@ -138,8 +138,8 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
     // XSS Options
     isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
     xssOption: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    attrWhiteList: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    tagWhiteList: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
+    attrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
+    tagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
 

+ 3 - 3
apps/app/src/pages/login/index.page.tsx

@@ -28,7 +28,7 @@ type Props = CommonProps & {
   pageWithMetaStr: string,
   isMailerSetup: boolean,
   enabledStrategies: unknown,
-  registrationWhiteList: string[],
+  registrationWhitelist: string[],
   isLocalStrategySetup: boolean,
   isLdapStrategySetup: boolean,
   isLdapSetupFailed: boolean,
@@ -60,7 +60,7 @@ const LoginPage: NextPage<Props> = (props: Props) => {
         isLdapStrategySetup={props.isLdapStrategySetup}
         isLdapSetupFailed={props.isLdapSetupFailed}
         isEmailAuthenticationEnabled={props.isEmailAuthenticationEnabled}
-        registrationWhiteList={props.registrationWhiteList}
+        registrationWhitelist={props.registrationWhitelist}
         isPasswordResetEnabled={props.isPasswordResetEnabled}
         isMailerSetup={props.isMailerSetup}
         registrationMode={props.registrationMode}
@@ -113,7 +113,7 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
   props.isLocalStrategySetup = passportService.isLocalStrategySetup;
   props.isLdapStrategySetup = passportService.isLdapStrategySetup;
   props.isLdapSetupFailed = configManager.getConfig('crowi', 'security:passport-ldap:isEnabled') && !props.isLdapStrategySetup;
-  props.registrationWhiteList = configManager.getConfig('crowi', 'security:registrationWhiteList');
+  props.registrationWhitelist = configManager.getConfig('crowi', 'security:registrationWhitelist');
   props.isEmailAuthenticationEnabled = configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled');
   props.registrationMode = configManager.getConfig('crowi', 'security:registrationMode');
 }

+ 6 - 6
apps/app/src/pages/me/[[...path]].page.tsx

@@ -17,7 +17,7 @@ import {
   useCurrentUser, useIsSearchPage,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useCsrfToken, useIsSearchScopeChildrenAsDefault,
-  useRegistrationWhiteList, useShowPageLimitationXL, useRendererConfig,
+  useRegistrationWhitelist, useShowPageLimitationXL, useRendererConfig,
 } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 
@@ -38,7 +38,7 @@ type Props = CommonProps & {
   showPageLimitationXL: number,
 
   // config
-  registrationWhiteList: string[],
+  registrationWhitelist: string[],
 };
 
 const PersonalSettings = dynamic(() => import('~/components/Me/PersonalSettings'), { ssr: false });
@@ -82,7 +82,7 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
 
   useCurrentUser(props.currentUser ?? null);
 
-  useRegistrationWhiteList(props.registrationWhiteList);
+  useRegistrationWhitelist(props.registrationWhitelist);
 
   useShowPageLimitationXL(props.showPageLimitationXL);
 
@@ -143,7 +143,7 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
   props.isSearchServiceReachable = searchService.isReachable;
   props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
 
-  props.registrationWhiteList = configManager.getConfig('crowi', 'security:registrationWhiteList');
+  props.registrationWhitelist = configManager.getConfig('crowi', 'security:registrationWhitelist');
 
   props.showPageLimitationXL = crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL');
 
@@ -164,8 +164,8 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
     // XSS Options
     isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
     xssOption: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    attrWhiteList: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    tagWhiteList: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
+    attrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
+    tagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
 }

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

@@ -161,8 +161,8 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
     // XSS Options
     isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
     xssOption: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    attrWhiteList: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    tagWhiteList: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
+    attrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
+    tagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
     highlightJsStyleBorder: configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
 }

+ 2 - 2
apps/app/src/server/models/activity.ts

@@ -1,6 +1,6 @@
 import { Ref, IPage } from '@growi/core';
 import {
-  Types, Document, Model, Schema,
+  Types, Document, Model, Schema, SortOrder,
 } from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 
@@ -119,7 +119,7 @@ activitySchema.statics.updateByParameters = async function(activityId: string, p
 };
 
 activitySchema.statics.findSnapshotUsernamesByUsernameRegexWithTotalCount = async function(
-    q: string, option: { sortOpt: number | string, offset: number, limit: number},
+    q: string, option: { sortOpt: SortOrder, offset: number, limit: number},
 ): Promise<{usernames: string[], totalCount: number}> {
   const opt = option || {};
   const sortOpt = opt.sortOpt || 1;

+ 1 - 0
apps/app/src/server/models/bookmark-folder.ts

@@ -199,6 +199,7 @@ bookmarkFolderSchema.statics.findUserRootBookmarksItem = async function(userId:
   const bookmarkIdsInFolders = await this.distinct('bookmarks', { owner: userId });
   const userRootBookmarks: MyBookmarkList = await Bookmark.find({
     _id: { $nin: bookmarkIdsInFolders },
+    user: userId,
   }).populate({
     path: 'page',
     model: 'Page',

+ 3 - 3
apps/app/src/server/models/config.ts

@@ -57,7 +57,7 @@ export const defaultCrowiConfigs: { [key: string]: any } = {
   'security:restrictGuestMode'      : 'Deny',
 
   'security:registrationMode'      : 'Open',
-  'security:registrationWhiteList' : [],
+  'security:registrationWhitelist' : [],
 
   'security:list-policy:hideRestrictedByOwner' : false,
   'security:list-policy:hideRestrictedByGroup' : false,
@@ -142,8 +142,8 @@ export const defaultCrowiConfigs: { [key: string]: any } = {
 
 export const defaultMarkdownConfigs: { [key: string]: any } = {
   // don't use it, but won't turn it off
-  'markdown:xss:tagWhiteList': [],
-  'markdown:xss:attrWhiteList': [],
+  'markdown:xss:tagWhitelist': [],
+  'markdown:xss:attrWhitelist': [],
 
   'markdown:rehypeSanitize:isEnabledPrevention': true,
   'markdown:rehypeSanitize:option': RehypeSanitizeOption.RECOMMENDED,

+ 1 - 1
apps/app/src/server/models/user.js

@@ -347,7 +347,7 @@ module.exports = function(crowi) {
   userSchema.statics.isEmailValid = function(email, callback) {
     validateCrowi();
 
-    const whitelist = crowi.configManager.getConfig('crowi', 'security:registrationWhiteList');
+    const whitelist = crowi.configManager.getConfig('crowi', 'security:registrationWhitelist');
 
     if (Array.isArray(whitelist) && whitelist.length > 0) {
       return whitelist.some((allowedEmail) => {

+ 13 - 13
apps/app/src/server/routes/apiv3/markdown-setting.js

@@ -26,8 +26,8 @@ const validator = {
   ],
   xssSetting: [
     body('isEnabledXss').isBoolean(),
-    body('tagWhiteList').isArray(),
-    body('attrWhiteList').isString(),
+    body('tagWhitelist').isArray(),
+    body('attrWhitelist').isString(),
   ],
 };
 
@@ -73,15 +73,15 @@ const validator = {
  *          xssOption:
  *            type: number
  *            description: number of xss option
- *          tagWhiteList:
+ *          tagWhitelist:
  *            type: array
- *            description: array of tag whiteList
+ *            description: array of tag whitelist
  *            items:
  *              type: string
  *              description: tag whitelist
- *          attrWhiteList:
+ *          attrWhitelist:
  *            type: array
- *            description: array of attr whiteList
+ *            description: array of attr whitelist
  *            items:
  *              type: string
  *              description: attr whitelist
@@ -122,8 +122,8 @@ module.exports = (crowi) => {
       isIndentSizeForced: await crowi.configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
       isEnabledXss: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
       xssOption: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-      tagWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
-      attrWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes'),
+      tagWhitelist: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
+      attrWhitelist: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes'),
     };
 
     return res.apiv3({ markdownParams });
@@ -235,7 +235,7 @@ module.exports = (crowi) => {
     }
 
     try {
-      JSON.parse(req.body.attrWhiteList);
+      JSON.parse(req.body.attrWhitelist);
     }
     catch (err) {
       const msg = 'Error occurred in updating xss';
@@ -246,8 +246,8 @@ module.exports = (crowi) => {
     const reqestXssParams = {
       'markdown:rehypeSanitize:isEnabledPrevention': req.body.isEnabledXss,
       'markdown:rehypeSanitize:option': req.body.xssOption,
-      'markdown:rehypeSanitize:tagNames': req.body.tagWhiteList,
-      'markdown:rehypeSanitize:attributes': req.body.attrWhiteList,
+      'markdown:rehypeSanitize:tagNames': req.body.tagWhitelist,
+      'markdown:rehypeSanitize:attributes': req.body.attrWhitelist,
     };
 
     try {
@@ -255,8 +255,8 @@ module.exports = (crowi) => {
       const xssParams = {
         isEnabledXss: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
         xssOption: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-        tagWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
-        attrWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes'),
+        tagWhitelist: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
+        attrWhitelist: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes'),
       };
 
       const parameters = { action: SupportedAction.ACTION_ADMIN_MARKDOWN_XSS_UPDATE };

+ 6 - 6
apps/app/src/server/routes/apiv3/security-setting.js

@@ -41,7 +41,7 @@ const validator = {
     body('registrationMode').isString().isIn([
       'Open', 'Restricted', 'Closed',
     ]),
-    body('registrationWhiteList').if(value => value != null).isArray().customSanitizer((value, { req }) => {
+    body('registrationWhitelist').if(value => value != null).isArray().customSanitizer((value, { req }) => {
       return value.filter(email => email !== '');
     }),
   ],
@@ -145,12 +145,12 @@ const validator = {
  *          registrationMode:
  *            type: string
  *            description: type of registrationMode
- *          registrationWhiteList:
+ *          registrationWhitelist:
  *            type: array
  *            description: array of regsitrationList
  *            items:
  *              type: string
- *              description: registration whiteList
+ *              description: registration whitelist
  *      LdapAuthSetting:
  *        type: object
  *        properties:
@@ -363,7 +363,7 @@ module.exports = (crowi) => {
       localSetting: {
         useOnlyEnvVarsForSomeOptions: await crowi.configManager.getConfig('crowi', 'security:passport-local:useOnlyEnvVarsForSomeOptions'),
         registrationMode: await crowi.configManager.getConfig('crowi', 'security:registrationMode'),
-        registrationWhiteList: await crowi.configManager.getConfig('crowi', 'security:registrationWhiteList'),
+        registrationWhitelist: await crowi.configManager.getConfig('crowi', 'security:registrationWhitelist'),
         isPasswordResetEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-local:isPasswordResetEnabled'),
         isEmailAuthenticationEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled'),
       },
@@ -791,7 +791,7 @@ module.exports = (crowi) => {
   router.put('/local-setting', loginRequiredStrictly, adminRequired, addActivity, validator.localSetting, apiV3FormValidator, async(req, res) => {
     const requestParams = {
       'security:registrationMode': req.body.registrationMode,
-      'security:registrationWhiteList': req.body.registrationWhiteList,
+      'security:registrationWhitelist': req.body.registrationWhitelist,
       'security:passport-local:isPasswordResetEnabled': req.body.isPasswordResetEnabled,
       'security:passport-local:isEmailAuthenticationEnabled': req.body.isEmailAuthenticationEnabled,
     };
@@ -800,7 +800,7 @@ module.exports = (crowi) => {
 
       const localSettingParams = {
         registrationMode: await crowi.configManager.getConfig('crowi', 'security:registrationMode'),
-        registrationWhiteList: await crowi.configManager.getConfig('crowi', 'security:registrationWhiteList'),
+        registrationWhitelist: await crowi.configManager.getConfig('crowi', 'security:registrationWhitelist'),
         isPasswordResetEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-local:isPasswordResetEnabled'),
         isEmailAuthenticationEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled'),
       };

+ 2 - 2
apps/app/src/server/routes/page.js

@@ -155,8 +155,8 @@ module.exports = function(crowi, app) {
   const Xss = require('~/services/xss/index');
   const initializedConfig = {
     isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
-    tagWhiteList: xssService.getTagWhiteList(),
-    attrWhiteList: xssService.getAttrWhiteList(),
+    tagWhitelist: xssService.getTagWhitelist(),
+    attrWhitelist: xssService.getAttrWhitelist(),
   };
   const xssOption = new XssOption(initializedConfig);
   const xss = new Xss(xssOption);

+ 1 - 1
apps/app/src/server/service/config-loader.ts

@@ -330,7 +330,7 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
   },
   DISABLE_LINK_SHARING: {
     ns:      'crowi',
-    key:     'security:disableSharing',
+    key:     'security:disableLinkSharing',
     type:    ValueType.BOOLEAN,
     default: false,
   },

+ 3 - 3
apps/app/src/server/service/in-app-notification.ts

@@ -2,7 +2,7 @@ import {
   HasObjectId, SubscriptionStatusType, Ref, IPage, IUser,
 } from '@growi/core';
 import { subDays } from 'date-fns';
-import { Types } from 'mongoose';
+import { Types, FilterQuery, UpdateQuery } from 'mongoose';
 
 import { AllEssentialActions, SupportedAction } from '~/interfaces/activity';
 import { InAppNotificationStatuses, PaginateResult } from '~/interfaces/in-app-notification';
@@ -87,10 +87,10 @@ export default class InAppNotificationService {
     const now = createdAt || Date.now();
     const lastWeek = subDays(now, 7);
     const operations = users.map((user) => {
-      const filter = {
+      const filter: FilterQuery<InAppNotificationDocument> = {
         user, target, action, createdAt: { $gt: lastWeek }, snapshot,
       };
-      const parameters = {
+      const parameters: UpdateQuery<InAppNotificationDocument> = {
         user,
         targetModel,
         target,

+ 7 - 7
apps/app/src/server/service/page.ts

@@ -10,7 +10,7 @@ import {
 } from '@growi/core';
 import { collectAncestorPaths } from '@growi/core/dist/utils/page-path-utils/collect-ancestor-paths';
 import escapeStringRegexp from 'escape-string-regexp';
-import mongoose, { ObjectId, QueryCursor } from 'mongoose';
+import mongoose, { ObjectId, Cursor } from 'mongoose';
 import streamToPromise from 'stream-to-promise';
 
 import { SupportedAction } from '~/interfaces/activity';
@@ -60,7 +60,7 @@ class PageCursorsForDescendantsFactory {
 
   private shouldIncludeEmpty: boolean;
 
-  private initialCursor: QueryCursor<any> | never[]; // TODO: wait for mongoose update
+  private initialCursor: Cursor<any> | never[]; // TODO: wait for mongoose update
 
   private Page: PageModel;
 
@@ -100,7 +100,7 @@ class PageCursorsForDescendantsFactory {
   /**
    * Generator that unorderedly yields descendant pages
    */
-  private async* generateOnlyDescendants(cursor: QueryCursor<any>) {
+  private async* generateOnlyDescendants(cursor: Cursor<any>) {
     for await (const page of cursor) {
       const nextCursor = await this.generateCursorToFindChildren(page);
       if (!this.isNeverArray(nextCursor)) {
@@ -111,7 +111,7 @@ class PageCursorsForDescendantsFactory {
     }
   }
 
-  private async generateCursorToFindChildren(page: any): Promise<QueryCursor<any> | never[]> {
+  private async generateCursorToFindChildren(page: any): Promise<Cursor<any> | never[]> {
     if (page == null) {
       return [];
     }
@@ -121,12 +121,12 @@ class PageCursorsForDescendantsFactory {
     const builder = new PageQueryBuilder(this.Page.find(), this.shouldIncludeEmpty);
     builder.addConditionToFilteringByParentId(page._id);
 
-    const cursor = builder.query.lean().cursor({ batchSize: BULK_REINDEX_SIZE }) as QueryCursor<any>;
+    const cursor = builder.query.lean().cursor({ batchSize: BULK_REINDEX_SIZE }) as Cursor<any>;
 
     return cursor;
   }
 
-  private isNeverArray(val: QueryCursor<any> | never[]): val is never[] {
+  private isNeverArray(val: Cursor<any> | never[]): val is never[] {
     return 'length' in val && val.length === 0;
   }
 
@@ -3206,7 +3206,7 @@ class PageService {
   /**
    * Recount descendantCount of pages one by one
    */
-  async recountAndUpdateDescendantCountOfPages(pageCursor: QueryCursor<any>, batchSize:number): Promise<void> {
+  async recountAndUpdateDescendantCountOfPages(pageCursor: Cursor<any>, batchSize:number): Promise<void> {
     const Page = this.crowi.model('Page');
     const recountWriteStream = new Writable({
       objectMode: true,

+ 1 - 0
apps/app/src/server/service/search.ts

@@ -25,6 +25,7 @@ const logger = loggerFactory('growi:service:search');
 const nonNullable = <T>(value: T): value is NonNullable<T> => value != null;
 
 // options for filtering xss
+// Do not change the property key name to 'whitelist" because it depends on the 'xss' library
 const filterXssOptions = {
   whiteList: {
     em: ['class'],

+ 6 - 6
apps/app/src/server/service/xss.js

@@ -20,7 +20,7 @@ class XssSerivce {
     return this.xss.process(value);
   }
 
-  getTagWhiteList() {
+  getTagWhitelist() {
     const isEnabledXssPrevention = this.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention');
     const xssOpiton = this.configManager.getConfig('markdown', 'markdown:xss:option');
 
@@ -32,8 +32,8 @@ class XssSerivce {
         case 2: // recommended
           return tags;
 
-        case 3: // custom white list
-          return this.configManager.getConfig('markdown', 'markdown:xss:tagWhiteList');
+        case 3: // custom whitelist
+          return this.configManager.getConfig('markdown', 'markdown:xss:tagWhitelist');
 
         default:
           return [];
@@ -44,7 +44,7 @@ class XssSerivce {
     }
   }
 
-  getAttrWhiteList() {
+  getAttrWhitelist() {
     const isEnabledXssPrevention = this.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention');
     const xssOpiton = this.configManager.getConfig('markdown', 'markdown:xss:option');
 
@@ -56,8 +56,8 @@ class XssSerivce {
         case 2: // recommended
           return attrs;
 
-        case 3: // custom white list
-          return this.configManager.getConfig('markdown', 'markdown:xss:attrWhiteList');
+        case 3: // custom whitelist
+          return this.configManager.getConfig('markdown', 'markdown:xss:attrWhitelist');
 
         default:
           return [];

+ 2 - 1
apps/app/src/server/util/mongoose-utils.ts

@@ -29,7 +29,8 @@ export const getModelSafely = <T>(modelName: string): Model<T & Document> | null
   return null;
 };
 
-export const getOrCreateModel = <Interface, Method>(modelName: string, schema: Schema<Interface>): Method & Model<Interface & Document> => {
+// TODO: Do not use any type
+export const getOrCreateModel = <Interface, Method>(modelName: string, schema: any): any => {
   if (mongoose.modelNames().includes(modelName)) {
     return mongoose.model<Interface & Document, Method & Model<Interface & Document>>(modelName);
   }

+ 2 - 2
apps/app/src/services/renderer/renderer.tsx

@@ -57,8 +57,8 @@ let isInjectedCustomSanitaizeOption = false;
 
 export const injectCustomSanitizeOption = (config: RendererConfig): void => {
   if (!isInjectedCustomSanitaizeOption && config.isEnabledXssPrevention && config.xssOption === RehypeSanitizeOption.CUSTOM) {
-    commonSanitizeOption.tagNames = baseSanitizeSchema.tagNames.concat(config.tagWhiteList ?? []);
-    commonSanitizeOption.attributes = deepmerge(baseSanitizeSchema.attributes, config.attrWhiteList ?? {});
+    commonSanitizeOption.tagNames = baseSanitizeSchema.tagNames.concat(config.tagWhitelist ?? []);
+    commonSanitizeOption.attributes = deepmerge(baseSanitizeSchema.attributes, config.attrWhitelist ?? {});
     isInjectedCustomSanitaizeOption = true;
   }
 };

+ 6 - 6
apps/app/src/services/xss/index.js

@@ -10,17 +10,17 @@ class Xss {
 
     xssOption = xssOption || {}; // eslint-disable-line no-param-reassign
 
-    const tagWhiteList = xssOption.tagWhiteList || [];
-    const attrWhiteList = xssOption.attrWhiteList || [];
+    const tagWhitelist = xssOption.tagWhitelist || [];
+    const attrWhitelist = xssOption.attrWhitelist || [];
 
-    const whiteListContent = {};
+    const whitelistContent = {};
 
     // default
     const option = {
       stripIgnoreTag: true,
       stripIgnoreTagBody: false, // see https://github.com/weseek/growi/pull/505
       css: false,
-      whiteList: whiteListContent,
+      whitelist: whitelistContent,
       escapeHtml: (html) => { return html }, // resolve https://github.com/weseek/growi/issues/221
       onTag: (tag, html, options) => {
         // pass autolink
@@ -30,8 +30,8 @@ class Xss {
       },
     };
 
-    tagWhiteList.forEach((tag) => {
-      whiteListContent[tag] = attrWhiteList;
+    tagWhitelist.forEach((tag) => {
+      whitelistContent[tag] = attrWhitelist;
     });
 
     // create the XSS Filter instance

+ 9 - 8
apps/app/src/services/xss/xssOption.ts

@@ -1,31 +1,32 @@
 import { defaultSchema as sanitizeDefaultSchema } from 'rehype-sanitize';
+
 import type { RehypeSanitizeOption } from '~/interfaces/rehype';
 
-type tagWhiteList = typeof sanitizeDefaultSchema.tagNames;
-type attrWhiteList = typeof sanitizeDefaultSchema.attributes;
+type tagWhitelist = typeof sanitizeDefaultSchema.tagNames;
+type attrWhitelist = typeof sanitizeDefaultSchema.attributes;
 
 export type XssOptionConfig = {
   isEnabledXssPrevention: boolean,
   xssOption: RehypeSanitizeOption,
-  tagWhiteList: tagWhiteList,
-  attrWhiteList: attrWhiteList,
+  tagWhitelist: tagWhitelist,
+  attrWhitelist: attrWhitelist,
 }
 
 export default class XssOption {
 
   isEnabledXssPrevention: boolean;
 
-  tagWhiteList: any[];
+  tagWhitelist: any[];
 
-  attrWhiteList: any[];
+  attrWhitelist: any[];
 
   constructor(config: XssOptionConfig) {
     const recommendedWhitelist = require('~/services/xss/recommended-whitelist');
     const initializedConfig: Partial<XssOptionConfig> = (config != null) ? config : {};
 
     this.isEnabledXssPrevention = initializedConfig.isEnabledXssPrevention || true;
-    this.tagWhiteList = initializedConfig.tagWhiteList || recommendedWhitelist.tags;
-    this.attrWhiteList = initializedConfig.attrWhiteList || recommendedWhitelist.attrs;
+    this.tagWhitelist = initializedConfig.tagWhitelist || recommendedWhitelist.tags;
+    this.attrWhitelist = initializedConfig.attrWhitelist || recommendedWhitelist.attrs;
   }
 
 }

+ 2 - 2
apps/app/src/stores/context.tsx

@@ -68,8 +68,8 @@ export const useDisableLinkSharing = (initialData?: Nullable<boolean>): SWRRespo
   return useContextSWR<Nullable<boolean>, Error>('disableLinkSharing', initialData);
 };
 
-export const useRegistrationWhiteList = (initialData?: Nullable<string[]>): SWRResponse<Nullable<string[]>, Error> => {
-  return useContextSWR<Nullable<string[]>, Error>('registrationWhiteList', initialData);
+export const useRegistrationWhitelist = (initialData?: Nullable<string[]>): SWRResponse<Nullable<string[]>, Error> => {
+  return useContextSWR<Nullable<string[]>, Error>('registrationWhitelist', initialData);
 };
 
 export const useHackmdUri = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {

+ 29 - 0
apps/app/src/stores/modal.tsx

@@ -3,6 +3,7 @@ import { useCallback, useMemo } from 'react';
 import type { IAttachmentHasId } from '@growi/core';
 import { SWRResponse } from 'swr';
 
+import Linker from '~/client/models/Linker';
 import MarkdownTable from '~/client/models/MarkdownTable';
 import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 import { IPageToDeleteWithMeta, IPageToRenameWithMeta } from '~/interfaces/page';
@@ -695,4 +696,32 @@ export const useDeleteAttachmentModal = (): SWRResponse<DeleteAttachmentModalSta
     open,
     close,
   };
+
+/*
+ * LinkEditModal
+ */
+type LinkEditModalStatus = {
+  isOpened: boolean,
+  defaultMarkdownLink?: Linker,
+  onSave?: (linkText: string) => void
+}
+
+type LinkEditModalUtils = {
+  open(defaultMarkdownLink: Linker, onSave: (linkText: string) => void): void,
+  close(): void,
+}
+
+export const useLinkEditModal = (): SWRResponse<LinkEditModalStatus, Error> & LinkEditModalUtils => {
+
+  const initialStatus: LinkEditModalStatus = { isOpened: false };
+  const swrResponse = useStaticSWR<LinkEditModalStatus, Error>('linkEditModal', undefined, { fallbackData: initialStatus });
+
+  return Object.assign(swrResponse, {
+    open: (defaultMarkdownLink: Linker, onSave: (linkText: string) => void) => {
+      swrResponse.mutate({ isOpened: true, defaultMarkdownLink, onSave });
+    },
+    close: () => {
+      swrResponse.mutate({ isOpened: false });
+    },
+  });
 };

+ 0 - 137
apps/app/src/styles/_search.scss

@@ -1,137 +0,0 @@
-// styles for admin user search
-.admin-user-page {
-  .search-typeahead {
-    .search-clear {
-      top: 7px;
-      right: 4px;
-    }
-  }
-}
-
-// layout
-.on-search {
-  .page-wrapper {
-    padding-bottom: unset;
-  }
-
-  .search-control-include-options {
-    .card-body {
-      padding: 5px 10px;
-    }
-  }
-  .search-result-list {
-    .search-result-list-scroll {
-      // subtract the height of GrowiNavbar + (SearchControl component + other factors)
-      height: calc(100vh - (($grw-navbar-height + $grw-navbar-border-width) + 110px));
-      overflow-y: scroll;
-
-      @include media-breakpoint-down(sm) {
-        height: calc(100vh - (($grw-navbar-height + $grw-navbar-border-width + $grw-navbar-bottom-height) + 123px));
-      }
-    }
-
-    .search-result-keyword {
-      font-size: 17.5px;
-      font-weight: bold;
-    }
-    .search-result-select-group {
-      > select {
-        max-width: 8rem;
-      }
-    }
-
-    // list group
-    .page-list {
-      // not show top label in search result list
-      .page-list-meta {
-        .top-label {
-          display: none;
-        }
-      }
-    }
-  }
-
-  .search-result-content {
-    .search-result-content-nav {
-      min-height: $grw-subnav-search-preview-min-height;
-      overflow: auto;
-
-      .grw-subnav {
-        min-height: inherit;
-      }
-    }
-
-    .search-result-content {
-      height: calc(100vh - ($grw-navbar-height + $grw-navbar-border-width));
-
-      > h2 {
-        margin-right: 10px;
-        font-size: 22px;
-        line-height: 1em;
-      }
-
-      &:first-child > h2 {
-        margin-top: 0;
-      }
-
-      .search-result-content-body-container {
-        overflow-y: auto;
-
-        .wiki {
-          padding: 16px;
-          font-size: 13px;
-        }
-      }
-    }
-  }
-}
-
-// support for your search
-.grw-search-table {
-  caption {
-    display: table-header-group;
-  }
-}
-
-@include media-breakpoint-down(sm) {
-  .grw-search-table {
-    th {
-      text-align: right;
-    }
-
-    td {
-      overflow-wrap: anywhere;
-      white-space: normal !important;
-    }
-
-    @include media-breakpoint-down(xs) {
-      th,
-      td {
-        display: block;
-      }
-
-      th {
-        text-align: left;
-      }
-
-      td {
-        padding-top: 0 !important;
-        border-top: none !important;
-      }
-    }
-  }
-}
-
-
-// style to apply when displaying search page
-.on-search {
-  // set sidebar height shown in search page
-  $search-page-sidebar-height: calc(100vh - ($grw-navbar-height + $grw-navbar-border-width));
-
-  .grw-sidebar {
-    height: $search-page-sidebar-height;
-    .data-layout-container {
-      height: 100%;
-    }
-  }
-}

+ 5 - 1
apps/app/src/styles/organisms/_wiki.scss

@@ -11,7 +11,7 @@
     }
   }
 
-  font-size: 15px;
+  font-size: 16px;
 
   // @extend .text-break;
   // https://github.com/twbs/bootstrap/blob/v4.6.1/scss/utilities/_text.scss#L65-L68
@@ -83,6 +83,10 @@
     font-weight: normal;
   }
 
+  table {
+    font-size: 0.95em;
+  }
+
   blockquote {
     padding: 0 20px;
     margin: 0 0 30px 0;

+ 0 - 1
apps/app/src/styles/style-app.scss

@@ -21,7 +21,6 @@
 @import 'mirror_mode';
 @import 'modal';
 @import 'page-path';
-@import 'search';
 @import 'tag';
 @import 'installer';
 

+ 1 - 1
apps/app/test/integration/service/questionnaire-cron.test.ts

@@ -328,7 +328,7 @@ describe('QuestionnaireCronService', () => {
     await crowi.questionnaireCronService.executeJob();
 
     const savedOrders = await QuestionnaireOrder.find()
-      .select('-condition._id -questions._id')
+      .select('-condition._id -questions._id -questions.createdAt -questions.updatedAt')
       .sort({ _id: 1 });
     expect(JSON.parse(JSON.stringify(savedOrders))).toEqual([
       {

+ 2 - 2
apps/slackbot-proxy/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "6.1.0-slackbot-proxy.0",
+  "version": "6.1.1-slackbot-proxy.0",
   "license": "MIT",
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -25,7 +25,7 @@
   },
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^6.1.0-RC.0",
+    "@growi/slack": "^6.1.1-RC.0",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "6.1.0-RC.0",
+  "version": "6.1.1-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "6.1.0-RC.0",
+  "version": "6.1.1-RC.0",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/hackmd/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/hackmd",
-  "version": "6.1.0-RC.0",
+  "version": "6.1.1-RC.0",
   "description": "GROWI js and css files to use hackmd",
   "license": "MIT",
   "type": "module",

+ 2 - 2
packages/presentation/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/presentation",
-  "version": "6.1.0-RC.0",
+  "version": "6.1.1-RC.0",
   "description": "GROWI plugin for presentation",
   "license": "MIT",
   "keywords": ["growi", "growi-plugin"],
@@ -18,7 +18,7 @@
     "lint": "run-p lint:*"
   },
   "dependencies": {
-    "@growi/core": "^6.1.0-RC.0"
+    "@growi/core": "^6.1.1-RC.0"
   },
   "devDependencies": {
     "@marp-team/marp-core": "^3.6.0",

+ 1 - 1
packages/preset-themes/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@growi/preset-themes",
   "description": "GROWI preset themes",
-  "version": "6.1.0-RC.0",
+  "version": "6.1.1-RC.0",
   "license": "MIT",
   "main": "dist/libs/preset-themes.umd.js",
   "module": "dist/libs/preset-themes.mjs",

+ 6 - 11
packages/remark-attachment-refs/package.json

@@ -1,18 +1,13 @@
 {
   "name": "@growi/remark-attachment-refs",
-  "version": "6.1.0-RC.0",
+  "version": "6.1.1-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
-  "keywords": [
-    "growi",
-    "growi-plugin"
-  ],
+  "keywords": ["growi", "growi-plugin"],
   "main": "dist/index.js",
   "module": "dist/index.mjs",
   "types": "dist/index.d.ts",
-  "files": [
-    "dist"
-  ],
+  "files": ["dist"],
   "scripts": {
     "build": "run-p build:*",
     "build:server": "vite build -c vite.server.config.ts",
@@ -31,9 +26,9 @@
   "dependencies": {
     "bunyan": "^1.8.15",
     "universal-bunyan": "^0.9.2",
-    "@growi/core": "^6.1.0-RC.0",
-    "@growi/remark-growi-directive": "^6.1.0-RC.0",
-    "@growi/ui": "^6.1.0-RC.0"
+    "@growi/core": "^6.1.1-RC.0",
+    "@growi/remark-growi-directive": "^6.1.1-RC.0",
+    "@growi/ui": "^6.1.1-RC.0"
   },
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",

+ 1 - 1
packages/remark-drawio/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-drawio",
-  "version": "6.1.0-RC.0",
+  "version": "6.1.1-RC.0",
   "description": "remark plugin to draw diagrams with draw.io (diagrams.net)",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/remark-growi-directive/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-growi-directive",
-  "version": "6.1.0-RC.0",
+  "version": "6.1.1-RC.0",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "license": "MIT",
   "keywords": [

+ 4 - 4
packages/remark-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-lsx",
-  "version": "6.1.0-RC.0",
+  "version": "6.1.1-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": ["growi", "growi-plugin"],
@@ -25,9 +25,9 @@
     "escape-string-regexp": "5.0.0 or above exports only ESM"
   },
   "dependencies": {
-    "@growi/core": "^6.1.0-RC.0",
-    "@growi/remark-growi-directive": "^6.1.0-RC.0",
-    "@growi/ui": "^6.1.0-RC.0",
+    "@growi/core": "^6.1.1-RC.0",
+    "@growi/remark-growi-directive": "^6.1.1-RC.0",
+    "@growi/ui": "^6.1.1-RC.0",
     "escape-string-regexp": "^4.0.0",
     "swr": "^2.0.3"
   },

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slack",
-  "version": "6.1.0-RC.0",
+  "version": "6.1.1-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "module": "dist/index.mjs",

+ 2 - 2
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "6.1.0-RC.0",
+  "version": "6.1.1-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": ["growi"],
@@ -16,7 +16,7 @@
     "lint": "npm-run-all -p lint:*"
   },
   "dependencies": {
-    "@growi/core": "^6.1.0-RC.0"
+    "@growi/core": "^6.1.1-RC.0"
   },
   "devDependencies": {
     "react": "^18.2.0"

Разница между файлами не показана из-за своего большого размера
+ 659 - 0
yarn.lock


Некоторые файлы не были показаны из-за большого количества измененных файлов