فهرست منبع

Merge branch 'feat/add-test-for-normalize-parent-by-path' of https://github.com/weseek/growi into feat/add-test-for-normalize-parent-by-path

Taichi Masuyama 3 سال پیش
والد
کامیت
1abc6f99d9
31فایلهای تغییر یافته به همراه227 افزوده شده و 85 حذف شده
  1. 26 1
      CHANGELOG.md
  2. 1 1
      lerna.json
  3. 1 1
      package.json
  4. 2 2
      packages/app/docker/README.md
  5. 7 7
      packages/app/package.json
  6. 0 4
      packages/app/resource/Contributor.js
  7. 9 2
      packages/app/resource/locales/en_US/translation.json
  8. 9 2
      packages/app/resource/locales/ja_JP/translation.json
  9. 9 2
      packages/app/resource/locales/zh_CN/translation.json
  10. 18 8
      packages/app/src/components/BookmarkButtons.tsx
  11. 21 9
      packages/app/src/components/LikeButtons.tsx
  12. 5 5
      packages/app/src/components/PageEditor/CommentMentionHelper.ts
  13. 4 1
      packages/app/src/components/PageEditor/EmojiPickerHelper.ts
  14. 16 6
      packages/app/src/components/SubscribeButton.tsx
  15. 6 8
      packages/app/src/components/TableOfContents.jsx
  16. 6 1
      packages/app/src/components/User/SeenUserInfo.tsx
  17. 16 2
      packages/app/src/server/models/user.js
  18. 43 6
      packages/app/src/server/routes/apiv3/users.js
  19. 7 2
      packages/app/src/server/util/middlewares.js
  20. 4 2
      packages/app/src/server/views/layout/layout.html
  21. 3 1
      packages/app/src/server/views/private-legacy-pages.html
  22. 3 1
      packages/app/src/server/views/search.html
  23. 2 2
      packages/app/src/styles/_layout.scss
  24. 1 1
      packages/codemirror-textlint/package.json
  25. 1 1
      packages/core/package.json
  26. 1 1
      packages/plugin-attachment-refs/package.json
  27. 1 1
      packages/plugin-lsx/package.json
  28. 1 1
      packages/plugin-pukiwiki-like-linker/package.json
  29. 1 1
      packages/slack/package.json
  30. 2 2
      packages/slackbot-proxy/package.json
  31. 1 1
      packages/ui/package.json

+ 26 - 1
CHANGELOG.md

@@ -1,9 +1,34 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v5.0.5...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v5.0.6...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v5.0.6](https://github.com/weseek/growi/compare/v5.0.5...v5.0.6) - 2022-05-27
+
+### 💎 Features
+
+- feat: Emoji - replace emojione to emojimart (#5668) @kaoritokashiki
+- feat: Show username suggestion for mention in comment (#5856) @mudana-grune
+- feat: Send in-app notification when containing username mention in comment  (#5906) @mudana-grune
+- feat: Customize menu in navbar for guest user (#5858) @yukendev
+- feat: Admin only page convert by path (#5902) @hakumizuki
+- feat: Fix grant alert (#5903) @hakumizuki
+
+### 🚀 Improvement
+
+- imprv: Automatic login after registration (#5860) @hiroki-hgs
+- imprv: Add tooltip to SubNavButtons (#5887) @miya
+- imprv: Mixin of argument-of-override-list-group-item-for-pagetree for dark theme (#5904) @shukmos
+- imprv: Move code to the appropriate place for fix browser auto-complete email wiith username (#5892) @Yohei-Shiina
+- imprv: Initial rendering when opening Custom Sidebar (#5880) @Kami-jo
+- imprv: Add contributors to staff credit (#5841) @hiroki-hgs
+
+### 🐛 Bug Fixes
+
+- fix: Can not toggle textlint function on v5.0.x (#5854) @kaoritokashiki
+- fix(google-oauth2): Automatically bind external accounts  does not work on v5.0.x (#5886) @kaoritokashiki
+
 ## [v5.0.5](https://github.com/weseek/growi/compare/v5.0.4...v5.0.5) - 2022-05-16
 
 ### 💎 Features

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

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

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

@@ -10,8 +10,8 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`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)
+* [`5.0.6`, `5.0`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.6/docker/Dockerfile)
+* [`5.0.6-nocdn`, `5.0-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.6/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)

+ 7 - 7
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "5.0.6-RC.0",
+  "version": "5.0.7-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.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",
+    "@growi/codemirror-textlint": "^5.0.7-RC.0",
+    "@growi/plugin-attachment-refs": "^5.0.7-RC.0",
+    "@growi/plugin-lsx": "^5.0.7-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^5.0.7-RC.0",
+    "@growi/slack": "^5.0.7-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.6-RC.0",
+    "@growi/ui": "^5.0.7-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",

+ 0 - 4
packages/app/resource/Contributor.js

@@ -24,7 +24,6 @@ const contributors = [
         members: [
           { name: 'utsushiiro' },
           { name: 'mayumorita' },
-          { name: 'TatsuyaIse' },
           { name: 'shinoka7' },
           { name: 'SeiyaTashiro' },
           { name: 'TsuyoshiSuzukief' },
@@ -35,7 +34,6 @@ const contributors = [
           { name: 'kaishuu0123' },
           { name: 'kouki-o' },
           { name: 'Angola' },
-          { name: 'Yohei-Shiina' },
           { name: 'shukmos' },
           { name: 'sooouh' },
           { name: 'ryouhek' },
@@ -50,14 +48,12 @@ const contributors = [
           { name: 'makotoshiraishi' },
           { name: 'yamagai' },
           { name: 'stevenfukase' },
-          { name: 'miya' },
           { name: 'kaho819' },
           { name: 'yuto-oweseek' },
           { name: 'maow89126' },
           { name: 'kntowd' },
           { name: 'yukendev' },
           { name: 'asami-n' },
-          { name: 'ryohi15' },
           { name: 'yoshiro-s' },
           { name: 'kuimac' },
           { name: 'akira-sugiyama' },

+ 9 - 2
packages/app/resource/locales/en_US/translation.json

@@ -15,8 +15,6 @@
   "Move/Rename": "Move/Rename",
   "Redirected": "Redirected",
   "Unlinked": "Unlinked",
-  "Like!": "Like!",
-  "Seen by": "Seen by",
   "Done": "Done",
   "Cancel": "Cancel",
   "Create": "Create",
@@ -1102,5 +1100,14 @@
       "description": "You need to modify the permission settings for this page.",
       "btn_label": "Revision"
     }
+  },
+  "tooltip": {
+    "like": "Like!",
+    "cancel_like": "Cancel Like",
+    "bookmark": "Bookmark",
+    "cancel_bookmark": "Cancel Bookmark",
+    "receive_notifications": "Receive Notifications",
+    "stop_notification": "Stop Notification",
+    "footprints": "Footprints"
   }
 }

+ 9 - 2
packages/app/resource/locales/ja_JP/translation.json

@@ -15,8 +15,6 @@
   "Move/Rename": "移動/名前変更",
   "Redirected": "リダイレクトされました",
   "Unlinked": "リダイレクト削除",
-  "Like!": "いいね!",
-  "Seen by": "見た人",
   "Done": "完了",
   "Cancel": "キャンセル",
   "Create": "作成",
@@ -1095,5 +1093,14 @@
       "description": "このページの権限設定を修正する必要があります。",
       "btn_label": "修正"
     }
+  },
+  "tooltip": {
+    "like": "いいね!",
+    "cancel_like": "いいねを取り消す",
+    "bookmark": "ブックマーク",
+    "cancel_bookmark": "ブックマークを取り消す",
+    "receive_notifications": "通知を受け取る",
+    "stop_notification": "通知を止める",
+    "footprints": "足跡"
   }
 }

+ 9 - 2
packages/app/resource/locales/zh_CN/translation.json

@@ -16,8 +16,6 @@
 	"Move/Rename": "移动/重命名",
 	"Redirected": "重定向",
 	"Unlinked": "Unlinked",
-	"Like!": "Like!",
-	"Seen by": "Seen by",
   "Done": "Done",
   "Cancel": "取消",
 	"Create": "创建",
@@ -1105,5 +1103,14 @@
       "description": "本页的授权设置需要修改。",
       "btn_label": "修改"
     }
+  },
+  "tooltip": {
+    "like": "很好!",
+    "cancel_like": "取消喜欢",
+    "bookmark": "书签",
+    "cancel_bookmark": "取消书签",
+    "receive_notifications": "接收通知",
+    "stop_notification": "停止通知",
+    "footprints": "脚印"
   }
 }

+ 18 - 8
packages/app/src/components/BookmarkButtons.tsx

@@ -1,12 +1,13 @@
-import React, { FC, useState } from 'react';
+import React, { FC, useState, useCallback } from 'react';
 
-import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
 import { useTranslation } from 'react-i18next';
+import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
+
+import { useIsGuestUser } from '~/stores/context';
 
 import { IUser } from '../interfaces/user';
 
 import UserPictureList from './User/UserPictureList';
-import { useIsGuestUser } from '~/stores/context';
 
 interface Props {
   bookmarkCount?: number
@@ -37,6 +38,17 @@ const BookmarkButtons: FC<Props> = (props: Props) => {
     }
   };
 
+  const getTooltipMessage = useCallback(() => {
+    if (isGuestUser) {
+      return 'Not available for guest';
+    }
+
+    if (isBookmarked) {
+      return 'tooltip.cancel_bookmark';
+    }
+    return 'tooltip.bookmark';
+  }, [isGuestUser, isBookmarked]);
+
   return (
     <div className="btn-group" role="group" aria-label="Bookmark buttons">
       <button
@@ -49,11 +61,9 @@ const BookmarkButtons: FC<Props> = (props: Props) => {
         <i className={`fa ${isBookmarked ? 'fa-bookmark' : 'fa-bookmark-o'}`}></i>
       </button>
 
-      {isGuestUser && (
-        <UncontrolledTooltip placement="top" target="bookmark-button" fade={false}>
-          {t('Not available for guest')}
-        </UncontrolledTooltip>
-      )}
+      <UncontrolledTooltip placement="top" target="bookmark-button" fade={false}>
+        {t(getTooltipMessage())}
+      </UncontrolledTooltip>
 
       { !hideTotalNumber && (
         <>

+ 21 - 9
packages/app/src/components/LikeButtons.tsx

@@ -1,13 +1,15 @@
-import React, { FC, useState } from 'react';
+import React, { FC, useState, useCallback } from 'react';
 
-import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
 import { useTranslation } from 'react-i18next';
-import UserPictureList from './User/UserPictureList';
-import { withUnstatedContainers } from './UnstatedUtils';
+import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
 
 import AppContainer from '~/client/services/AppContainer';
+
 import { IUser } from '../interfaces/user';
 
+import { withUnstatedContainers } from './UnstatedUtils';
+import UserPictureList from './User/UserPictureList';
+
 type LikeButtonsProps = {
 
   hideTotalNumber?: boolean,
@@ -32,6 +34,17 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
     hideTotalNumber, isGuestUser, isLiked, sumOfLikers, onLikeClicked,
   } = props;
 
+  const getTooltipMessage = useCallback(() => {
+    if (isGuestUser) {
+      return 'Not available for guest';
+    }
+
+    if (isLiked) {
+      return 'tooltip.cancel_like';
+    }
+    return 'tooltip.like';
+  }, [isGuestUser, isLiked]);
+
   return (
     <div className="btn-group" role="group" aria-label="Like buttons">
       <button
@@ -43,11 +56,10 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
       >
         <i className={`fa ${isLiked ? 'fa-heart' : 'fa-heart-o'}`}></i>
       </button>
-      { isGuestUser && (
-        <UncontrolledTooltip placement="top" target="like-button" fade={false}>
-          {t('Not available for guest')}
-        </UncontrolledTooltip>
-      )}
+
+      <UncontrolledTooltip placement="top" target="like-button" fade={false}>
+        {t(getTooltipMessage())}
+      </UncontrolledTooltip>
 
       { !hideTotalNumber && (
         <>

+ 5 - 5
packages/app/src/components/PageEditor/CommentMentionHelper.ts

@@ -48,12 +48,12 @@ export default class CommentMentionHelper {
     });
   }
 
-  getUsersList = async(username) => {
+  getUsersList = async(q: string) => {
     const limit = 20;
-    const { data } = await apiv3Get('/users/list', { username, limit });
-    return data.users.map(user => ({
-      text: `@${user.username} `,
-      displayText: user.username,
+    const { data } = await apiv3Get('/users/usernames', { q, limit });
+    return data.activeUser.usernames.map(username => ({
+      text: `@${username} `,
+      displayText: username,
     }));
   }
 

+ 4 - 1
packages/app/src/components/PageEditor/EmojiPickerHelper.ts

@@ -2,6 +2,9 @@ import { CSSProperties } from 'react';
 
 import i18n from 'i18next';
 
+// https://regex101.com/r/Gqhor8/1
+const EMOJI_PATTERN = new RegExp(/\B:[^:\s]+/);
+
 export default class EmojiPickerHelper {
 
 editor;
@@ -10,7 +13,7 @@ pattern: RegExp;
 
 constructor(editor) {
   this.editor = editor;
-  this.pattern = /:[^:\s]+/;
+  this.pattern = EMOJI_PATTERN;
 }
 
 setStyle = ():CSSProperties => {

+ 16 - 6
packages/app/src/components/SubscribeButton.tsx

@@ -1,7 +1,8 @@
-import React, { FC } from 'react';
+import React, { FC, useCallback } from 'react';
 
 import { useTranslation } from 'react-i18next';
 import { UncontrolledTooltip } from 'reactstrap';
+
 import { SubscriptionStatusType } from '~/interfaces/subscription';
 
 
@@ -20,6 +21,17 @@ const SubscribeButton: FC<Props> = (props: Props) => {
   const buttonClass = `${isSubscribing ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`;
   const iconClass = isSubscribing === false ? 'fa fa-eye-slash' : 'fa fa-eye';
 
+  const getTooltipMessage = useCallback(() => {
+    if (isGuestUser) {
+      return 'Not available for guest';
+    }
+
+    if (isSubscribing) {
+      return 'tooltip.stop_notification';
+    }
+    return 'tooltip.receive_notifications';
+  }, [isGuestUser, isSubscribing]);
+
   return (
     <>
       <button
@@ -31,11 +43,9 @@ const SubscribeButton: FC<Props> = (props: Props) => {
         <i className={iconClass}></i>
       </button>
 
-      {isGuestUser && (
-        <UncontrolledTooltip placement="top" target="subscribe-button" fade={false}>
-          {t('Not available for guest')}
-        </UncontrolledTooltip>
-      )}
+      <UncontrolledTooltip placement="top" target="subscribe-button" fade={false}>
+        {t(getTooltipMessage())}
+      </UncontrolledTooltip>
     </>
   );
 

+ 6 - 8
packages/app/src/components/TableOfContents.jsx

@@ -1,16 +1,16 @@
 import React, { useCallback, useEffect } from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import loggerFactory from '~/utils/logger';
 
 
 import PageContainer from '~/client/services/PageContainer';
-import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
 import { blinkElem } from '~/client/util/blink-section-header';
+import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
+import loggerFactory from '~/utils/logger';
 
-import { withUnstatedContainers } from './UnstatedUtils';
 
 import { StickyStretchableScroller } from './StickyStretchableScroller';
+import { withUnstatedContainers } from './UnstatedUtils';
 
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:TableOfContents');
@@ -21,7 +21,7 @@ const logger = loggerFactory('growi:TableOfContents');
  */
 const TableOfContents = (props) => {
 
-  const { t, pageContainer } = props;
+  const { pageContainer } = props;
   const { pageUser } = pageContainer.state;
   const isUserPage = pageUser != null;
 
@@ -87,9 +87,7 @@ const TableOfContents = (props) => {
 const TableOfContentsWrapper = withUnstatedContainers(TableOfContents, [PageContainer]);
 
 TableOfContents.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 };
 
-export default withTranslation()(TableOfContentsWrapper);
+export default TableOfContentsWrapper;

+ 6 - 1
packages/app/src/components/User/SeenUserInfo.tsx

@@ -1,7 +1,8 @@
 import React, { FC, useState } from 'react';
 
-import { Popover, PopoverBody } from 'reactstrap';
 import { FootstampIcon } from '@growi/ui';
+import { useTranslation } from 'react-i18next';
+import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
 
 import { IUser } from '~/interfaces/user';
 
@@ -14,6 +15,7 @@ interface Props {
 }
 
 const SeenUserInfo: FC<Props> = (props: Props) => {
+  const { t } = useTranslation();
   const [isPopoverOpen, setIsPopoverOpen] = useState(false);
 
   const { seenUsers, sumOfSeenUsers, disabled } = props;
@@ -35,6 +37,9 @@ const SeenUserInfo: FC<Props> = (props: Props) => {
           </div>
         </PopoverBody>
       </Popover>
+      <UncontrolledTooltip placement="top" target="btn-seen-user" fade={false}>
+        {t('tooltip.footprints')}
+      </UncontrolledTooltip>
     </div>
   );
 };

+ 16 - 2
packages/app/src/server/models/user.js

@@ -715,8 +715,22 @@ module.exports = function(crowi) {
     return users;
   };
 
-  userSchema.statics.findUserByUsernameRegex = async function(username, limit) {
-    return this.find({ username: { $regex: username, $options: 'i' } }).limit(limit);
+  userSchema.statics.findUserByUsernameRegexWithTotalCount = async function(username, status, option) {
+    const opt = option || {};
+    const sortOpt = opt.sortOpt || { username: 1 };
+    const offset = opt.offset || 0;
+    const limit = opt.limit || 10;
+
+    const conditions = { username: { $regex: username, $options: 'i' }, status: { $in: status } };
+
+    const users = await this.find(conditions)
+      .sort(sortOpt)
+      .skip(offset)
+      .limit(limit);
+
+    const totalCount = (await this.find(conditions).distinct('username')).length;
+
+    return { users, totalCount };
   };
 
   class UserUpperLimitException {

+ 43 - 6
packages/app/src/server/routes/apiv3/users.js

@@ -120,6 +120,13 @@ module.exports = (crowi) => {
     query('limit').if(value => value != null).isInt({ max: 300 }).withMessage('You should set less than 300 or not to set limit.'),
   ];
 
+  validator.usernames = [
+    query('q').isString().withMessage('q is required'),
+    query('offset').optional().isInt().withMessage('offset must be a number'),
+    query('limit').optional().isInt({ max: 20 }).withMessage('You should set less than 20 or not to set limit.'),
+    query('options').optional().isString().withMessage('options must be string'),
+  ];
+
   const sendEmailByUserList = async(userList) => {
     const { appService, mailService } = crowi;
     const appTitle = appService.getAppTitle();
@@ -899,17 +906,11 @@ module.exports = (crowi) => {
    */
   router.get('/list', accessTokenParser, loginRequired, async(req, res) => {
     const userIds = req.query.userIds || null;
-    const username = req.query.username || null;
-    const limit = req.query.limit || 20;
 
     let userFetcher;
     if (userIds !== null && userIds.split(',').length > 0) {
       userFetcher = User.findUsersByIds(userIds.split(','));
     }
-    // Get username list by matching pattern from username mention
-    else if (username !== null) {
-      userFetcher = User.findUserByUsernameRegex(username, limit);
-    }
     else {
       userFetcher = User.findAllUsers();
     }
@@ -932,5 +933,41 @@ module.exports = (crowi) => {
     return res.apiv3(data);
   });
 
+  router.get('/usernames', accessTokenParser, loginRequired, validator.usernames, apiV3FormValidator, async(req, res) => {
+    const q = req.query.q;
+    const offset = +req.query.offset || 0;
+    const limit = +req.query.limit || 10;
+
+    try {
+      const options = JSON.parse(req.query.options || '{}');
+      const data = {};
+
+      if (options.isIncludeActiveUser == null || options.isIncludeActiveUser) {
+        const activeUserData = await User.findUserByUsernameRegexWithTotalCount(q, [User.STATUS_ACTIVE], { offset, limit });
+        const activeUsernames = activeUserData.users.map(user => user.username);
+        Object.assign(data, { activeUser: { usernames: activeUsernames, totalCount: activeUserData.totalCount } });
+      }
+
+      if (options.isIncludeInactiveUser) {
+        const inactiveUserStates = [User.STATUS_REGISTERED, User.STATUS_SUSPENDED, User.STATUS_DELETED, User.STATUS_INVITED];
+        const inactiveUserData = await User.findUserByUsernameRegexWithTotalCount(q, inactiveUserStates, { offset, limit });
+        const inactiveUsernames = inactiveUserData.users.map(user => user.username);
+        Object.assign(data, { inactiveUser: { usernames: inactiveUsernames, totalCount: inactiveUserData.totalCount } });
+      }
+
+      if (options.isIncludeMixedUsername) {
+        const allUsernames = [...data.activeUser?.usernames || [], ...data.inactiveUser?.usernames || []];
+        const distinctUsernames = Array.from(new Set(allUsernames));
+        Object.assign(data, { mixedUsernames: distinctUsernames });
+      }
+
+      return res.apiv3(data);
+    }
+    catch (err) {
+      logger.error('Failed to get usernames', err);
+      return res.apiv3Err(err);
+    }
+  });
+
   return router;
 };

+ 7 - 2
packages/app/src/server/util/middlewares.js

@@ -4,10 +4,10 @@ import loggerFactory from '~/utils/logger';
 // all new middlewares should be an independent file under /server/middlewares
 // eslint-disable-next-line no-unused-vars
 
-const { formatDistanceStrict } = require('date-fns');
 const { pathUtils } = require('@growi/core');
-const md5 = require('md5');
+const { formatDistanceStrict } = require('date-fns');
 const entities = require('entities');
+const md5 = require('md5');
 
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:lib:middlewares');
@@ -153,6 +153,11 @@ module.exports = (crowi) => {
         return list.slice(start, end);
       });
 
+      swig.setFilter('push', (list, element) => {
+        list.push(element);
+        return list;
+      });
+
       next();
     };
   };

+ 4 - 2
packages/app/src/server/views/layout/layout.html

@@ -59,11 +59,13 @@
 {% endblock %}
 
 {% block html_body %}
+{% set additionalBodyClasses = []; %}
+{% block html_additional_body_classes %}{% endblock %}
 {% if getConfig('crowi', 'customize:isContainerFluid') %}
-  {% set additionalBodyClass = 'growi-layout-fluid' %}
+  {% set additionalBodyClasses = additionalBodyClasses|push('growi-layout-fluid') %}
 {% endif %}
 <body
-  class="{% block html_base_css %}{% endblock %} growi {{ additionalBodyClass }}"
+  class="{% block html_base_css %}{% endblock %} growi {{ additionalBodyClasses|join(' ') }}"
   data-plugin-enabled="{{ getConfig('crowi', 'plugin:isEnabledPlugins') }}"
   {% block html_base_attr %}{% endblock %}
   data-csrftoken="{{ csrf() }}"

+ 3 - 1
packages/app/src/server/views/private-legacy-pages.html

@@ -12,7 +12,9 @@
 {% endblock %}
 
 <!-- add .on-search to body tag class in layout -->
-{% set additionalBodyClass = 'on-search' %}
+{% block html_additional_body_classes %}
+  {% set additionalBodyClasses = additionalBodyClasses|push('on-search') %}
+{% endblock %}
 
 {% block layout_main %}
 <div id="grw-fav-sticky-trigger" class="sticky-top"></div>

+ 3 - 1
packages/app/src/server/views/search.html

@@ -12,7 +12,9 @@
 {% endblock %}
 
 <!-- add .on-search to body tag class in layout -->
-{% set additionalBodyClass = 'on-search' %}
+{% block html_additional_body_classes %}
+  {% set additionalBodyClasses = additionalBodyClasses|push('on-search') %}
+{% endblock %}
 
 {% block layout_main %}
 <div id="grw-fav-sticky-trigger" class="sticky-top"></div>

+ 2 - 2
packages/app/src/styles/_layout.scss

@@ -66,8 +66,8 @@ body.growi-layout-fluid .grw-container-convertible {
 
 .grw-side-contents-sticky-container {
   position: sticky;
-  // growisubnavigation + grw-navbar-boder
-  top: calc(100px + 4px);
+  // growisubnavigation + grw-navbar-boder + some spacing
+  top: calc(100px + 4px + 20px);
   margin-top: 5px;
 }
 

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/codemirror-textlint",
-  "version": "5.0.6-RC.0",
+  "version": "5.0.7-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.6-RC.0",
+  "version": "5.0.7-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.6-RC.0",
+  "version": "5.0.7-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.6-RC.0",
+  "version": "5.0.7-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.6-RC.0",
+  "version": "5.0.7-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.6-RC.0",
+  "version": "5.0.7-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.6-slackbot-proxy.0",
+  "version": "5.0.7-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.6-RC.0",
+    "@growi/slack": "^5.0.7-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.6-RC.0",
+  "version": "5.0.7-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [