Quellcode durchsuchen

Merge pull request #1609 from weseek/imprv/devide-admin-jsx

Imprv/devide admin jsx
Yuki Takei vor 6 Jahren
Ursprung
Commit
f33d60be72

+ 15 - 2
.github/workflows/ci.yml

@@ -189,6 +189,13 @@ jobs:
       uses: actions/setup-node@v1
       with:
         node-version: ${{ matrix.node-version }}
+    - name: Cache/Restore node_modules/.cache
+      uses: actions/cache@v1
+      with:
+        path: node_modules/.cache
+        key: ${{ runner.OS }}-node_modules_cache-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        restore-keys: |
+          ${{ runner.os }}-node_modules_cache-${{ matrix.node-version }}-
     - name: Get yarn cache dir
       id: cache-yarn
       run: echo "::set-output name=dir::$(yarn cache dir)"
@@ -208,9 +215,9 @@ jobs:
         echo -n "node " && node -v
         echo -n "npm " && npm -v
         yarn list --depth=0
-    - name: yarn build:prod
+    - name: yarn build:prod:analyze
       run: |
-        yarn build:prod
+        yarn build:prod:analyze
     - name: yarn install --production
       run: |
         yarn install --production
@@ -229,6 +236,12 @@ jobs:
       env:
         MONGO_URI: mongodb://localhost:27017/growi
 
+    - name: Upload report as artifact
+      uses: actions/upload-artifact@v1
+      with:
+        name: Report
+        path: report
+
     - name: Slack Notification
       uses: homoluctus/slatify@master
       if: failure()

+ 2 - 0
CHANGES.md

@@ -9,6 +9,8 @@
     * Introduced by 3.6.4
 * Fix: Ensure not to get unrelated indices information in Elasticsearch Management
     * Introduced by 3.6.6
+* Support: Optimize bundles
+* Support: Optimize build-prod job with caching node_modules/.cache
 
 ## v3.6.6
 

+ 1 - 0
config/webpack.common.js

@@ -21,6 +21,7 @@ module.exports = (options) => {
     mode: options.mode,
     entry: Object.assign({
       'js/app':                       './src/client/js/app',
+      'js/admin':                     './src/client/js/admin',
       'js/installer':                 './src/client/js/installer',
       'js/legacy':                    './src/client/js/legacy/crowi',
       'js/legacy-presentation':       './src/client/js/legacy/crowi-presentation',

+ 97 - 0
src/client/js/admin.jsx

@@ -0,0 +1,97 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Provider } from 'unstated';
+import { I18nextProvider } from 'react-i18next';
+
+import loggerFactory from '@alias/logger';
+
+import AdminHome from './components/Admin/AdminHome/AdminHome';
+import UserGroupDetailPage from './components/Admin/UserGroupDetail/UserGroupDetailPage';
+import NotificationSetting from './components/Admin/Notification/NotificationSetting';
+import ManageGlobalNotification from './components/Admin/Notification/ManageGlobalNotification';
+import MarkdownSetting from './components/Admin/MarkdownSetting/MarkDownSetting';
+import UserManagement from './components/Admin/UserManagement';
+import AppSettingsPage from './components/Admin/App/AppSettingsPage';
+import ManageExternalAccount from './components/Admin/ManageExternalAccount';
+import UserGroupPage from './components/Admin/UserGroup/UserGroupPage';
+import Customize from './components/Admin/Customize/Customize';
+import ImportDataPage from './components/Admin/ImportDataPage';
+import ExportArchiveDataPage from './components/Admin/ExportArchiveDataPage';
+import FullTextSearchManagement from './components/Admin/FullTextSearchManagement';
+
+import AdminHomeContainer from './services/AdminHomeContainer';
+import AdminCustomizeContainer from './services/AdminCustomizeContainer';
+import AdminUserGroupDetailContainer from './services/AdminUserGroupDetailContainer';
+import AdminUsersContainer from './services/AdminUsersContainer';
+import AdminAppContainer from './services/AdminAppContainer';
+import AdminMarkDownContainer from './services/AdminMarkDownContainer';
+import AdminExternalAccountsContainer from './services/AdminExternalAccountsContainer';
+import AdminNotificationContainer from './services/AdminNotificationContainer';
+
+import { appContainer, componentMappings } from './bootstrap';
+
+const logger = loggerFactory('growi:admin');
+
+const { i18n } = appContainer;
+const websocketContainer = appContainer.getContainer('WebsocketContainer');
+
+// create unstated container instance
+const adminAppContainer = new AdminAppContainer(appContainer);
+const adminHomeContainer = new AdminHomeContainer(appContainer);
+const adminCustomizeContainer = new AdminCustomizeContainer(appContainer);
+const adminUsersContainer = new AdminUsersContainer(appContainer);
+const adminExternalAccountsContainer = new AdminExternalAccountsContainer(appContainer);
+const adminNotificationContainer = new AdminNotificationContainer(appContainer);
+const adminMarkDownContainer = new AdminMarkDownContainer(appContainer);
+const adminUserGroupDetailContainer = new AdminUserGroupDetailContainer(appContainer);
+const injectableContainers = [
+  appContainer,
+  websocketContainer,
+  adminAppContainer,
+  adminHomeContainer,
+  adminCustomizeContainer,
+  adminUsersContainer,
+  adminExternalAccountsContainer,
+  adminNotificationContainer,
+  adminNotificationContainer,
+  adminMarkDownContainer,
+  adminUserGroupDetailContainer,
+];
+
+logger.info('unstated containers have been initialized');
+
+/**
+ * define components
+ *  key: id of element
+ *  value: React Element
+ */
+Object.assign(componentMappings, {
+  'admin-home': <AdminHome />,
+  'admin-app': <AppSettingsPage />,
+  'admin-markdown-setting': <MarkdownSetting />,
+  'admin-customize': <Customize />,
+  'admin-importer': <ImportDataPage />,
+  'admin-export-page': <ExportArchiveDataPage />,
+  'admin-notification-setting': <NotificationSetting />,
+  'admin-global-notification-setting': <ManageGlobalNotification />,
+  'admin-user-page': <UserManagement />,
+  'admin-external-account-setting': <ManageExternalAccount />,
+  'admin-user-group-detail': <UserGroupDetailPage />,
+  'admin-full-text-search-management': <FullTextSearchManagement />,
+  'admin-user-group-page': <UserGroupPage />,
+});
+
+
+Object.keys(componentMappings).forEach((key) => {
+  const elem = document.getElementById(key);
+  if (elem) {
+    ReactDOM.render(
+      <I18nextProvider i18n={i18n}>
+        <Provider inject={injectableContainers}>
+          {componentMappings[key]}
+        </Provider>
+      </I18nextProvider>,
+      elem,
+    );
+  }
+});

+ 10 - 147
src/client/js/app.jsx

@@ -4,9 +4,7 @@ import { Provider } from 'unstated';
 import { I18nextProvider } from 'react-i18next';
 
 import loggerFactory from '@alias/logger';
-import Xss from '@commons/service/xss';
 
-import HeaderSearchBox from './components/HeaderSearchBox';
 import SearchPage from './components/SearchPage';
 import TagsList from './components/TagsList';
 import PageEditor from './components/PageEditor';
@@ -29,53 +27,23 @@ import BookmarkButton from './components/BookmarkButton';
 import LikeButton from './components/LikeButton';
 import PagePathAutoComplete from './components/PagePathAutoComplete';
 import RecentCreated from './components/RecentCreated/RecentCreated';
-import StaffCredit from './components/StaffCredit/StaffCredit';
 import MyDraftList from './components/MyDraftList/MyDraftList';
 import UserPictureList from './components/User/UserPictureList';
 import TableOfContents from './components/TableOfContents';
 
-import AdminHome from './components/Admin/AdminHome/AdminHome';
-import UserGroupDetailPage from './components/Admin/UserGroupDetail/UserGroupDetailPage';
-import NotificationSetting from './components/Admin/Notification/NotificationSetting';
-import ManageGlobalNotification from './components/Admin/Notification/ManageGlobalNotification';
-import MarkdownSetting from './components/Admin/MarkdownSetting/MarkDownSetting';
-import UserManagement from './components/Admin/UserManagement';
-import AppSettingsPage from './components/Admin/App/AppSettingsPage';
-import ManageExternalAccount from './components/Admin/ManageExternalAccount';
-import UserGroupPage from './components/Admin/UserGroup/UserGroupPage';
-import Customize from './components/Admin/Customize/Customize';
-import ImportDataPage from './components/Admin/ImportDataPage';
-import ExportArchiveDataPage from './components/Admin/ExportArchiveDataPage';
-import FullTextSearchManagement from './components/Admin/FullTextSearchManagement';
-
-import AppContainer from './services/AppContainer';
 import PageContainer from './services/PageContainer';
 import CommentContainer from './services/CommentContainer';
 import EditorContainer from './services/EditorContainer';
 import TagContainer from './services/TagContainer';
-import AdminHomeContainer from './services/AdminHomeContainer';
-import AdminCustomizeContainer from './services/AdminCustomizeContainer';
-import UserGroupDetailContainer from './services/UserGroupDetailContainer';
-import AdminUsersContainer from './services/AdminUsersContainer';
-import AdminAppContainer from './services/AdminAppContainer';
-import WebsocketContainer from './services/WebsocketContainer';
-import AdminMarkDownContainer from './services/AdminMarkDownContainer';
-import AdminExternalAccountsContainer from './services/AdminExternalAccountsContainer';
-import AdminNotificationContainer from './services/AdminNotificationContainer';
 
-const logger = loggerFactory('growi:app');
+import { appContainer, componentMappings } from './bootstrap';
 
-if (!window) {
-  window = {};
-}
+const logger = loggerFactory('growi:app');
 
-// setup xss library
-const xss = new Xss();
-window.xss = xss;
+const { i18n } = appContainer;
+const websocketContainer = appContainer.getContainer('WebsocketContainer');
 
 // create unstated container instance
-const appContainer = new AppContainer();
-const websocketContainer = new WebsocketContainer(appContainer);
 const pageContainer = new PageContainer(appContainer);
 const commentContainer = new CommentContainer(appContainer);
 const editorContainer = new EditorContainer(appContainer, defaultEditorOptions, defaultPreviewOptions);
@@ -86,19 +54,12 @@ const injectableContainers = [
 
 logger.info('unstated containers have been initialized');
 
-appContainer.initPlugins();
-appContainer.injectToWindow();
-
-const i18n = appContainer.i18n;
-
 /**
  * define components
  *  key: id of element
  *  value: React Element
  */
-let componentMappings = {
-  'search-top': <HeaderSearchBox />,
-  'search-sidebar': <HeaderSearchBox crowi={appContainer} />,
+Object.assign(componentMappings, {
   'search-page': <SearchPage crowi={appContainer} />,
 
   // 'revision-history': <PageHistory pageId={pageId} />,
@@ -113,16 +74,11 @@ let componentMappings = {
 
   'user-created-list': <RecentCreated />,
   'user-draft-list': <MyDraftList />,
-
-  'admin-full-text-search-management': <FullTextSearchManagement />,
-
-  'staff-credit': <StaffCredit />,
-  'admin-importer': <ImportDataPage />,
-};
+});
 
 // additional definitions if data exists
 if (pageContainer.state.pageId != null) {
-  componentMappings = Object.assign({
+  Object.assign(componentMappings, {
     'page-editor-with-hackmd': <PageEditorByHackmd />,
     'page-comments-list': <PageComments />,
     'page-attachment': <PageAttachment />,
@@ -136,15 +92,15 @@ if (pageContainer.state.pageId != null) {
     'bookmark-button-lg': <BookmarkButton pageId={pageContainer.state.pageId} crowi={appContainer} size="lg" />,
     'rename-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
     'duplicate-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
-  }, componentMappings);
+  });
 }
 if (pageContainer.state.path != null) {
-  componentMappings = Object.assign({
+  Object.assign(componentMappings, {
     // eslint-disable-next-line quote-props
     'page': <Page />,
     'revision-path': <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} />,
     'tag-label': <TagLabels />,
-  }, componentMappings);
+  });
 }
 
 Object.keys(componentMappings).forEach((key) => {
@@ -161,99 +117,6 @@ Object.keys(componentMappings).forEach((key) => {
   }
 });
 
-// create unstated container instance for admin
-const adminHomeContainer = new AdminHomeContainer(appContainer);
-const adminCustomizeContainer = new AdminCustomizeContainer(appContainer);
-const adminUsersContainer = new AdminUsersContainer(appContainer);
-const adminExternalAccountsContainer = new AdminExternalAccountsContainer(appContainer);
-const adminNotificationContainer = new AdminNotificationContainer(appContainer);
-const adminMarkDownContainer = new AdminMarkDownContainer(appContainer);
-const adminContainers = {
-  'admin-home': adminHomeContainer,
-  'admin-customize': adminCustomizeContainer,
-  'admin-user-page': adminUsersContainer,
-  'admin-external-account-setting': adminExternalAccountsContainer,
-  'admin-notification-setting': adminNotificationContainer,
-  'admin-global-notification-setting': adminNotificationContainer,
-  'admin-markdown-setting': adminMarkDownContainer,
-  'admin-export-page': websocketContainer,
-};
-
-// render for admin
-const adminAppElem = document.getElementById('admin-app');
-if (adminAppElem != null) {
-  const adminAppContainer = new AdminAppContainer(appContainer);
-  ReactDOM.render(
-    <Provider inject={[injectableContainers, adminAppContainer]}>
-      <I18nextProvider i18n={i18n}>
-        <AppSettingsPage />
-      </I18nextProvider>
-    </Provider>,
-    adminAppElem,
-  );
-}
-
-/**
- * define components
- *  key: id of element
- *  value: React Element
- */
-const adminComponentMappings = {
-  'admin-home': <AdminHome />,
-  'admin-customize': <Customize />,
-  'admin-user-page': <UserManagement />,
-  'admin-external-account-setting': <ManageExternalAccount />,
-  'admin-notification-setting': <NotificationSetting />,
-  'admin-global-notification-setting': <ManageGlobalNotification />,
-  'admin-markdown-setting': <MarkdownSetting />,
-  'admin-export-page': <ExportArchiveDataPage crowi={appContainer} />,
-};
-
-
-Object.keys(adminComponentMappings).forEach((key) => {
-  const adminElem = document.getElementById(key);
-  if (adminElem) {
-    ReactDOM.render(
-      <Provider inject={[injectableContainers, adminContainers[key]]}>
-        <I18nextProvider i18n={i18n}>
-          {adminComponentMappings[key]}
-        </I18nextProvider>
-      </Provider>,
-      adminElem,
-    );
-  }
-});
-
-const adminUserGroupDetailElem = document.getElementById('admin-user-group-detail');
-if (adminUserGroupDetailElem != null) {
-  const userGroupDetailContainer = new UserGroupDetailContainer(appContainer);
-  ReactDOM.render(
-    <Provider inject={[userGroupDetailContainer]}>
-      <I18nextProvider i18n={i18n}>
-        <UserGroupDetailPage />
-      </I18nextProvider>
-    </Provider>,
-    adminUserGroupDetailElem,
-  );
-}
-
-const adminUserGroupPageElem = document.getElementById('admin-user-group-page');
-if (adminUserGroupPageElem != null) {
-  const isAclEnabled = adminUserGroupPageElem.getAttribute('data-isAclEnabled') === 'true';
-
-  ReactDOM.render(
-    <Provider inject={[websocketContainer]}>
-      <I18nextProvider i18n={i18n}>
-        <UserGroupPage
-          crowi={appContainer}
-          isAclEnabled={isAclEnabled}
-        />
-      </I18nextProvider>
-    </Provider>,
-    adminUserGroupPageElem,
-  );
-}
-
 // うわーもうー (commented by Crowi team -- 2018.03.23 Yuki Takei)
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
   ReactDOM.render(

+ 44 - 0
src/client/js/bootstrap.jsx

@@ -0,0 +1,44 @@
+import React from 'react';
+
+import loggerFactory from '@alias/logger';
+import Xss from '@commons/service/xss';
+
+import HeaderSearchBox from './components/HeaderSearchBox';
+import StaffCredit from './components/StaffCredit/StaffCredit';
+
+import AppContainer from './services/AppContainer';
+import WebsocketContainer from './services/WebsocketContainer';
+
+const logger = loggerFactory('growi:app');
+
+if (!window) {
+  window = {};
+}
+
+// setup xss library
+const xss = new Xss();
+window.xss = xss;
+
+// create unstated container instance
+const appContainer = new AppContainer();
+// eslint-disable-next-line no-unused-vars
+const websocketContainer = new WebsocketContainer(appContainer);
+
+logger.info('unstated containers have been initialized');
+
+appContainer.initPlugins();
+appContainer.injectToWindow();
+
+/**
+ * define components
+ *  key: id of element
+ *  value: React Element
+ */
+const componentMappings = {
+  'search-top': <HeaderSearchBox />,
+  'search-sidebar': <HeaderSearchBox crowi={appContainer} />,
+
+  'staff-credit': <StaffCredit />,
+};
+
+export { appContainer, componentMappings };

+ 1 - 1
src/client/js/components/Admin/UserGroup/UserGroupCreateForm.jsx

@@ -113,7 +113,7 @@ UserGroupCreateForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
-  isAclEnabled: PropTypes.bool,
+  isAclEnabled: PropTypes.bool.isRequired,
   onCreate: PropTypes.func.isRequired,
 };
 

+ 4 - 4
src/client/js/components/Admin/UserGroup/UserGroupPage.jsx

@@ -138,15 +138,17 @@ class UserGroupPage extends React.Component {
   }
 
   render() {
+    const { isAclEnabled } = this.props.appContainer.config;
+
     return (
       <Fragment>
         <UserGroupCreateForm
-          isAclEnabled={this.props.isAclEnabled}
+          isAclEnabled={isAclEnabled}
           onCreate={this.addUserGroup}
         />
         <UserGroupTable
           userGroups={this.state.userGroups}
-          isAclEnabled={this.props.isAclEnabled}
+          isAclEnabled={isAclEnabled}
           onDelete={this.showDeleteModal}
           userGroupRelations={this.state.userGroupRelations}
         />
@@ -179,8 +181,6 @@ const UserGroupPageWrapper = (props) => {
 
 UserGroupPage.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  isAclEnabled: PropTypes.bool,
 };
 
 export default UserGroupPageWrapper;

+ 1 - 1
src/client/js/components/Admin/UserGroup/UserGroupTable.jsx

@@ -127,7 +127,7 @@ UserGroupTable.propTypes = {
 
   userGroups: PropTypes.arrayOf(PropTypes.object).isRequired,
   userGroupRelations: PropTypes.object.isRequired,
-  isAclEnabled: PropTypes.bool,
+  isAclEnabled: PropTypes.bool.isRequired,
   onDelete: PropTypes.func.isRequired,
 };
 

+ 11 - 8
src/client/js/components/Admin/UserGroupDetail/UserGroupEditForm.jsx

@@ -5,7 +5,7 @@ import dateFnsFormat from 'date-fns/format';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
+import AdminUserGroupDetailContainer from '../../../services/AdminUserGroupDetailContainer';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 class UserGroupEditForm extends React.Component {
@@ -13,9 +13,12 @@ class UserGroupEditForm extends React.Component {
   constructor(props) {
     super(props);
 
+    const { adminUserGroupDetailContainer } = props;
+    const { userGroup } = adminUserGroupDetailContainer.state;
+
     this.state = {
-      name: props.userGroupDetailContainer.state.userGroup.name,
-      nameCache: props.userGroupDetailContainer.state.userGroup.name, // cache for name. update every submit
+      name: userGroup.name,
+      nameCache: userGroup.name, // cache for name. update every submit
     };
 
     this.xss = window.xss;
@@ -35,7 +38,7 @@ class UserGroupEditForm extends React.Component {
     e.preventDefault();
 
     try {
-      const res = await this.props.userGroupDetailContainer.updateUserGroup({
+      const res = await this.props.adminUserGroupDetailContainer.updateUserGroup({
         name: this.state.name,
       });
 
@@ -55,7 +58,7 @@ class UserGroupEditForm extends React.Component {
   }
 
   render() {
-    const { t, userGroupDetailContainer } = this.props;
+    const { t, adminUserGroupDetailContainer } = this.props;
 
     return (
       <form className="form-horizontal" onSubmit={this.handleSubmit}>
@@ -73,7 +76,7 @@ class UserGroupEditForm extends React.Component {
               <input
                 type="text"
                 className="form-control"
-                value={dateFnsFormat(new Date(userGroupDetailContainer.state.userGroup.createdAt), 'yyyy-MM-dd')}
+                value={dateFnsFormat(new Date(adminUserGroupDetailContainer.state.userGroup.createdAt), 'yyyy-MM-dd')}
                 disabled
               />
             </div>
@@ -93,14 +96,14 @@ class UserGroupEditForm extends React.Component {
 UserGroupEditForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
+  adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 
 /**
  * Wrapper component for using unstated
  */
 const UserGroupEditFormWrapper = (props) => {
-  return createSubscribedElement(UserGroupEditForm, props, [AppContainer, UserGroupDetailContainer]);
+  return createSubscribedElement(UserGroupEditForm, props, [AppContainer, AdminUserGroupDetailContainer]);
 };
 
 export default withTranslation()(UserGroupEditFormWrapper);

+ 6 - 6
src/client/js/components/Admin/UserGroupDetail/UserGroupPageList.jsx

@@ -6,7 +6,7 @@ import Page from '../../PageList/Page';
 import PaginationWrapper from '../../PaginationWrapper';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
+import AdminUserGroupDetailContainer from '../../../services/AdminUserGroupDetailContainer';
 import { toastError } from '../../../util/apiNotification';
 
 class UserGroupPageList extends React.Component {
@@ -33,7 +33,7 @@ class UserGroupPageList extends React.Component {
     const offset = (pageNum - 1) * limit;
 
     try {
-      const res = await this.props.appContainer.apiv3.get(`/user-groups/${this.props.userGroupDetailContainer.state.userGroup._id}/pages`, {
+      const res = await this.props.appContainer.apiv3.get(`/user-groups/${this.props.adminUserGroupDetailContainer.state.userGroup._id}/pages`, {
         limit,
         offset,
       });
@@ -51,14 +51,14 @@ class UserGroupPageList extends React.Component {
   }
 
   render() {
-    const { t, userGroupDetailContainer } = this.props;
+    const { t, adminUserGroupDetailContainer } = this.props;
 
     return (
       <Fragment>
         <ul className="page-list-ul page-list-ul-flat">
           {this.state.currentPages.map((page) => { return <Page key={page._id} page={page} /> })}
         </ul>
-        {userGroupDetailContainer.state.relatedPages.length === 0 ? <p>{t('admin:user_group_management.no_pages')}</p> : null}
+        {adminUserGroupDetailContainer.state.relatedPages.length === 0 ? <p>{t('admin:user_group_management.no_pages')}</p> : null}
         <PaginationWrapper
           activePage={this.state.activePage}
           changePage={this.handlePageChange}
@@ -74,14 +74,14 @@ class UserGroupPageList extends React.Component {
 UserGroupPageList.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
+  adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 
 /**
  * Wrapper component for using unstated
  */
 const UserGroupPageListWrapper = (props) => {
-  return createSubscribedElement(UserGroupPageList, props, [AppContainer, UserGroupDetailContainer]);
+  return createSubscribedElement(UserGroupPageList, props, [AppContainer, AdminUserGroupDetailContainer]);
 };
 
 export default withTranslation()(UserGroupPageListWrapper);

+ 15 - 10
src/client/js/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx

@@ -6,7 +6,7 @@ import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 import { debounce } from 'throttle-debounce';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
+import AdminUserGroupDetailContainer from '../../../services/AdminUserGroupDetailContainer';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import UserPicture from '../../User/UserPicture';
 
@@ -36,16 +36,19 @@ class UserGroupUserFormByInput extends React.Component {
   }
 
   async addUserBySubmit() {
+    const { adminUserGroupDetailContainer } = this.props;
+    const { userGroup } = adminUserGroupDetailContainer.state;
+
     if (this.state.inputUser.length === 0) { return }
     const userName = this.state.inputUser[0].username;
 
     try {
-      await this.props.userGroupDetailContainer.addUserByUsername(userName);
-      toastSuccess(`Added "${this.xss.process(userName)}" to "${this.xss.process(this.props.userGroupDetailContainer.state.userGroup.name)}"`);
+      await adminUserGroupDetailContainer.addUserByUsername(userName);
+      toastSuccess(`Added "${this.xss.process(userName)}" to "${this.xss.process(userGroup.name)}"`);
       this.setState({ inputUser: '' });
     }
     catch (err) {
-      toastError(new Error(`Unable to add "${this.xss.process(userName)}" to "${this.xss.process(this.props.userGroupDetailContainer.state.userGroup.name)}"`));
+      toastError(new Error(`Unable to add "${this.xss.process(userName)}" to "${this.xss.process(userGroup.name)}"`));
     }
   }
 
@@ -54,8 +57,10 @@ class UserGroupUserFormByInput extends React.Component {
   }
 
   async searhApplicableUsers() {
+    const { adminUserGroupDetailContainer } = this.props;
+
     try {
-      const users = await this.props.userGroupDetailContainer.fetchApplicableUsers(this.state.keyword);
+      const users = await adminUserGroupDetailContainer.fetchApplicableUsers(this.state.keyword);
       this.setState({ applicableUsers: users, isLoading: false });
     }
     catch (err) {
@@ -89,14 +94,14 @@ class UserGroupUserFormByInput extends React.Component {
   }
 
   renderMenuItemChildren(option) {
-    const { userGroupDetailContainer } = this.props;
+    const { adminUserGroupDetailContainer } = this.props;
     const user = option;
     return (
       <React.Fragment>
         <UserPicture user={user} size="sm" withoutLink />
         <strong className="ml-2">{user.username}</strong>
-        {userGroupDetailContainer.state.isAlsoNameSearched && <span className="ml-2">{user.name}</span>}
-        {userGroupDetailContainer.state.isAlsoMailSearched && <span className="ml-2">{user.email}</span>}
+        {adminUserGroupDetailContainer.state.isAlsoNameSearched && <span className="ml-2">{user.name}</span>}
+        {adminUserGroupDetailContainer.state.isAlsoMailSearched && <span className="ml-2">{user.email}</span>}
       </React.Fragment>
     );
   }
@@ -151,14 +156,14 @@ class UserGroupUserFormByInput extends React.Component {
 UserGroupUserFormByInput.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
+  adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 
 /**
  * Wrapper component for using unstated
  */
 const UserGroupUserFormByInputWrapper = (props) => {
-  return createSubscribedElement(UserGroupUserFormByInput, props, [AppContainer, UserGroupDetailContainer]);
+  return createSubscribedElement(UserGroupUserFormByInput, props, [AppContainer, AdminUserGroupDetailContainer]);
 };
 
 export default withTranslation()(UserGroupUserFormByInputWrapper);

+ 15 - 15
src/client/js/components/Admin/UserGroupDetail/UserGroupUserModal.jsx

@@ -6,17 +6,17 @@ import Modal from 'react-bootstrap/es/Modal';
 import UserGroupUserFormByInput from './UserGroupUserFormByInput';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
+import AdminUserGroupDetailContainer from '../../../services/AdminUserGroupDetailContainer';
 import RadioButtonForSerchUserOption from './RadioButtonForSerchUserOption';
 import CheckBoxForSerchUserOption from './CheckBoxForSerchUserOption';
 
 class UserGroupUserModal extends React.Component {
 
   render() {
-    const { t, userGroupDetailContainer } = this.props;
+    const { t, adminUserGroupDetailContainer } = this.props;
 
     return (
-      <Modal show={userGroupDetailContainer.state.isUserGroupUserModalOpen} onHide={userGroupDetailContainer.closeUserGroupUserModal}>
+      <Modal show={adminUserGroupDetailContainer.state.isUserGroupUserModalOpen} onHide={adminUserGroupDetailContainer.closeUserGroupUserModal}>
         <Modal.Header closeButton>
           <Modal.Title>{t('admin:user_group_management.add_modal.add_user')}</Modal.Title>
         </Modal.Header>
@@ -30,15 +30,15 @@ class UserGroupUserModal extends React.Component {
               <div className="mb-5">
                 <CheckBoxForSerchUserOption
                   option="Mail"
-                  checked={userGroupDetailContainer.state.isAlsoMailSearched}
-                  onChange={userGroupDetailContainer.switchIsAlsoMailSearched}
+                  checked={adminUserGroupDetailContainer.state.isAlsoMailSearched}
+                  onChange={adminUserGroupDetailContainer.switchIsAlsoMailSearched}
                 />
               </div>
               <div className="mb-5">
                 <CheckBoxForSerchUserOption
                   option="Name"
-                  checked={userGroupDetailContainer.state.isAlsoNameSearched}
-                  onChange={userGroupDetailContainer.switchIsAlsoNameSearched}
+                  checked={adminUserGroupDetailContainer.state.isAlsoNameSearched}
+                  onChange={adminUserGroupDetailContainer.switchIsAlsoNameSearched}
                 />
               </div>
             </div>
@@ -46,22 +46,22 @@ class UserGroupUserModal extends React.Component {
               <div className="mb-5">
                 <RadioButtonForSerchUserOption
                   searchType="forward"
-                  checked={userGroupDetailContainer.state.searchType === 'forward'}
-                  onChange={() => { userGroupDetailContainer.switchSearchType('forward') }}
+                  checked={adminUserGroupDetailContainer.state.searchType === 'forward'}
+                  onChange={() => { adminUserGroupDetailContainer.switchSearchType('forward') }}
                 />
               </div>
               <div className="mb-5">
                 <RadioButtonForSerchUserOption
                   searchType="partial"
-                  checked={userGroupDetailContainer.state.searchType === 'partial'}
-                  onChange={() => { userGroupDetailContainer.switchSearchType('partial') }}
+                  checked={adminUserGroupDetailContainer.state.searchType === 'partial'}
+                  onChange={() => { adminUserGroupDetailContainer.switchSearchType('partial') }}
                 />
               </div>
               <div className="mb-5">
                 <RadioButtonForSerchUserOption
                   searchType="backward"
-                  checked={userGroupDetailContainer.state.searchType === 'backword'}
-                  onChange={() => { userGroupDetailContainer.switchSearchType('backword') }}
+                  checked={adminUserGroupDetailContainer.state.searchType === 'backword'}
+                  onChange={() => { adminUserGroupDetailContainer.switchSearchType('backword') }}
                 />
               </div>
             </div>
@@ -76,14 +76,14 @@ class UserGroupUserModal extends React.Component {
 UserGroupUserModal.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
+  adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 
 /**
  * Wrapper component for using unstated
  */
 const UserGroupUserModalWrapper = (props) => {
-  return createSubscribedElement(UserGroupUserModal, props, [AppContainer, UserGroupDetailContainer]);
+  return createSubscribedElement(UserGroupUserModal, props, [AppContainer, AdminUserGroupDetailContainer]);
 };
 
 export default withTranslation()(UserGroupUserModalWrapper);

+ 9 - 9
src/client/js/components/Admin/UserGroupDetail/UserGroupUserTable.jsx

@@ -6,7 +6,7 @@ import dateFnsFormat from 'date-fns/format';
 import UserPicture from '../../User/UserPicture';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
+import AdminUserGroupDetailContainer from '../../../services/AdminUserGroupDetailContainer';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 class UserGroupUserTable extends React.Component {
@@ -21,17 +21,17 @@ class UserGroupUserTable extends React.Component {
 
   async removeUser(username) {
     try {
-      await this.props.userGroupDetailContainer.removeUserByUsername(username);
-      toastSuccess(`Removed "${this.xss.process(username)}" from "${this.xss.process(this.props.userGroupDetailContainer.state.userGroup.name)}"`);
+      await this.props.adminUserGroupDetailContainer.removeUserByUsername(username);
+      toastSuccess(`Removed "${this.xss.process(username)}" from "${this.xss.process(this.props.adminUserGroupDetailContainer.state.userGroup.name)}"`);
     }
     catch (err) {
       // eslint-disable-next-line max-len
-      toastError(new Error(`Unable to remove "${this.xss.process(username)}" from "${this.xss.process(this.props.userGroupDetailContainer.state.userGroup.name)}"`));
+      toastError(new Error(`Unable to remove "${this.xss.process(username)}" from "${this.xss.process(this.props.adminUserGroupDetailContainer.state.userGroup.name)}"`));
     }
   }
 
   render() {
-    const { t, userGroupDetailContainer } = this.props;
+    const { t, adminUserGroupDetailContainer } = this.props;
 
     return (
       <table className="table table-bordered table-user-list">
@@ -48,7 +48,7 @@ class UserGroupUserTable extends React.Component {
           </tr>
         </thead>
         <tbody>
-          {userGroupDetailContainer.state.userGroupRelations.map((sRelation) => {
+          {adminUserGroupDetailContainer.state.userGroupRelations.map((sRelation) => {
             const { relatedUser } = sRelation;
 
             return (
@@ -83,7 +83,7 @@ class UserGroupUserTable extends React.Component {
           <tr>
             <td></td>
             <td className="text-center">
-              <button className="btn btn-default" type="button" onClick={userGroupDetailContainer.openUserGroupUserModal}>
+              <button className="btn btn-default" type="button" onClick={adminUserGroupDetailContainer.openUserGroupUserModal}>
                 <i className="ti-plus"></i>
               </button>
             </td>
@@ -103,14 +103,14 @@ class UserGroupUserTable extends React.Component {
 UserGroupUserTable.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
+  adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 
 /**
  * Wrapper component for using unstated
  */
 const UserGroupUserTableWrapper = (props) => {
-  return createSubscribedElement(UserGroupUserTable, props, [AppContainer, UserGroupDetailContainer]);
+  return createSubscribedElement(UserGroupUserTable, props, [AppContainer, AdminUserGroupDetailContainer]);
 };
 
 export default withTranslation()(UserGroupUserTableWrapper);

+ 15 - 9
src/client/js/services/UserGroupDetailContainer.js → src/client/js/services/AdminUserGroupDetailContainer.js

@@ -5,22 +5,28 @@ import loggerFactory from '@alias/logger';
 import { toastError } from '../util/apiNotification';
 
 // eslint-disable-next-line no-unused-vars
-const logger = loggerFactory('growi:services:UserGroupDetailContainer');
+const logger = loggerFactory('growi:services:AdminUserGroupDetailContainer');
 
 /**
  * Service container for admin user group detail page (UserGroupDetailPage.jsx)
  * @extends {Container} unstated Container
  */
-export default class UserGroupDetailContainer extends Container {
+export default class AdminAdminUserGroupDetailContainer extends Container {
 
   constructor(appContainer) {
     super();
 
     this.appContainer = appContainer;
 
+    const rootElem = document.getElementById('admin-user-group-detail');
+
+    if (rootElem == null) {
+      return;
+    }
+
     this.state = {
       // TODO: [SPA] get userGroup from props
-      userGroup: JSON.parse(document.getElementById('admin-user-group-detail').getAttribute('data-user-group')),
+      userGroup: JSON.parse(rootElem.getAttribute('data-user-group')),
       userGroupRelations: [],
       relatedPages: [],
       isUserGroupUserModalOpen: false,
@@ -43,7 +49,7 @@ export default class UserGroupDetailContainer extends Container {
    * Workaround for the mangling in production build to break constructor.name
    */
   static getClassName() {
-    return 'UserGroupDetailContainer';
+    return 'AdminUserGroupDetailContainer';
   }
 
   /**
@@ -94,7 +100,7 @@ export default class UserGroupDetailContainer extends Container {
   /**
    * update user group
    *
-   * @memberOf UserGroupDetailContainer
+   * @memberOf AdminUserGroupDetailContainer
    * @param {object} param update param for user group
    * @return {object} response object
    */
@@ -110,7 +116,7 @@ export default class UserGroupDetailContainer extends Container {
   /**
    * open a modal
    *
-   * @memberOf UserGroupDetailContainer
+   * @memberOf AdminUserGroupDetailContainer
    */
   async openUserGroupUserModal() {
     await this.setState({ isUserGroupUserModalOpen: true });
@@ -119,7 +125,7 @@ export default class UserGroupDetailContainer extends Container {
   /**
    * close a modal
    *
-   * @memberOf UserGroupDetailContainer
+   * @memberOf AdminUserGroupDetailContainer
    */
   async closeUserGroupUserModal() {
     await this.setState({ isUserGroupUserModalOpen: false });
@@ -146,7 +152,7 @@ export default class UserGroupDetailContainer extends Container {
   /**
    * update user group
    *
-   * @memberOf UserGroupDetailContainer
+   * @memberOf AdminUserGroupDetailContainer
    * @param {string} username username of the user to be added to the group
    */
   async addUserByUsername(username) {
@@ -167,7 +173,7 @@ export default class UserGroupDetailContainer extends Container {
   /**
    * update user group
    *
-   * @memberOf UserGroupDetailContainer
+   * @memberOf AdminUserGroupDetailContainer
    * @param {string} username username of the user to be removed from the group
    */
   async removeUserByUsername(username) {

+ 1 - 1
src/client/js/services/AdminUsersContainer.js

@@ -3,7 +3,7 @@ import { Container } from 'unstated';
 import loggerFactory from '@alias/logger';
 
 // eslint-disable-next-line no-unused-vars
-const logger = loggerFactory('growi:services:UserGroupDetailContainer');
+const logger = loggerFactory('growi:services:AdminUserGroupDetailContainer');
 
 /**
  * Service container for admin users page (Users.jsx)

+ 1 - 1
src/server/routes/apiv3/user-group.js

@@ -65,7 +65,7 @@ module.exports = (crowi) => {
     try {
       const page = parseInt(req.query.page) || 1;
       const result = await UserGroup.findUserGroupsWithPagination({ page });
-      const { docs: userGroups, total: totalUserGroups, limit: pagingLimit } = result;
+      const { docs: userGroups, totalDocs: totalUserGroups, limit: pagingLimit } = result;
       return res.apiv3({ userGroups, totalUserGroups, pagingLimit });
     }
     catch (err) {

+ 1 - 9
src/server/views/admin/user-groups.html

@@ -16,15 +16,7 @@
     <div class="col-md-3">
       {% include './widget/menu.html' with {current: 'user-group'} %}
     </div>
-    <div
-      id ="admin-user-group-page"
-      class="col-md-9"
-      data-isAclEnabled="{{ isAclEnabled }}"
-    >
-      <!-- Reactify Paginator start -->
-      <!-- {% include '../widget/pager.html' with {path: "/admin/user-groups", pager: pager} %} -->
-      <!-- Reactify Paginator end -->
-    </div>
+    <div id ="admin-user-group-page" class="col-md-9"></div>
   </div>
 </div>
 {% endblock content_main %}

+ 2 - 2
src/server/views/layout/admin.html

@@ -4,8 +4,8 @@
 {% block html_base_css %}admin-page{% endblock %}
 
 
-{% block html_additional_headers %}
-  {% parent %}
+{% block html_head_loading_app %}
+<script src="{{ webpack_asset('js/admin.js') }}" defer></script>
 {% endblock %}
 
 {# disable custom script in admin page #}