Przeglądaj źródła

render DescendantsPageListModal in [[...path

yohei0125 3 lat temu
rodzic
commit
b45d1ea0cb
77 zmienionych plików z 1066 dodań i 887 usunięć
  1. 0 3
      packages/app-next/.eslintrc.json
  2. 0 35
      packages/app-next/.gitignore
  3. 0 20
      packages/app-next/tsconfig.json
  4. 2 2
      packages/app/next.config.js
  5. 4 2
      packages/app/package.json
  6. 0 0
      packages/app/public/static/locales/en_US/admin.json
  7. 0 0
      packages/app/public/static/locales/ja_JP/admin.json
  8. 0 0
      packages/app/public/static/locales/zh_CN/admin.json
  9. 6 6
      packages/app/src/client/admin.jsx
  10. 1 5
      packages/app/src/client/services/AdminAppContainer.js
  11. 1 4
      packages/app/src/client/services/AdminCustomizeContainer.js
  12. 1 4
      packages/app/src/client/services/AdminImportContainer.js
  13. 1 4
      packages/app/src/client/services/AdminMarkDownContainer.js
  14. 1 3
      packages/app/src/client/services/AdminSlackIntegrationLegacyContainer.js
  15. 1 0
      packages/app/src/client/services/PageContainer.js
  16. 0 51
      packages/app/src/components/Admin/App/AppSettingsPage.jsx
  17. 22 1
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  18. 0 56
      packages/app/src/components/Admin/CustomCssEditor.jsx
  19. 0 57
      packages/app/src/components/Admin/CustomHeaderEditor.jsx
  20. 0 56
      packages/app/src/components/Admin/CustomScriptEditor.jsx
  21. 18 22
      packages/app/src/components/Admin/Customize/Customize.jsx
  22. 4 3
      packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx
  23. 4 3
      packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.tsx
  24. 4 3
      packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx
  25. 4 2
      packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx
  26. 2 13
      packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx
  27. 1 0
      packages/app/src/components/Admin/Customize/ThemeColorBox.jsx
  28. 23 1
      packages/app/src/components/Admin/ImportData/ImportDataPageContents.jsx
  29. 0 52
      packages/app/src/components/Admin/ImportDataPage.jsx
  30. 18 23
      packages/app/src/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx
  31. 0 49
      packages/app/src/components/Admin/MarkdownSetting/MarkDownSetting.jsx
  32. 36 3
      packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx
  33. 1 1
      packages/app/src/components/Admin/SlackIntegration/BotTypeCard.jsx
  34. 2 1
      packages/app/src/components/Admin/SlackIntegration/Bridge.jsx
  35. 3 2
      packages/app/src/components/Admin/SlackIntegration/ConfirmBotChangeModal.jsx
  36. 14 18
      packages/app/src/components/Layout/Admin.module.scss
  37. 4 1
      packages/app/src/components/Layout/AdminLayout.tsx
  38. 2 0
      packages/app/src/components/Layout/BasicLayout.tsx
  39. 0 199
      packages/app/src/components/PageAttachment.jsx
  40. 151 0
      packages/app/src/components/PageAttachment.tsx
  41. 3 1
      packages/app/src/components/PagePresentationModal.jsx
  42. 4 2
      packages/app/src/components/PagePresentationModal.module.scss
  43. 11 10
      packages/app/src/components/Theme/ThemeFireRed.module.scss
  44. 8 0
      packages/app/src/components/Theme/ThemeFireRed.tsx
  45. 9 8
      packages/app/src/components/Theme/ThemeIsland.module.scss
  46. 8 0
      packages/app/src/components/Theme/ThemeIsland.tsx
  47. 11 10
      packages/app/src/components/Theme/ThemeMonoBlue.module.scss
  48. 8 0
      packages/app/src/components/Theme/ThemeMonoBlue.tsx
  49. 7 7
      packages/app/src/components/Theme/ThemeNature.module.scss
  50. 8 0
      packages/app/src/components/Theme/ThemeNature.tsx
  51. 9 8
      packages/app/src/components/Theme/ThemeSpring.module.scss
  52. 8 0
      packages/app/src/components/Theme/ThemeSpring.tsx
  53. 7 7
      packages/app/src/components/Theme/ThemeWood.module.scss
  54. 8 0
      packages/app/src/components/Theme/ThemeWood.tsx
  55. 18 0
      packages/app/src/components/Theme/utils/ThemeProvider.tsx
  56. 10 1
      packages/app/src/interfaces/attachment.ts
  57. 3 3
      packages/app/src/interfaces/crowi-request.ts
  58. 0 4
      packages/app/src/interfaces/user-ui-settings.ts
  59. 74 36
      packages/app/src/pages/[[...path]].page.tsx
  60. 1 1
      packages/app/src/pages/_app.page.tsx
  61. 0 0
      packages/app/src/pages/_search.page.tsx
  62. 37 20
      packages/app/src/pages/admin/[[...path]].page.tsx
  63. 1 1
      packages/app/src/pages/installer.page.tsx
  64. 1 1
      packages/app/src/pages/utils/commons.ts
  65. 13 0
      packages/app/src/pages/utils/objectid-transformer.ts
  66. 7 5
      packages/app/src/server/middlewares/inject-user-ui-settings-to-localvars.ts
  67. 4 2
      packages/app/src/server/models/user-ui-settings.ts
  68. 3 3
      packages/app/src/server/routes/apiv3/attachment.js
  69. 4 6
      packages/app/src/server/routes/index.js
  70. 0 2
      packages/app/src/services/renderer/renderer.tsx
  71. 52 0
      packages/app/src/stores/attachment.tsx
  72. 4 0
      packages/app/src/stores/context.tsx
  73. 2 2
      packages/app/src/stores/page.tsx
  74. 27 1
      packages/app/src/utils/axios.ts
  75. 1 0
      packages/core/src/index.ts
  76. 3 1
      packages/slackbot-proxy/src/controllers/slack.ts
  77. 361 40
      yarn.lock

+ 0 - 3
packages/app-next/.eslintrc.json

@@ -1,3 +0,0 @@
-{
-  "extends": "next/core-web-vitals"
-}

+ 0 - 35
packages/app-next/.gitignore

@@ -1,35 +0,0 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
-
-# testing
-/coverage
-
-# next.js
-/.next/
-/out/
-
-# production
-/build
-
-# misc
-.DS_Store
-*.pem
-
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-.pnpm-debug.log*
-
-# local env files
-.env*.local
-
-# vercel
-.vercel
-
-# typescript
-*.tsbuildinfo

+ 0 - 20
packages/app-next/tsconfig.json

@@ -1,20 +0,0 @@
-{
-  "compilerOptions": {
-    "target": "es5",
-    "lib": ["dom", "dom.iterable", "esnext"],
-    "allowJs": true,
-    "skipLibCheck": true,
-    "strict": true,
-    "forceConsistentCasingInFileNames": true,
-    "noEmit": true,
-    "esModuleInterop": true,
-    "module": "esnext",
-    "moduleResolution": "node",
-    "resolveJsonModule": true,
-    "isolatedModules": true,
-    "jsx": "preserve",
-    "incremental": true
-  },
-  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
-  "exclude": ["node_modules"]
-}

+ 2 - 2
packages/app/next.config.js

@@ -1,5 +1,6 @@
 import eazyLogger from 'eazy-logger';
 import { I18NextHMRPlugin } from 'i18next-hmr/plugin';
+import { withSuperjson } from 'next-superjson';
 import { WebpackManifestPlugin } from 'webpack-manifest-plugin';
 
 import { i18n, localePath } from './src/next-i18next.config';
@@ -63,7 +64,6 @@ const nextConfig = {
 
   /** @param config {import('next').NextConfig} */
   webpack(config, options) {
-
     // Avoid "Module not found: Can't resolve 'fs'"
     // See: https://stackoverflow.com/a/68511591
     if (!options.isServer) {
@@ -98,4 +98,4 @@ const nextConfig = {
 
 };
 
-module.exports = withTM(nextConfig);
+module.exports = withSuperjson()(withTM(nextConfig));

+ 4 - 2
packages/app/package.json

@@ -127,7 +127,6 @@
     "next": "^12.1.6",
     "next-i18next": "^11.0.0",
     "next-themes": "^0.2.0",
-    "next-transpile-modules": "^9.0.0",
     "nocache": "^3.0.1",
     "nodemailer": "^6.6.2",
     "nodemailer-ses-transport": "~1.5.0",
@@ -160,12 +159,12 @@
     "rehype-toc": "^3.0.2",
     "remark-breaks": "^3.0.2",
     "remark-emoji": "^3.0.2",
-    "remark-footnotes": "^4.0.1",
     "remark-gfm": "^3.0.1",
     "rimraf": "^3.0.0",
     "socket.io": "^4.2.0",
     "stream-to-promise": "^3.0.0",
     "string-width": "=4.2.2",
+    "superjson": "^1.9.1",
     "swagger-jsdoc": "^6.1.0",
     "swig-templates": "^2.0.2",
     "uglifycss": "^0.0.29",
@@ -189,6 +188,7 @@
     "@types/jquery": "^3.5.8",
     "@types/multer": "^1.4.5",
     "autoprefixer": "^9.0.0",
+    "babel-loader": "^8.2.5",
     "bootstrap": "^4.6.1",
     "browser-sync": "^2.27.7",
     "bunyan-debug": "^2.0.0",
@@ -214,6 +214,8 @@
     "markdown-table": "^1.1.1",
     "material-icons": "^1.11.3",
     "morgan": "^1.10.0",
+    "next-superjson": "^0.0.4",
+    "next-transpile-modules": "^9.0.0",
     "normalize-path": "^3.0.0",
     "penpal": "^4.0.0",
     "plantuml-encoder": "^1.2.5",

+ 0 - 0
packages/app/public/static/locales/en_US/admin/admin.json → packages/app/public/static/locales/en_US/admin.json


+ 0 - 0
packages/app/public/static/locales/ja_JP/admin/admin.json → packages/app/public/static/locales/ja_JP/admin.json


+ 0 - 0
packages/app/public/static/locales/zh_CN/admin/admin.json → packages/app/public/static/locales/zh_CN/admin.json


+ 6 - 6
packages/app/src/client/admin.jsx

@@ -30,16 +30,16 @@ import loggerFactory from '~/utils/logger';
 import { swrGlobalConfiguration } from '~/utils/swr-utils';
 
 import AdminHome from '../components/Admin/AdminHome/AdminHome';
-import AppSettingsPage from '../components/Admin/App/AppSettingsPage';
+// import AppSettingsPage from '../components/Admin/App/AppSettingsPage';
 import { AuditLogManagement } from '../components/Admin/AuditLogManagement';
 import AdminNavigation from '../components/Admin/Common/AdminNavigation';
 import Customize from '../components/Admin/Customize/Customize';
 import ExportArchiveDataPage from '../components/Admin/ExportArchiveDataPage';
 import FullTextSearchManagement from '../components/Admin/FullTextSearchManagement';
-import ImportDataPage from '../components/Admin/ImportDataPage';
+// import ImportDataPage from '../components/Admin/ImportDataPage';
 import LegacySlackIntegration from '../components/Admin/LegacySlackIntegration/LegacySlackIntegration';
 import ManageExternalAccount from '../components/Admin/ManageExternalAccount';
-import MarkdownSetting from '../components/Admin/MarkdownSetting/MarkDownSetting';
+// import MarkdownSetting from '../components/Admin/MarkdownSetting/MarkDownSetting';
 import ManageGlobalNotification from '../components/Admin/Notification/ManageGlobalNotification';
 import NotificationSetting from '../components/Admin/Notification/NotificationSetting';
 import SecurityManagement from '../components/Admin/Security/SecurityManagement';
@@ -94,10 +94,10 @@ logger.info('unstated containers have been initialized');
  */
 Object.assign(componentMappings, {
   'admin-home': <AdminHome />,
-  'admin-app': <AppSettingsPage />,
-  'admin-markdown-setting': <MarkdownSetting />,
+  // 'admin-app': <AppSettingsPage />,
+  // 'admin-markdown-setting': <MarkdownSetting />,
   'admin-customize': <Customize />,
-  'admin-importer': <ImportDataPage />,
+  // 'admin-importer': <ImportDataPage />,
   'admin-export-page': <ExportArchiveDataPage />,
   'admin-notification-setting': <NotificationSetting />,
   'admin-slack-integration': <SlackIntegration />,

+ 1 - 5
packages/app/src/client/services/AdminAppContainer.js

@@ -11,13 +11,9 @@ export default class AdminAppContainer extends Container {
   constructor() {
     super();
 
-    this.dummyTitle = 0;
-    this.dummyTitleForError = 1;
-
     this.state = {
       retrieveError: null,
-      // set dummy value tile for using suspense
-      title: this.dummyTitle,
+      title: '',
       confidential: '',
       globalLang: '',
       isEmailPublishedForNewUser: true,

+ 1 - 4
packages/app/src/client/services/AdminCustomizeContainer.js

@@ -17,13 +17,10 @@ export default class AdminCustomizeContainer extends Container {
   constructor() {
     super();
 
-    this.dummyCurrentTheme = 0;
-    this.dummyCurrentThemeForError = 1;
-
     this.state = {
       retrieveError: null,
       // set dummy value tile for using suspense
-      currentTheme: this.dummyCurrentTheme,
+      currentTheme: 'default',
       isEnabledTimeline: false,
       isSavedStatesOfTabChanges: false,
       isEnabledAttachTitleHeader: false,

+ 1 - 4
packages/app/src/client/services/AdminImportContainer.js

@@ -18,13 +18,10 @@ export default class AdminImportContainer extends Container {
     super();
 
     this.appContainer = appContainer;
-    this.dummyEsaTeamName = 0;
-    this.dummyEsaTeamNameForError = 1;
 
     this.state = {
       retrieveError: null,
-      // set dummy value tile for using suspense
-      esaTeamName: this.dummyEsaTeamName,
+      esaTeamName: '',
       esaAccessToken: '',
       qiitaTeamName: '',
       qiitaAccessToken: '',

+ 1 - 4
packages/app/src/client/services/AdminMarkDownContainer.js

@@ -12,13 +12,10 @@ export default class AdminMarkDownContainer extends Container {
     super();
 
     this.appContainer = appContainer;
-    this.dummyIsEnabledLinebreaks = 0;
-    this.dummyIsEnabledLinebreaksForError = 1;
 
     this.state = {
       retrieveError: null,
-      // set dummy value tile for using suspense
-      isEnabledLinebreaks: this.dummyIsEnabledLinebreaks,
+      isEnabledLinebreaks: false,
       isEnabledLinebreaksInComments: false,
       adminPreferredIndentSize: 4,
       isIndentSizeForced: false,

+ 1 - 3
packages/app/src/client/services/AdminSlackIntegrationLegacyContainer.js

@@ -12,14 +12,12 @@ export default class AdminSlackIntegrationLegacyContainer extends Container {
     super();
 
     this.appContainer = appContainer;
-    this.dummyWebhookUrl = 0;
-    this.dummyWebhookUrlForError = 1;
 
     this.state = {
       isSlackbotConfigured: false,
       retrieveError: null,
       selectSlackOption: 'Incoming Webhooks',
-      webhookUrl: this.dummyWebhookUrl,
+      webhookUrl: '',
       isIncomingWebhookPrioritized: false,
       slackToken: '',
     };

+ 1 - 0
packages/app/src/client/services/PageContainer.js

@@ -135,6 +135,7 @@ export default class PageContainer extends Container {
 
   /**
    * initialize state for markdown data
+   * [Already SWRized]
    */
   initStateMarkdown() {
     let pageContent = '';

+ 0 - 51
packages/app/src/components/Admin/App/AppSettingsPage.jsx

@@ -1,51 +0,0 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
-
-import AdminAppContainer from '~/client/services/AdminAppContainer';
-import { toastError } from '~/client/util/apiNotification';
-import { toArrayIfNot } from '~/utils/array-utils';
-import loggerFactory from '~/utils/logger';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-import AppSettingsPageContents from './AppSettingsPageContents';
-
-const logger = loggerFactory('growi:appSettings');
-
-let retrieveErrors = null;
-function AppSettingsPage(props) {
-  if (props.adminAppContainer.state.title === props.adminAppContainer.dummyTitle) {
-    throw (async() => {
-      try {
-        await props.adminAppContainer.retrieveAppSettingsData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        logger.error(errs);
-        props.adminAppContainer.setState({
-          title: props.adminAppContainer.dummyTitleForError,
-        });
-        retrieveErrors = errs;
-      }
-    })();
-  }
-
-  if (props.adminAppContainer.state.title === props.adminAppContainer.dummyTitleForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
-
-  return <AppSettingsPageContents />;
-}
-
-AppSettingsPage.propTypes = {
-  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const AppSettingsPageWithUnstatedContainer = withUnstatedContainers(AppSettingsPage, [AdminAppContainer]);
-
-export default AppSettingsPageWithUnstatedContainer;

+ 22 - 1
packages/app/src/components/Admin/App/AppSettingsPageContents.tsx

@@ -1,8 +1,12 @@
-import React from 'react';
+import React, { useEffect } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
+import { toastError } from '~/client/util/apiNotification';
+import { toArrayIfNot } from '~/utils/array-utils';
+import loggerFactory from '~/utils/logger';
+
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -14,6 +18,8 @@ import PluginSetting from './PluginSetting';
 import SiteUrlSetting from './SiteUrlSetting';
 import V5PageMigration from './V5PageMigration';
 
+const logger = loggerFactory('growi:appSettings');
+
 type Props = {
   adminAppContainer: AdminAppContainer,
 }
@@ -23,6 +29,21 @@ const AppSettingsPageContents = (props: Props) => {
   const { adminAppContainer } = props;
   const { isV5Compatible } = adminAppContainer.state;
 
+  useEffect(() => {
+    const fetchAppSettingsData = async() => {
+      await adminAppContainer.retrieveAppSettingsData();
+    };
+
+    try {
+      fetchAppSettingsData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+      logger.error(errs);
+    }
+  }, [adminAppContainer]);
+
   return (
     <div data-testid="admin-app-settings">
       {

+ 0 - 56
packages/app/src/components/Admin/CustomCssEditor.jsx

@@ -1,56 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { UnControlled as CodeMirror } from 'react-codemirror2';
-
-require('codemirror/addon/lint/css-lint');
-require('codemirror/addon/hint/css-hint');
-require('codemirror/addon/hint/show-hint');
-require('codemirror/addon/edit/matchbrackets');
-require('codemirror/addon/edit/closebrackets');
-require('codemirror/mode/css/css');
-require('~/client/util/codemirror/autorefresh.ext');
-
-require('jquery-ui/ui/widgets/resizable');
-
-export default class CustomCssEditor extends React.Component {
-
-  render() {
-
-    return (
-      <CodeMirror
-        value={this.props.value}
-        autoFocus
-        detach
-        options={{
-          mode: 'css',
-          lineNumbers: true,
-          tabSize: 2,
-          indentUnit: 2,
-          theme: 'eclipse',
-          autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
-          matchBrackets: true,
-          autoCloseBrackets: true,
-          extraKeys: { 'Ctrl-Space': 'autocomplete' },
-        }}
-        editorDidMount={(editor, next) => {
-          // resizable with jquery.ui
-          $(editor.getWrapperElement()).resizable({
-            resize() {
-              editor.setSize($(this).width(), $(this).height());
-            },
-          });
-        }}
-        onChange={(editor, data, value) => {
-          this.props.onChange(value);
-        }}
-      />
-    );
-  }
-
-}
-
-CustomCssEditor.propTypes = {
-  value: PropTypes.string.isRequired,
-  onChange: PropTypes.func.isRequired,
-};

+ 0 - 57
packages/app/src/components/Admin/CustomHeaderEditor.jsx

@@ -1,57 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { UnControlled as CodeMirror } from 'react-codemirror2';
-
-require('codemirror/addon/hint/show-hint');
-require('codemirror/addon/edit/matchbrackets');
-require('codemirror/addon/edit/closebrackets');
-require('codemirror/mode/htmlmixed/htmlmixed');
-require('codemirror/addon/hint/html-hint');
-require('codemirror/addon/edit/closetag');
-require('~/client/util/codemirror/autorefresh.ext');
-
-require('jquery-ui/ui/widgets/resizable');
-
-export default class CustomHeaderEditor extends React.Component {
-
-  render() {
-
-    return (
-      <CodeMirror
-        value={this.props.value}
-        autoFocus
-        detach
-        options={{
-          mode: 'htmlmixed',
-          autoCloseTags: true,
-          lineNumbers: true,
-          tabSize: 2,
-          indentUnit: 2,
-          theme: 'eclipse',
-          autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
-          matchBrackets: true,
-          autoCloseBrackets: true,
-          extraKeys: { 'Ctrl-Space': 'autocomplete' },
-        }}
-        editorDidMount={(editor, next) => {
-          // resizable with jquery.ui
-          $(editor.getWrapperElement()).resizable({
-            resize() {
-              editor.setSize($(this).width(), $(this).height());
-            },
-          });
-        }}
-        onChange={(editor, data, value) => {
-          this.props.onChange(value);
-        }}
-      />
-    );
-  }
-
-}
-
-CustomHeaderEditor.propTypes = {
-  value: PropTypes.string.isRequired,
-  onChange: PropTypes.func.isRequired,
-};

+ 0 - 56
packages/app/src/components/Admin/CustomScriptEditor.jsx

@@ -1,56 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { UnControlled as CodeMirror } from 'react-codemirror2';
-
-require('codemirror/addon/lint/javascript-lint');
-require('codemirror/addon/hint/javascript-hint');
-require('codemirror/addon/hint/show-hint');
-require('codemirror/addon/edit/matchbrackets');
-require('codemirror/addon/edit/closebrackets');
-require('codemirror/mode/javascript/javascript');
-require('~/client/util/codemirror/autorefresh.ext');
-
-require('jquery-ui/ui/widgets/resizable');
-
-export default class CustomScriptEditor extends React.Component {
-
-  render() {
-
-    return (
-      <CodeMirror
-        value={this.props.value}
-        autoFocus
-        detach
-        options={{
-          mode: 'javascript',
-          lineNumbers: true,
-          tabSize: 2,
-          indentUnit: 2,
-          theme: 'eclipse',
-          autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
-          matchBrackets: true,
-          autoCloseBrackets: true,
-          extraKeys: { 'Ctrl-Space': 'autocomplete' },
-        }}
-        editorDidMount={(editor, next) => {
-          // resizable with jquery.ui
-          $(editor.getWrapperElement()).resizable({
-            resize() {
-              editor.setSize($(this).width(), $(this).height());
-            },
-          });
-        }}
-        onChange={(editor, data, value) => {
-          this.props.onChange(value);
-        }}
-      />
-    );
-  }
-
-}
-
-CustomScriptEditor.propTypes = {
-  value: PropTypes.string.isRequired,
-  onChange: PropTypes.func.isRequired,
-};

+ 18 - 22
packages/app/src/components/Admin/Customize/Customize.jsx

@@ -1,5 +1,5 @@
 
-import React from 'react';
+import React, { useEffect } from 'react';
 
 import PropTypes from 'prop-types';
 
@@ -23,28 +23,24 @@ import CustomizeTitle from './CustomizeTitle';
 
 const logger = loggerFactory('growi:services:AdminCustomizePage');
 
-const retrieveErrors = null;
 function Customize(props) {
   const { adminCustomizeContainer } = props;
 
-  // if (adminCustomizeContainer.state.currentTheme === adminCustomizeContainer.dummyCurrentTheme) {
-  //   throw (async() => {
-  //     try {
-  //       await adminCustomizeContainer.retrieveCustomizeData();
-  //     }
-  //     catch (err) {
-  //       const errs = toArrayIfNot(err);
-  //       toastError(errs);
-  //       logger.error(errs);
-  //       retrieveErrors = errs;
-  //       adminCustomizeContainer.setState({ currentTheme: adminCustomizeContainer.dummyCurrentThemeForError });
-  //     }
-  //   })();
-  // }
+  useEffect(() => {
+    async function fetchCustomizeSettingsData() {
+      await adminCustomizeContainer.retrieveCustomizeData();
+    }
+
+    try {
+      fetchCustomizeSettingsData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+      logger.error(errs);
+    }
+  }, [adminCustomizeContainer]);
 
-  // if (adminCustomizeContainer.state.currentTheme === adminCustomizeContainer.dummyCurrentThemeForError) {
-  //   throw new Error(`${retrieveErrors.length} errors occured`);
-  // }
 
   return (
     <div data-testid="admin-customize">
@@ -55,7 +51,9 @@ function Customize(props) {
         <CustomizeThemeSetting />
       </div>
       <div className="mb-5">
-        <CustomizeSidebarSetting />
+        {/* TODO: [resolve browser err] A component is changing an uncontrolled input to be controlled. by https://redmine.weseek.co.jp/issues/101155
+          <CustomizeSidebarSetting />
+        */}
       </div>
       <div className="mb-5">
         <CustomizeFunctionSetting />
@@ -66,7 +64,6 @@ function Customize(props) {
       <div className="mb-5">
         <CustomizeTitle />
       </div>
-      {/* TODO: show CustomizeHeaderSetting, CustomizeCssSetting and CustomizeScriptSetting by https://redmine.weseek.co.jp/issues/100534
       <div className="mb-5">
         <CustomizeHeaderSetting />
       </div>
@@ -76,7 +73,6 @@ function Customize(props) {
       <div className="mb-5">
         <CustomizeScriptSetting />
       </div>
-    */}
       <div className="mb-5">
         <CustomizeLogoSetting />
       </div>

+ 4 - 3
packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx

@@ -8,7 +8,6 @@ import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import CustomCssEditor from '../CustomCssEditor';
 
 type Props = {
   adminCustomizeContainer: AdminCustomizeContainer
@@ -43,9 +42,11 @@ const CustomizeCssSetting = (props: Props): JSX.Element => {
           </Card>
 
           <div className="form-group">
-            <CustomCssEditor
+            <textarea
+              className="form-control"
+              name="customizeCss"
               value={adminCustomizeContainer.state.currentCustomizeCss || ''}
-              onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeCss(inputValue) }}
+              onChange={(e) => { adminCustomizeContainer.changeCustomizeCss(e.target.value) }}
             />
             <p className="form-text text-muted text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />

+ 4 - 3
packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.tsx

@@ -8,7 +8,6 @@ import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import CustomHeaderEditor from '../CustomHeaderEditor';
 
 type Props = {
   adminCustomizeContainer: AdminCustomizeContainer
@@ -54,9 +53,11 @@ const CustomizeHeaderSetting = (props: Props): JSX.Element => {
           </div>
 
           <div className="form-group">
-            <CustomHeaderEditor
+            <textarea
+              className="form-control"
+              name="customizeHeader"
               value={adminCustomizeContainer.state.currentCustomizeHeader || ''}
-              onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeHeader(inputValue) }}
+              onChange={(e) => { adminCustomizeContainer.changeCustomizeHeader(e.target.value) }}
             />
             <p className="form-text text-muted text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true"></i>

+ 4 - 3
packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx

@@ -8,7 +8,6 @@ import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import CustomScriptEditor from '../CustomScriptEditor';
 
 type Props = {
   adminCustomizeContainer: AdminCustomizeContainer
@@ -84,9 +83,11 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
           </div>
 
           <div className="form-group">
-            <CustomScriptEditor
+            <textarea
+              className="form-control"
+              name="customizeScript"
               value={adminCustomizeContainer.state.currentCustomizeScript || ''}
-              onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeScript(inputValue) }}
+              onChange={(e) => { adminCustomizeContainer.changeCustomizeScript(e.target.value) }}
             />
             <p className="form-text text-muted text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />

+ 4 - 2
packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx

@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import { GrowiThemes } from '~/interfaces/theme';
+import { useGrowiTheme } from '~/stores/context';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -51,6 +52,7 @@ const CustomizeThemeOptions = (props) => {
 
   const { adminCustomizeContainer } = props;
   const { t } = useTranslation();
+  const { mutate: mutateGrowiTheme } = useGrowiTheme();
   const { currentLayout, currentTheme } = adminCustomizeContainer.state;
 
   return (
@@ -64,7 +66,7 @@ const CustomizeThemeOptions = (props) => {
               <ThemeColorBox
                 key={theme.name}
                 isSelected={currentTheme === theme.name}
-                onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
+                onSelected={() => mutateGrowiTheme(theme.name)}
                 {...theme}
               />
             );
@@ -80,7 +82,7 @@ const CustomizeThemeOptions = (props) => {
               <ThemeColorBox
                 key={theme.name}
                 isSelected={currentTheme === theme.name}
-                onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
+                onSelected={() => mutateGrowiTheme(theme.name)}
                 {...theme}
               />
             );

+ 2 - 13
packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx

@@ -19,7 +19,7 @@ const CustomizeThemeSetting = (props: Props): JSX.Element => {
   const { adminCustomizeContainer } = props;
   const { t } = useTranslation();
 
-  const onClickSubmit = useCallback(async() => {
+  const submitHandler = useCallback(async() => {
     try {
       await adminCustomizeContainer.updateCustomizeTheme();
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.theme') }));
@@ -29,24 +29,13 @@ const CustomizeThemeSetting = (props: Props): JSX.Element => {
     }
   }, [t, adminCustomizeContainer]);
 
-  const renderDevAlert = useCallback(() => {
-    if (process.env.NODE_ENV === 'development') {
-      return (
-        <div className="alert alert-warning">
-          <strong>DEBUG MESSAGE:</strong> Live preview for theme is disabled in development mode.
-        </div>
-      );
-    }
-  }, []);
-
   return (
     <React.Fragment>
       <div className="row">
         <div className="col-12">
           <h2 className="admin-setting-header">{t('admin:customize_setting.theme')}</h2>
-          {renderDevAlert()}
           <CustomizeThemeOptions />
-          <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
+          <AdminUpdateButtonRow onClick={submitHandler} disabled={adminCustomizeContainer.state.retrieveError != null} />
         </div>
       </div>
     </React.Fragment>

+ 1 - 0
packages/app/src/components/Admin/Customize/ThemeColorBox.jsx

@@ -1,4 +1,5 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
 
 

+ 23 - 1
packages/app/src/components/Admin/ImportData/ImportDataPageContents.jsx

@@ -1,14 +1,19 @@
-import React from 'react';
+import React, { useEffect } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 
 import AdminImportContainer from '~/client/services/AdminImportContainer';
+import { toastError } from '~/client/util/apiNotification';
+import { toArrayIfNot } from '~/utils/array-utils';
+import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import GrowiArchiveSection from './GrowiArchiveSection';
 
+const logger = loggerFactory('growi:importer');
+
 class ImportDataPageContents extends React.Component {
 
   render() {
@@ -242,6 +247,23 @@ ImportDataPageContents.propTypes = {
 const ImportDataPageContentsWrapperFc = (props) => {
   const { t } = useTranslation();
 
+  const { adminImportContainer } = props;
+
+  useEffect(() => {
+    const fetchImportSettingsData = async() => {
+      await adminImportContainer.retrieveImportSettingsData();
+    };
+
+    try {
+      fetchImportSettingsData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+      logger.error(errs);
+    }
+  }, [adminImportContainer]);
+
   return <ImportDataPageContents t={t} {...props} />;
 };
 

+ 0 - 52
packages/app/src/components/Admin/ImportDataPage.jsx

@@ -1,52 +0,0 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
-
-import AdminImportContainer from '~/client/services/AdminImportContainer';
-import { toastError } from '~/client/util/apiNotification';
-import { toArrayIfNot } from '~/utils/array-utils';
-import loggerFactory from '~/utils/logger';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
-
-import ImportDataPageContents from './ImportData/ImportDataPageContents';
-
-const logger = loggerFactory('growi:importer');
-
-let retrieveErrors = null;
-function ImportDataPage(props) {
-  const { adminImportContainer } = props;
-
-  if (adminImportContainer.state.esaTeamName === adminImportContainer.dummyEsaTeamName) {
-    throw (async() => {
-      try {
-        await adminImportContainer.retrieveImportSettingsData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        logger.error(errs);
-        retrieveErrors = errs;
-        adminImportContainer.setState({ esaTeamName: adminImportContainer.dummyEsaTeamNameForError });
-      }
-    })();
-  }
-
-  if (adminImportContainer.state.esaTeamName === adminImportContainer.dummyEsaTeamNameForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
-
-  return <ImportDataPageContents />;
-}
-
-ImportDataPage.propTypes = {
-  adminImportContainer: PropTypes.instanceOf(AdminImportContainer).isRequired,
-};
-
-
-/**
- * Wrapper component for using unstated
- */
-const ImportDataPageWithUnstatedContainer = withUnstatedContainers(ImportDataPage, [AdminImportContainer]);
-
-export default ImportDataPageWithUnstatedContainer;

+ 18 - 23
packages/app/src/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx

@@ -15,31 +15,26 @@ import SlackConfiguration from './SlackConfiguration';
 
 const logger = loggerFactory('growi:NotificationSetting');
 
-const retrieveErrors = null;
-function LegacySlackIntegration(props) {
+const LegacySlackIntegration = (props) => {
   const { t } = useTranslation();
   const { adminSlackIntegrationLegacyContainer } = props;
 
-  if (adminSlackIntegrationLegacyContainer.state.webhookUrl === adminSlackIntegrationLegacyContainer.dummyWebhookUrl) {
-    // TODO: Omit AdminSlackIntegrationLegacyContainer by https://redmine.weseek.co.jp/issues/100947
-
-    // throw (async() => {
-    //   try {
-    //     await adminSlackIntegrationLegacyContainer.retrieveData();
-    //   }
-    //   catch (err) {
-    //     const errs = toArrayIfNot(err);
-    //     toastError(errs);
-    //     logger.error(errs);
-    //     retrieveErrors = errs;
-    //     adminSlackIntegrationLegacyContainer.setState({ webhookUrl: adminSlackIntegrationLegacyContainer.dummyWebhookUrlForError });
-    //   }
-    // })();
-  }
-
-  if (adminSlackIntegrationLegacyContainer.state.webhookUrl === adminSlackIntegrationLegacyContainer.dummyWebhookUrlForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
+
+  useEffect(() => {
+    const fetchLegacySlackIntegrationData = async() => {
+      await adminSlackIntegrationLegacyContainer.retrieveData();
+    };
+
+    try {
+      fetchLegacySlackIntegrationData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+      logger.error(errs);
+    }
+  }, [adminSlackIntegrationLegacyContainer]);
+
 
   const isDisabled = adminSlackIntegrationLegacyContainer.state.isSlackbotConfigured;
 
@@ -62,7 +57,7 @@ function LegacySlackIntegration(props) {
       <SlackConfiguration />
     </div>
   );
-}
+};
 
 const LegacySlackIntegrationWithUnstatedContainer = withUnstatedContainers(LegacySlackIntegration, [AdminSlackIntegrationLegacyContainer]);
 

+ 0 - 49
packages/app/src/components/Admin/MarkdownSetting/MarkDownSetting.jsx

@@ -1,49 +0,0 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
-
-import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import { toastError } from '~/client/util/apiNotification';
-import { toArrayIfNot } from '~/utils/array-utils';
-import loggerFactory from '~/utils/logger';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-import MarkDownSettingContents from './MarkDownSettingContents';
-
-
-const logger = loggerFactory('growi:MarkDown');
-
-let retrieveErrors = null;
-function MarkdownSetting(props) {
-  const { adminMarkDownContainer } = props;
-
-  if (adminMarkDownContainer.state.isEnabledLinebreaks === adminMarkDownContainer.dummyIsEnabledLinebreaks) {
-    throw (async() => {
-      try {
-        await adminMarkDownContainer.retrieveMarkdownData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        logger.error(errs);
-        retrieveErrors = errs;
-        adminMarkDownContainer.setState({ isEnabledLinebreaks: adminMarkDownContainer.dummyIsEnabledLinebreaksForError });
-      }
-    })();
-  }
-
-  if (adminMarkDownContainer.state.isEnabledLinebreaks === adminMarkDownContainer.dummyIsEnabledLinebreaksForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
-
-  return <MarkDownSettingContents />;
-}
-
-const MarkdownSettingWithUnstatedContainer = withUnstatedContainers(MarkdownSetting, [AdminMarkDownContainer]);
-
-MarkdownSetting.propTypes = {
-  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
-};
-
-export default MarkdownSettingWithUnstatedContainer;

+ 36 - 3
packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx

@@ -1,15 +1,44 @@
-import React from 'react';
+import React, { useEffect } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 
+import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
+import { toastError } from '~/client/util/apiNotification';
+import { toArrayIfNot } from '~/utils/array-utils';
+import loggerFactory from '~/utils/logger';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
 import IndentForm from './IndentForm';
 import LineBreakForm from './LineBreakForm';
 import PresentationForm from './PresentationForm';
 import XssForm from './XssForm';
 
-const MarkDownSettingContents = React.memo((): JSX.Element => {
+const logger = loggerFactory('growi:MarkDown');
+
+type Props ={
+  adminMarkDownContainer: AdminMarkDownContainer
+}
+
+const MarkDownSettingContents = React.memo((props: Props): JSX.Element => {
   const { t } = useTranslation();
+  const { adminMarkDownContainer } = props;
+
+  useEffect(() => {
+    const fetchMarkdownData = async() => {
+      await adminMarkDownContainer.retrieveMarkdownData();
+    };
+
+    try {
+      fetchMarkdownData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+      logger.error(errs);
+    }
+  }, [adminMarkDownContainer]);
 
   return (
     <div data-testid="admin-markdown">
@@ -45,4 +74,8 @@ const MarkDownSettingContents = React.memo((): JSX.Element => {
 });
 MarkDownSettingContents.displayName = 'MarkDownSettingContents';
 
-export default MarkDownSettingContents;
+
+const MarkdownSettingWithUnstatedContainer = withUnstatedContainers(MarkDownSettingContents, [AdminMarkDownContainer]);
+
+
+export default MarkdownSettingWithUnstatedContainer;

+ 1 - 1
packages/app/src/components/Admin/SlackIntegration/BotTypeCard.jsx

@@ -31,7 +31,7 @@ const botDetails = {
 };
 
 const BotTypeCard = (props) => {
-  const { t } = useTranslation('admin');
+  const { t } = useTranslation();
 
   const isBotTypeOfficial = props.botType === SlackbotType.OFFICIAL;
 

+ 2 - 1
packages/app/src/components/Admin/SlackIntegration/Bridge.jsx

@@ -1,4 +1,5 @@
 import React from 'react';
+
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import { UncontrolledTooltip } from 'reactstrap';
@@ -20,7 +21,7 @@ const BridgeCore = (props) => {
   return (
     <>
       <div id="grw-bridge-container" className={`grw-bridge-container ${withProxy ? 'with-proxy' : ''}`}>
-        <p className="label">
+        <p className={`${withProxy ? 'mt-0' : 'mt-2'}`}>
           <i className={iconClass} />
           <small
             className="ml-2 d-none d-lg-inline"

+ 3 - 2
packages/app/src/components/Admin/SlackIntegration/ConfirmBotChangeModal.jsx

@@ -1,12 +1,13 @@
 import React from 'react';
-import PropTypes from 'prop-types';
+
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
 const ConfirmBotChangeModal = (props) => {
-  const { t } = useTranslation('admin');
+  const { t } = useTranslation();
 
   const handleCancelButton = () => {
     if (props.onCancelClick != null) {

+ 14 - 18
packages/app/src/styles/_admin.scss → packages/app/src/components/Layout/Admin.module.scss

@@ -1,14 +1,17 @@
+@use '~/styles/bootstrap/init' as *;
+@use '~/styles/mixins';
+
 $slack-work-space-name-card-background: #fff5ff;
 $slack-work-space-name-card-border: #efc1f6;
 
-.admin-page {
+.admin-page :global {
   .title {
     padding-top: 1rem;
     padding-bottom: 1rem;
 
     line-height: 1em;
 
-    @include variable-font-size(28px);
+    @include mixins.variable-font-size(28px);
     line-height: 1.1em;
   }
 
@@ -28,8 +31,6 @@ $slack-work-space-name-card-border: #efc1f6;
   }
 
   .admin-customize {
-    @import 'hljs';
-
     .ss-container img {
       padding: 0.5em;
       background-color: $gray-300;
@@ -169,15 +170,8 @@ $slack-work-space-name-card-border: #efc1f6;
 
     // switch layout for Bridge component
     .grw-bridge-container {
-      .label {
-        @extend .mt-5;
-      }
-
       // with ProxyCircle
       &.with-proxy {
-        .label {
-          @extend .mt-0;
-        }
         .hr-container {
           margin-top: 40px;
           @include media-breakpoint-up(lg) {
@@ -293,13 +287,15 @@ $slack-work-space-name-card-border: #efc1f6;
       background-color: rgba($info, 0.1);
     }
   }
-}
 
-.admin-navigation {
-  & > a + a {
-    margin-top: 2px;
-  }
-  &.sticky-top {
-    top: 30px;
+  .admin-navigation {
+    & > a + a {
+      margin-top: 2px;
+    }
+    &.sticky-top {
+      top: 30px;
+    }
   }
 }
+
+

+ 4 - 1
packages/app/src/components/Layout/AdminLayout.tsx

@@ -7,6 +7,9 @@ import { GrowiNavbar } from '../Navbar/GrowiNavbar';
 
 import { RawLayout } from './RawLayout';
 
+import styles from './Admin.module.scss';
+
+
 // import { injectableContainers } from '~/client/admin';
 
 type Props = {
@@ -29,7 +32,7 @@ const AdminLayout = ({
   const SystemVersion = dynamic(() => import('../SystemVersion'), { ssr: false });
 
   return (
-    <RawLayout title={title}>
+    <RawLayout title={title} className={`admin-page ${styles['admin-page']}`}>
       <GrowiNavbar />
 
       <header className="py-0">

+ 2 - 0
packages/app/src/components/Layout/BasicLayout.tsx

@@ -26,6 +26,7 @@ export const BasicLayout = ({ children, title, className }: Props): JSX.Element
   const PageDuplicateModal = dynamic(() => import('../PageDuplicateModal'), { ssr: false });
   const PageDeleteModal = dynamic(() => import('../PageDeleteModal'), { ssr: false });
   const PageRenameModal = dynamic(() => import('../PageRenameModal'), { ssr: false });
+  const PagePresentationModal = dynamic(() => import('../PagePresentationModal'), { ssr: false });
 
   return (
     <RawLayout title={title} className={className}>
@@ -47,6 +48,7 @@ export const BasicLayout = ({ children, title, className }: Props): JSX.Element
       <PageDuplicateModal />
       <PageDeleteModal />
       <PageRenameModal />
+      <PagePresentationModal />
       {/* <HotkeysManager /> */}
 
       <ShortcutsModal />

+ 0 - 199
packages/app/src/components/PageAttachment.jsx

@@ -1,199 +0,0 @@
-/* eslint-disable react/no-access-state-in-setstate */
-import React from 'react';
-
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-
-import PageContainer from '~/client/services/PageContainer';
-import { apiPost } from '~/client/util/apiv1-client';
-import { apiv3Get } from '~/client/util/apiv3-client';
-import { useIsGuestUser } from '~/stores/context';
-
-import DeleteAttachmentModal from './PageAttachment/DeleteAttachmentModal';
-import PageAttachmentList from './PageAttachment/PageAttachmentList';
-import PaginationWrapper from './PaginationWrapper';
-import { withUnstatedContainers } from './UnstatedUtils';
-
-
-class PageAttachment extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      activePage: 1,
-      totalAttachments: 0,
-      limit: Infinity,
-      attachments: [],
-      inUse: {},
-      attachmentToDelete: null,
-      deleting: false,
-      deleteError: '',
-    };
-
-    this.handlePage = this.handlePage.bind(this);
-    this.onAttachmentDeleteClicked = this.onAttachmentDeleteClicked.bind(this);
-    this.onAttachmentDeleteClickedConfirm = this.onAttachmentDeleteClickedConfirm.bind(this);
-  }
-
-
-  async handlePage(selectedPage) {
-    const { pageId } = this.props.pageContainer.state;
-    const page = selectedPage;
-
-    if (!pageId) { return }
-
-    const res = await apiv3Get('/attachment/list', { pageId, page });
-    const attachments = res.data.paginateResult.docs;
-    const totalAttachments = res.data.paginateResult.totalDocs;
-    const pagingLimit = res.data.paginateResult.limit;
-
-    const inUse = {};
-
-    for (const attachment of attachments) {
-      inUse[attachment._id] = this.checkIfFileInUse(attachment);
-    }
-    this.setState({
-      activePage: selectedPage,
-      totalAttachments,
-      limit: pagingLimit,
-      attachments,
-      inUse,
-    });
-  }
-
-
-  async componentDidMount() {
-    await this.handlePage(1);
-    this.setState({
-      activePage: 1,
-    });
-  }
-
-  checkIfFileInUse(attachment) {
-    const { markdown } = this.props.pageContainer.state;
-
-    if (markdown.match(attachment._id)) {
-      return true;
-    }
-    return false;
-  }
-
-  onAttachmentDeleteClicked(attachment) {
-    this.setState({
-      attachmentToDelete: attachment,
-    });
-  }
-
-  onAttachmentDeleteClickedConfirm(attachment) {
-    const attachmentId = attachment._id;
-    this.setState({
-      deleting: true,
-    });
-
-    apiPost('/attachments.remove', { attachment_id: attachmentId })
-      .then((res) => {
-        this.setState({
-          attachments: this.state.attachments.filter((at) => {
-            // comparing ObjectId
-            // eslint-disable-next-line eqeqeq
-            return at._id != attachmentId;
-          }),
-          attachmentToDelete: null,
-          deleting: false,
-        });
-      }).catch((err) => {
-        this.setState({
-          deleteError: 'Something went wrong.',
-          deleting: false,
-        });
-      });
-  }
-
-
-  render() {
-    const { t, isGuestUser } = this.props;
-
-    if (this.state.attachments.length === 0) {
-      return (
-        <div data-testid="page-attachment">
-          {t('No_attachments_yet')}
-        </div>
-      );
-    }
-
-    let deleteAttachmentModal = '';
-    if (!isGuestUser) {
-      const attachmentToDelete = this.state.attachmentToDelete;
-      const deleteModalClose = () => {
-        this.setState({ attachmentToDelete: null, deleteError: '' });
-      };
-      const showModal = attachmentToDelete !== null;
-
-      let deleteInUse = null;
-      if (attachmentToDelete !== null) {
-        deleteInUse = this.state.inUse[attachmentToDelete._id] || false;
-      }
-
-      deleteAttachmentModal = (
-        <DeleteAttachmentModal
-          isOpen={showModal}
-          animation="false"
-          toggle={deleteModalClose}
-          attachmentToDelete={attachmentToDelete}
-          inUse={deleteInUse}
-          deleting={this.state.deleting}
-          deleteError={this.state.deleteError}
-          onAttachmentDeleteClickedConfirm={this.onAttachmentDeleteClickedConfirm}
-        />
-      );
-    }
-
-    return (
-      <div data-testid="page-attachment">
-        <PageAttachmentList
-          attachments={this.state.attachments}
-          inUse={this.state.inUse}
-          onAttachmentDeleteClicked={this.onAttachmentDeleteClicked}
-          isUserLoggedIn={!isGuestUser}
-        />
-
-        {deleteAttachmentModal}
-
-        <PaginationWrapper
-          activePage={this.state.activePage}
-          changePage={this.handlePage}
-          totalItemsCount={this.state.totalAttachments}
-          pagingLimit={this.state.limit}
-          align="center"
-        />
-      </div>
-    );
-  }
-
-}
-
-PageAttachment.propTypes = {
-  t: PropTypes.func.isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-
-  isGuestUser: PropTypes.bool.isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const PageAttachmentUnstatedWrapper = withUnstatedContainers(PageAttachment, [PageContainer]);
-
-const PageAttachmentWrapper = (props) => {
-  const { t } = useTranslation();
-  const { data: isGuestUser } = useIsGuestUser();
-
-  if (isGuestUser == null) {
-    return <></>;
-  }
-
-  return <PageAttachmentUnstatedWrapper {...props} t={t} isGuestUser={isGuestUser} />;
-};
-
-export default PageAttachmentWrapper;

+ 151 - 0
packages/app/src/components/PageAttachment.tsx

@@ -0,0 +1,151 @@
+import React, { useCallback, useEffect, useState } from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+import { useSWRxAttachments } from '~/stores/attachment';
+import { useEditingMarkdown, useCurrentPageId, useIsGuestUser } from '~/stores/context';
+
+import DeleteAttachmentModal from './PageAttachment/DeleteAttachmentModal';
+import PageAttachmentList from './PageAttachment/PageAttachmentList';
+import PaginationWrapper from './PaginationWrapper';
+
+// Utility
+const checkIfFileInUse = (markdown: string, attachment) => {
+  return markdown.match(attachment._id);
+};
+
+// Custom hook that handles processes related to inUseAttachments
+const useInUseAttachments = (attachments) => {
+  const { data: markdown } = useEditingMarkdown();
+  const [inUse, setInUse] = useState<any>({});
+
+  // Update inUse when either of attachments or markdown is updated
+  useEffect(() => {
+    if (markdown == null) {
+      return;
+    }
+
+    const newInUse = {};
+
+    for (const attachment of attachments) {
+      newInUse[attachment._id] = checkIfFileInUse(markdown, attachment);
+    }
+
+    setInUse(newInUse);
+  }, [attachments, markdown]);
+
+  return inUse;
+};
+
+const PageAttachment = (): JSX.Element => {
+  const { t } = useTranslation();
+
+  // Static SWRs
+  const { data: pageId } = useCurrentPageId();
+  const { data: isGuestUser } = useIsGuestUser();
+
+  // States
+  const [pageNumber, setPageNumber] = useState(1);
+  const [attachmentToDelete, setAttachmentToDelete] = useState<any>(undefined);
+  const [deleting, setDeleting] = useState(false);
+  const [deleteError, setDeleteError] = useState('');
+
+  // SWRs
+  const { data: dataAttachments, remove } = useSWRxAttachments(pageId, pageNumber);
+  const {
+    attachments = [],
+    totalAttachments = 0,
+    limit,
+  } = dataAttachments ?? {};
+
+  // Custom hooks
+  const inUseAttachments = useInUseAttachments(attachments);
+
+  // Methods
+  const onChangePageHandler = useCallback((newPageNumber: number) => {
+    setPageNumber(newPageNumber);
+  }, []);
+
+  const onAttachmentDeleteClicked = useCallback((attachment) => {
+    setAttachmentToDelete(attachment);
+  }, []);
+
+  const onAttachmentDeleteClickedConfirmHandler = useCallback(async(attachment) => {
+    setDeleting(true);
+
+    try {
+      await remove({ attachment_id: attachment._id });
+
+      setAttachmentToDelete(null);
+      setDeleting(false);
+    }
+    catch {
+      setDeleteError('Something went wrong.');
+      setDeleting(false);
+    }
+  }, [remove]);
+
+  const onToggleHandler = useCallback(() => {
+    setAttachmentToDelete(null);
+    setDeleteError('');
+  }, []);
+
+  // Renderers
+  const renderDeleteAttachmentModal = useCallback(() => {
+    if (isGuestUser) {
+      return <></>;
+    }
+
+    if (attachments.length === 0) {
+      return (
+        <div data-testid="page-attachment">
+          {t('No_attachments_yet')}
+        </div>
+      );
+    }
+
+    let deleteInUse = null;
+    if (attachmentToDelete != null) {
+      deleteInUse = inUseAttachments[attachmentToDelete._id] || false;
+    }
+
+    const isOpen = attachmentToDelete != null;
+
+    return (
+      <DeleteAttachmentModal
+        isOpen={isOpen}
+        animation="false"
+        toggle={onToggleHandler}
+        attachmentToDelete={attachmentToDelete}
+        inUse={deleteInUse}
+        deleting={deleting}
+        deleteError={deleteError}
+        onAttachmentDeleteClickedConfirm={onAttachmentDeleteClickedConfirmHandler}
+      />
+    );
+  // eslint-disable-next-line max-len
+  }, [attachmentToDelete, attachments.length, deleteError, deleting, inUseAttachments, isGuestUser, onAttachmentDeleteClickedConfirmHandler, onToggleHandler, t]);
+
+  return (
+    <div data-testid="page-attachment">
+      <PageAttachmentList
+        attachments={attachments}
+        inUse={inUseAttachments}
+        onAttachmentDeleteClicked={onAttachmentDeleteClicked}
+        isUserLoggedIn={!isGuestUser}
+      />
+
+      {renderDeleteAttachmentModal()}
+
+      <PaginationWrapper
+        activePage={pageNumber}
+        changePage={onChangePageHandler}
+        totalItemsCount={totalAttachments}
+        pagingLimit={limit}
+        align="center"
+      />
+    </div>
+  );
+};
+
+export default PageAttachment;

+ 3 - 1
packages/app/src/components/PagePresentationModal.jsx

@@ -6,6 +6,8 @@ import {
 
 import { usePagePresentationModal } from '~/stores/modal';
 
+import styles from './PagePresentationModal.module.scss';
+
 const PagePresentationModal = () => {
 
   const { data: presentationData, close: closePresentationModal } = usePagePresentationModal();
@@ -15,7 +17,7 @@ const PagePresentationModal = () => {
       isOpen={presentationData.isOpened}
       toggle={closePresentationModal}
       data-testid="page-presentation-modal"
-      className="grw-presentation-modal"
+      className={`grw-presentation-modal ${styles['grw-presentation-modal']}`}
       unmountOnClose={false}
     >
       <ModalBody className="modal-body">

+ 4 - 2
packages/app/src/styles/_page-presentation.scss → packages/app/src/components/PagePresentationModal.module.scss

@@ -1,5 +1,7 @@
-.grw-presentation-modal {
-  @include expand-modal-fullscreen(false, false);
+@use '~/styles/mixins' as mi;
+
+.grw-presentation-modal :global {
+  @include mi.expand-modal-fullscreen(false, false);
 
   .modal-body {
     background: black;

+ 11 - 10
packages/app/src/components/Theme/ThemeFireRed.module.scss

@@ -1,7 +1,8 @@
-@import '../variables';
-@import '../override-bootstrap-variables';
+@use '../../styles/variables' as *;
+@use '../../styles/bootstrap/variables' as *;
+@use '../../styles/theme/mixins/page-editor-mode-manager';
 
-html[light] {
+.theme[data-color-scheme='light'] :global {
   // Theme colors
   $themecolor: #ea5532;
   $themelight: #ffffff;
@@ -70,8 +71,8 @@ html[light] {
   // admin theme box
   $color-theme-color-box: $primary;
 
-  @import 'apply-colors';
-  @import 'apply-colors-light';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-light';
 
   // Navs {
   .nav-tabs {
@@ -89,12 +90,12 @@ html[light] {
   // Button
   .btn-group.grw-page-editor-mode-manager {
     .btn.btn-outline-primary {
-      @include btn-page-editor-mode-manager(#ffffff, $primary, $primary, lighten($primary, 20%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager(#ffffff, $primary, $primary, lighten($primary, 20%));
     }
   }
 }
 
-html[dark] {
+.theme[data-color-scheme='dark'] :global {
   // Theme colors
   $themecolor: #ea5532;
   $themedark: #333333;
@@ -171,8 +172,8 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: $primary;
 
-  @import 'apply-colors';
-  @import 'apply-colors-dark';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-dark';
 
   // Navs
   .nav-tabs {
@@ -198,7 +199,7 @@ html[dark] {
   // Button
   .btn-group.grw-page-editor-mode-manager {
     .btn.btn-outline-primary {
-      @include btn-page-editor-mode-manager(#ffffff, $primary, $primary, darken($primary, 20%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager(#ffffff, $primary, $primary, darken($primary, 20%));
     }
   }
 }

+ 8 - 0
packages/app/src/components/Theme/ThemeFireRed.tsx

@@ -0,0 +1,8 @@
+import { ThemeInjector } from './utils/ThemeInjector';
+
+import styles from './ThemeFireRed.module.scss';
+
+const ThemeFireRed = ({ children }: { children: JSX.Element }): JSX.Element => {
+  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+};
+export default ThemeFireRed;

+ 9 - 8
packages/app/src/components/Theme/ThemeIsland.module.scss

@@ -1,11 +1,12 @@
-@import '../variables';
-@import '../override-bootstrap-variables';
+@use '../../styles/variables' as *;
+@use '../../styles/bootstrap/variables' as *;
+@use '../../styles/theme/mixins/page-editor-mode-manager';
+@use '../../styles/mixins';
 
 $color-primary: #97cbc3;
 $color-themelight: rgba(183, 226, 219, 1);
 
-html[light],
-html[dark] {
+.theme :global {
   $primary: $color-primary;
   // Background colors
   $bgcolor-card: $gray-50;
@@ -81,8 +82,8 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: darken($primary, 15%);
 
-  @import 'apply-colors';
-  @import 'apply-colors-light';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-light';
 
   .rbt-menu {
     background: lighten($color-themelight, 5%);
@@ -106,7 +107,7 @@ html[dark] {
   // Button
   .btn-group.grw-page-editor-mode-manager {
     .btn.btn-outline-primary {
-      @include btn-page-editor-mode-manager(darken($primary, 50%), lighten($primary, 5%), darken($primary, 5%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager(darken($primary, 50%), lighten($primary, 5%), darken($primary, 5%));
     }
   }
 
@@ -124,7 +125,7 @@ html[dark] {
     // Pagetree
     .grw-pagetree {
       .grw-pagetree-triangle-btn {
-        @include button-outline-svg-icon-variant($gray-400, $bgcolor-sidebar);
+        @include mixins.button-outline-svg-icon-variant($gray-400, $bgcolor-sidebar);
       }
     }
   }

+ 8 - 0
packages/app/src/components/Theme/ThemeIsland.tsx

@@ -0,0 +1,8 @@
+import { ThemeInjector } from './utils/ThemeInjector';
+
+import styles from './ThemeIsland.module.scss';
+
+const ThemeIsland = ({ children }: { children: JSX.Element }): JSX.Element => {
+  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+};
+export default ThemeIsland;

+ 11 - 10
packages/app/src/components/Theme/ThemeMonoBlue.module.scss

@@ -1,7 +1,8 @@
-@import '../variables';
-@import '../override-bootstrap-variables';
+@use '../../styles/variables' as *;
+@use '../../styles/bootstrap/variables' as *;
+@use '../../styles/theme/mixins/page-editor-mode-manager';
 
-html[light] {
+.theme[data-color-scheme='light'] :global {
   // Theme colors
   $themecolor: #00587a;
   $themelight: #f7fbfd;
@@ -70,8 +71,8 @@ html[light] {
   // admin theme box
   $color-theme-color-box: lighten($primary, 20%);
 
-  @import 'apply-colors';
-  @import 'apply-colors-light';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-light';
 
   // Navs {
   .nav-tabs {
@@ -89,12 +90,12 @@ html[light] {
   // Button
   .btn-group.grw-page-editor-mode-manager {
     .btn.btn-outline-primary {
-      @include btn-page-editor-mode-manager($primary, lighten($primary, 65%), lighten($primary, 70%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager($primary, lighten($primary, 65%), lighten($primary, 70%));
     }
   }
 }
 
-html[dark] {
+.theme[data-color-scheme='dark'] :global {
   // Theme colors
   $themecolor: #00587a;
   $themedark: #061f2f;
@@ -167,8 +168,8 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: $primary;
 
-  @import 'apply-colors';
-  @import 'apply-colors-dark';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-dark';
 
   // Navs
   .nav-tabs {
@@ -194,7 +195,7 @@ html[dark] {
   // Button
   .btn-group.grw-page-editor-mode-manager {
     .btn.btn-outline-primary {
-      @include btn-page-editor-mode-manager(lighten($primary, 30%), $primary, darken($primary, 10%), darken($primary, 20%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager(lighten($primary, 30%), $primary, darken($primary, 10%), darken($primary, 20%));
     }
   }
 }

+ 8 - 0
packages/app/src/components/Theme/ThemeMonoBlue.tsx

@@ -0,0 +1,8 @@
+import { ThemeInjector } from './utils/ThemeInjector';
+
+import styles from './ThemeMonoBlue.module.scss';
+
+const ThemeMonoBlue = ({ children }: { children: JSX.Element }): JSX.Element => {
+  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+};
+export default ThemeMonoBlue;

+ 7 - 7
packages/app/src/components/Theme/ThemeNature.module.scss

@@ -1,5 +1,6 @@
-@import '../variables';
-@import '../override-bootstrap-variables';
+@use '../../styles/variables' as *;
+@use '../../styles/bootstrap/variables' as *;
+@use '../../styles/theme/mixins/page-editor-mode-manager';
 
 // == Define Bootstrap theme colors
 //
@@ -35,8 +36,7 @@ $themecolor: #12b105;
 
 //== Light Mode
 //
-html[light],
-html[dark] {
+.theme :global {
   $primary: #460039;
   $light: $gray-100;
 
@@ -90,8 +90,8 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: lighten($primary, 20%);
 
-  @import 'apply-colors';
-  @import 'apply-colors-light';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-light';
 
   // Search Top
   .grw-global-search {
@@ -111,7 +111,7 @@ html[dark] {
   // Button
   .btn-group.grw-page-editor-mode-manager {
     .btn.btn-outline-primary {
-      @include btn-page-editor-mode-manager($bgcolor-navbar, lighten($bgcolor-navbar, 65%), lighten($bgcolor-navbar, 70%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager($bgcolor-navbar, lighten($bgcolor-navbar, 65%), lighten($bgcolor-navbar, 70%));
     }
   }
 }

+ 8 - 0
packages/app/src/components/Theme/ThemeNature.tsx

@@ -0,0 +1,8 @@
+import { ThemeInjector } from './utils/ThemeInjector';
+
+import styles from './ThemeNature.module.scss';
+
+const ThemeNature = ({ children }: { children: JSX.Element }): JSX.Element => {
+  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+};
+export default ThemeNature;

+ 9 - 8
packages/app/src/components/Theme/ThemeSpring.module.scss

@@ -1,5 +1,7 @@
-@import '../variables';
-@import '../override-bootstrap-variables';
+@use '../../styles/variables' as *;
+@use '../../styles/bootstrap/variables' as *;
+@use '../../styles/theme/mixins/page-editor-mode-manager';
+@use '../../styles/bootstrap/init' as bs;
 
 // == Define Bootstrap theme colors
 //
@@ -25,8 +27,7 @@ $accentcolor: #e08dbc;
 
 //== Light Mode
 //
-html[light],
-html[dark] {
+.theme :global {
   $primary: $themecolor;
   $secondary: $accentcolor;
 
@@ -90,17 +91,17 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: darken($primary, 20%);
 
-  @import 'apply-colors';
-  @import 'apply-colors-light';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-light';
 
   //Button
   // Outline buttons are applyed the accent color to this spring theme cuz the primary is too light and it looks like unable to click them.
   .btn.btn-outline-primary {
-    @include button-outline-variant($accentcolor, $accentcolor, lighten($accentcolor, 20%), $accentcolor);
+    @include bs.button-outline-variant($accentcolor, $accentcolor, lighten($accentcolor, 20%), $accentcolor);
   }
   .btn-group.grw-page-editor-mode-manager {
     .btn.btn-outline-primary {
-      @include btn-page-editor-mode-manager(darken($primary, 50%), lighten($primary, 5%), lighten($primary, 10%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager(darken($primary, 50%), lighten($primary, 5%), lighten($primary, 10%));
     }
   }
 

+ 8 - 0
packages/app/src/components/Theme/ThemeSpring.tsx

@@ -0,0 +1,8 @@
+import { ThemeInjector } from './utils/ThemeInjector';
+
+import styles from './ThemeSpring.module.scss';
+
+const ThemeSpring = ({ children }: { children: JSX.Element }): JSX.Element => {
+  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+};
+export default ThemeSpring;

+ 7 - 7
packages/app/src/components/Theme/ThemeWood.module.scss

@@ -1,5 +1,6 @@
-@import '../variables';
-@import '../override-bootstrap-variables';
+@use '../../styles/variables' as *;
+@use '../../styles/bootstrap/variables' as *;
+@use '../../styles/theme/mixins/page-editor-mode-manager';
 
 // == Define Bootstrap theme colors
 //
@@ -36,8 +37,7 @@ $themelight: #f5f3ee;
 
 //== Light Mode
 //
-html[light],
-html[dark] {
+.theme :global {
   $primary: #aaa45f;
 
   // Background colors
@@ -114,8 +114,8 @@ html[dark] {
   // portal
   $info: lighten($themecolor, 10%);
 
-  @import 'apply-colors';
-  @import 'apply-colors-light';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-light';
 
   /*
    * Modal
@@ -164,7 +164,7 @@ html[dark] {
   // Button
   .btn-group.grw-page-editor-mode-manager {
     .btn.btn-outline-primary {
-      @include btn-page-editor-mode-manager(darken($primary, 30%), lighten($primary, 15%), lighten($primary, 25%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager(darken($primary, 30%), lighten($primary, 15%), lighten($primary, 25%));
     }
   }
 }

+ 8 - 0
packages/app/src/components/Theme/ThemeWood.tsx

@@ -0,0 +1,8 @@
+import { ThemeInjector } from './utils/ThemeInjector';
+
+import styles from './ThemeWood.module.scss';
+
+const ThemeWood = ({ children }: { children: JSX.Element }): JSX.Element => {
+  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+};
+export default ThemeWood;

+ 18 - 0
packages/app/src/components/Theme/utils/ThemeProvider.tsx

@@ -10,7 +10,13 @@ const ThemeAntarctic = dynamic(() => import('../ThemeAntarctic'));
 const ThemeBlackboard = dynamic(() => import('../ThemeBlackboard'));
 const ThemeChristmas = dynamic(() => import('../ThemeChristmas'));
 const ThemeDefault = dynamic(() => import('../ThemeDefault'));
+const ThemeFireRed = dynamic(() => import('../ThemeFireRed'));
 const ThemeJadeGreen = dynamic(() => import('../ThemeJadeGreen'));
+const ThemeIsland = dynamic(() => import('../ThemeIsland'));
+const ThemeSpring = dynamic(() => import('../ThemeSpring'));
+const ThemeNature = dynamic(() => import('../ThemeNature'));
+const ThemeWood = dynamic(() => import('../ThemeWood'));
+const ThemeMonoBlue = dynamic(() => import('../ThemeMonoBlue'));
 
 
 type Props = {
@@ -26,8 +32,20 @@ export const ThemeProvider = ({ theme, children }: Props): JSX.Element => {
       return <ThemeBlackboard>{children}</ThemeBlackboard>;
     case GrowiThemes.CHRISTMAS:
       return <ThemeChristmas>{children}</ThemeChristmas>;
+    case GrowiThemes.FIRE_RED:
+      return <ThemeFireRed>{children}</ThemeFireRed>;
     case GrowiThemes.JADE_GREEN:
       return <ThemeJadeGreen>{children}</ThemeJadeGreen>;
+    case GrowiThemes.ISLAND:
+      return <ThemeIsland>{children}</ThemeIsland>;
+    case GrowiThemes.SPRING:
+      return <ThemeSpring>{children}</ThemeSpring>;
+    case GrowiThemes.NATURE:
+      return <ThemeNature>{children}</ThemeNature>;
+    case GrowiThemes.WOOD:
+      return <ThemeWood>{children}</ThemeWood>;
+    case GrowiThemes.MONO_BLUE:
+      return <ThemeMonoBlue>{children}</ThemeMonoBlue>;
     default:
       return <ThemeDefault>{children}</ThemeDefault>;
   }

+ 10 - 1
packages/app/src/interfaces/attachment.ts

@@ -1 +1,10 @@
-export type { IAttachment } from '@growi/core';
+import type { IAttachment } from '@growi/core';
+
+import type { PaginateResult } from './mongoose-utils';
+
+
+export type IResAttachmentList = {
+  data: {
+    paginateResult: PaginateResult<IAttachment>
+  }
+};

+ 3 - 3
packages/app/src/interfaces/crowi-request.ts

@@ -1,10 +1,10 @@
 import { Request } from 'express';
 
-import { IUserHasId } from './user';
+import { IUser, IUserHasId } from './user';
 
-export interface CrowiRequest extends Request {
+export interface CrowiRequest<U extends IUser = IUserHasId> extends Request {
 
-  user?: IUserHasId,
+  user?: U,
 
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   crowi: any,

+ 0 - 4
packages/app/src/interfaces/user-ui-settings.ts

@@ -1,10 +1,6 @@
-import { Ref } from '@growi/core';
-
 import { SidebarContentsType } from './ui';
-import { IUser } from './user';
 
 export interface IUserUISettings {
-  user: Ref<IUser> | null;
   isSidebarCollapsed: boolean,
   currentSidebarContents: SidebarContentsType,
   currentProductNavWidth: number,

+ 74 - 36
packages/app/src/pages/[[...path]].page.tsx

@@ -1,9 +1,10 @@
 import React, { useEffect } from 'react';
 
+
 import EventEmitter from 'events';
 
 import {
-  IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision, isClient, pagePathUtils, pathUtils,
+  IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision, isClient, isIPageInfoForEntity, isServer, IUser, IUserHasId, pagePathUtils, pathUtils,
 } from '@growi/core';
 import ExtensibleCustomError from 'extensible-custom-error';
 import {
@@ -13,6 +14,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
+import superjson from 'superjson';
 
 import { PageAlerts } from '~/components/PageAlert/PageAlerts';
 // import { PageComments } from '~/components/PageComment/PageComments';
@@ -25,10 +27,11 @@ import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CustomWindow } from '~/interfaces/global';
 import { RendererConfig } from '~/interfaces/services/renderer';
 import { ISidebarConfig } from '~/interfaces/sidebar-config';
+import { IUserUISettings } from '~/interfaces/user-ui-settings';
 import { PageModel, PageDocument } from '~/server/models/page';
-import UserUISettings, { UserUISettingsDocument } from '~/server/models/user-ui-settings';
+import UserUISettings from '~/server/models/user-ui-settings';
 import Xss from '~/services/xss';
-import { useSWRxCurrentPage, useSWRxPageInfo, useSWRxPage } from '~/stores/page';
+import { useSWRxCurrentPage, useSWRxPageInfo } from '~/stores/page';
 import {
   usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
 } from '~/stores/ui';
@@ -55,23 +58,56 @@ import {
   useHackmdUri,
   useIsAclEnabled, useIsUserPage, useIsNotCreatable,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
-  useIsSlackConfigured, useIsBlinkedHeaderAtBoot, useRendererConfig,
+  useIsSlackConfigured, useIsBlinkedHeaderAtBoot, useRendererConfig, useEditingMarkdown,
 } from '../stores/context';
 import { useXss } from '../stores/xss';
 
 import {
   CommonProps, getNextI18NextConfig, getServerSideCommonProps, useCustomTitle,
-} from './commons';
+} from './utils/commons';
+import { registerTransformerForObjectId } from './utils/objectid-transformer';
 // import { useCurrentPageSWR } from '../stores/page';
 
 
 const logger = loggerFactory('growi:pages:all');
+
 const {
   isPermalink: _isPermalink, isUsersHomePage, isTrashPage: _isTrashPage, isUserPage, isCreatablePage,
 } = pagePathUtils;
 const { removeHeadingSlash } = pathUtils;
 
 
+type IPageToShowRevisionWithMeta = IDataWithMeta<IPagePopulatedToShowRevision & PageDocument, IPageInfoForEntity>;
+type IPageToShowRevisionWithMetaSerialized = IDataWithMeta<string, string>;
+
+// register custom serializer
+registerTransformerForObjectId();
+
+superjson.registerCustom<IPageToShowRevisionWithMeta, IPageToShowRevisionWithMetaSerialized>(
+  {
+    isApplicable: (v): v is IPageToShowRevisionWithMeta => {
+      return v?.data != null
+        && v?.data.toObject != null
+        && v?.meta != null
+        && isIPageInfoForEntity(v.meta);
+    },
+    serialize: (v) => {
+      return {
+        data: superjson.stringify(v.data.toObject()),
+        meta: superjson.stringify(v.meta),
+      };
+    },
+    deserialize: (v) => {
+      return {
+        data: superjson.parse(v.data),
+        meta: v.meta != null ? superjson.parse(v.meta) : undefined,
+      };
+    },
+  },
+  'IPageToShowRevisionWithMetaTransformer',
+);
+
+
 const IdenticalPathPage = (): JSX.Element => {
   const IdenticalPathPage = dynamic(() => import('../components/IdenticalPathPage').then(mod => mod.IdenticalPathPage), { ssr: false });
   return <IdenticalPathPage />;
@@ -87,12 +123,10 @@ const DescendantsPageListModal = (): JSX.Element => {
   return <DescendantsPageListModal />;
 };
 
-type IPageToShowRevisionWithMeta = IDataWithMeta<IPagePopulatedToShowRevision, IPageInfoForEntity>;
-
 type Props = CommonProps & {
-  currentUser: string,
+  currentUser: IUser,
 
-  pageWithMetaStr: string,
+  pageWithMeta: IPageToShowRevisionWithMeta,
   // pageUser?: any,
   // redirectTo?: string;
   // redirectFrom?: string;
@@ -132,7 +166,7 @@ type Props = CommonProps & {
   rendererConfig: RendererConfig,
 
   // UI
-  userUISettings: UserUISettingsDocument | null
+  userUISettings?: IUserUISettings
   // Sidebar
   sidebarConfig: ISidebarConfig,
 };
@@ -144,7 +178,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   const UnsavedAlertDialog = dynamic(() => import('./UnsavedAlertDialog'), { ssr: false });
   const GrowiSubNavigationSwitcher = dynamic(() => import('../components/Navbar/GrowiSubNavigationSwitcher'), { ssr: false });
 
-  const { data: currentUser } = useCurrentUser(props.currentUser != null ? JSON.parse(props.currentUser) : null);
+  const { data: currentUser } = useCurrentUser(props.currentUser ?? null);
 
   // register global EventEmitter
   if (isClient()) {
@@ -201,10 +235,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
 
   // const { data: editorMode } = useEditorMode();
 
-  let pageWithMeta: IPageToShowRevisionWithMeta | undefined;
-  if (props.pageWithMetaStr != null) {
-    pageWithMeta = JSON.parse(props.pageWithMetaStr) as IPageToShowRevisionWithMeta;
-  }
+  const { pageWithMeta, userUISettings } = props;
 
   let shouldRenderPutbackPageModal = false;
   if (pageWithMeta != null) {
@@ -220,6 +251,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   useIsNotCreatable(props.isForbidden || !isCreatablePage(pageWithMeta?.data.path ?? '')); // TODO: need to include props.isIdentical
   useCurrentPagePath(pageWithMeta?.data.path);
   useCurrentPathname(props.currentPathname);
+  useEditingMarkdown(pageWithMeta?.data.revision.body);
 
   // sync pathname by Shallow Routing https://nextjs.org/docs/routing/shallow-routing
   useEffect(() => {
@@ -325,7 +357,7 @@ class MultiplePagesHitsError extends ExtensibleCustomError {
 
 }
 
-async function getPageData(context: GetServerSidePropsContext, props: Props): Promise<IPageToShowRevisionWithMeta|null> {
+async function injectPageData(context: GetServerSidePropsContext, props: Props): Promise<void> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { revisionId } = req.query;
@@ -348,8 +380,8 @@ async function getPageData(context: GetServerSidePropsContext, props: Props): Pr
     }
   }
 
-  const result: IPageToShowRevisionWithMeta = await pageService.findPageAndMetaDataByViewer(pageId, currentPathname, user, true); // includeEmpty = true, isSharedPage = false
-  const page = result?.data as unknown as PageDocument;
+  const pageWithMeta: IPageToShowRevisionWithMeta = await pageService.findPageAndMetaDataByViewer(pageId, currentPathname, user, true); // includeEmpty = true, isSharedPage = false
+  const page = pageWithMeta?.data as unknown as PageDocument;
 
   // populate & check if the revision is latest
   if (page != null) {
@@ -358,10 +390,20 @@ async function getPageData(context: GetServerSidePropsContext, props: Props): Pr
     props.isLatestRevision = page.isLatestRevision();
   }
 
-  return result;
+  props.pageWithMeta = pageWithMeta;
+}
+
+async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
+  const req = context.req as CrowiRequest<IUserHasId & any>;
+  const { user } = req;
+
+  const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
+  if (userUISettings != null) {
+    props.userUISettings = userUISettings.toObject();
+  }
 }
 
-async function injectRoutingInformation(context: GetServerSidePropsContext, props: Props, pageWithMeta: IPageToShowRevisionWithMeta|null): Promise<void> {
+async function injectRoutingInformation(context: GetServerSidePropsContext, props: Props): Promise<void> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const Page = crowi.model('Page') as PageModel;
@@ -370,7 +412,7 @@ async function injectRoutingInformation(context: GetServerSidePropsContext, prop
   const pageId = getPageIdFromPathname(currentPathname);
   const isPermalink = _isPermalink(currentPathname);
 
-  const page = pageWithMeta?.data;
+  const page = props.pageWithMeta?.data;
 
   if (props.isIdenticalPathPage) {
     // TBD
@@ -414,7 +456,7 @@ async function injectRoutingInformation(context: GetServerSidePropsContext, prop
 //   }
 // }
 
-async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
+function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const {
@@ -483,7 +525,7 @@ async function injectNextI18NextConfigurations(context: GetServerSidePropsContex
 }
 
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
-  const req: CrowiRequest = context.req as CrowiRequest;
+  const req = context.req as CrowiRequest<IUserHasId & any>;
   const { user } = req;
 
   const result = await getServerSideCommonProps(context);
@@ -496,10 +538,13 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   }
 
   const props: Props = result.props as Props;
-  let pageWithMeta;
+
+  if (user != null) {
+    props.currentUser = user.toObject();
+  }
+
   try {
-    pageWithMeta = await getPageData(context, props);
-    props.pageWithMetaStr = JSON.stringify(pageWithMeta);
+    await injectPageData(context, props);
   }
   catch (err) {
     if (err instanceof MultiplePagesHitsError) {
@@ -510,17 +555,10 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     }
   }
 
-  injectRoutingInformation(context, props, pageWithMeta);
+  await injectUserUISettings(context, props);
+  await injectRoutingInformation(context, props);
   injectServerConfigurations(context, props);
-  injectNextI18NextConfigurations(context, props, ['translation']);
-
-  if (user != null) {
-    props.currentUser = JSON.stringify(user);
-  }
-
-  // UI
-  const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
-  props.userUISettings = JSON.parse(JSON.stringify(userUISettings));
+  await injectNextI18NextConfigurations(context, props, ['translation']);
 
   return {
     props,

+ 1 - 1
packages/app/src/pages/_app.page.tsx

@@ -16,7 +16,7 @@ import {
   useAppTitle, useConfidential, useGrowiTheme, useGrowiVersion, useSiteUrl,
 } from '../stores/context';
 
-import { CommonProps } from './commons';
+import { CommonProps } from './utils/commons';
 // import { useInterceptorManager } from '~/stores/interceptor';
 
 const isDev = process.env.NODE_ENV === 'development';

+ 0 - 0
packages/app/src/pages/search.page.tsx → packages/app/src/pages/_search.page.tsx


+ 37 - 20
packages/app/src/pages/admin/[[...path]].page.tsx

@@ -4,32 +4,40 @@ import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 import { useTranslation } from 'next-i18next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
 
-import AdminHome from '~/components/Admin/AdminHome/AdminHome';
-import AppSettingsPageContents from '~/components/Admin/App/AppSettingsPageContents';
-import { AuditLogManagement } from '~/components/Admin/AuditLogManagement';
-import ElasticsearchManagement from '~/components/Admin/ElasticsearchManagement/ElasticsearchManagement';
-import ExportArchiveDataPage from '~/components/Admin/ExportArchiveDataPage';
-import DataImportPageContents from '~/components/Admin/ImportData/ImportDataPageContents';
-import LegacySlackIntegration from '~/components/Admin/LegacySlackIntegration/LegacySlackIntegration';
-import MarkDownSettingContents from '~/components/Admin/MarkdownSetting/MarkDownSettingContents';
-import NotificationSetting from '~/components/Admin/Notification/NotificationSetting';
-import SecurityManagementContents from '~/components/Admin/Security/SecurityManagementContents';
-import SlackIntegration from '~/components/Admin/SlackIntegration/SlackIntegration';
-import UserGroupPage from '~/components/Admin/UserGroup/UserGroupPage';
-import UserManagement from '~/components/Admin/UserManagement';
-import AdminLayout from '~/components/Layout/AdminLayout';
 import { CrowiRequest } from '~/interfaces/crowi-request';
-import { CommonProps, getServerSideCommonProps, useCustomTitle } from '~/pages/commons';
 import PluginUtils from '~/server/plugins/plugin-utils';
 import ConfigLoader from '~/server/service/config-loader';
 import {
   useCurrentUser, /* useSearchServiceConfigured, */ useIsSearchServiceReachable, useSiteUrl,
 } from '~/stores/context';
+
+import {
+  CommonProps, getServerSideCommonProps, useCustomTitle, getNextI18NextConfig,
+} from '../utils/commons';
 // import { useEnvVars } from '~/stores/admin-context';
 
+const AdminHome = dynamic(() => import('../../components/Admin/AdminHome/AdminHome'), { ssr: false });
+const AppSettingsPageContents = dynamic(() => import('../../components/Admin/App/AppSettingsPageContents'), { ssr: false });
+const SecurityManagementContents = dynamic(() => import('../../components/Admin/Security/SecurityManagementContents'), { ssr: false });
+const MarkDownSettingContents = dynamic(() => import('../../components/Admin/MarkdownSetting/MarkDownSettingContents'), { ssr: false });
+const CustomizeSettingContents = dynamic(() => import('../../components/Admin/Customize/Customize'), { ssr: false });
+const DataImportPageContents = dynamic(() => import('../../components/Admin/ImportData/ImportDataPageContents'), { ssr: false });
+const ExportArchiveDataPage = dynamic(() => import('../../components/Admin/ExportArchiveDataPage'), { ssr: false });
+const NotificationSetting = dynamic(() => import('../../components/Admin/Notification/NotificationSetting'), { ssr: false });
+const SlackIntegration = dynamic(() => import('../../components/Admin/SlackIntegration/SlackIntegration'), { ssr: false });
+const LegacySlackIntegration = dynamic(() => import('../../components/Admin/LegacySlackIntegration/LegacySlackIntegration'), { ssr: false });
+const UserManagement = dynamic(() => import('../../components/Admin/UserManagement'), { ssr: false });
+const UserGroupPage = dynamic(() => import('../../components/Admin/UserGroup/UserGroupPage'), { ssr: false });
+const ElasticsearchManagement = dynamic(() => import('../../components/Admin/ElasticsearchManagement/ElasticsearchManagement'), { ssr: false });
+// named export
+const AuditLogManagement = dynamic(() => import('../../components/Admin/AuditLogManagement').then(module => module.AuditLogManagement));
+
+const AdminLayout = dynamic(() => import('../../components/Layout/AdminLayout'), { ssr: false });
+
 const pluginUtils = new PluginUtils();
 
 type Props = CommonProps & {
@@ -45,18 +53,15 @@ type Props = CommonProps & {
   isSearchServiceReachable: boolean,
 
   siteUrl: string,
-
 };
 
 const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
 
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   const router = useRouter();
   const path = router.query.path || 'home';
   const name = Array.isArray(path) ? path[0] : path;
 
-  const CustomizeSettingContents = dynamic(() => import('../../components/Admin/Customize/Customize'), { ssr: false });
-
   const adminPagesMap = {
     home: {
       title: useCustomTitle(props, t('Wiki Management Home Page')),
@@ -145,6 +150,17 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
   );
 };
 
+/**
+ * for Server Side Translations
+ * @param context
+ * @param props
+ * @param namespacesRequired
+ */
+async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+  props._nextI18Next = nextI18NextConfig._nextI18Next;
+}
+
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
@@ -153,7 +169,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   } = crowi;
 
   const { user } = req;
-
   const result = await getServerSideCommonProps(context);
 
   // check for presence
@@ -167,6 +182,8 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     props.currentUser = JSON.stringify(user);
   }
 
+  injectNextI18NextConfigurations(context, props, ['admin']);
+
   props.siteUrl = appService.getSiteUrl();
   props.nodeVersion = crowi.runtimeVersions.versions.node ? crowi.runtimeVersions.versions.node.version.version : null;
   props.npmVersion = crowi.runtimeVersions.versions.npm ? crowi.runtimeVersions.versions.npm.version.version : null;

+ 1 - 1
packages/app/src/pages/installer.page.tsx

@@ -17,7 +17,7 @@ import {
 
 import {
   CommonProps, getNextI18NextConfig, getServerSideCommonProps, useCustomTitle,
-} from './commons';
+} from './utils/commons';
 
 
 const { isTrashPage: _isTrashPage } = pagePathUtils;

+ 1 - 1
packages/app/src/pages/commons.ts → packages/app/src/pages/utils/commons.ts

@@ -5,7 +5,7 @@ import { SSRConfig, UserConfig } from 'next-i18next';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { GrowiThemes } from '~/interfaces/theme';
 
-import * as nextI18NextConfig from '../next-i18next.config';
+import * as nextI18NextConfig from '../../next-i18next.config';
 
 export type CommonProps = {
   namespacesRequired: string[], // i18next

+ 13 - 0
packages/app/src/pages/utils/objectid-transformer.ts

@@ -0,0 +1,13 @@
+import { Types as MongooseTypes } from 'mongoose';
+import superjson from 'superjson';
+
+export const registerTransformerForObjectId = (): void => {
+  superjson.registerCustom<MongooseTypes.ObjectId|string, string>(
+    {
+      isApplicable: (v): v is MongooseTypes.ObjectId => v instanceof MongooseTypes.ObjectId,
+      serialize: v => (v instanceof MongooseTypes.ObjectId ? v.toHexString() : v),
+      deserialize: v => v,
+    },
+    'ObjectidTransformer',
+  );
+};

+ 7 - 5
packages/app/src/server/middlewares/inject-user-ui-settings-to-localvars.ts

@@ -5,16 +5,18 @@ import UserUISettings from '../models/user-ui-settings';
 
 const logger = loggerFactory('growi:middleware:inject-user-ui-settings-to-localvars');
 
-async function getSettings(userId: string): Promise<Partial<IUserUISettings> | null> {
+async function getSettings(userId: string): Promise<IUserUISettings | null> {
   const doc = await UserUISettings.findOne({ user: userId }).exec();
 
-  let partialDoc: Partial<IUserUISettings> | null = null;
+  let userUISettings: IUserUISettings | null = null;
   if (doc != null) {
-    partialDoc = doc.toObject();
-    delete partialDoc.user;
+    const obj = doc.toObject();
+    // omit user property
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    userUISettings = (({ user, ...rest }) => rest)(obj);
   }
 
-  return partialDoc;
+  return userUISettings;
 }
 
 module.exports = () => {

+ 4 - 2
packages/app/src/server/models/user-ui-settings.ts

@@ -1,14 +1,16 @@
+import { getOrCreateModel, Ref, IUser } from '@growi/core';
 import {
   Schema, Model, Document,
 } from 'mongoose';
 
-import { getOrCreateModel } from '@growi/core';
 
 import { SidebarContentsType } from '~/interfaces/ui';
 import { IUserUISettings } from '~/interfaces/user-ui-settings';
 
 
-export interface UserUISettingsDocument extends IUserUISettings, Document {}
+export interface UserUISettingsDocument extends IUserUISettings, Document {
+  user: Ref<IUser>,
+}
 export type UserUISettingsModel = Model<UserUISettingsDocument>
 
 const schema = new Schema<UserUISettingsDocument, UserUISettingsModel>({

+ 3 - 3
packages/app/src/server/routes/apiv3/attachment.js

@@ -28,7 +28,7 @@ module.exports = (crowi) => {
   const validator = {
     retrieveAttachments: [
       query('pageId').isMongoId().withMessage('pageId is required'),
-      query('page').optional().isInt().withMessage('page must be a number'),
+      query('pageNumber').optional().isInt().withMessage('pageNumber must be a number'),
       query('limit').optional().isInt({ max: 100 }).withMessage('You should set less than 100 or not to set limit.'),
     ],
   };
@@ -53,8 +53,8 @@ module.exports = (crowi) => {
   router.get('/list', accessTokenParser, loginRequired, validator.retrieveAttachments, apiV3FormValidator, async(req, res) => {
 
     const limit = req.query.limit || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS') || 10;
-    const page = req.query.page || 1;
-    const offset = (page - 1) * limit;
+    const pageNumber = req.query.pageNumber || 1;
+    const offset = (pageNumber - 1) * limit;
 
     try {
       const pageId = req.query.pageId;

+ 4 - 6
packages/app/src/server/routes/index.js

@@ -76,7 +76,7 @@ module.exports = function(crowi, app) {
 
   app.get('/_next/*'                  , next.delegateToNext);
 
-  app.get('/'                         , applicationInstalled, unavailableWhenMaintenanceMode, loginRequired, autoReconnectToSearch, injectUserUISettings, next.delegateToNext);
+  app.get('/'                         , applicationInstalled, unavailableWhenMaintenanceMode, loginRequired, autoReconnectToSearch, next.delegateToNext);
 
   app.get('/login/error/:reason'      , applicationInstalled, login.error);
   app.get('/login'                    , applicationInstalled, login.preLogin, login.login);
@@ -219,7 +219,7 @@ module.exports = function(crowi, app) {
   app.get('/attachment/:pageId/:fileName'       , loginRequired, attachment.api.obsoletedGetForMongoDB); // DEPRECATED: remains for backward compatibility for v3.3.x or below
   app.get('/download/:id([0-9a-z]{24})'         , loginRequired, attachment.api.download);
 
-  app.get('/_search'                            , loginRequired, injectUserUISettings, search.searchPage);
+  app.get('/_search'                            , loginRequired, next.delegateToNext);
 
   app.get('/trash$'                   , loginRequired, injectUserUISettings, page.trashPageShowWrapper);
   app.get('/trash/$'                  , loginRequired, (req, res) => res.redirect('/trash'));
@@ -249,9 +249,7 @@ module.exports = function(crowi, app) {
 
   app.use('/ogp', express.Router().get('/:pageId([0-9a-z]{0,})', loginRequired, ogp.pageIdRequired, ogp.ogpValidator, ogp.renderOgp));
 
-  app.get('/:id([0-9a-z]{24})'       , loginRequired , injectUserUISettings, next.delegateToNext);
-
-  app.get('/*/$'                   , loginRequired , injectUserUISettings, next.delegateToNext);
-  app.get('/*'                     , loginRequired , autoReconnectToSearch, injectUserUISettings, next.delegateToNext);
+  app.get('/*/$'                   , loginRequired, next.delegateToNext);
+  app.get('/*'                     , loginRequired, autoReconnectToSearch, next.delegateToNext);
 
 };

+ 0 - 2
packages/app/src/services/renderer/renderer.tsx

@@ -5,7 +5,6 @@ import slug from 'rehype-slug';
 import toc, { HtmlElementNode } from 'rehype-toc';
 import breaks from 'remark-breaks';
 import emoji from 'remark-emoji';
-import footnotes from 'remark-footnotes';
 import gfm from 'remark-gfm';
 
 import { Header } from '~/components/ReactMarkdownComponents/Header';
@@ -244,7 +243,6 @@ export const generateViewOptions = (
 
   // add remark plugins
   if (remarkPlugins != null) {
-    remarkPlugins.push(footnotes);
     remarkPlugins.push(emoji);
     if (config.isEnabledLinebreaks) {
       remarkPlugins.push(breaks);

+ 52 - 0
packages/app/src/stores/attachment.tsx

@@ -0,0 +1,52 @@
+import { useCallback } from 'react';
+
+import {
+  IAttachment, Nullable, SWRResponseWithUtils, withUtils,
+} from '@growi/core';
+import useSWR from 'swr';
+
+import { apiGet, apiPost } from '~/client/util/apiv1-client';
+import { IResAttachmentList } from '~/interfaces/attachment';
+
+type Util = {
+  remove(body: { attachment_id: string }): Promise<void>
+};
+
+type IDataAttachmentList = {
+  attachments: IAttachment[]
+  totalAttachments: number
+  limit: number
+};
+
+export const useSWRxAttachments = (pageId?: Nullable<string>, pageNumber?: number): SWRResponseWithUtils<Util, IDataAttachmentList, Error> => {
+  const shouldFetch = pageId != null && pageNumber != null;
+
+  const fetcher = useCallback(async(endpoint) => {
+    const res = await apiGet<IResAttachmentList>(endpoint, { pageId, pageNumber });
+    return {
+      attachments: res.data.paginateResult.docs,
+      totalAttachments: res.data.paginateResult.totalDocs,
+      limit: res.data.paginateResult.limit,
+    };
+  }, [pageId, pageNumber]);
+
+  const swrResponse = useSWR(
+    shouldFetch ? ['/attachments/list', pageId, pageNumber] : null,
+    fetcher,
+  );
+
+  // Utils
+  const remove = useCallback(async(body: { attachment_id: string }) => {
+    const { mutate } = swrResponse;
+
+    try {
+      await apiPost('/attachments.remove', body);
+      mutate();
+    }
+    catch (err) {
+      throw err;
+    }
+  }, [swrResponse]);
+
+  return withUtils<Util, IDataAttachmentList, Error>(swrResponse, { remove });
+};

+ 4 - 0
packages/app/src/stores/context.tsx

@@ -232,6 +232,10 @@ export const useIsBlinkedHeaderAtBoot = (initialData?: boolean): SWRResponse<boo
   return useStaticSWR('isBlinkedAtBoot', initialData);
 };
 
+export const useEditingMarkdown = (initialData?: string): SWRResponse<string, Error> => {
+  return useStaticSWR('currentMarkdown', initialData);
+};
+
 
 /** **********************************************************
  *                     Computed contexts

+ 2 - 2
packages/app/src/stores/page.tsx

@@ -17,14 +17,14 @@ import { useCurrentPageId } from './context';
 export const useSWRxPage = (pageId?: string|null, shareLinkId?: string): SWRResponse<IPagePopulatedToShowRevision, Error> => {
   return useSWR<IPagePopulatedToShowRevision, Error>(
     pageId != null ? ['/page', pageId, shareLinkId] : null,
-    (endpoint, pageId, shareLinkId) => apiv3Get(endpoint, { pageId, shareLinkId }).then(result => result.data.page),
+    (endpoint, pageId, shareLinkId) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { pageId, shareLinkId }).then(result => result.data.page),
   );
 };
 
 export const useSWRxPageByPath = (path?: string): SWRResponse<IPagePopulatedToShowRevision, Error> => {
   return useSWR<IPagePopulatedToShowRevision, Error>(
     path != null ? ['/page', path] : null,
-    (endpoint, path) => apiv3Get(endpoint, { path }).then(result => result.data.page),
+    (endpoint, path) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { path }).then(result => result.data.page),
   );
 };
 

+ 27 - 1
packages/app/src/utils/axios.ts

@@ -1,9 +1,35 @@
 // eslint-disable-next-line no-restricted-imports
 import axios from 'axios';
+import parseISO from 'date-fns/parseISO';
+import isIsoDate from 'is-iso-date';
 
-export default axios.create({
+const customAxios = axios.create({
   headers: {
     'X-Requested-With': 'XMLHttpRequest',
     'Content-Type': 'application/json',
   },
 });
+
+// add an interceptor to convert ISODate
+const convertDates = (body: any): void => {
+  if (body === null || body === undefined || typeof body !== 'object') {
+    return body;
+  }
+
+  for (const key of Object.keys(body)) {
+    const value = body[key];
+    if (isIsoDate(value)) {
+      body[key] = parseISO(value);
+    }
+    else if (typeof value === 'object') {
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      convertDates(value);
+    }
+  }
+};
+customAxios.interceptors.response.use((response) => {
+  convertDates(response.data);
+  return response;
+});
+
+export default customAxios;

+ 1 - 0
packages/core/src/index.ts

@@ -27,3 +27,4 @@ export * from './service/localstorage-manager';
 export * from './utils/basic-interceptor';
 export * from './utils/browser-utils';
 export * from './utils/mongoose-utils';
+export * from './utils/with-utils';

+ 3 - 1
packages/slackbot-proxy/src/controllers/slack.ts

@@ -1,5 +1,7 @@
 
 
+import { ServerResponse } from 'http';
+
 import {
   markdownSectionBlock, GrowiCommand, parseSlashCommand, respondRejectedErrors, generateWebClient,
   InvalidGrowiCommandError, requiredScopes, REQUEST_TIMEOUT_FOR_PTOG,
@@ -423,7 +425,7 @@ export class SlackCtrl {
   }
 
   @Get('/oauth_redirect')
-  async handleOauthRedirect(@Req() req: Req, @Res() serverRes: Res, @Res() platformRes: PlatformResponse): Promise<void|string> {
+  async handleOauthRedirect(@Req() req: Req, @Res() serverRes: ServerResponse, @Res() platformRes: PlatformResponse): Promise<void|string> {
 
     // create 'Add to Slack' url
     const addToSlackUrl = await this.installerService.installer.generateInstallUrl({

+ 361 - 40
yarn.lock

@@ -12,6 +12,14 @@
     loader-utils "^1.2.3"
     lodash "^4.17.15"
 
+"@ampproject/remapping@^2.1.0":
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
+  integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==
+  dependencies:
+    "@jridgewell/gen-mapping" "^0.1.0"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
 "@apidevtools/json-schema-ref-parser@^9.0.6":
   version "9.0.9"
   resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#d720f9256e3609621280584f2b47ae165359268b"
@@ -974,6 +982,13 @@
   dependencies:
     "@babel/highlight" "^7.14.5"
 
+"@babel/code-frame@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
+  integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
+  dependencies:
+    "@babel/highlight" "^7.18.6"
+
 "@babel/code-frame@^7.5.5":
   version "7.5.5"
   resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d"
@@ -993,6 +1008,11 @@
   resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.14.9.tgz#ac7996ceaafcf8f410119c8af0d1db4cf914a210"
   integrity sha512-p3QjZmMGHDGdpcwEYYWu7i7oJShJvtgMjJeb0W95PPhSm++3lm8YXYOh45Y6iCN9PkZLTZ7CIX5nFrp7pw7TXw==
 
+"@babel/compat-data@^7.18.8":
+  version "7.18.8"
+  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d"
+  integrity sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==
+
 "@babel/core@^7.1.0":
   version "7.4.5"
   resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.5.tgz#081f97e8ffca65a9b4b0fdc7e274e703f000c06a"
@@ -1013,6 +1033,27 @@
     semver "^5.4.1"
     source-map "^0.5.0"
 
+"@babel/core@^7.13.15":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.9.tgz#805461f967c77ff46c74ca0460ccf4fe933ddd59"
+  integrity sha512-1LIb1eL8APMy91/IMW+31ckrfBM4yCoLaVzoDhZUKSM4cu1L1nIidyxkCgzPAgrC5WEz36IPEr/eSeSF9pIn+g==
+  dependencies:
+    "@ampproject/remapping" "^2.1.0"
+    "@babel/code-frame" "^7.18.6"
+    "@babel/generator" "^7.18.9"
+    "@babel/helper-compilation-targets" "^7.18.9"
+    "@babel/helper-module-transforms" "^7.18.9"
+    "@babel/helpers" "^7.18.9"
+    "@babel/parser" "^7.18.9"
+    "@babel/template" "^7.18.6"
+    "@babel/traverse" "^7.18.9"
+    "@babel/types" "^7.18.9"
+    convert-source-map "^1.7.0"
+    debug "^4.1.0"
+    gensync "^1.0.0-beta.2"
+    json5 "^2.2.1"
+    semver "^6.3.0"
+
 "@babel/core@^7.7.2":
   version "7.14.8"
   resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.8.tgz#20cdf7c84b5d86d83fac8710a8bc605a7ba3f010"
@@ -1064,6 +1105,15 @@
     jsesc "^2.5.1"
     source-map "^0.5.0"
 
+"@babel/generator@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.9.tgz#68337e9ea8044d6ddc690fb29acae39359cca0a5"
+  integrity sha512-wt5Naw6lJrL1/SGkipMiFxJjtyczUWTP38deiP1PO60HsBjDeKk08CGC3S8iVuvf0FmTdgKwU1KIXzSKL1G0Ug==
+  dependencies:
+    "@babel/types" "^7.18.9"
+    "@jridgewell/gen-mapping" "^0.3.2"
+    jsesc "^2.5.1"
+
 "@babel/generator@^7.4.4":
   version "7.4.4"
   resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.4.tgz#174a215eb843fc392c7edcaabeaa873de6e8f041"
@@ -1105,6 +1155,21 @@
     browserslist "^4.16.6"
     semver "^6.3.0"
 
+"@babel/helper-compilation-targets@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz#69e64f57b524cde3e5ff6cc5a9f4a387ee5563bf"
+  integrity sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==
+  dependencies:
+    "@babel/compat-data" "^7.18.8"
+    "@babel/helper-validator-option" "^7.18.6"
+    browserslist "^4.20.2"
+    semver "^6.3.0"
+
+"@babel/helper-environment-visitor@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
+  integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
+
 "@babel/helper-function-name@^7.1.0":
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53"
@@ -1122,6 +1187,14 @@
     "@babel/template" "^7.14.5"
     "@babel/types" "^7.14.5"
 
+"@babel/helper-function-name@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz#940e6084a55dee867d33b4e487da2676365e86b0"
+  integrity sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==
+  dependencies:
+    "@babel/template" "^7.18.6"
+    "@babel/types" "^7.18.9"
+
 "@babel/helper-function-name@^7.7.4":
   version "7.7.4"
   resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz#ab6e041e7135d436d8f0a3eca15de5b67a341a2e"
@@ -1174,6 +1247,13 @@
   dependencies:
     "@babel/types" "^7.14.5"
 
+"@babel/helper-hoist-variables@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678"
+  integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==
+  dependencies:
+    "@babel/types" "^7.18.6"
+
 "@babel/helper-member-expression-to-functions@^7.14.5":
   version "7.14.7"
   resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.7.tgz#97e56244beb94211fe277bd818e3a329c66f7970"
@@ -1181,6 +1261,13 @@
   dependencies:
     "@babel/types" "^7.14.5"
 
+"@babel/helper-module-imports@^7.13.12", "@babel/helper-module-imports@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
+  integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
+  dependencies:
+    "@babel/types" "^7.18.6"
+
 "@babel/helper-module-imports@^7.14.5":
   version "7.14.5"
   resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz#6d1a44df6a38c957aa7c312da076429f11b422f3"
@@ -1202,6 +1289,20 @@
     "@babel/traverse" "^7.14.8"
     "@babel/types" "^7.14.8"
 
+"@babel/helper-module-transforms@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz#5a1079c005135ed627442df31a42887e80fcb712"
+  integrity sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==
+  dependencies:
+    "@babel/helper-environment-visitor" "^7.18.9"
+    "@babel/helper-module-imports" "^7.18.6"
+    "@babel/helper-simple-access" "^7.18.6"
+    "@babel/helper-split-export-declaration" "^7.18.6"
+    "@babel/helper-validator-identifier" "^7.18.6"
+    "@babel/template" "^7.18.6"
+    "@babel/traverse" "^7.18.9"
+    "@babel/types" "^7.18.9"
+
 "@babel/helper-optimise-call-expression@^7.14.5":
   version "7.14.5"
   resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz#f27395a8619e0665b3f0364cddb41c25d71b499c"
@@ -1219,6 +1320,11 @@
   resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9"
   integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==
 
+"@babel/helper-plugin-utils@^7.18.6":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz#4b8aea3b069d8cb8a72cdfe28ddf5ceca695ef2f"
+  integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w==
+
 "@babel/helper-plugin-utils@^7.8.0":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670"
@@ -1241,6 +1347,13 @@
   dependencies:
     "@babel/types" "^7.14.8"
 
+"@babel/helper-simple-access@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz#d6d8f51f4ac2978068df934b569f08f29788c7ea"
+  integrity sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==
+  dependencies:
+    "@babel/types" "^7.18.6"
+
 "@babel/helper-split-export-declaration@^7.14.5":
   version "7.14.5"
   resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz#22b23a54ef51c2b7605d851930c1976dd0bc693a"
@@ -1248,6 +1361,13 @@
   dependencies:
     "@babel/types" "^7.14.5"
 
+"@babel/helper-split-export-declaration@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075"
+  integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==
+  dependencies:
+    "@babel/types" "^7.18.6"
+
 "@babel/helper-split-export-declaration@^7.4.4":
   version "7.4.4"
   resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677"
@@ -1284,11 +1404,21 @@
   resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad"
   integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==
 
+"@babel/helper-validator-identifier@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
+  integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
+
 "@babel/helper-validator-option@^7.14.5":
   version "7.14.5"
   resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3"
   integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==
 
+"@babel/helper-validator-option@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8"
+  integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==
+
 "@babel/helpers@^7.14.8":
   version "7.14.8"
   resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.14.8.tgz#839f88f463025886cff7f85a35297007e2da1b77"
@@ -1298,6 +1428,15 @@
     "@babel/traverse" "^7.14.8"
     "@babel/types" "^7.14.8"
 
+"@babel/helpers@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.9.tgz#4bef3b893f253a1eced04516824ede94dcfe7ff9"
+  integrity sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==
+  dependencies:
+    "@babel/template" "^7.18.6"
+    "@babel/traverse" "^7.18.9"
+    "@babel/types" "^7.18.9"
+
 "@babel/helpers@^7.4.4":
   version "7.4.4"
   resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.4.4.tgz#868b0ef59c1dd4e78744562d5ce1b59c89f2f2a5"
@@ -1342,6 +1481,15 @@
     chalk "^2.0.0"
     js-tokens "^4.0.0"
 
+"@babel/highlight@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf"
+  integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.18.6"
+    chalk "^2.0.0"
+    js-tokens "^4.0.0"
+
 "@babel/highlight@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.3.tgz#28f173d04223eaaa59bc1d439a3836e6d1265797"
@@ -1361,6 +1509,11 @@
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.9.tgz#596c1ad67608070058ebf8df50c1eaf65db895a4"
   integrity sha512-RdUTOseXJ8POjjOeEBEvNMIZU/nm4yu2rufRkcibzkkg7DmQvXU8v3M4Xk9G7uuI86CDGkKcuDWgioqZm+mScQ==
 
+"@babel/parser@^7.18.6", "@babel/parser@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.9.tgz#f2dde0c682ccc264a9a8595efd030a5cc8fd2539"
+  integrity sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==
+
 "@babel/parser@^7.4.0":
   version "7.4.2"
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.2.tgz#b4521a400cb5a871eab3890787b4bc1326d38d91"
@@ -1410,6 +1563,13 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
+"@babel/plugin-syntax-jsx@^7.12.13":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0"
+  integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.18.6"
+
 "@babel/plugin-syntax-logical-assignment-operators@^7.8.3":
   version "7.10.4"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699"
@@ -1459,6 +1619,13 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.14.5"
 
+"@babel/plugin-syntax-typescript@^7.12.13":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz#1c09cd25795c7c2b8a4ba9ae49394576d4133285"
+  integrity sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.18.6"
+
 "@babel/plugin-syntax-typescript@^7.7.2":
   version "7.14.5"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.14.5.tgz#b82c6ce471b165b5ce420cf92914d6fb46225716"
@@ -1546,6 +1713,15 @@
     "@babel/parser" "^7.14.5"
     "@babel/types" "^7.14.5"
 
+"@babel/template@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.6.tgz#1283f4993e00b929d6e2d3c72fdc9168a2977a31"
+  integrity sha512-JoDWzPe+wgBsTTgdnIma3iHNFC7YVJoPssVBDjiHfNlyt4YcunDtcDOUmfVDfCK5MfdsaIoX9PkijPhjH3nYUw==
+  dependencies:
+    "@babel/code-frame" "^7.18.6"
+    "@babel/parser" "^7.18.6"
+    "@babel/types" "^7.18.6"
+
 "@babel/template@^7.4.4":
   version "7.4.4"
   resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237"
@@ -1603,6 +1779,22 @@
     debug "^4.1.0"
     globals "^11.1.0"
 
+"@babel/traverse@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.9.tgz#deeff3e8f1bad9786874cb2feda7a2d77a904f98"
+  integrity sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg==
+  dependencies:
+    "@babel/code-frame" "^7.18.6"
+    "@babel/generator" "^7.18.9"
+    "@babel/helper-environment-visitor" "^7.18.9"
+    "@babel/helper-function-name" "^7.18.9"
+    "@babel/helper-hoist-variables" "^7.18.6"
+    "@babel/helper-split-export-declaration" "^7.18.6"
+    "@babel/parser" "^7.18.9"
+    "@babel/types" "^7.18.9"
+    debug "^4.1.0"
+    globals "^11.1.0"
+
 "@babel/traverse@^7.7.4":
   version "7.7.4"
   resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.7.4.tgz#9c1e7c60fb679fe4fcfaa42500833333c2058558"
@@ -1641,6 +1833,14 @@
     lodash "^4.17.11"
     to-fast-properties "^2.0.0"
 
+"@babel/types@^7.13.17", "@babel/types@^7.18.6", "@babel/types@^7.18.9":
+  version "7.18.9"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.9.tgz#7148d64ba133d8d73a41b3172ac4b83a1452205f"
+  integrity sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.18.6"
+    to-fast-properties "^2.0.0"
+
 "@babel/types@^7.14.5", "@babel/types@^7.14.8", "@babel/types@^7.14.9", "@babel/types@^7.3.3":
   version "7.14.9"
   resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.9.tgz#f2b19c3f2f77c5708d67fe8f6046e9cea2b5036d"
@@ -2121,11 +2321,33 @@
     comment-json "^4.1.0"
     find-up "^4.1.0"
 
+"@jridgewell/gen-mapping@^0.1.0":
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996"
+  integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==
+  dependencies:
+    "@jridgewell/set-array" "^1.0.0"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+
+"@jridgewell/gen-mapping@^0.3.2":
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
+  integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
+  dependencies:
+    "@jridgewell/set-array" "^1.0.1"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
 "@jridgewell/resolve-uri@^3.0.3":
   version "3.0.8"
   resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.8.tgz#687cc2bbf243f4e9a868ecf2262318e2658873a1"
   integrity sha512-YK5G9LaddzGbcucK4c8h5tWFmMPBvRZ/uyWmN1/SbBdIvqGUdWGkJ5BAaccgs6XbzVLsqbPJrBSFwKv3kT9i7w==
 
+"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
+  integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
+
 "@jridgewell/sourcemap-codec@^1.4.10":
   version "1.4.14"
   resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
@@ -2139,6 +2361,14 @@
     "@jridgewell/resolve-uri" "^3.0.3"
     "@jridgewell/sourcemap-codec" "^1.4.10"
 
+"@jridgewell/trace-mapping@^0.3.9":
+  version "0.3.14"
+  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed"
+  integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==
+  dependencies:
+    "@jridgewell/resolve-uri" "^3.0.3"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+
 "@jsdevtools/ono@^7.1.3":
   version "7.1.3"
   resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796"
@@ -4054,16 +4284,16 @@
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
   integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
 
+"@types/json-schema@^7.0.5", "@types/json-schema@^7.0.9":
+  version "7.0.11"
+  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
+  integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
+
 "@types/json-schema@^7.0.6":
   version "7.0.9"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
   integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==
 
-"@types/json-schema@^7.0.9":
-  version "7.0.11"
-  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
-  integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
-
 "@types/json5@^0.0.29":
   version "0.0.29"
   resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
@@ -4640,6 +4870,11 @@ aggregate-error@^3.0.0:
     clean-stack "^2.0.0"
     indent-string "^4.0.0"
 
+ajv-keywords@^3.5.2:
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
+  integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
+
 ajv@^5.5.2:
   version "5.5.2"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
@@ -5259,6 +5494,16 @@ babel-jest@^27.0.6:
     graceful-fs "^4.2.4"
     slash "^3.0.0"
 
+babel-loader@^8.2.2, babel-loader@^8.2.5:
+  version "8.2.5"
+  resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.5.tgz#d45f585e654d5a5d90f5350a779d7647c5ed512e"
+  integrity sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==
+  dependencies:
+    find-cache-dir "^3.3.1"
+    loader-utils "^2.0.0"
+    make-dir "^3.1.0"
+    schema-utils "^2.6.5"
+
 babel-plugin-istanbul@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz#e159ccdc9af95e0b570c75b4573b7c34d671d765"
@@ -5280,6 +5525,15 @@ babel-plugin-jest-hoist@^27.0.6:
     "@types/babel__core" "^7.0.0"
     "@types/babel__traverse" "^7.0.6"
 
+babel-plugin-superjson-next@^0.4.2:
+  version "0.4.3"
+  resolved "https://registry.yarnpkg.com/babel-plugin-superjson-next/-/babel-plugin-superjson-next-0.4.3.tgz#3d35d36443ad1e4fe4529a4567f264391d2a93fd"
+  integrity sha512-wfPsTPnEn1YaEkmaoomN4Z/Hm6nWVWFkASdetP/Ju2aPz/8XguAJwuThZIBH9dMTtXnyGXnbJufPQ0wRha0QcA==
+  dependencies:
+    "@babel/helper-module-imports" "^7.13.12"
+    "@babel/types" "^7.13.17"
+    hoist-non-react-statics "^3.3.2"
+
 babel-preset-current-node-syntax@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b"
@@ -5720,6 +5974,16 @@ browserslist@^4.16.6:
     node-releases "^2.0.1"
     picocolors "^1.0.0"
 
+browserslist@^4.20.2:
+  version "4.21.2"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.2.tgz#59a400757465535954946a400b841ed37e2b4ecf"
+  integrity sha512-MonuOgAtUB46uP5CezYbRaYKBNt2LxP0yX+Pmj4LkcDFGkn9Cbpi83d9sCjwQDErXsIJSzY5oKGDbgOlF/LPAA==
+  dependencies:
+    caniuse-lite "^1.0.30001366"
+    electron-to-chromium "^1.4.188"
+    node-releases "^2.0.6"
+    update-browserslist-db "^1.0.4"
+
 bs-logger@0.x:
   version "0.2.6"
   resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8"
@@ -6034,6 +6298,11 @@ caniuse-lite@^1.0.30001332:
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001354.tgz#95c5efdb64148bb4870771749b9a619304755ce5"
   integrity sha512-mImKeCkyGDAHNywYFA4bqnLAzTUvVkqPvhY4DV47X+Gl2c5Z8c3KNETnXp14GQt11LvxE8AwjzGxJ+rsikiOzg==
 
+caniuse-lite@^1.0.30001366:
+  version "1.0.30001367"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001367.tgz#2b97fe472e8fa29c78c5970615d7cd2ee414108a"
+  integrity sha512-XDgbeOHfifWV3GEES2B8rtsrADx4Jf+juKX2SICJcaUhjYBO3bR96kvEIHa15VU6ohtOhBZuPGGYGbXMRn0NCw==
+
 capital-case@^1.0.3, capital-case@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669"
@@ -6641,6 +6910,11 @@ common-tags@^1.8.0:
   resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6"
   integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==
 
+commondir@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+  integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
+
 compare-func@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-2.0.0.tgz#fb65e75edbddfd2e568554e8b5b05fff7a51fcb3"
@@ -7009,6 +7283,13 @@ copy-anything@^2.0.1:
   dependencies:
     is-what "^3.14.1"
 
+copy-anything@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.2.tgz#7189171ff5e1893b2287e8bf574b8cd448ed50b1"
+  integrity sha512-CzATjGXzUQ0EvuvgOCI6A4BGOo2bcVx8B+eC2nF862iv9fopnPQwlrbACakNCHRIJbCSBj+J/9JeDf60k64MkA==
+  dependencies:
+    is-what "^4.1.6"
+
 copy-descriptor@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
@@ -7966,6 +8247,11 @@ electron-to-chromium@^1.3.878:
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.884.tgz#0cd8c3a80271fd84a81f284c60fb3c9ecb33c166"
   integrity sha512-kOaCAa+biA98PwH5BpCkeUeTL6mCeg8p3Q3OhqzPyqhu/5QUnWAN2wr/3IK8xMQxIV76kfoQpP+Bn/wij/jXrg==
 
+electron-to-chromium@^1.4.188:
+  version "1.4.195"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.195.tgz#139b2d95a42a3f17df217589723a1deac71d1473"
+  integrity sha512-vefjEh0sk871xNmR5whJf9TEngX+KTKS3hOHpjoMpauKkwlGwtMz1H8IaIjAT/GNnX0TbGwAdmVoXCAzXf+PPg==
+
 emittery@^0.8.1:
   version "0.8.1"
   resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860"
@@ -9400,6 +9686,15 @@ finalhandler@~1.1.2:
     statuses "~1.5.0"
     unpipe "~1.0.0"
 
+find-cache-dir@^3.3.1:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b"
+  integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==
+  dependencies:
+    commondir "^1.0.1"
+    make-dir "^3.0.2"
+    pkg-dir "^4.1.0"
+
 find-index@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4"
@@ -11699,6 +11994,11 @@ is-what@^3.14.1:
   resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1"
   integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==
 
+is-what@^4.1.6:
+  version "4.1.7"
+  resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.7.tgz#c41dc1d2d2d6a9285c624c2505f61849c8b1f9cc"
+  integrity sha512-DBVOQNiPKnGMxRMLIYSwERAS5MVY1B7xYiGnpgctsOFvVDz9f9PFXXxMcTOHuoqYp4NK9qFYQaIC1NRRxLMpBQ==
+
 is-whitespace-character@^1.0.0:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.3.tgz#b3ad9546d916d7d3ffa78204bca0c26b56257fac"
@@ -12472,6 +12772,11 @@ json5@^2.1.0:
   dependencies:
     minimist "^1.2.0"
 
+json5@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
+  integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
+
 jsonfile@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66"
@@ -12903,6 +13208,15 @@ loader-utils@^1.2.3:
     emojis-list "^3.0.0"
     json5 "^1.0.1"
 
+loader-utils@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129"
+  integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==
+  dependencies:
+    big.js "^5.2.2"
+    emojis-list "^3.0.0"
+    json5 "^2.1.2"
+
 localtunnel@^2.0.1:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/localtunnel/-/localtunnel-2.0.2.tgz#528d50087151c4790f89c2db374fe7b0a48501f0"
@@ -13201,7 +13515,7 @@ macos-release@^2.2.0:
   resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.5.0.tgz#067c2c88b5f3fb3c56a375b2ec93826220fa1ff2"
   integrity sha512-EIgv+QZ9r+814gjJj0Bt5vSLJLzswGmSUbUpbi9AIr/fsN2IWFBl2NucV9PAiek+U1STK468tEkxmVYUtuAN3g==
 
-make-dir@3.1.0, make-dir@^3.1.0:
+make-dir@3.1.0, make-dir@^3.0.2, make-dir@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
   integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
@@ -13413,15 +13727,6 @@ mdast-util-footnote@^0.1.0:
     mdast-util-to-markdown "^0.6.0"
     micromark "~2.11.0"
 
-mdast-util-footnote@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/mdast-util-footnote/-/mdast-util-footnote-1.1.0.tgz#c81e583e2de6a3dd4e05a59e83639fb67b0c5686"
-  integrity sha512-M8tHiqL+7eS/ASWWs+nnmMKRLO1A1JnllNRzF2gLT4gpzLGvRiE0IugPI+/zfpi93IrtrxmlFolTdRPtWZocaA==
-  dependencies:
-    "@types/mdast" "^3.0.0"
-    mdast-util-to-markdown "^1.0.0"
-    micromark-util-normalize-identifier "^1.0.0"
-
 mdast-util-from-markdown@^0.8.0:
   version "0.8.5"
   resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz#d1ef2ca42bc377ecb0463a987910dae89bd9a28c"
@@ -13774,20 +14079,6 @@ micromark-extension-footnote@^0.3.0:
   dependencies:
     micromark "~2.11.0"
 
-micromark-extension-footnote@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/micromark-extension-footnote/-/micromark-extension-footnote-1.0.2.tgz#534c42211315406b43625fa29573c7094d5122c2"
-  integrity sha512-sD7t/hooONLnbPYgcQpBcTjh7mPEcD2R/jRS5DLYDNm0XbngbaJZ01F1aI3v6VXVMdAu3XtFKUecNRlXbgGk/Q==
-  dependencies:
-    micromark-core-commonmark "^1.0.0"
-    micromark-factory-space "^1.0.0"
-    micromark-util-character "^1.0.0"
-    micromark-util-chunked "^1.0.0"
-    micromark-util-normalize-identifier "^1.0.0"
-    micromark-util-resolve-all "^1.0.0"
-    micromark-util-symbol "^1.0.0"
-    uvu "^0.5.0"
-
 micromark-extension-frontmatter@^0.2.0:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/micromark-extension-frontmatter/-/micromark-extension-frontmatter-0.2.2.tgz#61b8e92e9213e1d3c13f5a59e7862f5ca98dfa53"
@@ -14831,6 +15122,17 @@ next-i18next@^11.0.0:
     i18next-fs-backend "^1.1.4"
     react-i18next "^11.16.2"
 
+next-superjson@^0.0.4:
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/next-superjson/-/next-superjson-0.0.4.tgz#273c41482cc3d2914cba6c05230ca473456a1cea"
+  integrity sha512-PYtoHbPcZYED8Vm9YCIQIZi/arANNnf6grwjkPuJXzWdY1TxJxrn9dCPmVj6ALvPn9YcDThwEA9WvHq/NyzMvw==
+  dependencies:
+    "@babel/core" "^7.13.15"
+    "@babel/plugin-syntax-jsx" "^7.12.13"
+    "@babel/plugin-syntax-typescript" "^7.12.13"
+    babel-loader "^8.2.2"
+    babel-plugin-superjson-next "^0.4.2"
+
 next-themes@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.2.0.tgz#fdc507f61e95b3ae513dee8d4783bcec8c02e3a3"
@@ -15028,6 +15330,11 @@ node-releases@^2.0.1:
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5"
   integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==
 
+node-releases@^2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
+  integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==
+
 nodemailer-ses-transport@~1.5.0:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/nodemailer-ses-transport/-/nodemailer-ses-transport-1.5.1.tgz#dc0598c1bf53e8652e632e8f31692ce022d7dea9"
@@ -16283,7 +16590,7 @@ pixelmatch@^5.2.1:
   dependencies:
     pngjs "^4.0.1"
 
-pkg-dir@^4.2.0:
+pkg-dir@^4.1.0, pkg-dir@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
   integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
@@ -17704,16 +18011,6 @@ remark-footnotes@^3.0.0:
     mdast-util-footnote "^0.1.0"
     micromark-extension-footnote "^0.3.0"
 
-remark-footnotes@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/remark-footnotes/-/remark-footnotes-4.0.1.tgz#17d3c87ddc240924e8b4590b34a3d79aa8ede50c"
-  integrity sha512-He6YzQFk/Wu2KgfjI80EyPXjt/G+WFaYfUH+xapqPQBdm3aTdEyzosXXv9a2FbTxGqgOfJ4q/TCB46v+wofRpQ==
-  dependencies:
-    "@types/mdast" "^3.0.0"
-    mdast-util-footnote "^1.0.0"
-    micromark-extension-footnote "^1.0.0"
-    unified "^10.0.0"
-
 remark-frontmatter@^1.3.3:
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/remark-frontmatter/-/remark-frontmatter-1.3.3.tgz#67ec63c89da5a84bb793ecec166e11b4eb47af10"
@@ -18222,6 +18519,15 @@ scheduler@^0.23.0:
   dependencies:
     loose-envify "^1.1.0"
 
+schema-utils@^2.6.5:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
+  integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
+  dependencies:
+    "@types/json-schema" "^7.0.5"
+    ajv "^6.12.4"
+    ajv-keywords "^3.5.2"
+
 scroll-into-view-if-needed@^2.2.20:
   version "2.2.29"
   resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz#551791a84b7e2287706511f8c68161e4990ab885"
@@ -19594,6 +19900,13 @@ subarg@^1.0.0:
   dependencies:
     minimist "^1.1.0"
 
+superjson@^1.9.1:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/superjson/-/superjson-1.9.1.tgz#e23bd2e8cf0f4ade131d6d769754cac7eaa8ab34"
+  integrity sha512-oT3HA2nPKlU1+5taFgz/HDy+GEaY+CWEbLzaRJVD4gZ7zMVVC4GDNFdgvAZt6/VuIk6D2R7RtPAiCHwmdzlMmg==
+  dependencies:
+    copy-anything "^3.0.2"
+
 supports-color@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@@ -21102,6 +21415,14 @@ upath@^2.0.1:
   resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b"
   integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==
 
+update-browserslist-db@^1.0.4:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz#be06a5eedd62f107b7c19eb5bcefb194411abf38"
+  integrity sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==
+  dependencies:
+    escalade "^3.1.1"
+    picocolors "^1.0.0"
+
 upper-case-first@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-2.0.2.tgz#992c3273f882abd19d1e02894cc147117f844324"