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

Merge branch 'master' into feat/gw7773-notification-by-posting-comment

Mudana-Grune 3 лет назад
Родитель
Сommit
4f91aaea07
45 измененных файлов с 724 добавлено и 366 удалено
  1. 0 5
      .github/workflows/release-rc.yml
  2. 0 5
      .github/workflows/release.yml
  3. 25 2
      CHANGELOG.md
  4. 1 1
      lerna.json
  5. 1 1
      package.json
  6. 0 34
      packages/app/bin/shrink-emojione-strategy.js
  7. 1 0
      packages/app/docker/Dockerfile
  8. 2 2
      packages/app/docker/README.md
  9. 9 7
      packages/app/package.json
  10. 2 9
      packages/app/resource/cdn-manifests.js
  11. 11 13
      packages/app/resource/locales/en_US/sandbox.md
  12. 30 0
      packages/app/resource/locales/en_US/translation.json
  13. 11 13
      packages/app/resource/locales/ja_JP/sandbox.md
  14. 30 0
      packages/app/resource/locales/ja_JP/translation.json
  15. 11 13
      packages/app/resource/locales/zh_CN/sandbox.md
  16. 30 0
      packages/app/resource/locales/zh_CN/translation.json
  17. 0 4
      packages/app/src/client/services/AppContainer.js
  18. 23 2
      packages/app/src/client/services/PageContainer.js
  19. 0 0
      packages/app/src/client/util/emojione/emoji_strategy_shrinked.json
  20. 66 0
      packages/app/src/client/util/markdown-it/emoji-mart-data.ts
  21. 5 19
      packages/app/src/client/util/markdown-it/emoji.js
  22. 0 2
      packages/app/src/components/PageComment/CommentEditor.jsx
  23. 26 31
      packages/app/src/components/PageEditor.jsx
  24. 69 12
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  25. 0 1
      packages/app/src/components/PageEditor/Editor.jsx
  26. 14 0
      packages/app/src/components/PageEditor/EditorIcon.jsx
  27. 0 149
      packages/app/src/components/PageEditor/EmojiAutoCompleteHelper.js
  28. 64 0
      packages/app/src/components/PageEditor/EmojiPicker.tsx
  29. 124 0
      packages/app/src/components/PageEditor/EmojiPickerHelper.ts
  30. 5 6
      packages/app/src/components/PageEditor/OptionsSelector.jsx
  31. 7 17
      packages/app/src/server/routes/apiv3/user-group.js
  32. 22 3
      packages/app/src/server/service/user-group.ts
  33. 0 1
      packages/app/src/styles/_override-bootstrap.scss
  34. 3 0
      packages/app/src/styles/_vendor.scss
  35. 8 0
      packages/app/src/styles/theme/_apply-colors.scss
  36. 88 0
      packages/app/test/integration/service/user-groups.test.ts
  37. 1 1
      packages/codemirror-textlint/package.json
  38. 1 1
      packages/core/package.json
  39. 1 1
      packages/plugin-attachment-refs/package.json
  40. 1 1
      packages/plugin-lsx/package.json
  41. 1 1
      packages/plugin-pukiwiki-like-linker/package.json
  42. 1 1
      packages/slack/package.json
  43. 2 2
      packages/slackbot-proxy/package.json
  44. 1 1
      packages/ui/package.json
  45. 27 5
      yarn.lock

+ 0 - 5
.github/workflows/release-rc.yml

@@ -54,8 +54,3 @@ jobs:
         cache-from: type=gha
         cache-to: type=gha,mode=max
         tags: ${{ steps.meta.outputs.tags }}
-
-    - name: Move cache
-      run: |
-        rm -rf /tmp/.buildx-cache
-        mv /tmp/.buildx-cache-new /tmp/.buildx-cache

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

@@ -183,11 +183,6 @@ jobs:
         cache-to: type=gha,mode=max
         tags: ${{ steps.meta.outputs.tags }}
 
-    - name: Move cache
-      run: |
-        rm -rf /tmp/.buildx-cache
-        mv /tmp/.buildx-cache-new /tmp/.buildx-cache
-
     - name: Update Docker Hub Description
       uses: peter-evans/dockerhub-description@v3
       with:

+ 25 - 2
CHANGELOG.md

@@ -1,9 +1,33 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v5.0.4...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v5.0.5...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v5.0.5](https://github.com/weseek/growi/compare/v5.0.4...v5.0.5) - 2022-05-16
+
+### 💎 Features
+
+- feat: Empty trash button in trash page (#5816) @yukendev
+
+### 🚀 Improvement
+
+- imprv: Count badge colors (#5835) @shukmos
+- imprv: List group background colors on PageTree (#5812) @shukmos
+- imprv: Page path auto complete function for page rename modal (#5805) @kaoritokashiki
+- imprv: Show toastr when converting is completed on Private Legacy Page (#5810) @yukendev
+- imprv: Create parent pages as needed by path that includes slash (#5809) @kaoritokashiki
+
+### 🐛 Bug Fixes
+
+- fix: Change the execution user of the official docker image to root (#5846) @yuki-takei
+- fix: Display admin link only with logged in (#5799) @hirokei-camel
+- fix: Error when renaming (#5793) @miya
+
+### 🧰 Maintenance
+
+- support: Typescriptize tag model (#5778) @kaoritokashiki
+
 ## [v5.0.4](https://github.com/weseek/growi/compare/v5.0.3...v5.0.4) - 2022-04-28
 
 ### 💎 Features
@@ -177,7 +201,6 @@
 - support: update nanoid yarn.lock v3.1.30 to v3.2.0 (#5216) @LuqmanHakim-Grune
 - support: update validator version (#5562) @LuqmanHakim-Grune
 
-
 ## [v4.5.15](https://github.com/weseek/growi/compare/v4.5.14...v4.5.15) - 2022-02-17
 
 ### 🚀 Improvement

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
   "npmClient": "yarn",
   "useWorkspaces": true,
-  "version": "5.0.5-RC.0",
+  "version": "5.0.6-RC.0",
   "packages": [
     "packages/*"
   ]

+ 1 - 1
package.json

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

+ 0 - 34
packages/app/bin/shrink-emojione-strategy.js

@@ -1,34 +0,0 @@
-/**
- * the tool to shrink emojione/emoji_strategy.json and output
- *
- * @author Yuki Takei <yuki@weseek.co.jp>
- */
-/*
-require('module-alias/register');
-
-const fs = require('graceful-fs');
-
-const helpers = require('@commons/util/helpers');
-
-const emojiStrategy = require('emojione/emoji_strategy.json');
-const markdownItEmojiFull = require('markdown-it-emoji/lib/data/full.json');
-
-const OUT = helpers.root('tmp/emoji_strategy_shrinked.json');
-
-const shrinkedMap = {};
-Object.keys(emojiStrategy).forEach((unicode) => {
-  const data = emojiStrategy[unicode];
-  const shortname = data.shortname.replace(/:/g, '');
-
-  // ignore if it isn't included in markdownItEmojiFull
-  if (markdownItEmojiFull[shortname] == null) {
-    return;
-  }
-
-  // add
-  shrinkedMap[unicode] = data;
-});
-
-// write
-fs.writeFileSync(OUT, JSON.stringify(shrinkedMap));
-*/

+ 1 - 0
packages/app/docker/Dockerfile

@@ -159,6 +159,7 @@ RUN rm node_modules.tar packages.tar
 
 COPY --chown=node:node --chmod=700 packages/app/docker/docker-entrypoint.sh /
 
+USER root
 WORKDIR ${appDir}/packages/app
 
 VOLUME /data

+ 2 - 2
packages/app/docker/README.md

@@ -10,8 +10,8 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`5.0.4`, `5.0`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.4/docker/Dockerfile)
-* [`5.0.4-nocdn`, `5.0-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.4/docker/Dockerfile)
+* [`5.0.5`, `5.0`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.5/docker/Dockerfile)
+* [`5.0.5-nocdn`, `5.0-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.5/docker/Dockerfile)
 * [`4.5.15`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.15/docker/Dockerfile)
 * [`4.5.15-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.15/docker/Dockerfile)
 * [`4.4.13`, `4.4` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)

+ 9 - 7
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "5.0.5-RC.0",
+  "version": "5.0.6-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -62,11 +62,11 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.0.5-RC.0",
-    "@growi/plugin-attachment-refs": "^5.0.5-RC.0",
-    "@growi/plugin-lsx": "^5.0.5-RC.0",
-    "@growi/plugin-pukiwiki-like-linker": "^5.0.5-RC.0",
-    "@growi/slack": "^5.0.5-RC.0",
+    "@growi/codemirror-textlint": "^5.0.6-RC.0",
+    "@growi/plugin-attachment-refs": "^5.0.6-RC.0",
+    "@growi/plugin-lsx": "^5.0.6-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^5.0.6-RC.0",
+    "@growi/slack": "^5.0.6-RC.0",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
@@ -167,7 +167,7 @@
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^5.0.5-RC.0",
+    "@growi/ui": "^5.0.6-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
@@ -187,6 +187,8 @@
     "csv-to-markdown-table": "^1.0.1",
     "diff2html": "^3.1.2",
     "eazy-logger": "^3.1.0",
+    "emoji-mart": "npm:panta82-emoji-mart@^3.0.1",
+    "markdown-it-emoji-mart": "^0.1.1",
     "eslint-plugin-cypress": "^2.12.1",
     "eslint-plugin-regex": "^1.8.0",
     "file-loader": "^5.0.2",

+ 2 - 9
packages/app/resource/cdn-manifests.js

@@ -3,7 +3,7 @@ module.exports = {
     {
       name: 'basis',
       // eslint-disable-next-line max-len
-      url: 'https://cdn.jsdelivr.net/combine/npm/emojione@3.1.2,npm/jquery@3.4.0,npm/popper.js@1.15.0,npm/bootstrap@4.5.0/dist/js/bootstrap.min.js,npm/scrollpos-styler@0.7.1,npm/jquery-slimscroll@1.3.8/jquery.slimscroll.min.js',
+      url: 'https://cdn.jsdelivr.net/combine/npm/jquery@3.4.0,npm/popper.js@1.15.0,npm/bootstrap@4.5.0/dist/js/bootstrap.min.js,npm/scrollpos-styler@0.7.1,npm/jquery-slimscroll@1.3.8/jquery.slimscroll.min.js',
       groups: ['basis'],
       args: {
         integrity: '',
@@ -138,14 +138,7 @@ module.exports = {
         integrity: '',
       },
     },
-    {
-      name: 'emojione',
-      url: 'https://cdn.jsdelivr.net/npm/emojione@3.1.2/extras/css/emojione.min.css',
-      groups: ['basis'],
-      args: {
-        integrity: '',
-      },
-    },
+
     {
       name: 'animate.css',
       url: 'https://cdn.jsdelivr.net/npm/animate.css@3.7.2/animate.min.css',

+ 11 - 13
packages/app/resource/locales/en_US/sandbox.md

@@ -12,7 +12,7 @@
   </div>
 </div>
 
-# :pencil: Block Elements
+# :memo: Block Elements
 
 ## Headers
 
@@ -160,7 +160,7 @@ ___
 
 
 
-# :pencil: Typography
+# :memo: Typography
 
 ## Strong Text
 
@@ -200,7 +200,7 @@ This is ___Italic & Bold___.
 This is ***Italic & Bold***.
 This is ___Italic & Bold___.
 
-# :pencil: Images
+# :memo: Images
 
 You can insert `<img>` tag using `![description](URL)`.
 
@@ -221,7 +221,7 @@ The size of the image can be set by using an HTML image tag
 <img src="https://octodex.github.com/images/dojocat.jpg" width="200px">
 
 
-# :pencil: Link
+# :memo: Link
 
 ## Markdown standard
 
@@ -259,7 +259,7 @@ Example of Bootstrap4 is [[here>./Bootstrap4]]
 [[./Bootstrap4]]  
 Example of Bootstrap4 is[[here>./Bootstrap4]]
 
-# :pencil: Lists
+# :memo: Lists
 
 ## Ul Bulleted list
 
@@ -319,7 +319,7 @@ The numbers don’t have to be in numerical order, but the list should start wit
 - [x] Task2
 
 
-# :pencil: Table
+# :memo: Table
 
 ## Markdown Standard
 
@@ -415,7 +415,7 @@ Content Cell,Content Cell
 :::
 
 
-# :pencil: Footnote
+# :memo: Footnote
 
 You can write a reference [^1] to a footnote. You can also add an inline footnote^[Inline_footnote].
 
@@ -428,15 +428,13 @@ Long footnotes can be written as [^longnote].
     Subsequent paragraphs are indented and belong to the previous footnote.
 
 
-# :pencil: Emoji
-
-See [emojione](https://www.emojione.com/)
+# :memo: Emoji
 
 :smiley: :smile: :laughing: :innocent: :drooling_face:
 
-:family: :family_man_boy: :family_man_girl: :family_man_girl_girl: :family_woman_girl_girl:
+:family: :man-boy: :man-girl: :man-girl-girl: :woman-girl-girl:
 
-:thumbsup: :thumbsdown: :open_hands: :raised_hands: :point_right:
+:+1: :-1: :open_hands: :raised_hands: :point_right:
 
 :apple: :green_apple: :strawberry: :cake: :hamburger:
 
@@ -444,7 +442,7 @@ See [emojione](https://www.emojione.com/)
 
 :hearts: :broken_heart: :heartbeat: :heartpulse: :heart_decoration:
 
-:watch: :gear: :gem: :wrench: :envelope:
+:watch: :gear: :gem: :wrench: :email:
 
 
 # :heavy_plus_sign: More..

+ 30 - 0
packages/app/resource/locales/en_US/translation.json

@@ -1022,6 +1022,36 @@
     "incorrect_token_or_expired_url": "The token is incorrect or the URL has expired. Please resend a password reset request via the link below.",
     "password_and_confirm_password_does_not_match": "Password and confirm password does not match"
   },
+  "emoji" :{
+    "title": "Pick an Emoji",
+    "search": "Search",
+    "clear": "Clear",
+    "notfound": "No Emoji Found",
+    "skintext": "Choose your default skin tone",
+    "categories": {
+      "search": "Search Results",
+      "recent": "Frequently Used",
+      "smileys": "Smileys & Emotion",
+      "people": "People & Body",
+      "nature": "Animals & Nature",
+      "foods": "Food & Drink",
+      "activity": "Activity",
+      "places": "Travel & Places",
+      "objects": "Objects",
+      "symbols": "Symbols",
+      "flags": "Flags",
+      "custom": "Custom"
+    },
+    "categorieslabel": "Emoji categories",
+    "skintones": {
+      "1": "Default Skin Tone",
+      "2": "Light Skin Tone",
+      "3": "Medium-Light Skin Tone",
+      "4": "Medium Skin Tone",
+      "5": "Medium-Dark Skin Tone",
+      "6": "Dark Skin Tone"
+    }
+  },
   "maintenance_mode":{
     "maintenance_mode": "Maintenance Mode",
     "growi_is_under_maintenance": "GROWI is under maintenance. Please wait until it ends.",

+ 11 - 13
packages/app/resource/locales/ja_JP/sandbox.md

@@ -12,7 +12,7 @@
   </div>
 </div>
 
-# :pencil: Block Elements
+# :memo: Block Elements
 
 ## Headers 見出し
 
@@ -159,7 +159,7 @@ ___
 
 
 
-# :pencil: Typography
+# :memo: Typography
 
 ## 強調
 
@@ -199,7 +199,7 @@ ___
 これは ***イタリック&ボールド*** です
 これは ___イタリック&ボールド___ です
 
-# :pencil: Images
+# :memo: Images
 
 `![Alt文字列](URL)` で`<img>`タグを挿入できます。
 
@@ -220,7 +220,7 @@ ___
 <img src="https://octodex.github.com/images/dojocat.jpg" width="200px">
 
 
-# :pencil: Link
+# :memo: Link
 
 ## Markdown 標準
 
@@ -258,7 +258,7 @@ Bootstrap4のExampleは[[こちら>./Bootstrap4]]
 [[./Bootstrap4]]  
 Bootstrap4のExampleは[[こちら>./Bootstrap4]]
 
-# :pencil: Lists
+# :memo: Lists
 
 ## Ul 箇条書きリスト
 
@@ -318,7 +318,7 @@ Bootstrap4のExampleは[[こちら>./Bootstrap4]]
 - [x] タスク2
 
 
-# :pencil: Table
+# :memo: Table
 
 ## Markdown 標準
 
@@ -414,7 +414,7 @@ Content Cell,Content Cell
 :::
 
 
-# :pencil: Footnote
+# :memo: Footnote
 
 脚注への参照[^1]を書くことができます。また、インラインの脚注^[インラインで記述できる脚注です]を入れる事も出来ます。
 
@@ -427,15 +427,13 @@ Content Cell,Content Cell
     後続の段落はインデントされて、前の脚注に属します。
 
 
-# :pencil: Emoji
-
-See [emojione](https://www.emojione.com/)
+# :memo: Emoji
 
 :smiley: :smile: :laughing: :innocent: :drooling_face:
 
-:family: :family_man_boy: :family_man_girl: :family_man_girl_girl: :family_woman_girl_girl:
+:family: :man-boy: :man-girl: :man-girl-girl: :woman-girl-girl:
 
-:thumbsup: :thumbsdown: :open_hands: :raised_hands: :point_right:
+:+1: :-1: :open_hands: :raised_hands: :point_right:
 
 :apple: :green_apple: :strawberry: :cake: :hamburger:
 
@@ -443,7 +441,7 @@ See [emojione](https://www.emojione.com/)
 
 :hearts: :broken_heart: :heartbeat: :heartpulse: :heart_decoration:
 
-:watch: :gear: :gem: :wrench: :envelope:
+:watch: :gear: :gem: :wrench: :email:
 
 
 

+ 30 - 0
packages/app/resource/locales/ja_JP/translation.json

@@ -1015,6 +1015,36 @@
     "incorrect_token_or_expired_url":"トークンが正しくないか、URLの有効期限が切れています。 以下のリンクからパスワードリセットリクエストを再送信してください。",
     "password_and_confirm_password_does_not_match": "パスワードと確認パスワードが一致しません"
   },
+  "emoji" :{
+    "title": "絵文字を選択",
+    "search": "探す",
+    "clear": "リセット",
+    "notfound": "絵文字が見つかりません",
+    "skintext": "デフォルトの肌の色を選択",
+    "categories": {
+      "search": "検索結果",
+      "recent": "最新履歴",
+      "smileys": "スマイリーと感情",
+      "people": "人と体",
+      "nature": "動物と自然",
+      "foods": "食べ物や飲み物",
+      "activity": "アクティビティ",
+      "places": "旅行と場所",
+      "objects": "オブジェクト",
+      "symbols": "シンボル",
+      "flags": "国旗",
+      "custom": "カスタマイズ"
+    },
+    "categorieslabel": "絵文字カテゴリ",
+    "skintones": {
+      "1": "デフォルトの肌の色",
+      "2": "明るい肌のトーン",
+      "3": "ミディアム-明るい肌のトーン",
+      "4": "ミディアムスキントーン",
+      "5": "ミディアムダークスキントーン",
+      "6": "肌の色が濃い"
+    }
+  },
   "maintenance_mode":{
     "maintenance_mode": "メンテナンスモード",
     "growi_is_under_maintenance": "GROWI はメンテナンス中です。終了するまでお待ちください",

+ 11 - 13
packages/app/resource/locales/zh_CN/sandbox.md

@@ -12,7 +12,7 @@
   </div>
 </div>
 
-# :pencil: Block Elements
+# :memo: Block Elements
 
 ## Headers
 
@@ -160,7 +160,7 @@ ___
 
 
 
-# :pencil: Typography
+# :memo: Typography
 
 ## Strong Text
 
@@ -200,7 +200,7 @@ This is ___Italic & Bold___.
 This is ***Italic & Bold***.
 This is ___Italic & Bold___.
 
-# :pencil: Images
+# :memo: Images
 
 You can insert `<img>` tag using `![description](URL)`.
 
@@ -221,7 +221,7 @@ The size of the image can be set by using an HTML image tag
 <img src="https://octodex.github.com/images/dojocat.jpg" width="200px">
 
 
-# :pencil: Link
+# :memo: Link
 
 ## Markdown standard
 
@@ -259,7 +259,7 @@ Example of Bootstrap4 is[[here>./Bootstrap4]]
 [[./Bootstrap4]]  
 Example of Bootstrap4 is [[here>./Bootstrap4]]
 
-# :pencil: Lists
+# :memo: Lists
 
 ## Ul Bulleted list
 
@@ -319,7 +319,7 @@ The numbers don’t have to be in numerical order, but the list should start wit
 - [x] Task2
 
 
-# :pencil: Table
+# :memo: Table
 
 ## Markdown Standard
 
@@ -415,7 +415,7 @@ Content Cell,Content Cell
 :::
 
 
-# :pencil: Footnote
+# :memo: Footnote
 
 You can write a reference [^1] to a footnote. You can also add an inline footnote^[Inline_footnote].
 
@@ -428,15 +428,13 @@ Long footnotes can be written as [^longnote].
     Subsequent paragraphs are indented and belong to the previous footnote.
 
 
-# :pencil: Emoji
-
-See [emojione](https://www.emojione.com/)
+# :memo: Emoji
 
 :smiley: :smile: :laughing: :innocent: :drooling_face:
 
-:family: :family_man_boy: :family_man_girl: :family_man_girl_girl: :family_woman_girl_girl:
+:family: :man-boy: :man-girl: :man-girl-girl: :woman-girl-girl:
 
-:thumbsup: :thumbsdown: :open_hands: :raised_hands: :point_right:
+:+1: :-1: :open_hands: :raised_hands: :point_right:
 
 :apple: :green_apple: :strawberry: :cake: :hamburger:
 
@@ -444,7 +442,7 @@ See [emojione](https://www.emojione.com/)
 
 :hearts: :broken_heart: :heartbeat: :heartpulse: :heart_decoration:
 
-:watch: :gear: :gem: :wrench: :envelope:
+:watch: :gear: :gem: :wrench: :email:
 
 
 # :heavy_plus_sign: More..

+ 30 - 0
packages/app/resource/locales/zh_CN/translation.json

@@ -1025,6 +1025,36 @@
     "incorrect_token_or_expired_url":"令牌不正确或 URL 已过期。 请通过以下链接重新发送密码重置请求",
     "password_and_confirm_password_does_not_match": "密码和确认密码不匹配"
   },
+  "emoji" :{
+    "title": "选择一个表情符号",
+    "search": "搜索",
+    "clear": "重置",
+    "notfound": "找不到表情符号",
+    "skintext": "选择您的默认肤色",
+    "categories": {
+      "search": "搜索结果",
+      "recent": "经常使用",
+      "smileys": "笑脸和情感",
+      "people": "人和身体",
+      "nature": "动物与自然",
+      "foods": "食物和饮料",
+      "activity": "活动",
+      "places": "旅行和地方",
+      "objects": "对象",
+      "symbols": "符号",
+      "flags": "旗帜",
+      "custom": "定制"
+    },
+    "categorieslabel": "表情符号类别",
+    "skintones": {
+      "1": "默认肤色",
+      "2": "浅肤色",
+      "3": "中浅肤色",
+      "4": "中等肤色",
+      "5": "中深肤色",
+      "6": "深色肤色"
+    }
+  },
   "maintenance_mode":{
     "maintenance_mode": "维护模式",
     "growi_is_under_maintenance": "GROWI正在进行维护。请等待,直到它结束。",

+ 0 - 4
packages/app/src/client/services/AppContainer.js

@@ -8,7 +8,6 @@ import {
 import {
   apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
 } from '../util/apiv3-client';
-import emojiStrategy from '../util/emojione/emoji_strategy_shrinked.json';
 import GrowiRenderer from '../util/GrowiRenderer';
 
 import {
@@ -266,9 +265,6 @@ export default class AppContainer extends Container {
     return renderer;
   }
 
-  getEmojiStrategy() {
-    return emojiStrategy;
-  }
 
   removeOldUserCache() {
     if (window.localStorage.userByName == null) {

+ 23 - 2
packages/app/src/client/services/PageContainer.js

@@ -17,6 +17,7 @@ import {
 import {
   DrawioInterceptor,
 } from '../util/interceptor/drawio-interceptor';
+import { emojiMartData } from '../util/markdown-it/emoji-mart-data';
 
 const { isTrashPage } = pagePathUtils;
 
@@ -194,12 +195,32 @@ export default class PageContainer extends Container {
     this.setState(newState);
   }
 
-  setTocHtml(tocHtml) {
+  async setTocHtml(tocHtml) {
     if (this.state.tocHtml !== tocHtml) {
-      this.setState({ tocHtml });
+      const tocHtmlWithEmoji = await this.colonsToEmoji(tocHtml);
+      this.setState({ tocHtml: tocHtmlWithEmoji });
     }
   }
 
+  /**
+   *
+   * @param {*} html TOC html string
+   * @returns TOC html with emoji (emoji-mart) in URL
+   */
+  async colonsToEmoji(html) {
+    // Emoji colons matching
+    const colons = ':[a-zA-Z0-9-_+]+:';
+    // Emoji with skin tone matching
+    const skin = ':skin-tone-[2-6]:';
+    const colonsRegex = new RegExp(`(${colons}${skin}|${colons})`, 'g');
+    const emojiData = await emojiMartData();
+    return html.replace(colonsRegex, (index, match) => {
+      const emojiName = match.slice(1, -1);
+      return emojiData[emojiName];
+    });
+
+  }
+
   /**
    * save success handler
    * @param {object} page Page instance

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
packages/app/src/client/util/emojione/emoji_strategy_shrinked.json


+ 66 - 0
packages/app/src/client/util/markdown-it/emoji-mart-data.ts

@@ -0,0 +1,66 @@
+import { Emoji } from 'emoji-mart';
+import data from 'emoji-mart/data/apple.json';
+
+const DEFAULT_EMOJI_SIZE = 24;
+
+/**
+ *
+ * Get native emoji with skin tone
+ * @param emoji Emoji object
+ * @param skin number
+ * @returns emoji data with skin tone
+ */
+const getEmojiSkinTone = async(emoji) => {
+  const emojiData = {};
+  [...Array(6).keys()].forEach((index) => {
+    if (index > 0) {
+      const elem = Emoji({
+        emoji,
+        skin: index + 1,
+        size: DEFAULT_EMOJI_SIZE,
+      });
+      if (elem) {
+        emojiData[`${emoji}::skin-tone-${index + 1}`] = elem.props['aria-label'].split(',')[0];
+      }
+    }
+  });
+  return emojiData;
+};
+
+/**
+ * Get native emoji from emoji array
+ * @param emojis array of emoji
+ * @returns emoji data
+ */
+
+const getNativeEmoji = async(emojis) => {
+  const emojiData = {};
+  emojis.forEach(async(emoji) => {
+    const emojiName = emoji[0];
+    const hasSkinVariation = emoji[1].skin_variations;
+    const elem = Emoji({
+      emoji: emojiName,
+      size: DEFAULT_EMOJI_SIZE,
+    });
+    if (elem != null) {
+      emojiData[emojiName] = elem.props['aria-label'].split(',')[0];
+      if (hasSkinVariation) {
+        const emojiWithSkinTone = await getEmojiSkinTone(emojiName);
+        Object.assign(emojiData, emojiWithSkinTone);
+      }
+    }
+  });
+  return emojiData;
+};
+
+/**
+ * Get native emoji mart data
+ * @returns native emoji mart data
+ */
+
+export const emojiMartData = () => {
+  const emojis = Object.entries(data.emojis).map((emoji) => {
+    return emoji;
+  });
+  return getNativeEmoji(emojis);
+};

+ 5 - 19
packages/app/src/client/util/markdown-it/emoji.js

@@ -1,3 +1,5 @@
+import { emojiMartData } from './emoji-mart-data';
+
 export default class EmojiConfigurer {
 
   constructor(crowi) {
@@ -5,25 +7,9 @@ export default class EmojiConfigurer {
   }
 
   configure(md) {
-    const emojiStrategy = this.crowi.getEmojiStrategy();
-
-    const emojiShortnameUnicodeMap = {};
-
-    /* eslint-disable guard-for-in, no-restricted-syntax */
-    for (const unicode in emojiStrategy) {
-      const data = emojiStrategy[unicode];
-      const shortname = data.shortname.replace(/:/g, '');
-      emojiShortnameUnicodeMap[shortname] = String.fromCharCode(unicode);
-    }
-    /* eslint-enable guard-for-in, no-restricted-syntax */
-
-    md.use(require('markdown-it-emoji'), { defs: emojiShortnameUnicodeMap });
-
-    // integrate markdown-it-emoji and emojione
-    md.renderer.rules.emoji = (token, idx) => {
-      const shortname = `:${token[idx].markup}:`;
-      return emojione.shortnameToImage(shortname);
-    };
+    emojiMartData().then((data) => {
+      md.use(require('markdown-it-emoji-mart'), { defs: data });
+    });
   }
 
 }

+ 0 - 2
packages/app/src/components/PageComment/CommentEditor.jsx

@@ -275,7 +275,6 @@ class CommentEditor extends React.Component {
     const { activeTab } = this.state;
 
     const commentPreview = this.state.isMarkdown ? this.getCommentHtml() : null;
-    const emojiStrategy = appContainer.getEmojiStrategy();
 
     const errorMessage = <span className="text-danger text-right mr-2">{this.state.errorMessage}</span>;
     const cancelButton = (
@@ -309,7 +308,6 @@ class CommentEditor extends React.Component {
                 isMobile={appContainer.isMobile}
                 isUploadable={this.state.isUploadable}
                 isUploadableFile={this.state.isUploadableFile}
-                emojiStrategy={emojiStrategy}
                 onChange={this.updateState}
                 onUpload={this.uploadHandler}
                 onCtrlEnter={this.ctrlEnterHandler}

+ 26 - 31
packages/app/src/components/PageEditor.jsx

@@ -328,40 +328,35 @@ class PageEditor extends React.Component {
 
     const config = this.props.appContainer.getConfig();
     const noCdn = envUtils.toBoolean(config.env.NO_CDN);
-    const emojiStrategy = this.props.appContainer.getEmojiStrategy();
 
     const { path } = this.props.pageContainer.state;
 
     return (
-      <>
-        <div className="d-flex flex-wrap">
-          <div className="page-editor-editor-container flex-grow-1 flex-basis-0 mw-0">
-            <Editor
-              ref={(c) => { this.editor = c }}
-              value={this.state.markdown}
-              noCdn={noCdn}
-              isMobile={this.props.appContainer.isMobile}
-              isUploadable={this.state.isUploadable}
-              isUploadableFile={this.state.isUploadableFile}
-              emojiStrategy={emojiStrategy}
-              onScroll={this.onEditorScroll}
-              onScrollCursorIntoView={this.onEditorScrollCursorIntoView}
-              onChange={this.onMarkdownChanged}
-              onUpload={this.onUpload}
-              onSave={this.onSaveWithShortcut}
-            />
-          </div>
-          <div className="d-none d-lg-block page-editor-preview-container flex-grow-1 flex-basis-0 mw-0">
-            <Preview
-              markdown={this.state.markdown}
-              pagePath={path}
-              // eslint-disable-next-line no-return-assign
-              inputRef={(el) => { return this.previewElement = el }}
-              isMathJaxEnabled={this.state.isMathJaxEnabled}
-              renderMathJaxOnInit={false}
-              onScroll={this.onPreviewScroll}
-            />
-          </div>
+      <div className="d-flex flex-wrap">
+        <div className="page-editor-editor-container flex-grow-1 flex-basis-0 mw-0">
+          <Editor
+            ref={(c) => { this.editor = c }}
+            value={this.state.markdown}
+            noCdn={noCdn}
+            isMobile={this.props.appContainer.isMobile}
+            isUploadable={this.state.isUploadable}
+            isUploadableFile={this.state.isUploadableFile}
+            onScroll={this.onEditorScroll}
+            onScrollCursorIntoView={this.onEditorScrollCursorIntoView}
+            onChange={this.onMarkdownChanged}
+            onUpload={this.onUpload}
+            onSave={this.onSaveWithShortcut}
+          />
+        </div>
+        <div className="d-none d-lg-block page-editor-preview-container flex-grow-1 flex-basis-0 mw-0">
+          <Preview
+            markdown={this.state.markdown}
+            // eslint-disable-next-line no-return-assign
+            inputRef={(el) => { return this.previewElement = el }}
+            isMathJaxEnabled={this.state.isMathJaxEnabled}
+            renderMathJaxOnInit={false}
+            onScroll={this.onPreviewScroll}
+          />
         </div>
         <ConflictDiffModal
           isOpen={this.props.pageContainer.state.isConflictDiffModalOpen}
@@ -370,7 +365,7 @@ class PageEditor extends React.Component {
           pageContainer={this.props.pageContainer}
           markdownOnEdit={this.state.markdown}
         />
-      </>
+      </div>
     );
   }
 

+ 69 - 12
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -18,7 +18,10 @@ import AbstractEditor from './AbstractEditor';
 import CommentMentionHelper from './CommentMentionHelper';
 import DrawioModal from './DrawioModal';
 import EditorIcon from './EditorIcon';
-import EmojiAutoCompleteHelper from './EmojiAutoCompleteHelper';
+import DrawioModal from './DrawioModal';
+import EditorIcon from './EditorIcon';
+import EmojiPicker from './EmojiPicker';
+import EmojiPickerHelper from './EmojiPickerHelper';
 import GridEditModal from './GridEditModal';
 import geu from './GridEditorUtil';
 import HandsontableModal from './HandsontableModal';
@@ -108,11 +111,12 @@ export default class CodeMirrorEditor extends AbstractEditor {
     this.state = {
       value: this.props.value,
       isGfmMode: this.props.isGfmMode,
-      isEnabledEmojiAutoComplete: false,
       isLoadingKeymap: false,
       isSimpleCheatsheetShown: this.props.isGfmMode && this.props.value.length === 0,
       isCheatsheetModalShown: false,
       additionalClassSet: new Set(),
+      isEmojiPickerShown: false,
+      emojiSearchText: null,
     };
 
     this.gridEditModal = React.createRef();
@@ -137,6 +141,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
     this.pasteHandler = this.pasteHandler.bind(this);
     this.cursorHandler = this.cursorHandler.bind(this);
     this.changeHandler = this.changeHandler.bind(this);
+    this.keyUpHandler = this.keyUpHandler.bind(this);
 
     this.updateCheatsheetStates = this.updateCheatsheetStates.bind(this);
 
@@ -151,6 +156,8 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
     this.foldDrawioSection = this.foldDrawioSection.bind(this);
     this.onSaveForDrawio = this.onSaveForDrawio.bind(this);
+    this.checkWhetherEmojiPickerShouldBeShown = this.checkWhetherEmojiPickerShouldBeShown.bind(this);
+
   }
 
   init() {
@@ -168,11 +175,6 @@ export default class CodeMirrorEditor extends AbstractEditor {
   }
 
   componentWillMount() {
-    if (this.props.emojiStrategy != null) {
-      this.emojiAutoCompleteHelper = new EmojiAutoCompleteHelper(this.props.emojiStrategy);
-      this.setState({ isEnabledEmojiAutoComplete: true });
-    }
-
     this.initializeTextlint();
   }
 
@@ -195,6 +197,8 @@ export default class CodeMirrorEditor extends AbstractEditor {
     if (this.props.isComment) {
       this.commentMentionHelper = new CommentMentionHelper(this.getCodeMirror());
     }
+    this.emojiPickerHelper = new EmojiPickerHelper(this.getCodeMirror());
+
   }
 
   componentWillReceiveProps(nextProps) {
@@ -255,7 +259,6 @@ export default class CodeMirrorEditor extends AbstractEditor {
     // update state
     this.setState({
       isGfmMode: bool,
-      isEnabledEmojiAutoComplete: bool,
     });
 
     this.updateCheatsheetStates(bool, null);
@@ -572,9 +575,11 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
     this.updateCheatsheetStates(null, value);
 
-    // Emoji AutoComplete
-    if (this.state.isEnabledEmojiAutoComplete) {
-      this.emojiAutoCompleteHelper.showHint(editor);
+  }
+
+  keyUpHandler(editor, event) {
+    if (event.key !== 'Backspace') {
+      this.checkWhetherEmojiPickerShouldBeShown();
     }
     // Show username hint on comment editor
     if (this.props.isComment) {
@@ -603,6 +608,26 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
   }
 
+  /**
+   * Show emoji picker component when emoji pattern (`:` + searchWord ) found
+   * eg `:a`, `:ap`
+   */
+  checkWhetherEmojiPickerShouldBeShown() {
+    const searchWord = this.emojiPickerHelper.getEmoji();
+
+    if (searchWord == null) {
+      this.setState({ isEmojiPickerShown: false });
+      this.setState({ emojiSearchText: null });
+    }
+    else {
+      this.setState({ emojiSearchText: searchWord });
+      // Show emoji picker after user stop typing
+      setTimeout(() => {
+        this.setState({ isEmojiPickerShown: true });
+      }, 700);
+    }
+  }
+
   /**
    * update states which related to cheatsheet
    * @param {boolean} isGfmModeTmp (use state.isGfmMode if null is set)
@@ -675,6 +700,24 @@ export default class CodeMirrorEditor extends AbstractEditor {
     );
   }
 
+  renderEmojiPicker() {
+    const { emojiSearchText } = this.state;
+    return this.state.isEmojiPickerShown
+      ? (
+        <div className="text-left">
+          <div className="mb-2 d-none d-md-block">
+            <EmojiPicker
+              onClose={() => this.setState({ isEmojiPickerShown: false, emojiSearchText: null })}
+              emojiSearchText={emojiSearchText}
+              emojiPickerHelper={this.emojiPickerHelper}
+              isOpen={this.state.isEmojiPickerShown}
+            />
+          </div>
+        </div>
+      )
+      : '';
+  }
+
   /**
    * return a function to replace a selected range with prefix + selection + suffix
    *
@@ -758,6 +801,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
     this.drawioModal.current.show(mdu.getMarkdownDrawioMxfile(this.getCodeMirror()));
   }
 
+
   // fold draw.io section (::: drawio ~ :::)
   foldDrawioSection() {
     const editor = this.getCodeMirror();
@@ -774,6 +818,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
     return range;
   }
 
+
   getNavbarItems() {
     return [
       <Button
@@ -911,9 +956,19 @@ export default class CodeMirrorEditor extends AbstractEditor {
       >
         <EditorIcon icon="Drawio" />
       </Button>,
+      <Button
+        key="nav-item-emoji"
+        color={null}
+        bssize="small"
+        title="Emoji"
+        onClick={() => this.setState({ isEmojiPickerShown: true })}
+      >
+        <EditorIcon icon="Emoji" />
+      </Button>,
     ];
   }
 
+
   render() {
     const lint = this.props.isTextlintEnabled ? this.codemirrorLintConfig : false;
     const additionalClasses = Array.from(this.state.additionalClassSet).join(' ');
@@ -948,6 +1003,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
             autoCloseTags: true,
             placeholder,
             matchBrackets: true,
+            emoji: true,
             matchTags: { bothTags: true },
             // folding
             foldGutter: this.props.lineNumbers,
@@ -980,11 +1036,13 @@ export default class CodeMirrorEditor extends AbstractEditor {
               this.props.onDragEnter(event);
             }
           }}
+          onKeyUp={this.keyUpHandler}
         />
 
         { this.renderLoadingKeymapOverlay() }
 
         { this.renderCheatsheetOverlay() }
+        { this.renderEmojiPicker() }
 
         <GridEditModal
           ref={this.gridEditModal}
@@ -1014,7 +1072,6 @@ CodeMirrorEditor.propTypes = Object.assign({
   editorOptions: PropTypes.object.isRequired,
   isTextlintEnabled: PropTypes.bool,
   textlintRules: PropTypes.array,
-  emojiStrategy: PropTypes.object,
   lineNumbers: PropTypes.bool,
   onMarkdownHelpButtonClicked: PropTypes.func,
   onAddAttachmentButtonClicked: PropTypes.func,

+ 0 - 1
packages/app/src/components/PageEditor/Editor.jsx

@@ -384,7 +384,6 @@ Editor.propTypes = Object.assign({
   isMobile: PropTypes.bool,
   isUploadable: PropTypes.bool,
   isUploadableFile: PropTypes.bool,
-  emojiStrategy: PropTypes.object,
   onChange: PropTypes.func,
   onUpload: PropTypes.func,
   editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,

+ 14 - 0
packages/app/src/components/PageEditor/EditorIcon.jsx

@@ -125,6 +125,20 @@ const EditorIcon = (props) => {
           <path d="M9.71,22.5a2.57,2.57,0,0,1-1.85-.79,2.79,2.79,0,0,1,0-4l9-9.23a3.21,3.21,0,0,1,1.59-.87,3.39,3.39,0,0,1,1.81.1,4.38,4.38,0,0,1,1.7,1.05,4.15,4.15,0,0,1,.46.56,3.73,3.73,0,0,1,.35.65,4.25,4.25,0,0,1,.2.72,3.91,3.91,0,0,1,.07.76,3.71,3.71,0,0,1-1.12,2.67l-6.79,7a.48.48,0,0,1-.34.16.51.51,0,0,1-.35-.13.48.48,0,0,1,0-.7l6.78-7a2.8,2.8,0,0,0,.84-2,2.58,2.58,0,0,0-.79-2,3.63,3.63,0,0,0-1.11-.75,2.41,2.41,0,0,0-1.31-.17,2.19,2.19,0,0,0-1.25.62l-9,9.22A1.8,1.8,0,0,0,8,19.69,1.78,1.78,0,0,0,8.58,21a1.81,1.81,0,0,0,.57.39,1.48,1.48,0,0,0,.66.1,2,2,0,0,0,1.28-.62l7.12-7.35.15-.16a1.15,1.15,0,0,0,.15-.2.9.9,0,0,0,.12-.24,1.17,1.17,0,0,0,.07-.25.52.52,0,0,0-.05-.27.75.75,0,0,0-.19-.26.73.73,0,0,0-.58-.27,1.29,1.29,0,0,0-.67.38l-5.36,5.53a.5.5,0,0,1-.22.13.46.46,0,0,1-.26,0,.48.48,0,0,1-.22-.12A.41.41,0,0,1,11,17.5a.5.5,0,0,1,.14-.35L16.5,11.6a2.19,2.19,0,0,1,1.29-.67,1.69,1.69,0,0,1,1.37.55,1.54,1.54,0,0,1,.53,1.31,2.26,2.26,0,0,1-.76,1.42L11.8,21.58a3.06,3.06,0,0,1-2,.91H9.71Z" />
         </svg>
       );
+    case 'Emoji':
+      return (
+        <svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30">
+          <g transform="translate(-435 -392)">
+            <rect width="30" height="30" transform="translate(435 392)" fillOpacity="0" />
+            <path d="M8,1a7,7,0,1,0,7,7A7.008,7.008,0,0,0,8,1M8,0A8,8,0,1,1,0,8,8,8,0,0,1,8,0Z" transform="translate(442 399)" />
+            <circle cx="1" cy="1" r="1" transform="translate(446 403)" />
+            <circle cx="1" cy="1" r="1" transform="translate(452 403)" />
+            <g transform="translate(445 406.5)">
+              <path d="M5,5.5a5.006,5.006,0,0,1-5-5,.5.5,0,1,1,1,0,4,4,0,0,0,8,0,.5.5,0,0,1,1,0A5.006,5.006,0,0,1,5,5.5Z" />
+            </g>
+          </g>
+        </svg>
+      );
   }
 
 

+ 0 - 149
packages/app/src/components/PageEditor/EmojiAutoCompleteHelper.js

@@ -1,149 +0,0 @@
-import UpdateDisplayUtil from '~/client/util/codemirror/update-display-util.ext';
-
-class EmojiAutoCompleteHelper {
-
-  constructor(emojiStrategy) {
-    this.emojiStrategy = emojiStrategy;
-
-    this.emojiShortnameImageMap = {};
-
-    this.initEmojiImageMap = this.initEmojiImageMap.bind(this);
-    this.showHint = this.showHint.bind(this);
-
-    this.initEmojiImageMap();
-  }
-
-  initEmojiImageMap() {
-    for (const data of Object.values(this.emojiStrategy)) {
-      const shortname = data.shortname;
-      // add image tag
-      this.emojiShortnameImageMap[shortname] = emojione.shortnameToImage(shortname);
-    }
-  }
-
-  /**
-   * try to find emoji terms and show hint
-   * @param {any} editor An editor instance of CodeMirror
-   */
-  showHint(editor) {
-    // see https://regex101.com/r/gy3i03/1
-    const pattern = /:[^:\s]+/;
-
-    const currentPos = editor.getCursor();
-    // find previous ':shortname'
-    const sc = editor.getSearchCursor(pattern, currentPos, { multiline: false });
-    if (sc.findPrevious()) {
-      const isInputtingEmoji = (currentPos.line === sc.to().line && currentPos.ch === sc.to().ch);
-      // return if it isn't inputting emoji
-      if (!isInputtingEmoji) {
-        return;
-      }
-    }
-    else {
-      return;
-    }
-
-    /*
-     * https://github.com/weseek/growi/issues/703 is caused
-     * because 'editor.display.viewOffset' is zero
-     *
-     * call stack:
-     *   1. https://github.com/codemirror/CodeMirror/blob/5.42.0/addon/hint/show-hint.js#L220
-     *   2. https://github.com/codemirror/CodeMirror/blob/5.42.0/src/edit/methods.js#L189
-     *   3. https://github.com/codemirror/CodeMirror/blob/5.42.0/src/measurement/position_measurement.js#L372
-     *   4. https://github.com/codemirror/CodeMirror/blob/5.42.0/src/measurement/position_measurement.js#L315
-     */
-    UpdateDisplayUtil.forceUpdateViewOffset(editor);
-
-    // see https://codemirror.net/doc/manual.html#addon_show-hint
-    editor.showHint({
-      completeSingle: false,
-      // closeOnUnfocus: false,  // for debug
-      hint: () => {
-        const matched = editor.getDoc().getRange(sc.from(), sc.to());
-        const term = matched.replace(':', ''); // remove ':' in the head
-
-        // get a list of shortnames
-        const shortnames = this.searchEmojiShortnames(term);
-        if (shortnames.length >= 1) {
-          return {
-            list: this.generateEmojiRenderer(shortnames),
-            from: sc.from(),
-            to: sc.to(),
-          };
-        }
-      },
-    });
-  }
-
-  /**
-   * see https://codemirror.net/doc/manual.html#addon_show-hint
-   * @param {string[]} emojiShortnames a list of shortname
-   */
-  generateEmojiRenderer(emojiShortnames) {
-    return emojiShortnames.map((shortname) => {
-      return {
-        text: shortname,
-        className: 'crowi-emoji-autocomplete',
-        render: (element) => {
-          element.innerHTML = `<div class="img-container">${this.emojiShortnameImageMap[shortname]}</div>`
-            + `<span class="shortname-container">${shortname}</span>`;
-        },
-      };
-    });
-  }
-
-  /**
-   * transplanted from https://github.com/emojione/emojione/blob/master/examples/OTHER.md
-   * @param {string} term
-   * @returns {string[]} a list of shortname
-   */
-  searchEmojiShortnames(term) {
-    const maxLength = 12;
-
-    const results1 = [];
-    const results2 = [];
-    const results3 = [];
-    const results4 = [];
-    const countLen1 = () => { return results1.length };
-    const countLen2 = () => { return countLen1() + results2.length };
-    const countLen3 = () => { return countLen2() + results3.length };
-    const countLen4 = () => { return countLen3() + results4.length };
-
-    // TODO performance tune
-    // when total length of all results is less than `maxLength`
-    for (const data of Object.values(this.emojiStrategy)) {
-      if (maxLength <= countLen1()) { break }
-      // prefix match to shortname
-      else if (data.shortname.indexOf(`:${term}`) > -1) {
-        results1.push(data.shortname);
-        continue;
-      }
-      else if (maxLength <= countLen2()) { continue }
-      // partial match to shortname
-      else if (data.shortname.indexOf(term) > -1) {
-        results2.push(data.shortname);
-        continue;
-      }
-      else if (maxLength <= countLen3()) { continue }
-      // partial match to elements of aliases
-      else if ((data.aliases != null) && data.aliases.find((elem) => { return elem.indexOf(term) > -1 })) {
-        results3.push(data.shortname);
-        continue;
-      }
-      else if (maxLength <= countLen4()) { continue }
-      // partial match to elements of keywords
-      else if ((data.keywords != null) && data.keywords.find((elem) => { return elem.indexOf(term) > -1 })) {
-        results4.push(data.shortname);
-      }
-    }
-
-    let results = results1.concat(results2).concat(results3).concat(results4);
-    results = results.slice(0, maxLength);
-
-    return results;
-  }
-
-}
-
-export default EmojiAutoCompleteHelper;

+ 64 - 0
packages/app/src/components/PageEditor/EmojiPicker.tsx

@@ -0,0 +1,64 @@
+import React, { FC } from 'react';
+
+import { Picker } from 'emoji-mart';
+import { Modal } from 'reactstrap';
+
+import { isDarkMode } from '~/client/util/color-scheme';
+
+import EmojiPickerHelper, { getEmojiTranslation } from './EmojiPickerHelper';
+
+type Props = {
+  onClose: () => void,
+  emojiSearchText: string,
+  emojiPickerHelper: EmojiPickerHelper,
+  isOpen: boolean
+}
+
+const EmojiPicker: FC<Props> = (props: Props) => {
+
+  const {
+    onClose, emojiSearchText, emojiPickerHelper, isOpen,
+  } = props;
+
+  // Set search emoji input and trigger search
+  const searchEmoji = () => {
+    const input = window.document.querySelector('[id^="emoji-mart-search"]') as HTMLInputElement;
+    if (emojiSearchText !== null) {
+
+      const valueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
+      valueSetter?.call(input, emojiSearchText);
+      const event = new Event('input', { bubbles: true });
+      input.dispatchEvent(event);
+      input.focus();
+    }
+  };
+
+  const selectEmoji = (emoji) => {
+    if (emojiSearchText !== null) {
+      emojiPickerHelper.addEmojiOnSearch(emoji);
+    }
+    else {
+      emojiPickerHelper.addEmoji(emoji);
+    }
+    onClose();
+  };
+
+
+  const translation = getEmojiTranslation();
+  const theme = isDarkMode() ? 'dark' : 'light';
+
+  return (
+    <Modal isOpen={isOpen} toggle={onClose} onOpened={searchEmoji} backdropClassName="emoji-picker-modal" fade={false}>
+      <Picker
+        onSelect={selectEmoji}
+        i18n={translation}
+        title={translation.title}
+        emojiTooltip
+        style={emojiPickerHelper.setStyle()}
+        theme={theme}
+      />
+    </Modal>
+  );
+};
+
+export default EmojiPicker;

+ 124 - 0
packages/app/src/components/PageEditor/EmojiPickerHelper.ts

@@ -0,0 +1,124 @@
+import { CSSProperties } from 'react';
+
+import i18n from 'i18next';
+
+export default class EmojiPickerHelper {
+
+editor;
+
+pattern: RegExp;
+
+constructor(editor) {
+  this.editor = editor;
+  this.pattern = /:[^:\s]+/;
+}
+
+setStyle = ():CSSProperties => {
+  const offset = 20;
+  const emojiPickerHeight = 420;
+  const cursorPos = this.editor.cursorCoords(true);
+  const editorPos = this.editor.getWrapperElement().getBoundingClientRect();
+  // Emoji Picker bottom position exceed editor's bottom position
+  if (cursorPos.bottom + emojiPickerHeight > editorPos.bottom) {
+    return {
+      top: editorPos.bottom - emojiPickerHeight,
+      left: cursorPos.left + offset,
+      position: 'fixed',
+    };
+  }
+  return {
+    top: cursorPos.top + offset,
+    left: cursorPos.left + offset,
+    position: 'fixed',
+  };
+}
+
+getSearchCursor =() => {
+  const currentPos = this.editor.getCursor();
+  const sc = this.editor.getSearchCursor(this.pattern, currentPos, { multiline: false });
+  return sc;
+}
+
+// Add emoji when triggered by search
+addEmojiOnSearch = (emoji) => {
+  const currentPos = this.editor.getCursor();
+  const sc = this.getSearchCursor();
+  if (sc.findPrevious()) {
+    sc.replace(`${emoji.colons} `, this.editor.getTokenAt(currentPos).string);
+    this.editor.focus();
+    this.editor.refresh();
+  }
+}
+
+
+// Add emoji when triggered by click emoji icon on top of editor
+addEmoji = (emoji) => {
+  const currentPos = this.editor.getCursor();
+  const doc = this.editor.getDoc();
+  doc.replaceRange(`${emoji.colons} `, currentPos);
+  this.editor.focus();
+  this.editor.refresh();
+}
+
+getEmoji = () => {
+  const sc = this.getSearchCursor();
+  const currentPos = this.editor.getCursor();
+
+  if (sc.findPrevious()) {
+    const isInputtingEmoji = (currentPos.line === sc.to().line && currentPos.ch === sc.to().ch);
+    // current search cursor position
+    if (!isInputtingEmoji) {
+      return;
+    }
+    const pos = {
+      line: sc.to().line,
+      ch: sc.to().ch,
+    };
+    const currentSearchText = sc.matches(true, pos).match[0];
+    const searchWord = currentSearchText.replace(':', '');
+    return searchWord;
+  }
+
+  return;
+}
+
+}
+
+export const getEmojiTranslation = () => {
+
+  const categories = {};
+  [
+    'search',
+    'recent',
+    'smileys',
+    'people',
+    'nature',
+    'foods',
+    'activity',
+    'places',
+    'objects',
+    'symbols',
+    'flags',
+    'custom',
+  ].forEach((category) => {
+    categories[category] = i18n.t(`emoji.categories.${category}`);
+  });
+
+  const skintones = {};
+  (Array.from(Array(6).keys())).forEach((tone) => {
+    skintones[tone + 1] = i18n.t(`emoji.skintones.${tone + 1}`);
+  });
+
+  const translation = {
+    search: i18n.t('emoji.search'),
+    clear: i18n.t('emoji.clear'),
+    notfound: i18n.t('emoji.notfound'),
+    skintext: i18n.t('emoji.skintext'),
+    categories,
+    categorieslabel: i18n.t('emoji.categorieslabel'),
+    skintones,
+    title: i18n.t('emoji.title'),
+  };
+
+  return translation;
+};

+ 5 - 6
packages/app/src/components/PageEditor/OptionsSelector.jsx

@@ -1,16 +1,17 @@
 import React from 'react';
-import PropTypes from 'prop-types';
 
+import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
-
 import {
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
 } from 'reactstrap';
 
-import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import EditorContainer from '~/client/services/EditorContainer';
 import { toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../UnstatedUtils';
+
 import { DownloadDictModal } from './DownloadDictModal';
 
 
@@ -146,9 +147,7 @@ class OptionsSelector extends React.Component {
     const { editorContainer } = this.props;
     const newVal = !editorContainer.state.isTextlintEnabled;
     editorContainer.setState({ isTextlintEnabled: newVal });
-    if (this.state.isSkipAskingAgainChecked) {
-      this.updateIsTextlintEnabledToDB(newVal);
-    }
+    this.updateIsTextlintEnabledToDB(newVal);
   }
 
   switchTextlintEnabledHandler() {

+ 7 - 17
packages/app/src/server/routes/apiv3/user-group.js

@@ -1,6 +1,6 @@
-import loggerFactory from '~/utils/logger';
-import { excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
 import UserGroup from '~/server/models/user-group';
+import { excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
+import loggerFactory from '~/utils/logger';
 
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 
@@ -12,12 +12,10 @@ const router = express.Router();
 
 const { body, param, query } = require('express-validator');
 const { sanitizeQuery } = require('express-validator');
-
 const mongoose = require('mongoose');
 
-const ErrorV3 = require('../../models/vo/error-apiv3');
-
 const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
+const ErrorV3 = require('../../models/vo/error-apiv3');
 const { toPagingLimit, toPagingOffset } = require('../../util/express-validator/sanitizer');
 
 const { ObjectId } = mongoose.Types;
@@ -699,21 +697,13 @@ module.exports = (crowi) => {
    *                      description: the associative entity between user and userGroup
    */
   router.delete('/:id/users/:username', loginRequiredStrictly, adminRequired, validator.users.delete, apiV3FormValidator, async(req, res) => {
-    const { id, username } = req.params;
+    const { id: userGroupId, username } = req.params;
 
     try {
-      const [userGroup, user] = await Promise.all([
-        UserGroup.findById(id),
-        User.findUserByUsername(username),
-      ]);
-
-      const groupsOfRelationsToDelete = await UserGroup.findGroupsWithDescendantsRecursively([userGroup]);
-      const relatedGroupIdsToDelete = groupsOfRelationsToDelete.map(g => g._id);
-
-      const deleteManyRes = await UserGroupRelation.deleteMany({ relatedUser: user._id, relatedGroup: { $in: relatedGroupIdsToDelete } });
-      const serializedUser = serializeUserSecurely(user);
+      const removedUserRes = await crowi.userGroupService.removeUserByUsername(userGroupId, username);
+      const serializedUser = serializeUserSecurely(removedUserRes.user);
 
-      return res.apiv3({ user: serializedUser, deletedGroupsCount: deleteManyRes.deletedCount });
+      return res.apiv3({ user: serializedUser, deletedGroupsCount: removedUserRes.deletedGroupsCount });
     }
     catch (err) {
       const msg = 'Error occurred while removing the user from groups.';

+ 22 - 3
packages/app/src/server/service/user-group.ts

@@ -1,8 +1,11 @@
 import mongoose from 'mongoose';
 
-import loggerFactory from '~/utils/logger';
-import UserGroup, { UserGroupDocument } from '~/server/models/user-group';
+
+import { IUser } from '~/interfaces/user';
+import { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
+import UserGroup from '~/server/models/user-group';
 import { excludeTestIdsFromTargetIds, isIncludesObjectId } from '~/server/util/compare-objectId';
+import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:service:UserGroupService'); // eslint-disable-line no-unused-vars
 
@@ -76,7 +79,7 @@ class UserGroupService {
 
     // throw if parent was in self and its descendants
     const descendantsWithTarget = await UserGroup.findGroupsWithDescendantsRecursively([userGroup]);
-    if (isIncludesObjectId(descendantsWithTarget, parent._id)) {
+    if (isIncludesObjectId(descendantsWithTarget.map(d => d._id), parent._id)) {
       throw Error('It is not allowed to choose parent from descendant groups.');
     }
 
@@ -129,6 +132,22 @@ class UserGroupService {
     return deletedGroups;
   }
 
+  async removeUserByUsername(userGroupId: ObjectIdLike, username: string): Promise<{user: IUser, deletedGroupsCount: number}> {
+    const User = this.crowi.model('User');
+
+    const [userGroup, user] = await Promise.all([
+      UserGroup.findById(userGroupId),
+      User.findUserByUsername(username),
+    ]);
+
+    const groupsOfRelationsToDelete = await UserGroup.findGroupsWithDescendantsRecursively([userGroup]);
+    const relatedGroupIdsToDelete = groupsOfRelationsToDelete.map(g => g._id);
+
+    const deleteManyRes = await UserGroupRelation.deleteMany({ relatedUser: user._id, relatedGroup: { $in: relatedGroupIdsToDelete } });
+
+    return { user, deletedGroupsCount: deleteManyRes.deletedCount };
+  }
+
 }
 
 module.exports = UserGroupService;

+ 0 - 1
packages/app/src/styles/_override-bootstrap.scss

@@ -115,7 +115,6 @@
 
   //Modals
   .modal-open {
-    position: fixed;
     width: 100%;
     padding-right: 0 !important;
   }

+ 3 - 0
packages/app/src/styles/_vendor.scss

@@ -27,3 +27,6 @@
 
 // import SimpleBar styles
 @import '~simplebar/dist/simplebar.min.css';
+
+// Emoji-mart style
+@import '~emoji-mart/css/emoji-mart.css';

+ 8 - 0
packages/app/src/styles/theme/_apply-colors.scss

@@ -45,6 +45,7 @@ $nav-tabs-link-active-border-color: $bordercolor-nav-tabs-active;
 @import 'reboot-bootstrap-theme-colors';
 @import 'reboot-bootstrap-nav';
 @import 'reboot-toastr-colors';
+@import '~emoji-mart/css/emoji-mart'; // Emoji-mart style
 
 // determine variables with bootstrap function (These variables can be used after importing bootstrap above)
 $color-modal-header: color-yiq($primary) !default;
@@ -700,3 +701,10 @@ mark.rbt-highlight-text {
   height: 7px;
   background-color: $primary;
 }
+
+/*
+Emoji picker modal
+*/
+.emoji-picker-modal {
+  background-color: transparent !important;
+}

+ 88 - 0
packages/app/test/integration/service/user-groups.test.ts

@@ -5,6 +5,7 @@ import { getInstance } from '../setup-crowi';
 
 describe('UserGroupService', () => {
   let crowi;
+  let User;
   let UserGroup;
   let UserGroupRelation;
 
@@ -16,14 +17,26 @@ describe('UserGroupService', () => {
   const groupId6 = new mongoose.Types.ObjectId();
   const groupId7 = new mongoose.Types.ObjectId();
   const groupId8 = new mongoose.Types.ObjectId();
+  const groupId9 = new mongoose.Types.ObjectId();
+  const groupId10 = new mongoose.Types.ObjectId();
+  const groupId11 = new mongoose.Types.ObjectId();
+  const groupId12 = new mongoose.Types.ObjectId();
 
   const userId1 = new mongoose.Types.ObjectId();
 
   beforeAll(async() => {
     crowi = await getInstance();
+    User = mongoose.model('User');
     UserGroup = mongoose.model('UserGroup');
     UserGroupRelation = mongoose.model('UserGroupRelation');
 
+    await User.insertMany([
+      // ug -> User Group
+      {
+        _id: userId1, name: 'ug_test_user1', username: 'ug_test_user1', email: 'ug_test_user1@example.com',
+      },
+    ]);
+
 
     // Create Groups
     await UserGroup.insertMany([
@@ -76,6 +89,28 @@ describe('UserGroupService', () => {
         name: 'v5_group8',
         description: 'description8',
       },
+      {
+        _id: groupId9,
+        name: 'v5_group9',
+        description: 'description9',
+      },
+      {
+        _id: groupId10,
+        name: 'v5_group10',
+        description: 'description10',
+        parent: groupId9,
+      },
+      {
+        _id: groupId11,
+        name: 'v5_group11',
+        description: 'descriptio11',
+      },
+      {
+        _id: groupId12,
+        name: 'v5_group12',
+        description: 'description12',
+        parent: groupId11,
+      },
     ]);
 
     // Create UserGroupRelations
@@ -92,6 +127,22 @@ describe('UserGroupService', () => {
         relatedGroup: groupId8,
         relatedUser: userId1,
       },
+      {
+        relatedGroup: groupId9,
+        relatedUser: userId1,
+      },
+      {
+        relatedGroup: groupId10,
+        relatedUser: userId1,
+      },
+      {
+        relatedGroup: groupId11,
+        relatedUser: userId1,
+      },
+      {
+        relatedGroup: groupId12,
+        relatedUser: userId1,
+      },
     ]);
 
   });
@@ -192,4 +243,41 @@ describe('UserGroupService', () => {
     expect(userGroupRelation8AfterUpdate).not.toBeNull();
   });
 
+  test('Should throw an error when trying to choose parent from descendant groups.', async() => {
+    const userGroup9 = await UserGroup.findOne({ _id: groupId9, parent: null });
+    const userGroup10 = await UserGroup.findOne({ _id: groupId10, parent: groupId9 });
+
+    const userGroupRelation9BeforeUpdate = await UserGroupRelation.findOne({ relatedGroup:  userGroup9._id, relatedUser: userId1 });
+    const userGroupRelation10BeforeUpdate = await UserGroupRelation.findOne({ relatedGroup:  userGroup10._id, relatedUser: userId1 });
+    expect(userGroupRelation9BeforeUpdate).not.toBeNull();
+    expect(userGroupRelation10BeforeUpdate).not.toBeNull();
+
+    const result = crowi.userGroupService.updateGroup(
+      userGroup9._id, userGroup9.name, userGroup9.description, userGroup10._id,
+    );
+    await expect(result).rejects.toThrow('It is not allowed to choose parent from descendant groups.');
+  });
+
+  test('User should be deleted from child groups when the user excluded from the parent group', async() => {
+    const userGroup11 = await UserGroup.findOne({ _id: groupId11, parent: null });
+    const userGroup12 = await UserGroup.findOne({ _id: groupId12, parent: groupId11 });
+
+    // Both groups have user1
+    const userGroupRelation11BeforeRemove = await UserGroupRelation.findOne({ relatedGroup:  userGroup11._id, relatedUser: userId1 });
+    const userGroupRelation12BeforeRemove = await UserGroupRelation.findOne({ relatedGroup:  userGroup12._id, relatedUser: userId1 });
+    expect(userGroupRelation11BeforeRemove).not.toBeNull();
+    expect(userGroupRelation12BeforeRemove).not.toBeNull();
+
+    // remove user1 from the parent group
+    await crowi.userGroupService.removeUserByUsername(
+      userGroup11._id, 'ug_test_user1',
+    );
+
+    // Both groups have not user1
+    const userGroupRelation11AfterRemove = await UserGroupRelation.findOne({ relatedGroup:  userGroup11._id, relatedUser: userId1 });
+    const userGroupRelation12AfterRemove = await UserGroupRelation.findOne({ relatedGroup:  userGroup12._id, relatedUser: userId1 });
+    await expect(userGroupRelation11AfterRemove).toBeNull();
+    await expect(userGroupRelation12AfterRemove).toBeNull();
+  });
+
 });

+ 1 - 1
packages/codemirror-textlint/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/codemirror-textlint",
-  "version": "5.0.5-RC.0",
+  "version": "5.0.6-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "scripts": {

+ 1 - 1
packages/core/package.json

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

+ 1 - 1
packages/plugin-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-attachment-refs",
-  "version": "5.0.5-RC.0",
+  "version": "5.0.6-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-lsx",
-  "version": "5.0.5-RC.0",
+  "version": "5.0.6-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-pukiwiki-like-linker/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-pukiwiki-like-linker",
-  "version": "5.0.5-RC.0",
+  "version": "5.0.6-RC.0",
   "description": "GROWI plugin to add PukiwikiLikeLinker",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slack",
-  "version": "5.0.5-RC.0",
+  "version": "5.0.6-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "5.0.5-slackbot-proxy.0",
+  "version": "5.0.6-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": "^5.0.5-RC.0",
+    "@growi/slack": "^5.0.6-RC.0",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "5.0.5-RC.0",
+  "version": "5.0.6-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [

+ 27 - 5
yarn.lock

@@ -7609,6 +7609,14 @@ emittery@^0.8.1:
   resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860"
   integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==
 
+"emoji-mart@npm:panta82-emoji-mart@^3.0.1":
+  version "3.0.1003"
+  resolved "https://registry.yarnpkg.com/panta82-emoji-mart/-/panta82-emoji-mart-3.0.1003.tgz#8febed01a0a731ba84caaddf1ba5b1ac724562ac"
+  integrity sha512-JLCNrxoyOb/m/0kGWJZK7QGl/+t82cQrFgbbieeevBxp+lD8pnAb4Bsa4kJzV7xNwMYlNlHDAZJsM//Xb5eJ2Q==
+  dependencies:
+    "@babel/runtime" "^7.0.0"
+    prop-types "^15.6.0"
+
 emoji-regex@^6.4.1:
   version "6.5.1"
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2"
@@ -12931,6 +12939,11 @@ markdown-it-drawio-viewer@^1.3.1:
     "@kaishuu0123/markdown-it-fence" "^1.0.1"
     xmldoc "^1.1.2"
 
+markdown-it-emoji-mart@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/markdown-it-emoji-mart/-/markdown-it-emoji-mart-0.1.1.tgz#65cb7206df88da4dd8c8d4449e62b6c0c9e548ab"
+  integrity sha1-ZctyBt+I2k3YyNREnmK2wMnlSKs=
+
 markdown-it-emoji@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz#9bee0e9a990a963ba96df6980c4fddb05dfb4dcc"
@@ -16283,6 +16296,15 @@ prop-types@^15.5.10, prop-types@^15.5.8:
     loose-envify "^1.3.1"
     object-assign "^4.1.1"
 
+prop-types@^15.6.0:
+  version "15.8.1"
+  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
+  integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
+  dependencies:
+    loose-envify "^1.4.0"
+    object-assign "^4.1.1"
+    react-is "^16.13.1"
+
 prop-types@^15.6.1:
   version "15.6.1"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
@@ -16735,16 +16757,16 @@ react-images@~1.0.0:
     react-transition-group "^2.2.1"
     react-view-pager "^0.6.0"
 
+react-is@^16.13.1, react-is@^16.8.1:
+  version "16.13.1"
+  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
+  integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+
 react-is@^16.7.0:
   version "16.13.0"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527"
   integrity sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA==
 
-react-is@^16.8.1:
-  version "16.13.1"
-  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
-  integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
-
 react-is@^17.0.1:
   version "17.0.2"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"

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