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

Merge branch 'master' into support/apply-bootstrap4

# Conflicts:
#	src/client/js/components/Admin/UserGroupDetail/UserGroupUserModal.jsx
#	src/client/js/components/PageComment/Comment.jsx
#	src/client/styles/scss/_comment.scss
yusuketk 6 лет назад
Родитель
Сommit
486ce4a400
44 измененных файлов с 587 добавлено и 664 удалено
  1. 45 7
      .github/workflows/ci.yml
  2. 9 1
      CHANGES.md
  3. 2 2
      config/webpack.common.js
  4. 97 0
      src/client/js/admin.jsx
  5. 10 147
      src/client/js/app.jsx
  6. 44 0
      src/client/js/bootstrap.jsx
  7. 2 2
      src/client/js/components/Admin/App/AwsSetting.jsx
  8. 1 0
      src/client/js/components/Admin/CustomCssEditor.jsx
  9. 1 0
      src/client/js/components/Admin/CustomHeaderEditor.jsx
  10. 1 0
      src/client/js/components/Admin/CustomScriptEditor.jsx
  11. 13 6
      src/client/js/components/Admin/Customize/Customize.jsx
  12. 0 4
      src/client/js/components/Admin/Customize/CustomizeBehaviorSetting.jsx
  13. 1 16
      src/client/js/components/Admin/Customize/CustomizeCssSetting.jsx
  14. 0 5
      src/client/js/components/Admin/Customize/CustomizeFunctionSetting.jsx
  15. 1 16
      src/client/js/components/Admin/Customize/CustomizeHeaderSetting.jsx
  16. 21 25
      src/client/js/components/Admin/Customize/CustomizeHighlightSetting.jsx
  17. 0 6
      src/client/js/components/Admin/Customize/CustomizeLayoutSetting.jsx
  18. 1 16
      src/client/js/components/Admin/Customize/CustomizeScriptSetting.jsx
  19. 1 6
      src/client/js/components/Admin/Customize/CustomizeTitle.jsx
  20. 1 1
      src/client/js/components/Admin/UserGroup/UserGroupCreateForm.jsx
  21. 4 4
      src/client/js/components/Admin/UserGroup/UserGroupPage.jsx
  22. 1 1
      src/client/js/components/Admin/UserGroup/UserGroupTable.jsx
  23. 11 8
      src/client/js/components/Admin/UserGroupDetail/UserGroupEditForm.jsx
  24. 6 6
      src/client/js/components/Admin/UserGroupDetail/UserGroupPageList.jsx
  25. 15 10
      src/client/js/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx
  26. 14 14
      src/client/js/components/Admin/UserGroupDetail/UserGroupUserModal.jsx
  27. 9 9
      src/client/js/components/Admin/UserGroupDetail/UserGroupUserTable.jsx
  28. 1 1
      src/client/js/components/PageComment/Comment.jsx
  29. 13 3
      src/client/js/components/SavePageControls.jsx
  30. 0 5
      src/client/js/legacy/crowi-admin.js
  31. 0 233
      src/client/js/legacy/thirdparty-js/jQuery.style.switcher.js
  32. 7 7
      src/client/js/services/AdminAppContainer.js
  33. 153 54
      src/client/js/services/AdminCustomizeContainer.js
  34. 15 9
      src/client/js/services/AdminUserGroupDetailContainer.js
  35. 1 1
      src/client/js/services/AdminUsersContainer.js
  36. 1 1
      src/client/styles/agile-admin/inverse/colors/spring.scss
  37. 5 5
      src/server/routes/apiv3/app-settings.js
  38. 48 5
      src/server/routes/apiv3/customize-setting.js
  39. 1 1
      src/server/routes/apiv3/user-group.js
  40. 23 4
      src/server/service/search-delegator/elasticsearch.js
  41. 4 10
      src/server/views/admin/customize.html
  42. 1 9
      src/server/views/admin/user-groups.html
  43. 2 3
      src/server/views/layout/admin.html
  44. 1 1
      src/server/views/widget/headers/styles-theme.html

+ 45 - 7
.github/workflows/ci.yml

@@ -140,7 +140,23 @@ jobs:
       uses: actions/cache@v1
       uses: actions/cache@v1
       with:
       with:
         path: node_modules
         path: node_modules
-        key: ${{ runner.OS }}-node_modules-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        key: ${{ runner.OS }}-node_modules_dev-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+    - name: Get Date
+      id: date
+      run: |
+        echo ::set-output name=YmdH::$(date '+%Y%m%d%H')
+        echo ::set-output name=Ymd::$(date '+%Y%m%d')
+        echo ::set-output name=Ym::$(date '+%Y%m')
+        echo ::set-output name=Y::$(date '+%Y')
+    - name: Cache/Restore node_modules/.cache/hard-source
+      uses: actions/cache@v1
+      with:
+        path: node_modules/.cache
+        key: ${{ runner.OS }}-hard_source_webpack_dev-${{ matrix.node-version }}-${{ steps.date.outputs.YmdH }}
+        restore-keys: |
+          ${{ runner.os }}-hard_source_webpack_dev-${{ matrix.node-version }}-${{ steps.date.outputs.Ymd }}
+          ${{ runner.os }}-hard_source_webpack_dev-${{ matrix.node-version }}-${{ steps.date.outputs.Ym }}
+          ${{ runner.os }}-hard_source_webpack_dev-${{ matrix.node-version }}-${{ steps.date.outputs.Y }}
     - name: Get yarn cache dir
     - name: Get yarn cache dir
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
       id: cache-yarn
       id: cache-yarn
@@ -150,9 +166,9 @@ jobs:
       uses: actions/cache@v1
       uses: actions/cache@v1
       with:
       with:
         path: ${{ steps.cache-yarn.outputs.dir }}
         path: ${{ steps.cache-yarn.outputs.dir }}
-        key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
         restore-keys: |
         restore-keys: |
-          ${{ runner.os }}-yarn-${{ matrix.node-version }}-
+          ${{ runner.os }}-yarn-
     - name: Install dependencies
     - name: Install dependencies
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
       run: |
       run: |
@@ -189,6 +205,22 @@ jobs:
       uses: actions/setup-node@v1
       uses: actions/setup-node@v1
       with:
       with:
         node-version: ${{ matrix.node-version }}
         node-version: ${{ matrix.node-version }}
+    - name: Get Date
+      id: date
+      run: |
+        echo ::set-output name=YmdH::$(date '+%Y%m%d%H')
+        echo ::set-output name=Ymd::$(date '+%Y%m%d')
+        echo ::set-output name=Ym::$(date '+%Y%m')
+        echo ::set-output name=Y::$(date '+%Y')
+    - name: Cache/Restore node_modules
+      uses: actions/cache@v1
+      with:
+        path: node_modules
+        key: ${{ runner.OS }}-node_modules_prod-${{ matrix.node-version }}-${{ steps.date.outputs.YmdH }}
+        restore-keys: |
+          ${{ runner.os }}-node_modules_prod-${{ matrix.node-version }}-${{ steps.date.outputs.Ymd }}
+          ${{ runner.os }}-node_modules_prod-${{ matrix.node-version }}-${{ steps.date.outputs.Ym }}
+          ${{ runner.os }}-node_modules_prod-${{ matrix.node-version }}-${{ steps.date.outputs.Y }}
     - name: Get yarn cache dir
     - name: Get yarn cache dir
       id: cache-yarn
       id: cache-yarn
       run: echo "::set-output name=dir::$(yarn cache dir)"
       run: echo "::set-output name=dir::$(yarn cache dir)"
@@ -196,9 +228,9 @@ jobs:
       uses: actions/cache@v1
       uses: actions/cache@v1
       with:
       with:
         path: ${{ steps.cache-yarn.outputs.dir }}
         path: ${{ steps.cache-yarn.outputs.dir }}
-        key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
         restore-keys: |
         restore-keys: |
-          ${{ runner.os }}-yarn-${{ matrix.node-version }}-
+          ${{ runner.os }}-yarn-
     - name: Install dependencies
     - name: Install dependencies
       run: |
       run: |
         yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
         yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
@@ -208,9 +240,9 @@ jobs:
         echo -n "node " && node -v
         echo -n "node " && node -v
         echo -n "npm " && npm -v
         echo -n "npm " && npm -v
         yarn list --depth=0
         yarn list --depth=0
-    - name: yarn build:prod
+    - name: yarn build:prod:analyze
       run: |
       run: |
-        yarn build:prod
+        yarn build:prod:analyze
     - name: yarn install --production
     - name: yarn install --production
       run: |
       run: |
         yarn install --production
         yarn install --production
@@ -229,6 +261,12 @@ jobs:
       env:
       env:
         MONGO_URI: mongodb://localhost:27017/growi
         MONGO_URI: mongodb://localhost:27017/growi
 
 
+    - name: Upload report as artifact
+      uses: actions/upload-artifact@v1
+      with:
+        name: Report
+        path: report
+
     - name: Slack Notification
     - name: Slack Notification
       uses: homoluctus/slatify@master
       uses: homoluctus/slatify@master
       if: failure()
       if: failure()

+ 9 - 1
CHANGES.md

@@ -2,7 +2,15 @@
 
 
 ## 3.6.7-RC
 ## 3.6.7-RC
 
 
-*
+* Imprv: Show error toastr when saving page is failed because of empty document
+* Fix: Admin Customise missed preview functions
+    * Introduced by 3.6.2
+* Fix: AWS doesn't work
+    * 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
 ## v3.6.6
 
 

+ 2 - 2
config/webpack.common.js

@@ -21,9 +21,9 @@ module.exports = (options) => {
     mode: options.mode,
     mode: options.mode,
     entry: Object.assign({
     entry: Object.assign({
       'js/app':                       './src/client/js/app',
       'js/app':                       './src/client/js/app',
+      'js/admin':                     './src/client/js/admin',
       'js/installer':                 './src/client/js/installer',
       'js/installer':                 './src/client/js/installer',
       'js/legacy':                    './src/client/js/legacy/crowi',
       'js/legacy':                    './src/client/js/legacy/crowi',
-      'js/legacy-admin':              './src/client/js/legacy/crowi-admin',
       'js/legacy-presentation':       './src/client/js/legacy/crowi-presentation',
       'js/legacy-presentation':       './src/client/js/legacy/crowi-presentation',
       'js/plugin':                    './src/client/js/plugin',
       'js/plugin':                    './src/client/js/plugin',
       'js/ie11-polyfill':             './src/client/js/ie11-polyfill',
       'js/ie11-polyfill':             './src/client/js/ie11-polyfill',
@@ -157,7 +157,7 @@ module.exports = (options) => {
             test: /\.(sc|sa|c)ss$/,
             test: /\.(sc|sa|c)ss$/,
             chunks: (chunk) => {
             chunks: (chunk) => {
               // ignore patterns
               // ignore patterns
-              return chunk.name != null && !chunk.name.match(/style-|theme-|legacy-admin|legacy-presentation/);
+              return chunk.name != null && !chunk.name.match(/style-|theme-|legacy-presentation/);
             },
             },
             name: 'styles/style-commons',
             name: 'styles/style-commons',
             minSize: 1,
             minSize: 1,

+ 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 { I18nextProvider } from 'react-i18next';
 
 
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
-import Xss from '@commons/service/xss';
 
 
-import HeaderSearchBox from './components/HeaderSearchBox';
 import SearchPage from './components/SearchPage';
 import SearchPage from './components/SearchPage';
 import TagsList from './components/TagsList';
 import TagsList from './components/TagsList';
 import PageEditor from './components/PageEditor';
 import PageEditor from './components/PageEditor';
@@ -29,53 +27,23 @@ import BookmarkButton from './components/BookmarkButton';
 import LikeButton from './components/LikeButton';
 import LikeButton from './components/LikeButton';
 import PagePathAutoComplete from './components/PagePathAutoComplete';
 import PagePathAutoComplete from './components/PagePathAutoComplete';
 import RecentCreated from './components/RecentCreated/RecentCreated';
 import RecentCreated from './components/RecentCreated/RecentCreated';
-import StaffCredit from './components/StaffCredit/StaffCredit';
 import MyDraftList from './components/MyDraftList/MyDraftList';
 import MyDraftList from './components/MyDraftList/MyDraftList';
 import UserPictureList from './components/User/UserPictureList';
 import UserPictureList from './components/User/UserPictureList';
 import TableOfContents from './components/TableOfContents';
 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 PageContainer from './services/PageContainer';
 import CommentContainer from './services/CommentContainer';
 import CommentContainer from './services/CommentContainer';
 import EditorContainer from './services/EditorContainer';
 import EditorContainer from './services/EditorContainer';
 import TagContainer from './services/TagContainer';
 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
 // create unstated container instance
-const appContainer = new AppContainer();
-const websocketContainer = new WebsocketContainer(appContainer);
 const pageContainer = new PageContainer(appContainer);
 const pageContainer = new PageContainer(appContainer);
 const commentContainer = new CommentContainer(appContainer);
 const commentContainer = new CommentContainer(appContainer);
 const editorContainer = new EditorContainer(appContainer, defaultEditorOptions, defaultPreviewOptions);
 const editorContainer = new EditorContainer(appContainer, defaultEditorOptions, defaultPreviewOptions);
@@ -86,19 +54,12 @@ const injectableContainers = [
 
 
 logger.info('unstated containers have been initialized');
 logger.info('unstated containers have been initialized');
 
 
-appContainer.initPlugins();
-appContainer.injectToWindow();
-
-const i18n = appContainer.i18n;
-
 /**
 /**
  * define components
  * define components
  *  key: id of element
  *  key: id of element
  *  value: React Element
  *  value: React Element
  */
  */
-let componentMappings = {
-  'search-top': <HeaderSearchBox />,
-  'search-sidebar': <HeaderSearchBox crowi={appContainer} />,
+Object.assign(componentMappings, {
   'search-page': <SearchPage crowi={appContainer} />,
   'search-page': <SearchPage crowi={appContainer} />,
 
 
   // 'revision-history': <PageHistory pageId={pageId} />,
   // 'revision-history': <PageHistory pageId={pageId} />,
@@ -113,16 +74,11 @@ let componentMappings = {
 
 
   'user-created-list': <RecentCreated />,
   'user-created-list': <RecentCreated />,
   'user-draft-list': <MyDraftList />,
   'user-draft-list': <MyDraftList />,
-
-  'admin-full-text-search-management': <FullTextSearchManagement />,
-
-  'staff-credit': <StaffCredit />,
-  'admin-importer': <ImportDataPage />,
-};
+});
 
 
 // additional definitions if data exists
 // additional definitions if data exists
 if (pageContainer.state.pageId != null) {
 if (pageContainer.state.pageId != null) {
-  componentMappings = Object.assign({
+  Object.assign(componentMappings, {
     'page-editor-with-hackmd': <PageEditorByHackmd />,
     'page-editor-with-hackmd': <PageEditorByHackmd />,
     'page-comments-list': <PageComments />,
     'page-comments-list': <PageComments />,
     'page-attachment': <PageAttachment />,
     'page-attachment': <PageAttachment />,
@@ -136,15 +92,15 @@ if (pageContainer.state.pageId != null) {
     'bookmark-button-lg': <BookmarkButton pageId={pageContainer.state.pageId} crowi={appContainer} size="lg" />,
     'bookmark-button-lg': <BookmarkButton pageId={pageContainer.state.pageId} crowi={appContainer} size="lg" />,
     'rename-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
     'rename-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
     'duplicate-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) {
 if (pageContainer.state.path != null) {
-  componentMappings = Object.assign({
+  Object.assign(componentMappings, {
     // eslint-disable-next-line quote-props
     // eslint-disable-next-line quote-props
     'page': <Page />,
     'page': <Page />,
     'revision-path': <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} />,
     'revision-path': <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} />,
     'tag-label': <TagLabels />,
     'tag-label': <TagLabels />,
-  }, componentMappings);
+  });
 }
 }
 
 
 Object.keys(componentMappings).forEach((key) => {
 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)
 // うわーもうー (commented by Crowi team -- 2018.03.23 Yuki Takei)
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
   ReactDOM.render(
   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 };

+ 2 - 2
src/client/js/components/Admin/App/AwsSetting.jsx

@@ -125,9 +125,9 @@ class AwsSetting extends React.Component {
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
-              defaultValue={adminAppContainer.state.secretKey || ''}
+              defaultValue={adminAppContainer.state.secretAccessKey || ''}
               onChange={(e) => {
               onChange={(e) => {
-                adminAppContainer.changeSecretKey(e.target.value);
+                adminAppContainer.changeSecretAccessKey(e.target.value);
               }}
               }}
             />
             />
           </div>
           </div>

+ 1 - 0
src/client/js/components/Admin/CustomCssEditor.jsx

@@ -21,6 +21,7 @@ export default class CustomCssEditor extends React.Component {
       <CodeMirror
       <CodeMirror
         value={this.props.value}
         value={this.props.value}
         autoFocus
         autoFocus
+        detach
         options={{
         options={{
           mode: 'css',
           mode: 'css',
           lineNumbers: true,
           lineNumbers: true,

+ 1 - 0
src/client/js/components/Admin/CustomHeaderEditor.jsx

@@ -19,6 +19,7 @@ export default class CustomHeaderEditor extends React.Component {
       <CodeMirror
       <CodeMirror
         value={this.props.value}
         value={this.props.value}
         autoFocus
         autoFocus
+        detach
         options={{
         options={{
           mode: 'htmlmixed',
           mode: 'htmlmixed',
           lineNumbers: true,
           lineNumbers: true,

+ 1 - 0
src/client/js/components/Admin/CustomScriptEditor.jsx

@@ -21,6 +21,7 @@ export default class CustomScriptEditor extends React.Component {
       <CodeMirror
       <CodeMirror
         value={this.props.value}
         value={this.props.value}
         autoFocus
         autoFocus
+        detach
         options={{
         options={{
           mode: 'javascript',
           mode: 'javascript',
           lineNumbers: true,
           lineNumbers: true,

+ 13 - 6
src/client/js/components/Admin/Customize/Customize.jsx

@@ -3,8 +3,6 @@ import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import loggerFactory from '@alias/logger';
-
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 
 
@@ -20,25 +18,34 @@ import CustomizeScriptSetting from './CustomizeScriptSetting';
 import CustomizeHeaderSetting from './CustomizeHeaderSetting';
 import CustomizeHeaderSetting from './CustomizeHeaderSetting';
 import CustomizeTitle from './CustomizeTitle';
 import CustomizeTitle from './CustomizeTitle';
 
 
-const logger = loggerFactory('growi:Customize');
-
 class Customize extends React.Component {
 class Customize extends React.Component {
 
 
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isRetrieving: true,
+    };
+
+  }
+
   async componentDidMount() {
   async componentDidMount() {
     const { adminCustomizeContainer } = this.props;
     const { adminCustomizeContainer } = this.props;
 
 
     try {
     try {
       await adminCustomizeContainer.retrieveCustomizeData();
       await adminCustomizeContainer.retrieveCustomizeData();
+      this.setState({ isRetrieving: false });
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
-      adminCustomizeContainer.setState({ retrieveError: err });
-      logger.error(err);
     }
     }
 
 
   }
   }
 
 
   render() {
   render() {
+    if (this.state.isRetrieving) {
+      return null;
+    }
 
 
     return (
     return (
       <Fragment>
       <Fragment>

+ 0 - 4
src/client/js/components/Admin/Customize/CustomizeBehaviorSetting.jsx

@@ -3,8 +3,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import loggerFactory from '@alias/logger';
-
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
@@ -14,7 +12,6 @@ import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import CustomizeBehaviorOption from './CustomizeBehaviorOption';
 import CustomizeBehaviorOption from './CustomizeBehaviorOption';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
-const logger = loggerFactory('growi:Customize');
 
 
 class CustomizeBehaviorSetting extends React.Component {
 class CustomizeBehaviorSetting extends React.Component {
 
 
@@ -33,7 +30,6 @@ class CustomizeBehaviorSetting extends React.Component {
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
-      logger.error(err);
     }
     }
   }
   }
 
 

+ 1 - 16
src/client/js/components/Admin/Customize/CustomizeCssSetting.jsx

@@ -2,8 +2,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import loggerFactory from '@alias/logger';
-
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
@@ -13,25 +11,14 @@ import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import CustomCssEditor from '../CustomCssEditor';
 import CustomCssEditor from '../CustomCssEditor';
 
 
-const logger = loggerFactory('growi:Customize');
-
 class CustomizeCssSetting extends React.Component {
 class CustomizeCssSetting extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
-    this.state = {
-      editorInputValue: '',
-    };
-
     this.onClickSubmit = this.onClickSubmit.bind(this);
     this.onClickSubmit = this.onClickSubmit.bind(this);
   }
   }
 
 
-  componentDidMount() {
-    const { customizeCss } = this.props.appContainer.getConfig();
-    this.setState({ editorInputValue: customizeCss || '' });
-  }
-
   async onClickSubmit() {
   async onClickSubmit() {
     const { t, adminCustomizeContainer } = this.props;
     const { t, adminCustomizeContainer } = this.props;
 
 
@@ -41,7 +28,6 @@ class CustomizeCssSetting extends React.Component {
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
-      logger.error(err);
     }
     }
   }
   }
 
 
@@ -58,8 +44,7 @@ class CustomizeCssSetting extends React.Component {
         <div className="form-group">
         <div className="form-group">
           <div className="col-xs-12">
           <div className="col-xs-12">
             <CustomCssEditor
             <CustomCssEditor
-              // The value passed must be immutable
-              value={this.state.editorInputValue}
+              value={adminCustomizeContainer.state.currentCustomizeCss || ''}
               onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeCss(inputValue) }}
               onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeCss(inputValue) }}
             />
             />
           </div>
           </div>

+ 0 - 5
src/client/js/components/Admin/Customize/CustomizeFunctionSetting.jsx

@@ -2,8 +2,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import loggerFactory from '@alias/logger';
-
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
@@ -13,8 +11,6 @@ import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import CustomizeFunctionOption from './CustomizeFunctionOption';
 import CustomizeFunctionOption from './CustomizeFunctionOption';
 
 
-const logger = loggerFactory('growi:importer');
-
 class CustomizeBehaviorSetting extends React.Component {
 class CustomizeBehaviorSetting extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
@@ -32,7 +28,6 @@ class CustomizeBehaviorSetting extends React.Component {
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
-      logger.error(err);
     }
     }
   }
   }
 
 

+ 1 - 16
src/client/js/components/Admin/Customize/CustomizeHeaderSetting.jsx

@@ -2,8 +2,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import loggerFactory from '@alias/logger';
-
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
@@ -13,25 +11,14 @@ import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import CustomHeaderEditor from '../CustomHeaderEditor';
 import CustomHeaderEditor from '../CustomHeaderEditor';
 
 
-const logger = loggerFactory('growi:Customize');
-
 class CustomizeHeaderSetting extends React.Component {
 class CustomizeHeaderSetting extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
-    this.state = {
-      editorInputValue: '',
-    };
-
     this.onClickSubmit = this.onClickSubmit.bind(this);
     this.onClickSubmit = this.onClickSubmit.bind(this);
   }
   }
 
 
-  componentDidMount() {
-    const { customizeHeader } = this.props.appContainer.getConfig();
-    this.setState({ editorInputValue: customizeHeader || '' });
-  }
-
   async onClickSubmit() {
   async onClickSubmit() {
     const { t, adminCustomizeContainer } = this.props;
     const { t, adminCustomizeContainer } = this.props;
 
 
@@ -41,7 +28,6 @@ class CustomizeHeaderSetting extends React.Component {
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
-      logger.error(err);
     }
     }
   }
   }
 
 
@@ -68,8 +54,7 @@ class CustomizeHeaderSetting extends React.Component {
 
 
         <div className="col-xs-12">
         <div className="col-xs-12">
           <CustomHeaderEditor
           <CustomHeaderEditor
-            // The value passed must be immutable
-            value={this.state.editorInputValue}
+            value={adminCustomizeContainer.state.currentCustomizeHeader || ''}
             onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeHeader(inputValue) }}
             onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeHeader(inputValue) }}
           />
           />
         </div>
         </div>

+ 21 - 25
src/client/js/components/Admin/Customize/CustomizeHighlightSetting.jsx

@@ -3,8 +3,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import loggerFactory from '@alias/logger';
-
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
@@ -13,8 +11,6 @@ import AppContainer from '../../../services/AppContainer';
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
-const logger = loggerFactory('growi:customizeHighlight');
-
 class CustomizeHighlightSetting extends React.Component {
 class CustomizeHighlightSetting extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
@@ -32,25 +28,29 @@ class CustomizeHighlightSetting extends React.Component {
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
-      logger.error(err);
     }
     }
   }
   }
 
 
-  getDemoFunction() {
-    return `function $initHighlight(block, cls) {
-    try {
+  renderHljsDemo() {
+    const { adminCustomizeContainer } = this.props;
 
 
-      if (cls.search(/\bno\-highlight\b/) !== -1) {
-        return \`\${process(block, true, 0x0F)} class="\${cls}"\`;
-      }
-    }
-    catch (e) {
-      /* handle exception */
-    }
-    for (let i = 0 / 2; i < classes.length; i++) {
-      if (checkCondition(classes[i]) === undefined) { console.log('undefined') }
-    }
-  };`;
+    /* eslint-disable max-len */
+    const html = `<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MersenneTwister</span>(<span class="hljs-params">seed</span>) </span>{
+  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">arguments</span>.length == <span class="hljs-number">0</span>) {
+    seed = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().getTime();
+  }
+
+  <span class="hljs-keyword">this</span>._mt = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Array</span>(<span class="hljs-number">624</span>);
+  <span class="hljs-keyword">this</span>.setSeed(seed);
+}</span>`;
+    /* eslint-enable max-len */
+
+    return (
+      <pre className={`hljs ${!adminCustomizeContainer.state.isHighlightJsStyleBorderEnabled && 'hljs-no-border'}`}>
+        {/* eslint-disable-next-line react/no-danger */}
+        <code dangerouslySetInnerHTML={{ __html: html }}></code>
+      </pre>
+    );
   }
   }
 
 
   render() {
   render() {
@@ -65,7 +65,7 @@ class CustomizeHighlightSetting extends React.Component {
 
 
       menuItem.push(
       menuItem.push(
         <li key={styleId} role="presentation" type="button" onClick={() => adminCustomizeContainer.switchHighlightJsStyle(styleId, styleName, isBorderEnable)}>
         <li key={styleId} role="presentation" type="button" onClick={() => adminCustomizeContainer.switchHighlightJsStyle(styleId, styleName, isBorderEnable)}>
-          <a role="menuitem">{styleName}</a>
+          <a role="button">{styleName}</a>
         </li>,
         </li>,
       );
       );
     });
     });
@@ -115,11 +115,7 @@ class CustomizeHighlightSetting extends React.Component {
         <div className="help-block">
         <div className="help-block">
           <label>Examples:</label>
           <label>Examples:</label>
           <div className="wiki">
           <div className="wiki">
-            <pre className={`hljs ${!adminCustomizeContainer.state.isHighlightJsStyleBorderEnabled && 'hljs-no-border'}`}>
-              <code className="highlightjs-demo">
-                {this.getDemoFunction()}
-              </code>
-            </pre>
+            {this.renderHljsDemo()}
           </div>
           </div>
         </div>
         </div>
 
 

+ 0 - 6
src/client/js/components/Admin/Customize/CustomizeLayoutSetting.jsx

@@ -2,8 +2,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import loggerFactory from '@alias/logger';
-
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
@@ -14,9 +12,6 @@ import CustomizeThemeOptions from './CustomizeThemeOptions';
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
-const logger = loggerFactory('growi:importer');
-
-
 class CustomizeLayoutSetting extends React.Component {
 class CustomizeLayoutSetting extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
@@ -34,7 +29,6 @@ class CustomizeLayoutSetting extends React.Component {
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
-      logger.error(err);
     }
     }
   }
   }
 
 

+ 1 - 16
src/client/js/components/Admin/Customize/CustomizeScriptSetting.jsx

@@ -2,8 +2,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import loggerFactory from '@alias/logger';
-
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
@@ -13,25 +11,14 @@ import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import CustomScriptEditor from '../CustomScriptEditor';
 import CustomScriptEditor from '../CustomScriptEditor';
 
 
-const logger = loggerFactory('growi:customizeScript');
-
 class CustomizeScriptSetting extends React.Component {
 class CustomizeScriptSetting extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
-    this.state = {
-      editorInputValue: '',
-    };
-
     this.onClickSubmit = this.onClickSubmit.bind(this);
     this.onClickSubmit = this.onClickSubmit.bind(this);
   }
   }
 
 
-  componentDidMount() {
-    const { customizeScript } = this.props.appContainer.getConfig();
-    this.setState({ editorInputValue: customizeScript || '' });
-  }
-
   async onClickSubmit() {
   async onClickSubmit() {
     const { t, adminCustomizeContainer } = this.props;
     const { t, adminCustomizeContainer } = this.props;
 
 
@@ -41,7 +28,6 @@ class CustomizeScriptSetting extends React.Component {
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
-      logger.error(err);
     }
     }
   }
   }
 
 
@@ -89,8 +75,7 @@ class CustomizeScriptSetting extends React.Component {
         <div className="form-group">
         <div className="form-group">
           <div className="col-xs-12">
           <div className="col-xs-12">
             <CustomScriptEditor
             <CustomScriptEditor
-              // The value passed must be immutable
-              value={this.state.editorInputValue}
+              value={adminCustomizeContainer.state.currentCustomizeScript || ''}
               onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeScript(inputValue) }}
               onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeScript(inputValue) }}
             />
             />
           </div>
           </div>

+ 1 - 6
src/client/js/components/Admin/Customize/CustomizeTitle.jsx

@@ -2,16 +2,12 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import loggerFactory from '@alias/logger';
-
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
-const logger = loggerFactory('growi:Customize');
-
 class CustomizeTitle extends React.Component {
 class CustomizeTitle extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
@@ -29,7 +25,6 @@ class CustomizeTitle extends React.Component {
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
-      logger.error(err);
     }
     }
   }
   }
 
 
@@ -42,7 +37,7 @@ class CustomizeTitle extends React.Component {
         <h2 className="admin-setting-header">{t('admin:customize_setting.custom_title')}</h2>
         <h2 className="admin-setting-header">{t('admin:customize_setting.custom_title')}</h2>
         <p
         <p
           className="well"
           className="well"
-          // eslint-disable-next-line react/no-danger, max-len
+          // eslint-disable-next-line react/no-danger
           dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail') }}
           dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail') }}
         />
         />
         {/* TODO i18n */}
         {/* TODO i18n */}

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

@@ -113,7 +113,7 @@ UserGroupCreateForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
 
-  isAclEnabled: PropTypes.bool,
+  isAclEnabled: PropTypes.bool.isRequired,
   onCreate: PropTypes.func.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() {
   render() {
+    const { isAclEnabled } = this.props.appContainer.config;
+
     return (
     return (
       <Fragment>
       <Fragment>
         <UserGroupCreateForm
         <UserGroupCreateForm
-          isAclEnabled={this.props.isAclEnabled}
+          isAclEnabled={isAclEnabled}
           onCreate={this.addUserGroup}
           onCreate={this.addUserGroup}
         />
         />
         <UserGroupTable
         <UserGroupTable
           userGroups={this.state.userGroups}
           userGroups={this.state.userGroups}
-          isAclEnabled={this.props.isAclEnabled}
+          isAclEnabled={isAclEnabled}
           onDelete={this.showDeleteModal}
           onDelete={this.showDeleteModal}
           userGroupRelations={this.state.userGroupRelations}
           userGroupRelations={this.state.userGroupRelations}
         />
         />
@@ -179,8 +181,6 @@ const UserGroupPageWrapper = (props) => {
 
 
 UserGroupPage.propTypes = {
 UserGroupPage.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  isAclEnabled: PropTypes.bool,
 };
 };
 
 
 export default UserGroupPageWrapper;
 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,
   userGroups: PropTypes.arrayOf(PropTypes.object).isRequired,
   userGroupRelations: PropTypes.object.isRequired,
   userGroupRelations: PropTypes.object.isRequired,
-  isAclEnabled: PropTypes.bool,
+  isAclEnabled: PropTypes.bool.isRequired,
   onDelete: PropTypes.func.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 { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
+import AdminUserGroupDetailContainer from '../../../services/AdminUserGroupDetailContainer';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
 class UserGroupEditForm extends React.Component {
 class UserGroupEditForm extends React.Component {
@@ -13,9 +13,12 @@ class UserGroupEditForm extends React.Component {
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
+    const { adminUserGroupDetailContainer } = props;
+    const { userGroup } = adminUserGroupDetailContainer.state;
+
     this.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;
     this.xss = window.xss;
@@ -35,7 +38,7 @@ class UserGroupEditForm extends React.Component {
     e.preventDefault();
     e.preventDefault();
 
 
     try {
     try {
-      const res = await this.props.userGroupDetailContainer.updateUserGroup({
+      const res = await this.props.adminUserGroupDetailContainer.updateUserGroup({
         name: this.state.name,
         name: this.state.name,
       });
       });
 
 
@@ -55,7 +58,7 @@ class UserGroupEditForm extends React.Component {
   }
   }
 
 
   render() {
   render() {
-    const { t, userGroupDetailContainer } = this.props;
+    const { t, adminUserGroupDetailContainer } = this.props;
 
 
     return (
     return (
       <form className="form-horizontal" onSubmit={this.handleSubmit}>
       <form className="form-horizontal" onSubmit={this.handleSubmit}>
@@ -73,7 +76,7 @@ class UserGroupEditForm extends React.Component {
               <input
               <input
                 type="text"
                 type="text"
                 className="form-control"
                 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
                 disabled
               />
               />
             </div>
             </div>
@@ -93,14 +96,14 @@ class UserGroupEditForm extends React.Component {
 UserGroupEditForm.propTypes = {
 UserGroupEditForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
+  adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 };
 
 
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
 const UserGroupEditFormWrapper = (props) => {
 const UserGroupEditFormWrapper = (props) => {
-  return createSubscribedElement(UserGroupEditForm, props, [AppContainer, UserGroupDetailContainer]);
+  return createSubscribedElement(UserGroupEditForm, props, [AppContainer, AdminUserGroupDetailContainer]);
 };
 };
 
 
 export default withTranslation()(UserGroupEditFormWrapper);
 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 PaginationWrapper from '../../PaginationWrapper';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
+import AdminUserGroupDetailContainer from '../../../services/AdminUserGroupDetailContainer';
 import { toastError } from '../../../util/apiNotification';
 import { toastError } from '../../../util/apiNotification';
 
 
 class UserGroupPageList extends React.Component {
 class UserGroupPageList extends React.Component {
@@ -33,7 +33,7 @@ class UserGroupPageList extends React.Component {
     const offset = (pageNum - 1) * limit;
     const offset = (pageNum - 1) * limit;
 
 
     try {
     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,
         limit,
         offset,
         offset,
       });
       });
@@ -51,14 +51,14 @@ class UserGroupPageList extends React.Component {
   }
   }
 
 
   render() {
   render() {
-    const { t, userGroupDetailContainer } = this.props;
+    const { t, adminUserGroupDetailContainer } = this.props;
 
 
     return (
     return (
       <Fragment>
       <Fragment>
         <ul className="page-list-ul page-list-ul-flat">
         <ul className="page-list-ul page-list-ul-flat">
           {this.state.currentPages.map((page) => { return <Page key={page._id} page={page} /> })}
           {this.state.currentPages.map((page) => { return <Page key={page._id} page={page} /> })}
         </ul>
         </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
         <PaginationWrapper
           activePage={this.state.activePage}
           activePage={this.state.activePage}
           changePage={this.handlePageChange}
           changePage={this.handlePageChange}
@@ -74,14 +74,14 @@ class UserGroupPageList extends React.Component {
 UserGroupPageList.propTypes = {
 UserGroupPageList.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
+  adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 };
 
 
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
 const UserGroupPageListWrapper = (props) => {
 const UserGroupPageListWrapper = (props) => {
-  return createSubscribedElement(UserGroupPageList, props, [AppContainer, UserGroupDetailContainer]);
+  return createSubscribedElement(UserGroupPageList, props, [AppContainer, AdminUserGroupDetailContainer]);
 };
 };
 
 
 export default withTranslation()(UserGroupPageListWrapper);
 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 { debounce } from 'throttle-debounce';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
+import AdminUserGroupDetailContainer from '../../../services/AdminUserGroupDetailContainer';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import UserPicture from '../../User/UserPicture';
 import UserPicture from '../../User/UserPicture';
 
 
@@ -36,16 +36,19 @@ class UserGroupUserFormByInput extends React.Component {
   }
   }
 
 
   async addUserBySubmit() {
   async addUserBySubmit() {
+    const { adminUserGroupDetailContainer } = this.props;
+    const { userGroup } = adminUserGroupDetailContainer.state;
+
     if (this.state.inputUser.length === 0) { return }
     if (this.state.inputUser.length === 0) { return }
     const userName = this.state.inputUser[0].username;
     const userName = this.state.inputUser[0].username;
 
 
     try {
     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: '' });
       this.setState({ inputUser: '' });
     }
     }
     catch (err) {
     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() {
   async searhApplicableUsers() {
+    const { adminUserGroupDetailContainer } = this.props;
+
     try {
     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 });
       this.setState({ applicableUsers: users, isLoading: false });
     }
     }
     catch (err) {
     catch (err) {
@@ -89,14 +94,14 @@ class UserGroupUserFormByInput extends React.Component {
   }
   }
 
 
   renderMenuItemChildren(option) {
   renderMenuItemChildren(option) {
-    const { userGroupDetailContainer } = this.props;
+    const { adminUserGroupDetailContainer } = this.props;
     const user = option;
     const user = option;
     return (
     return (
       <React.Fragment>
       <React.Fragment>
         <UserPicture user={user} size="sm" withoutLink />
         <UserPicture user={user} size="sm" withoutLink />
         <strong className="ml-2">{user.username}</strong>
         <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>
       </React.Fragment>
     );
     );
   }
   }
@@ -151,14 +156,14 @@ class UserGroupUserFormByInput extends React.Component {
 UserGroupUserFormByInput.propTypes = {
 UserGroupUserFormByInput.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
+  adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 };
 
 
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
 const UserGroupUserFormByInputWrapper = (props) => {
 const UserGroupUserFormByInputWrapper = (props) => {
-  return createSubscribedElement(UserGroupUserFormByInput, props, [AppContainer, UserGroupDetailContainer]);
+  return createSubscribedElement(UserGroupUserFormByInput, props, [AppContainer, AdminUserGroupDetailContainer]);
 };
 };
 
 
 export default withTranslation()(UserGroupUserFormByInputWrapper);
 export default withTranslation()(UserGroupUserFormByInputWrapper);

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

@@ -8,14 +8,14 @@ import {
 import UserGroupUserFormByInput from './UserGroupUserFormByInput';
 import UserGroupUserFormByInput from './UserGroupUserFormByInput';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
+import AdminUserGroupDetailContainer from '../../../services/AdminUserGroupDetailContainer';
 import RadioButtonForSerchUserOption from './RadioButtonForSerchUserOption';
 import RadioButtonForSerchUserOption from './RadioButtonForSerchUserOption';
 import CheckBoxForSerchUserOption from './CheckBoxForSerchUserOption';
 import CheckBoxForSerchUserOption from './CheckBoxForSerchUserOption';
 
 
 class UserGroupUserModal extends React.Component {
 class UserGroupUserModal extends React.Component {
 
 
   render() {
   render() {
-    const { t, userGroupDetailContainer } = this.props;
+    const { t, adminUserGroupDetailContainer } = this.props;
 
 
     return (
     return (
       <Modal isOpen={userGroupDetailContainer.state.isUserGroupUserModalOpen} toggle={userGroupDetailContainer.closeUserGroupUserModal}>
       <Modal isOpen={userGroupDetailContainer.state.isUserGroupUserModalOpen} toggle={userGroupDetailContainer.closeUserGroupUserModal}>
@@ -32,15 +32,15 @@ class UserGroupUserModal extends React.Component {
               <div className="mb-5">
               <div className="mb-5">
                 <CheckBoxForSerchUserOption
                 <CheckBoxForSerchUserOption
                   option="Mail"
                   option="Mail"
-                  checked={userGroupDetailContainer.state.isAlsoMailSearched}
-                  onChange={userGroupDetailContainer.switchIsAlsoMailSearched}
+                  checked={adminUserGroupDetailContainer.state.isAlsoMailSearched}
+                  onChange={adminUserGroupDetailContainer.switchIsAlsoMailSearched}
                 />
                 />
               </div>
               </div>
               <div className="mb-5">
               <div className="mb-5">
                 <CheckBoxForSerchUserOption
                 <CheckBoxForSerchUserOption
                   option="Name"
                   option="Name"
-                  checked={userGroupDetailContainer.state.isAlsoNameSearched}
-                  onChange={userGroupDetailContainer.switchIsAlsoNameSearched}
+                  checked={adminUserGroupDetailContainer.state.isAlsoNameSearched}
+                  onChange={adminUserGroupDetailContainer.switchIsAlsoNameSearched}
                 />
                 />
               </div>
               </div>
             </div>
             </div>
@@ -48,22 +48,22 @@ class UserGroupUserModal extends React.Component {
               <div className="mb-5">
               <div className="mb-5">
                 <RadioButtonForSerchUserOption
                 <RadioButtonForSerchUserOption
                   searchType="forward"
                   searchType="forward"
-                  checked={userGroupDetailContainer.state.searchType === 'forward'}
-                  onChange={() => { userGroupDetailContainer.switchSearchType('forward') }}
+                  checked={adminUserGroupDetailContainer.state.searchType === 'forward'}
+                  onChange={() => { adminUserGroupDetailContainer.switchSearchType('forward') }}
                 />
                 />
               </div>
               </div>
               <div className="mb-5">
               <div className="mb-5">
                 <RadioButtonForSerchUserOption
                 <RadioButtonForSerchUserOption
                   searchType="partial"
                   searchType="partial"
-                  checked={userGroupDetailContainer.state.searchType === 'partial'}
-                  onChange={() => { userGroupDetailContainer.switchSearchType('partial') }}
+                  checked={adminUserGroupDetailContainer.state.searchType === 'partial'}
+                  onChange={() => { adminUserGroupDetailContainer.switchSearchType('partial') }}
                 />
                 />
               </div>
               </div>
               <div className="mb-5">
               <div className="mb-5">
                 <RadioButtonForSerchUserOption
                 <RadioButtonForSerchUserOption
                   searchType="backward"
                   searchType="backward"
-                  checked={userGroupDetailContainer.state.searchType === 'backword'}
-                  onChange={() => { userGroupDetailContainer.switchSearchType('backword') }}
+                  checked={adminUserGroupDetailContainer.state.searchType === 'backword'}
+                  onChange={() => { adminUserGroupDetailContainer.switchSearchType('backword') }}
                 />
                 />
               </div>
               </div>
             </div>
             </div>
@@ -78,14 +78,14 @@ class UserGroupUserModal extends React.Component {
 UserGroupUserModal.propTypes = {
 UserGroupUserModal.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
+  adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 };
 
 
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
 const UserGroupUserModalWrapper = (props) => {
 const UserGroupUserModalWrapper = (props) => {
-  return createSubscribedElement(UserGroupUserModal, props, [AppContainer, UserGroupDetailContainer]);
+  return createSubscribedElement(UserGroupUserModal, props, [AppContainer, AdminUserGroupDetailContainer]);
 };
 };
 
 
 export default withTranslation()(UserGroupUserModalWrapper);
 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 UserPicture from '../../User/UserPicture';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
+import AdminUserGroupDetailContainer from '../../../services/AdminUserGroupDetailContainer';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
 class UserGroupUserTable extends React.Component {
 class UserGroupUserTable extends React.Component {
@@ -21,17 +21,17 @@ class UserGroupUserTable extends React.Component {
 
 
   async removeUser(username) {
   async removeUser(username) {
     try {
     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) {
     catch (err) {
       // eslint-disable-next-line max-len
       // 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() {
   render() {
-    const { t, userGroupDetailContainer } = this.props;
+    const { t, adminUserGroupDetailContainer } = this.props;
 
 
     return (
     return (
       <table className="table table-bordered table-user-list">
       <table className="table table-bordered table-user-list">
@@ -48,7 +48,7 @@ class UserGroupUserTable extends React.Component {
           </tr>
           </tr>
         </thead>
         </thead>
         <tbody>
         <tbody>
-          {userGroupDetailContainer.state.userGroupRelations.map((sRelation) => {
+          {adminUserGroupDetailContainer.state.userGroupRelations.map((sRelation) => {
             const { relatedUser } = sRelation;
             const { relatedUser } = sRelation;
 
 
             return (
             return (
@@ -83,7 +83,7 @@ class UserGroupUserTable extends React.Component {
           <tr>
           <tr>
             <td></td>
             <td></td>
             <td className="text-center">
             <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>
                 <i className="ti-plus"></i>
               </button>
               </button>
             </td>
             </td>
@@ -103,14 +103,14 @@ class UserGroupUserTable extends React.Component {
 UserGroupUserTable.propTypes = {
 UserGroupUserTable.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
+  adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 };
 
 
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
 const UserGroupUserTableWrapper = (props) => {
 const UserGroupUserTableWrapper = (props) => {
-  return createSubscribedElement(UserGroupUserTable, props, [AppContainer, UserGroupDetailContainer]);
+  return createSubscribedElement(UserGroupUserTable, props, [AppContainer, AdminUserGroupDetailContainer]);
 };
 };
 
 
 export default withTranslation()(UserGroupUserTableWrapper);
 export default withTranslation()(UserGroupUserTableWrapper);

+ 1 - 1
src/client/js/components/PageComment/Comment.jsx

@@ -289,7 +289,7 @@ class Comment extends React.Component {
             commentCreator={creator.username}
             commentCreator={creator.username}
           />
           />
         ) : (
         ) : (
-          <div className={rootClassName}>
+          <div id={commentId} className={rootClassName}>
             <UserPicture user={creator} />
             <UserPicture user={creator} />
             <div className="page-comment-main">
             <div className="page-comment-main">
               <div className="page-comment-creator">
               <div className="page-comment-creator">

+ 13 - 3
src/client/js/components/SavePageControls.jsx

@@ -8,6 +8,8 @@ import {
   DropdownToggle, DropdownMenu, DropdownItem,
   DropdownToggle, DropdownMenu, DropdownItem,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
+import loggerFactory from '@alias/logger';
+
 import PageContainer from '../services/PageContainer';
 import PageContainer from '../services/PageContainer';
 import AppContainer from '../services/AppContainer';
 import AppContainer from '../services/AppContainer';
 import EditorContainer from '../services/EditorContainer';
 import EditorContainer from '../services/EditorContainer';
@@ -16,6 +18,7 @@ import { createSubscribedElement } from './UnstatedUtils';
 import SlackNotification from './SlackNotification';
 import SlackNotification from './SlackNotification';
 import GrantSelector from './SavePageControls/GrantSelector';
 import GrantSelector from './SavePageControls/GrantSelector';
 
 
+const logger = loggerFactory('growi:SavePageControls');
 
 
 class SavePageControls extends React.Component {
 class SavePageControls extends React.Component {
 
 
@@ -46,12 +49,19 @@ class SavePageControls extends React.Component {
     this.props.editorContainer.setState(data);
     this.props.editorContainer.setState(data);
   }
   }
 
 
-  save() {
+  async save() {
     const { pageContainer, editorContainer } = this.props;
     const { pageContainer, editorContainer } = this.props;
     // disable unsaved warning
     // disable unsaved warning
     editorContainer.disableUnsavedWarning();
     editorContainer.disableUnsavedWarning();
-    // save
-    pageContainer.saveAndReload(editorContainer.getCurrentOptionsToSave());
+
+    try {
+      // save
+      await pageContainer.saveAndReload(editorContainer.getCurrentOptionsToSave());
+    }
+    catch (error) {
+      logger.error('failed to save', error);
+      pageContainer.showErrorToastr(error);
+    }
   }
   }
 
 
   saveAndOverwriteScopesOfDescendants() {
   saveAndOverwriteScopesOfDescendants() {

+ 0 - 5
src/client/js/legacy/crowi-admin.js

@@ -1,5 +0,0 @@
-require('./thirdparty-js/jQuery.style.switcher');
-
-$(() => {
-  $('#styleOptions').styleSwitcher();
-});

+ 0 - 233
src/client/js/legacy/thirdparty-js/jQuery.style.switcher.js

@@ -1,233 +0,0 @@
-/**
-@author Cameron Manavian
-jQuery Style Switcher
-
-The MIT License (MIT)
-
-Copyright (c) 2014 Cameron Manavian
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-**/
-(function ($) {
-	var jStyleSwitcher,
-		_defaultOptions = {
-			hasPreview: true,
-			defaultThemeId: 'jssDefault',
-			fullPath: 'css/',
-			cookie: {
-				expires: 30,
-				isManagingLoad: true
-			}
-		},
-		// private
-		_cookieKey = 'jss_selected',
-		_docCookies = {};
-
-	/*\
-	|*|
-	|*|  :: cookies.js ::
-	|*|
-	|*|  A complete cookies reader/writer framework with full unicode support.
-	|*|
-	|*|  revision #1
-	|*|
-	|*|  https://developer.mozilla.org/en-US/docs/Web/API/document.cookie
-	|*|
-	|*|  This framework is released under the GNU Public License, version 3 or later.
-	|*|  http://www.gnu.org/licenses/gpl-3.0-standalone.html
-	|*|
-	|*|  Syntaxes:
-	|*|
-	|*|  * docCookies.setItem(name, value[, end[, path[, domain[, secure]]]])
-	|*|  * docCookies.getItem(name)
-	|*|  * docCookies.removeItem(name[, path[, domain]])
-	|*|  * docCookies.hasItem(name)
-	|*|  * docCookies.keys()
-	|*|
-	\*/
-	_docCookies = {
-		getItem: function (sKey) {
-			if (!sKey) {
-				return null;
-			}
-			return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
-		},
-		setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) {
-			if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) {
-				return false;
-			}
-			var sExpires = "";
-			if (vEnd) {
-				switch (vEnd.constructor) {
-					case Number:
-						sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd;
-						break;
-					case String:
-						sExpires = "; expires=" + vEnd;
-						break;
-					case Date:
-						sExpires = "; expires=" + vEnd.toUTCString();
-						break;
-				}
-			}
-			document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "") + (bSecure ? "; secure" : "");
-			return true;
-		},
-		removeItem: function (sKey, sPath, sDomain) {
-			if (!this.hasItem(sKey)) {
-				return false;
-			}
-			document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "");
-			return true;
-		},
-		hasItem: function (sKey) {
-			if (!sKey) {
-				return false;
-			}
-			return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie);
-		},
-		keys: function () {
-			var aKeys = document.cookie.replace(/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:\1|$)/g, "").split(/\s*(?:\=[^;]*)?;\s*/);
-			for (var nLen = aKeys.length, nIdx = 0; nIdx < nLen; nIdx++) {
-				aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]);
-			}
-			return aKeys;
-		}
-	};
-
-	jStyleSwitcher = function ($root, config) {
-		return this.init($root, config);
-	};
-
-	jStyleSwitcher.prototype = {
-
-		/**
-		 * {Object} DOM reference to style option list
-		 */
-		$root: null,
-
-		/**
-		 * {Object} configs for the style switcher
-		 */
-		config: {},
-
-		/**
-		 * {Object} jQuery reference to <link> tag for swapping CSS
-		 */
-		$themeCss: null,
-
-		/**
-		 * {String} default theme page was loaded with
-		 */
-		defaultTheme: null,
-
-		init: function ($root, config) {
-			this.$root = $root;
-			this.config = config ? config : {};
-			this.setDefaultTheme();
-			if(this.defaultTheme) {
-				// try cookies
-				if (this.config.cookie) {
-					this.checkCookie();
-				}
-				// try hover
-				if (this.config.hasPreview) {
-					this.addHoverEvents();
-				}
-				// finally, add click events
-				this.addClickEvents();
-			} else {
-				this.$root.addClass('jssError error level0');
-			}
-		},
-
-		setDefaultTheme: function () {
-			this.$themeCss = $('link[id=' + this.config.defaultThemeId + ']');
-			if(this.$themeCss.length) {
-				this.defaultTheme = this.$themeCss.attr('href');
-			}
-		},
-
-		resetStyle: function() {
-			this.updateStyle(this.defaultTheme);
-		},
-
-		updateStyle: function(newStyle) {
-			this.$themeCss.attr('href', newStyle);
-		},
-
-		getFullAssetPath: function(asset) {
-			return this.config.fullPath + asset;
-		},
-
-		checkCookie: function () {
-			var styleCookie;
-			// if using cookies and using JavaScript to load css
-			if (this.config.cookie && this.config.cookie.isManagingLoad) {
-				// check if css is set in cookie
-				styleCookie = _docCookies.getItem(_cookieKey);
-				if (styleCookie) {
-					newStyle = this.getFullAssetPath(styleCookie);
-					// update link tag
-					this.updateStyle(newStyle);
-					// update default ref
-					this.defaultTheme = newStyle;
-				}
-			}
-		},
-
-		addHoverEvents: function () {
-			var self = this;
-			this.$root.find('a').hover(
-				function () {
-					var asset = $(this).data('theme'),
-						newStyle = self.getFullAssetPath(asset);
-					// update link tag
-					self.updateStyle(newStyle);
-				},
-				function () {
-					// reset link tag
-					self.resetStyle();
-				}
-			);
-		},
-
-		addClickEvents: function () {
-			var self = this;
-			this.$root.find('a').click(
-				function () {
-					var asset = $(this).data('theme'),
-            newStyle = self.getFullAssetPath(asset);
-					// update link tag
-					self.updateStyle(newStyle);
-					// update default ref
-					self.defaultTheme = newStyle;
-					// try to store cookie
-					if (self.config.cookie) {
-						_docCookies.setItem(_cookieKey, asset, self.config.cookie.expires, '/');
-					}
-				}
-			);
-		}
-	};
-
-	$.fn.styleSwitcher = function (options) {
-		return new jStyleSwitcher(this, $.extend(true, _defaultOptions, options));
-	};
-})(jQuery);

+ 7 - 7
src/client/js/services/AdminAppContainer.js

@@ -35,7 +35,7 @@ export default class AdminAppContainer extends Container {
       customEndpoint: '',
       customEndpoint: '',
       bucket: '',
       bucket: '',
       accessKeyId: '',
       accessKeyId: '',
-      secretKey: '',
+      secretAccessKey: '',
       isEnabledPlugins: true,
       isEnabledPlugins: true,
     };
     };
 
 
@@ -53,7 +53,7 @@ export default class AdminAppContainer extends Container {
     this.changeCustomEndpoint = this.changeCustomEndpoint.bind(this);
     this.changeCustomEndpoint = this.changeCustomEndpoint.bind(this);
     this.changeBucket = this.changeBucket.bind(this);
     this.changeBucket = this.changeBucket.bind(this);
     this.changeAccessKeyId = this.changeAccessKeyId.bind(this);
     this.changeAccessKeyId = this.changeAccessKeyId.bind(this);
-    this.changeSecretKey = this.changeSecretKey.bind(this);
+    this.changeSecretAccessKey = this.changeSecretAccessKey.bind(this);
     this.changeIsEnabledPlugins = this.changeIsEnabledPlugins.bind(this);
     this.changeIsEnabledPlugins = this.changeIsEnabledPlugins.bind(this);
     this.updateAppSettingHandler = this.updateAppSettingHandler.bind(this);
     this.updateAppSettingHandler = this.updateAppSettingHandler.bind(this);
     this.updateSiteUrlSettingHandler = this.updateSiteUrlSettingHandler.bind(this);
     this.updateSiteUrlSettingHandler = this.updateSiteUrlSettingHandler.bind(this);
@@ -94,7 +94,7 @@ export default class AdminAppContainer extends Container {
         customEndpoint: appSettingsParams.customEndpoint,
         customEndpoint: appSettingsParams.customEndpoint,
         bucket: appSettingsParams.bucket,
         bucket: appSettingsParams.bucket,
         accessKeyId: appSettingsParams.accessKeyId,
         accessKeyId: appSettingsParams.accessKeyId,
-        secretKey: appSettingsParams.secretKey,
+        secretAccessKey: appSettingsParams.secretAccessKey,
         isEnabledPlugins: appSettingsParams.isEnabledPlugins,
         isEnabledPlugins: appSettingsParams.isEnabledPlugins,
       });
       });
 
 
@@ -205,10 +205,10 @@ export default class AdminAppContainer extends Container {
   }
   }
 
 
   /**
   /**
-   * Change secret key
+   * Change secret access key
    */
    */
-  changeSecretKey(secretKey) {
-    this.setState({ secretKey });
+  changeSecretAccessKey(secretAccessKey) {
+    this.setState({ secretAccessKey });
   }
   }
 
 
   /**
   /**
@@ -276,7 +276,7 @@ export default class AdminAppContainer extends Container {
       customEndpoint: this.state.customEndpoint,
       customEndpoint: this.state.customEndpoint,
       bucket: this.state.bucket,
       bucket: this.state.bucket,
       accessKeyId: this.state.accessKeyId,
       accessKeyId: this.state.accessKeyId,
-      secretKey: this.state.secretKey,
+      secretAccessKey: this.state.secretAccessKey,
     });
     });
     const { awsSettingParams } = response.data;
     const { awsSettingParams } = response.data;
     return awsSettingParams;
     return awsSettingParams;

+ 153 - 54
src/client/js/services/AdminCustomizeContainer.js

@@ -88,8 +88,9 @@ export default class AdminCustomizeContainer extends Container {
       this.setState({ currentHighlightJsStyleName: this.state.highlightJsCssSelectorOptions[customizeParams.styleName].name });
       this.setState({ currentHighlightJsStyleName: this.state.highlightJsCssSelectorOptions[customizeParams.styleName].name });
     }
     }
     catch (err) {
     catch (err) {
+      this.setState({ retrieveError: err });
       logger.error(err);
       logger.error(err);
-      toastError(new Error('Failed to fetch data'));
+      throw new Error('Failed to fetch data');
     }
     }
   }
   }
 
 
@@ -109,6 +110,11 @@ export default class AdminCustomizeContainer extends Container {
       return;
       return;
     }
     }
     this.setState({ currentTheme: themeName });
     this.setState({ currentTheme: themeName });
+
+    // preview if production
+    if (process.env.NODE_ENV !== 'development') {
+      this.previewTheme(themeName);
+    }
   }
   }
 
 
   /**
   /**
@@ -161,6 +167,8 @@ export default class AdminCustomizeContainer extends Container {
     this.setState({ currentHighlightJsStyleName: styleName });
     this.setState({ currentHighlightJsStyleName: styleName });
     // recommended settings are applied
     // recommended settings are applied
     this.setState({ isHighlightJsStyleBorderEnabled: isBorderEnable });
     this.setState({ isHighlightJsStyleBorderEnabled: isBorderEnable });
+
+    this.previewHighlightJsStyle(styleId);
   }
   }
 
 
   /**
   /**
@@ -198,102 +206,185 @@ export default class AdminCustomizeContainer extends Container {
     this.setState({ currentCustomizeScript: inpuValue });
     this.setState({ currentCustomizeScript: inpuValue });
   }
   }
 
 
+  /**
+   * Preview theme
+   * @param {string} themeName
+   */
+  async previewTheme(themeName) {
+    try {
+      // get theme asset path
+      const response = await this.appContainer.apiv3.get('/customize-setting/layout-theme/asset-path', { themeName });
+      const { assetPath } = response.data;
+
+      const themeLink = document.getElementById('grw-theme-link');
+      themeLink.setAttribute('href', assetPath);
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  /**
+   * Preview hljs style
+   * @param {string} styleId
+   */
+  previewHighlightJsStyle(styleId) {
+    const styleLInk = document.querySelectorAll('#grw-hljs-container-for-demo link')[0];
+    // replace css url
+    // see https://regex101.com/r/gBNZYu/4
+    styleLInk.href = styleLInk.href.replace(/[^/]+\.css$/, `${styleId}.css`);
+  }
 
 
   /**
   /**
    * Update layout
    * Update layout
    * @memberOf AdminCustomizeContainer
    * @memberOf AdminCustomizeContainer
-   * @return {Array} Appearance
    */
    */
   async updateCustomizeLayoutAndTheme() {
   async updateCustomizeLayoutAndTheme() {
-    const response = await this.appContainer.apiv3.put('/customize-setting/layoutTheme', {
-      layoutType: this.state.currentLayout,
-      themeType: this.state.currentTheme,
-    });
-    const { customizedParams } = response.data;
-    return customizedParams;
+    try {
+      const response = await this.appContainer.apiv3.put('/customize-setting/layout-theme', {
+        layoutType: this.state.currentLayout,
+        themeType: this.state.currentTheme,
+      });
+      const { customizedParams } = response.data;
+      this.setState({
+        layoutType: customizedParams.layoutType,
+        themeType: customizedParams.themeType,
+      });
+    }
+    catch (err) {
+      logger.error(err);
+      throw new Error('Failed to update data');
+    }
   }
   }
 
 
   /**
   /**
    * Update behavior
    * Update behavior
    * @memberOf AdminCustomizeContainer
    * @memberOf AdminCustomizeContainer
-   * @return {string} Behavior
    */
    */
   async updateCustomizeBehavior() {
   async updateCustomizeBehavior() {
-    const response = await this.appContainer.apiv3.put('/customize-setting/behavior', {
-      behaviorType: this.state.currentBehavior,
-    });
-    const { customizedParams } = response.data;
-    return customizedParams;
+    try {
+      const response = await this.appContainer.apiv3.put('/customize-setting/behavior', {
+        behaviorType: this.state.currentBehavior,
+      });
+      const { customizedParams } = response.data;
+      this.setState({
+        behaviorType: customizedParams.behaviorType,
+      });
+    }
+    catch (err) {
+      logger.error(err);
+      throw new Error('Failed to update data');
+    }
   }
   }
 
 
   /**
   /**
    * Update function
    * Update function
    * @memberOf AdminCustomizeContainer
    * @memberOf AdminCustomizeContainer
-   * @return {string} Functions
    */
    */
   async updateCustomizeFunction() {
   async updateCustomizeFunction() {
-    const response = await this.appContainer.apiv3.put('/customize-setting/function', {
-      isEnabledTimeline: this.state.isEnabledTimeline,
-      isSavedStatesOfTabChanges: this.state.isSavedStatesOfTabChanges,
-      isEnabledAttachTitleHeader: this.state.isEnabledAttachTitleHeader,
-      recentCreatedLimit: this.state.currentRecentCreatedLimit,
-      isEnabledStaleNotification: this.state.isEnabledStaleNotification,
-    });
-    const { customizedParams } = response.data;
-    return customizedParams;
+    try {
+      const response = await this.appContainer.apiv3.put('/customize-setting/function', {
+        isEnabledTimeline: this.state.isEnabledTimeline,
+        isSavedStatesOfTabChanges: this.state.isSavedStatesOfTabChanges,
+        isEnabledAttachTitleHeader: this.state.isEnabledAttachTitleHeader,
+        recentCreatedLimit: this.state.currentRecentCreatedLimit,
+        isEnabledStaleNotification: this.state.isEnabledStaleNotification,
+      });
+      const { customizedParams } = response.data;
+      this.setState({
+        isEnabledTimeline: customizedParams.isEnabledTimeline,
+        isSavedStatesOfTabChanges: customizedParams.isSavedStatesOfTabChanges,
+        isEnabledAttachTitleHeader: customizedParams.isEnabledAttachTitleHeader,
+        recentCreatedLimit: customizedParams.currentRecentCreatedLimit,
+        isEnabledStaleNotification: customizedParams.isEnabledStaleNotification,
+      });
+    }
+    catch (err) {
+      logger.error(err);
+      throw new Error('Failed to update data');
+    }
   }
   }
 
 
   /**
   /**
    * Update code highlight
    * Update code highlight
    * @memberOf AdminCustomizeContainer
    * @memberOf AdminCustomizeContainer
-   * @return {Array} Code highlight
    */
    */
   async updateHighlightJsStyle() {
   async updateHighlightJsStyle() {
-    const response = await this.appContainer.apiv3.put('/customize-setting/highlight', {
-      highlightJsStyle: this.state.currentHighlightJsStyleId,
-      highlightJsStyleBorder: this.state.isHighlightJsStyleBorderEnabled,
-    });
-    const { customizedParams } = response.data;
-    return customizedParams;
+    try {
+      const response = await this.appContainer.apiv3.put('/customize-setting/highlight', {
+        highlightJsStyle: this.state.currentHighlightJsStyleId,
+        highlightJsStyleBorder: this.state.isHighlightJsStyleBorderEnabled,
+      });
+      const { customizedParams } = response.data;
+      this.setState({
+        highlightJsStyle: customizedParams.highlightJsStyle,
+        highlightJsStyleBorder: customizedParams.highlightJsStyleBorder,
+      });
+    }
+    catch (err) {
+      logger.error(err);
+      throw new Error('Failed to update data');
+    }
   }
   }
 
 
   /**
   /**
    * Update customTitle
    * Update customTitle
    * @memberOf AdminCustomizeContainer
    * @memberOf AdminCustomizeContainer
-   * @return {string} Customize title
    */
    */
   async updateCustomizeTitle() {
   async updateCustomizeTitle() {
-    const response = await this.appContainer.apiv3.put('/customize-setting/customize-title', {
-      customizeTitle: this.state.currentCustomizeTitle,
-    });
-    const { customizedParams } = response.data;
-    return customizedParams;
+    try {
+      const response = await this.appContainer.apiv3.put('/customize-setting/customize-title', {
+        customizeTitle: this.state.currentCustomizeTitle,
+      });
+      const { customizedParams } = response.data;
+      this.setState({
+        customizeTitle: customizedParams.customizeTitle,
+      });
+    }
+    catch (err) {
+      logger.error(err);
+      throw new Error('Failed to update data');
+    }
   }
   }
 
 
   /**
   /**
    * Update customHeader
    * Update customHeader
    * @memberOf AdminCustomizeContainer
    * @memberOf AdminCustomizeContainer
-   * @return {string} Customize html header
    */
    */
   async updateCustomizeHeader() {
   async updateCustomizeHeader() {
-    const response = await this.appContainer.apiv3.put('/customize-setting/customize-header', {
-      customizeHeader: this.state.currentCustomizeHeader,
-    });
-    const { customizedParams } = response.data;
-    return customizedParams;
+    try {
+      const response = await this.appContainer.apiv3.put('/customize-setting/customize-header', {
+        customizeHeader: this.state.currentCustomizeHeader,
+      });
+      const { customizedParams } = response.data;
+      this.setState({
+        currentCustomizeHeader: customizedParams.customizeHeader,
+      });
+    }
+    catch (err) {
+      logger.error(err);
+      throw new Error('Failed to update data');
+    }
   }
   }
 
 
   /**
   /**
    * Update customCss
    * Update customCss
    * @memberOf AdminCustomizeContainer
    * @memberOf AdminCustomizeContainer
-   * @return {string} Customize css
    */
    */
   async updateCustomizeCss() {
   async updateCustomizeCss() {
-    const response = await this.appContainer.apiv3.put('/customize-setting/customize-css', {
-      customizeCss: this.state.currentCustomizeCss,
-    });
-    const { customizedParams } = response.data;
-    return customizedParams;
+    try {
+      const response = await this.appContainer.apiv3.put('/customize-setting/customize-css', {
+        customizeCss: this.state.currentCustomizeCss,
+      });
+      const { customizedParams } = response.data;
+      this.setState({
+        currentCustomizeCss: customizedParams.customizeCss,
+      });
+    }
+    catch (err) {
+      logger.error(err);
+      throw new Error('Failed to update data');
+    }
   }
   }
 
 
   /**
   /**
@@ -302,11 +393,19 @@ export default class AdminCustomizeContainer extends Container {
    * @return {string} Customize scripts
    * @return {string} Customize scripts
    */
    */
   async updateCustomizeScript() {
   async updateCustomizeScript() {
-    const response = await this.appContainer.apiv3.put('/customize-setting/customize-script', {
-      customizeScript: this.state.currentCustomizeScript,
-    });
-    const { customizedParams } = response.data;
-    return customizedParams;
+    try {
+      const response = await this.appContainer.apiv3.put('/customize-setting/customize-script', {
+        customizeScript: this.state.currentCustomizeScript,
+      });
+      const { customizedParams } = response.data;
+      this.setState({
+        currentCustomizeScript: customizedParams.customizeScript,
+      });
+    }
+    catch (err) {
+      logger.error(err);
+      throw new Error('Failed to update data');
+    }
   }
   }
 
 
 }
 }

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

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

@@ -3,7 +3,7 @@ import { Container } from 'unstated';
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
 
 
 // eslint-disable-next-line no-unused-vars
 // 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)
  * Service container for admin users page (Users.jsx)

+ 1 - 1
src/client/styles/agile-admin/inverse/colors/spring.scss

@@ -34,7 +34,7 @@ $wikilinktext-hover: lighten($wikilinktext, 20%);
   background-color: rgba(255, 255, 255, 0.8);
   background-color: rgba(255, 255, 255, 0.8);
   background-image: url('/images/themes/spring/spring04.svg');
   background-image: url('/images/themes/spring/spring04.svg');
   background-attachment: fixed;
   background-attachment: fixed;
-  background-position: center center;
+  background-position: bottom;
   background-size: cover;
   background-size: cover;
 }
 }
 
 

+ 5 - 5
src/server/routes/apiv3/app-settings.js

@@ -83,7 +83,7 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  *          accessKeyId:
  *          accessKeyId:
  *            type: string
  *            type: string
  *            description: accesskey id for authentification of AWS
  *            description: accesskey id for authentification of AWS
- *          secretKey:
+ *          secretAccessKey:
  *            type: string
  *            type: string
  *            description: secret key for authentification of AWS
  *            description: secret key for authentification of AWS
  *      PluginSettingParams:
  *      PluginSettingParams:
@@ -126,7 +126,7 @@ module.exports = (crowi) => {
       body('customEndpoint').trim().matches(/^(https?:\/\/[^/]+|)$/).withMessage('カスタムエンドポイントは、http(s)://で始まるURLを指定してください。また、末尾の/は不要です。'),
       body('customEndpoint').trim().matches(/^(https?:\/\/[^/]+|)$/).withMessage('カスタムエンドポイントは、http(s)://で始まるURLを指定してください。また、末尾の/は不要です。'),
       body('bucket').trim(),
       body('bucket').trim(),
       body('accessKeyId').trim().matches(/^[\da-zA-Z]+$/),
       body('accessKeyId').trim().matches(/^[\da-zA-Z]+$/),
-      body('secretKey').trim(),
+      body('secretAccessKey').trim(),
     ],
     ],
     pluginSetting: [
     pluginSetting: [
       body('isEnabledPlugins').isBoolean(),
       body('isEnabledPlugins').isBoolean(),
@@ -170,7 +170,7 @@ module.exports = (crowi) => {
       customEndpoint: crowi.configManager.getConfig('crowi', 'aws:customEndpoint'),
       customEndpoint: crowi.configManager.getConfig('crowi', 'aws:customEndpoint'),
       bucket: crowi.configManager.getConfig('crowi', 'aws:bucket'),
       bucket: crowi.configManager.getConfig('crowi', 'aws:bucket'),
       accessKeyId: crowi.configManager.getConfig('crowi', 'aws:accessKeyId'),
       accessKeyId: crowi.configManager.getConfig('crowi', 'aws:accessKeyId'),
-      secretKey: crowi.configManager.getConfig('crowi', 'aws:secretKey'),
+      secretAccessKey: crowi.configManager.getConfig('crowi', 'aws:secretAccessKey'),
       isEnabledPlugins: crowi.configManager.getConfig('crowi', 'plugin:isEnabledPlugins'),
       isEnabledPlugins: crowi.configManager.getConfig('crowi', 'plugin:isEnabledPlugins'),
     };
     };
     return res.apiv3({ appSettingsParams });
     return res.apiv3({ appSettingsParams });
@@ -410,7 +410,7 @@ module.exports = (crowi) => {
       'aws:customEndpoint': req.body.customEndpoint,
       'aws:customEndpoint': req.body.customEndpoint,
       'aws:bucket': req.body.bucket,
       'aws:bucket': req.body.bucket,
       'aws:accessKeyId': req.body.accessKeyId,
       'aws:accessKeyId': req.body.accessKeyId,
-      'aws:secretKey': req.body.secretKey,
+      'aws:secretAccessKey': req.body.secretAccessKey,
     };
     };
 
 
     try {
     try {
@@ -420,7 +420,7 @@ module.exports = (crowi) => {
         customEndpoint: crowi.configManager.getConfig('crowi', 'aws:customEndpoint'),
         customEndpoint: crowi.configManager.getConfig('crowi', 'aws:customEndpoint'),
         bucket: crowi.configManager.getConfig('crowi', 'aws:bucket'),
         bucket: crowi.configManager.getConfig('crowi', 'aws:bucket'),
         accessKeyId: crowi.configManager.getConfig('crowi', 'aws:accessKeyId'),
         accessKeyId: crowi.configManager.getConfig('crowi', 'aws:accessKeyId'),
-        secretKey: crowi.configManager.getConfig('crowi', 'aws:secretKey'),
+        secretAccessKey: crowi.configManager.getConfig('crowi', 'aws:secretAccessKey'),
       };
       };
       return res.apiv3({ awsSettingParams });
       return res.apiv3({ awsSettingParams });
     }
     }

+ 48 - 5
src/server/routes/apiv3/customize-setting.js

@@ -7,7 +7,7 @@ const express = require('express');
 
 
 const router = express.Router();
 const router = express.Router();
 
 
-const { body } = require('express-validator/check');
+const { body, query } = require('express-validator');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
 /**
 /**
@@ -92,6 +92,11 @@ module.exports = (crowi) => {
   const { ApiV3FormValidator } = crowi.middlewares;
   const { ApiV3FormValidator } = crowi.middlewares;
 
 
   const validator = {
   const validator = {
+    themeAssetPath: [
+      query('themeName').isString().isIn([
+        'default', 'nature', 'mono-blue', 'wood', 'island', 'christmas', 'antarctic', 'default-dark', 'future', 'blue-night', 'halloween', 'spring',
+      ]),
+    ],
     layoutTheme: [
     layoutTheme: [
       body('layoutType').isString().isIn(['growi', 'kibela', 'crowi']),
       body('layoutType').isString().isIn(['growi', 'kibela', 'crowi']),
       body('themeType').isString().isIn([
       body('themeType').isString().isIn([
@@ -163,7 +168,7 @@ module.exports = (crowi) => {
       styleBorder: await crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
       styleBorder: await crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
       customizeTitle: await crowi.configManager.getConfig('crowi', 'customize:title'),
       customizeTitle: await crowi.configManager.getConfig('crowi', 'customize:title'),
       customizeHeader: await crowi.configManager.getConfig('crowi', 'customize:header'),
       customizeHeader: await crowi.configManager.getConfig('crowi', 'customize:header'),
-      customizeCss: await crowi.configManager.getConfig('crowi', 'customize:header'),
+      customizeCss: await crowi.configManager.getConfig('crowi', 'customize:css'),
       customizeScript: await crowi.configManager.getConfig('crowi', 'customize:script'),
       customizeScript: await crowi.configManager.getConfig('crowi', 'customize:script'),
     };
     };
 
 
@@ -173,11 +178,49 @@ module.exports = (crowi) => {
   /**
   /**
    * @swagger
    * @swagger
    *
    *
-   *    /customize-setting/layoutTheme:
+   *    /customize-setting/layout-theme/asset-path:
+   *      put:
+   *        tags: [CustomizeSetting]
+   *        operationId: getLayoutThemeAssetPath
+   *        summary: /customize-setting/layout-theme/asset-path
+   *        description: Get layout theme asset path
+   *        parameters:
+   *          - name: themeName
+   *            in: query
+   *            required: true
+   *            schema:
+   *              type: string
+   *        responses:
+   *          200:
+   *            description: Succeeded to update layout and theme
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    assetPath:
+   *                      type: string
+   */
+  router.get('/layout-theme/asset-path', loginRequiredStrictly, adminRequired, validator.themeAssetPath, ApiV3FormValidator, async(req, res) => {
+    const themeName = req.query.themeName;
+
+    const webpackAssetKey = `styles/theme-${themeName}.css`;
+    const assetPath = res.locals.webpack_asset(webpackAssetKey);
+
+    if (assetPath == null) {
+      return res.apiv3Err(new ErrorV3(`The asset for '${webpackAssetKey}' is undefined.`, 'invalid-asset'));
+    }
+
+    return res.apiv3({ assetPath });
+  });
+
+  /**
+   * @swagger
+   *
+   *    /customize-setting/layout-theme:
    *      put:
    *      put:
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
    *        operationId: updateLayoutThemeCustomizeSetting
    *        operationId: updateLayoutThemeCustomizeSetting
-   *        summary: /customize-setting/layoutTheme
+   *        summary: /customize-setting/layout-theme
    *        description: Update layout and theme
    *        description: Update layout and theme
    *        requestBody:
    *        requestBody:
    *          required: true
    *          required: true
@@ -193,7 +236,7 @@ module.exports = (crowi) => {
    *                schema:
    *                schema:
    *                  $ref: '#/components/schemas/CustomizeLayoutTheme'
    *                  $ref: '#/components/schemas/CustomizeLayoutTheme'
    */
    */
-  router.put('/layoutTheme', loginRequiredStrictly, adminRequired, csrf, validator.layoutTheme, ApiV3FormValidator, async(req, res) => {
+  router.put('/layout-theme', loginRequiredStrictly, adminRequired, csrf, validator.layoutTheme, ApiV3FormValidator, async(req, res) => {
     const requestParams = {
     const requestParams = {
       'customize:layout': req.body.layoutType,
       'customize:layout': req.body.layoutType,
       'customize:theme': req.body.themeType,
       'customize:theme': req.body.themeType,

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

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

+ 23 - 4
src/server/service/search-delegator/elasticsearch.js

@@ -126,17 +126,36 @@ class ElasticsearchDelegator {
     return { esVersion, esNodeInfos };
     return { esVersion, esNodeInfos };
   }
   }
 
 
+  /**
+   * Return information for Admin Full Text Search Management page
+   */
   async getInfoForAdmin() {
   async getInfoForAdmin() {
     const { client, indexName, aliasName } = this;
     const { client, indexName, aliasName } = this;
 
 
     const tmpIndexName = `${indexName}-tmp`;
     const tmpIndexName = `${indexName}-tmp`;
 
 
-    const { indices } = await client.indices.stats({ index: `${indexName}*`, ignore_unavailable: true, metric: ['docs', 'store', 'indexing'] });
+    // check existence
+    const isExistsMainIndex = await client.indices.exists({ index: indexName });
+    const isExistsTmpIndex = await client.indices.exists({ index: tmpIndexName });
+
+    // create indices name list
+    const existingIndices = [];
+    if (isExistsMainIndex) { existingIndices.push(indexName) }
+    if (isExistsTmpIndex) { existingIndices.push(tmpIndexName) }
+
+    // results when there is no indices
+    if (existingIndices.length === 0) {
+      return {
+        indices: [],
+        aliases: [],
+        isNormalized: false,
+      };
+    }
+
+    const { indices } = await client.indices.stats({ index: existingIndices, ignore_unavailable: true, metric: ['docs', 'store', 'indexing'] });
+    const aliases = await client.indices.getAlias({ index: existingIndices });
 
 
-    const aliases = await client.indices.getAlias({ index: `${indexName}*` });
-    const isExistsMainIndex = aliases[indexName] != null;
     const isMainIndexHasAlias = isExistsMainIndex && aliases[indexName].aliases != null && aliases[indexName].aliases[aliasName] != null;
     const isMainIndexHasAlias = isExistsMainIndex && aliases[indexName].aliases != null && aliases[indexName].aliases[aliasName] != null;
-    const isExistsTmpIndex = aliases[tmpIndexName] != null;
     const isTmpIndexHasAlias = isExistsTmpIndex && aliases[tmpIndexName].aliases != null && aliases[tmpIndexName].aliases[aliasName] != null;
     const isTmpIndexHasAlias = isExistsTmpIndex && aliases[tmpIndexName].aliases != null && aliases[tmpIndexName].aliases[aliasName] != null;
 
 
     const isNormalized = isExistsMainIndex && isMainIndexHasAlias && !isExistsTmpIndex && !isTmpIndexHasAlias;
     const isNormalized = isExistsMainIndex && isMainIndexHasAlias && !isExistsTmpIndex && !isTmpIndexHasAlias;

+ 4 - 10
src/server/views/admin/customize.html

@@ -1,16 +1,6 @@
 {% extends '../layout/admin.html' %}
 {% extends '../layout/admin.html' %}
 {% block html_title %}{{ customizeService.generateCustomTitle(t('Customize')) }}{% endblock %}
 {% block html_title %}{{ customizeService.generateCustomTitle(t('Customize')) }}{% endblock %}
 
 
-{% block theme_css_block %}
-{% set themeName = getConfig('crowi', 'customize:theme') %}
-
-{% if env === 'development' %}
-<script src="{{ webpack_asset('styles/theme-' + themeName + '.js') }}"></script>
-{% else %}
-<link rel="stylesheet" id="jssDefault" {# append id for theme selector #} href="{{ webpack_asset('styles/theme-' + themeName + '.css') }}" />
-{% endif %}
-{% endblock %}
-
 {% block html_additional_headers %}
 {% block html_additional_headers %}
 {% parent %}
 {% parent %}
 <!-- CodeMirror -->
 <!-- CodeMirror -->
@@ -45,6 +35,10 @@
   </div>
   </div>
   {% endif %}
   {% endif %}
 
 
+  <div id="grw-hljs-container-for-demo">
+    {{ cdnHighlightJsStyleTag(getConfig('crowi', 'customize:highlightJsStyle')) }}
+  </div>
+
   <div class="row">
   <div class="row">
     <div class="col-md-3">
     <div class="col-md-3">
       {% include './widget/menu.html' with {current: 'customize'} %}
       {% include './widget/menu.html' with {current: 'customize'} %}

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

@@ -16,15 +16,7 @@
     <div class="col-md-3">
     <div class="col-md-3">
       {% include './widget/menu.html' with {current: 'user-group'} %}
       {% include './widget/menu.html' with {current: 'user-group'} %}
     </div>
     </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>
 </div>
 </div>
 {% endblock content_main %}
 {% endblock content_main %}

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

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

+ 1 - 1
src/server/views/widget/headers/styles-theme.html

@@ -1,5 +1,5 @@
 {% if env === 'development' %}
 {% if env === 'development' %}
   <script src="{{ webpack_asset('styles/theme-' + themeName + '.js') }}"></script>
   <script src="{{ webpack_asset('styles/theme-' + themeName + '.js') }}"></script>
 {% else %}
 {% else %}
-  <link rel="stylesheet" href="{{ webpack_asset('styles/theme-' + themeName + '.css') }}">
+  <link rel="stylesheet" id="grw-theme-link" href="{{ webpack_asset('styles/theme-' + themeName + '.css') }}">
 {% endif %}
 {% endif %}