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

Merge pull request #4503 from weseek/master

Release v4.4.10
Yuki Takei 4 лет назад
Родитель
Сommit
ed2f38cac3
40 измененных файлов с 652 добавлено и 631 удалено
  1. 12 9
      .devcontainer/devcontainer.json
  2. 0 4
      .devcontainer/docker-compose.yml
  3. 1 1
      lerna.json
  4. 1 1
      package.json
  5. 13 12
      packages/app/package.json
  6. 8 3
      packages/app/src/client/app.jsx
  7. 21 97
      packages/app/src/client/services/AppContainer.js
  8. 1 1
      packages/app/src/client/services/PageContainer.js
  9. 63 0
      packages/app/src/client/util/apiv1-client.ts
  10. 0 12
      packages/app/src/client/util/apiv1ErrorHandler.js
  11. 74 0
      packages/app/src/client/util/apiv3-client.ts
  12. 0 21
      packages/app/src/client/util/apiv3ErrorHandler.js
  13. 2 2
      packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx
  14. 8 1
      packages/app/src/components/PageEditor/DrawioModal.jsx
  15. 17 25
      packages/app/src/components/PageList.jsx
  16. 2 2
      packages/app/src/components/Sidebar/CustomSidebar.jsx
  17. 79 18
      packages/app/src/components/Sidebar/RecentChanges.jsx
  18. 7 0
      packages/app/src/interfaces/page-tag-relation.ts
  19. 14 0
      packages/app/src/interfaces/page.ts
  20. 5 0
      packages/app/src/interfaces/paging-result.ts
  21. 9 0
      packages/app/src/interfaces/revision.ts
  22. 4 0
      packages/app/src/interfaces/tag.ts
  23. 18 0
      packages/app/src/interfaces/user.ts
  24. 78 7
      packages/app/src/server/routes/apiv3/page.js
  25. 33 0
      packages/app/src/stores/page.tsx
  26. 4 1
      packages/app/src/styles/_layout.scss
  27. 1 5
      packages/app/src/styles/_recent-changes.scss
  28. 3 1
      packages/app/src/styles/_sidebar.scss
  29. 0 1
      packages/app/src/styles/_variables.scss
  30. 3 7
      packages/app/src/styles/theme/_apply-colors.scss
  31. 9 0
      packages/app/src/utils/swr-utils.ts
  32. 1 1
      packages/codemirror-textlint/package.json
  33. 1 1
      packages/core/package.json
  34. 1 1
      packages/plugin-attachment-refs/package.json
  35. 1 1
      packages/plugin-lsx/package.json
  36. 2 2
      packages/plugin-pukiwiki-like-linker/package.json
  37. 1 1
      packages/slack/package.json
  38. 2 2
      packages/slackbot-proxy/package.json
  39. 1 1
      packages/ui/package.json
  40. 152 390
      yarn.lock

+ 12 - 9
.devcontainer/devcontainer.json

@@ -9,20 +9,23 @@
 
 	// Set *default* container specific settings.json values on container create.
 	"settings": {
-		"terminal.integrated.shell.linux": "/bin/bash"
+		"terminal.integrated.defaultProfile.linux": "bash"
 	},
 
 	// Add the IDs of extensions you want installed when the container is created.
 	"extensions": [
-		"dbaeumer.vscode-eslint",
-		"eamodio.gitlens",
+    "dbaeumer.vscode-eslint",
+    "mhutchie.git-graph",
+    "eamodio.gitlens",
+    "github.vscode-pull-request-github",
+    "cschleiden.vscode-github-actions",
     "firsttris.vscode-jest-runner",
-		"msjsdiag.debugger-for-chrome",
-		"firefox-devtools.vscode-firefox-debug",
-		"editorconfig.editorconfig",
-		"esbenp.prettier-vscode",
-		"shinnn.stylelint",
-		"hex-ci.stylelint-plus",
+    "msjsdiag.debugger-for-chrome",
+    "firefox-devtools.vscode-firefox-debug",
+    "editorconfig.editorconfig",
+    "esbenp.prettier-vscode",
+    "shinnn.stylelint",
+    "hex-ci.stylelint-plus",
 	],
 
 	// Uncomment the next line if you want start specific services in your Docker Compose config.

+ 0 - 4
.devcontainer/docker-compose.yml

@@ -17,10 +17,6 @@ services:
       context: .
       dockerfile: Dockerfile
 
-    ports:
-      - 3000:3000
-      - 3001:3001 # for browser-sync
-
     volumes:
       - ..:/workspace/growi:delegated
       - node_modules:/workspace/growi/node_modules

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

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

+ 13 - 12
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "4.4.9",
+  "version": "4.4.10-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -33,7 +33,7 @@
     "predev:ci": "run-p resources:*",
     "lint:typecheck": "npx tsc",
     "lint:eslint": "eslint --quiet \"**/*.{js,jsx,ts,tsx}\"",
-    "lint:styles": "stylelint src/**/*.scss",
+    "lint:styles": "stylelint src/**/*.scss --custom-syntax postcss-scss",
     "lint:swagger2openapi": "node node_modules/.bin/oas-validate tmp/swagger.json",
     "lint": "run-p lint:*",
     "test": "cross-env NODE_ENV=test jest --passWithNoTests -- ",
@@ -57,11 +57,11 @@
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^4.4.9",
-    "@growi/plugin-attachment-refs": "^4.4.9",
-    "@growi/plugin-pukiwiki-like-linker": "^4.4.9",
-    "@growi/plugin-lsx": "^4.4.9",
-    "@growi/slack": "^4.4.9",
+    "@growi/codemirror-textlint": "^4.4.10-RC.0",
+    "@growi/plugin-attachment-refs": "^4.4.10-RC.0",
+    "@growi/plugin-lsx": "^4.4.10-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^4.4.10-RC.0",
+    "@growi/slack": "^4.4.10-RC.0",
     "@promster/express": "^5.1.0",
     "@promster/server": "^6.0.3",
     "@slack/events-api": "^3.0.0",
@@ -125,7 +125,7 @@
     "passport-github": "^1.1.0",
     "passport-google-oauth20": "^2.0.0",
     "passport-http": "^0.3.0",
-    "passport-ldapauth": "^2.0.0",
+    "passport-ldapauth": "^3.0.1",
     "passport-local": "^1.0.0",
     "passport-saml": "^2.2.0",
     "passport-twitter": "^1.0.4",
@@ -157,7 +157,7 @@
     "@alienfast/i18next-loader": "^1.0.16",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/navigation-next": "^8.0.5",
-    "@growi/ui": "^4.4.9",
+    "@growi/ui": "^4.4.10-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
@@ -201,7 +201,6 @@
     "mini-css-extract-plugin": "^0.9.0",
     "morgan": "^1.10.0",
     "node-dev": "^4.0.0",
-    "node-sass": "^4.14.1",
     "normalize-path": "^3.0.0",
     "null-loader": "^3.0.0",
     "on-headers": "^1.0.1",
@@ -223,15 +222,17 @@
     "reactstrap": "^8.9.0",
     "replacestream": "^4.0.3",
     "reveal.js": "^3.5.0",
-    "sass-loader": "^8.0.0",
+    "sass": "^1.43.4",
+    "sass-loader": "^10.1.1",
     "simple-load-script": "^1.0.2",
     "socket.io-client": "^4.2.0",
     "sticky-events": "^3.1.3",
     "style-loader": "^1.0.0",
     "styled-components": "^5.0.1",
-    "stylelint": "^13.2.0",
+    "stylelint": "^14.0.1",
     "stylelint-config-recess-order": "^2.0.1",
     "swagger2openapi": "^5.3.1",
+    "swr": "^1.0.1",
     "terser-webpack-plugin": "^4.1.0",
     "throttle-debounce": "^2.0.0",
     "toastr": "^2.1.2",

+ 8 - 3
packages/app/src/client/app.jsx

@@ -3,7 +3,10 @@ import ReactDOM from 'react-dom';
 import { Provider } from 'unstated';
 import { I18nextProvider } from 'react-i18next';
 
+import { SWRConfig } from 'swr';
+
 import loggerFactory from '~/utils/logger';
+import { swrGlobalConfiguration } from '~/utils/swr-utils';
 
 import ErrorBoundary from '../components/ErrorBoudary';
 import Sidebar from '../components/Sidebar';
@@ -156,9 +159,11 @@ Object.keys(componentMappings).forEach((key) => {
     ReactDOM.render(
       <I18nextProvider i18n={i18n}>
         <ErrorBoundary>
-          <Provider inject={injectableContainers}>
-            {componentMappings[key]}
-          </Provider>
+          <SWRConfig value={swrGlobalConfiguration}>
+            <Provider inject={injectableContainers}>
+              {componentMappings[key]}
+            </Provider>
+          </SWRConfig>
         </ErrorBoundary>
       </I18nextProvider>,
       elem,

+ 21 - 97
packages/app/src/client/services/AppContainer.js

@@ -1,10 +1,13 @@
 import { Container } from 'unstated';
 
-import urljoin from 'url-join';
-
-import axios from '~/utils/axios';
 import InterceptorManager from '~/services/interceptor-manager';
 
+import {
+  apiDelete, apiGet, apiPost, apiRequest,
+} from '../util/apiv1-client';
+import {
+  apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
+} from '../util/apiv3-client';
 import emojiStrategy from '../util/emojione/emoji_strategy_shrinked.json';
 import GrowiRenderer from '../util/GrowiRenderer';
 
@@ -12,10 +15,8 @@ import {
   mediaQueryListForDarkMode,
   applyColorScheme,
 } from '../util/color-scheme';
-import Apiv1ErrorHandler from '../util/apiv1ErrorHandler';
 
 import { i18nFactory } from '../util/i18n';
-import apiv3ErrorHandler from '../util/apiv3ErrorHandler';
 
 /**
  * Service container related to options for Application
@@ -28,13 +29,11 @@ export default class AppContainer extends Container {
 
     this.state = {
       preferDarkModeByMediaQuery: false,
-
-      // stetes for contents
-      recentlyUpdatedPages: [],
     };
 
+    // get csrf token from body element
+    // DO NOT REMOVE: uploading attachment data requires appContainer.csrfToken
     const body = document.querySelector('body');
-
     this.csrfToken = body.dataset.csrftoken;
 
     this.config = JSON.parse(document.getElementById('growi-context-hydrate').textContent || '{}');
@@ -60,17 +59,21 @@ export default class AppContainer extends Container {
     this.componentInstances = {};
     this.rendererInstances = {};
 
-    this.apiGet = this.apiGet.bind(this);
-    this.apiPost = this.apiPost.bind(this);
-    this.apiDelete = this.apiDelete.bind(this);
-    this.apiRequest = this.apiRequest.bind(this);
+    this.apiGet = apiGet;
+    this.apiPost = apiPost;
+    this.apiDelete = apiDelete;
+    this.apiRequest = apiRequest;
+
+    this.apiv3Get = apiv3Get;
+    this.apiv3Post = apiv3Post;
+    this.apiv3Put = apiv3Put;
+    this.apiv3Delete = apiv3Delete;
 
-    this.apiv3Root = '/_api/v3';
     this.apiv3 = {
-      get: this.apiv3Get.bind(this),
-      post: this.apiv3Post.bind(this),
-      put: this.apiv3Put.bind(this),
-      delete: this.apiv3Delete.bind(this),
+      get: apiv3Get,
+      post: apiv3Post,
+      put: apiv3Put,
+      delete: apiv3Delete,
     };
   }
 
@@ -279,11 +282,6 @@ export default class AppContainer extends Container {
     });
   }
 
-  async retrieveRecentlyUpdated() {
-    const { data } = await this.apiv3Get('/pages/recent');
-    this.setState({ recentlyUpdatedPages: data.pages });
-  }
-
   launchHandsontableModal(componentKind, beginLineNumber, endLineNumber) {
     let targetComponent;
     switch (componentKind) {
@@ -304,78 +302,4 @@ export default class AppContainer extends Container {
     targetComponent.launchDrawioModal(beginLineNumber, endLineNumber);
   }
 
-  async apiGet(path, params) {
-    return this.apiRequest('get', path, { params });
-  }
-
-  async apiPost(path, params) {
-    if (!params._csrf) {
-      params._csrf = this.csrfToken;
-    }
-
-    return this.apiRequest('post', path, params);
-  }
-
-  async apiDelete(path, params) {
-    if (!params._csrf) {
-      params._csrf = this.csrfToken;
-    }
-
-    return this.apiRequest('delete', path, { data: params });
-  }
-
-  async apiRequest(method, path, params) {
-    const res = await axios[method](`/_api${path}`, params);
-    if (res.data.ok) {
-      return res.data;
-    }
-
-    // Return error code if code is exist
-    if (res.data.code != null) {
-      const error = new Apiv1ErrorHandler(res.data.error, res.data.code);
-      throw error;
-    }
-
-    throw new Error(res.data.error);
-  }
-
-  async apiv3Request(method, path, params) {
-    try {
-      const res = await axios[method](urljoin(this.apiv3Root, path), params);
-      return res.data;
-    }
-    catch (err) {
-      const errors = apiv3ErrorHandler(err);
-      throw errors;
-    }
-  }
-
-  async apiv3Get(path, params) {
-    return this.apiv3Request('get', path, { params });
-  }
-
-  async apiv3Post(path, params = {}) {
-    if (!params._csrf) {
-      params._csrf = this.csrfToken;
-    }
-
-    return this.apiv3Request('post', path, params);
-  }
-
-  async apiv3Put(path, params = {}) {
-    if (!params._csrf) {
-      params._csrf = this.csrfToken;
-    }
-
-    return this.apiv3Request('put', path, params);
-  }
-
-  async apiv3Delete(path, params = {}) {
-    if (!params._csrf) {
-      params._csrf = this.csrfToken;
-    }
-
-    return this.apiv3Request('delete', path, { params });
-  }
-
 }

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

@@ -277,7 +277,7 @@ export default class PageContainer extends Container {
         data: {
           likerIds, sumOfLikers, isLiked, seenUserIds, sumOfSeenUsers, isSeen,
         },
-      } = await this.appContainer.apiv3Get('/page/info', { _id: this.state.pageId });
+      } = await this.appContainer.apiv3Get('/page/info', { pageId: this.state.pageId });
 
       await this.setState({
         sumOfLikers,

+ 63 - 0
packages/app/src/client/util/apiv1-client.ts

@@ -0,0 +1,63 @@
+import * as urljoin from 'url-join';
+
+import axios from '~/utils/axios';
+
+const apiv1Root = '/_api';
+
+// get csrf token from body element
+const body = document.querySelector('body');
+const csrfToken = body?.dataset.csrftoken;
+
+
+type ParamWithCsrfKey = {
+  _csrf: string,
+}
+
+class Apiv1ErrorHandler extends Error {
+
+  code;
+
+  constructor(message = '', code = '') {
+    super();
+
+    this.message = message;
+    this.code = code;
+  }
+
+}
+
+export async function apiRequest(method: string, path: string, params: unknown): Promise<unknown> {
+  const res = await axios[method](urljoin(apiv1Root, path), params);
+
+  if (res.data.ok) {
+    return res.data;
+  }
+
+  // Return error code if code is exist
+  if (res.data.code != null) {
+    const error = new Apiv1ErrorHandler(res.data.error, res.data.code);
+    throw error;
+  }
+
+  throw new Error(res.data.error);
+}
+
+export async function apiGet(path: string, params: unknown = {}): Promise<unknown> {
+  return apiRequest('get', path, { params });
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiPost(path: string, params: any & ParamWithCsrfKey = {}): Promise<unknown> {
+  if (params._csrf == null) {
+    params._csrf = csrfToken;
+  }
+  return apiRequest('post', path, params);
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiDelete(path: string, params: any & ParamWithCsrfKey = {}): Promise<unknown> {
+  if (params._csrf == null) {
+    params._csrf = csrfToken;
+  }
+  return apiRequest('delete', path, { data: params });
+}

+ 0 - 12
packages/app/src/client/util/apiv1ErrorHandler.js

@@ -1,12 +0,0 @@
-class Apiv1ErrorHandler extends Error {
-
-  constructor(message = '', code = '') {
-    super();
-
-    this.message = message;
-    this.code = code;
-  }
-
-}
-
-module.exports = Apiv1ErrorHandler;

+ 74 - 0
packages/app/src/client/util/apiv3-client.ts

@@ -0,0 +1,74 @@
+import * as urljoin from 'url-join';
+
+// eslint-disable-next-line no-restricted-imports
+import { AxiosResponse } from 'axios';
+
+import loggerFactory from '~/utils/logger';
+import axios from '~/utils/axios';
+import { toArrayIfNot } from '~/utils/array-utils';
+
+const apiv3Root = '/_api/v3';
+
+const logger = loggerFactory('growi:apiv3');
+
+// get csrf token from body element
+const body = document.querySelector('body');
+const csrfToken = body?.dataset.csrftoken;
+
+
+type ParamWithCsrfKey = {
+  _csrf: string,
+}
+
+const apiv3ErrorHandler = (_err) => {
+  // extract api errors from general 400 err
+  const err = _err.response ? _err.response.data.errors : _err;
+  const errs = toArrayIfNot(err);
+
+  for (const err of errs) {
+    logger.error(err.message);
+  }
+
+  return errs;
+};
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiv3Request<T = any>(method: string, path: string, params: unknown): Promise<AxiosResponse<T>> {
+  try {
+    const res = await axios[method](urljoin(apiv3Root, path), params);
+    return res.data;
+  }
+  catch (err) {
+    const errors = apiv3ErrorHandler(err);
+    throw errors;
+  }
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiv3Get<T = any>(path: string, params: unknown = {}): Promise<AxiosResponse<T>> {
+  return apiv3Request('get', path, { params });
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiv3Post<T = any>(path: string, params: any & ParamWithCsrfKey = {}): Promise<AxiosResponse<T>> {
+  if (params._csrf == null) {
+    params._csrf = csrfToken;
+  }
+  return apiv3Request('post', path, params);
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiv3Put<T = any>(path: string, params: any & ParamWithCsrfKey = {}): Promise<AxiosResponse<T>> {
+  if (params._csrf == null) {
+    params._csrf = csrfToken;
+  }
+  return apiv3Request('put', path, params);
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiv3Delete<T = any>(path: string, params: any & ParamWithCsrfKey = {}): Promise<AxiosResponse<T>> {
+  if (params._csrf == null) {
+    params._csrf = csrfToken;
+  }
+  return apiv3Request('delete', path, { params });
+}

+ 0 - 21
packages/app/src/client/util/apiv3ErrorHandler.js

@@ -1,21 +0,0 @@
-// API v3 sends an array of errors in res.data.errors.
-// API v3 errors need to extracted from an error object in order to properly handle them.
-
-import loggerFactory from '~/utils/logger';
-import { toArrayIfNot } from '~/utils/array-utils';
-
-const logger = loggerFactory('growi:apiv3');
-
-const apiv3ErrorHandler = (_err, header = 'Error') => {
-  // extract api errors from general 400 err
-  const err = _err.response ? _err.response.data.errors : _err;
-  const errs = toArrayIfNot(err);
-
-  for (const err of errs) {
-    logger.error(err.message);
-  }
-
-  return errs;
-};
-
-export default apiv3ErrorHandler;

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

@@ -26,8 +26,6 @@ class CustomizeThemeOptions extends React.Component {
       name: 'fire-red',  bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#EA5532',
     }, {
       name: 'jade-green',  bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#38B48B',
-    }, {
-      name: 'blackboard',  bg: '#223729', topbar: '#563E23', sidebar: '#7B5932', theme: '#DA8506',
     }];
 
     const uniqueTheme = [{
@@ -48,6 +46,8 @@ class CustomizeThemeOptions extends React.Component {
       name: 'halloween',  bg: '#030003', topbar: '#aa4a04', sidebar: '#162b33', theme: '#e9af2b',
     }, {
       name: 'kibela',  bg: '#f4f5f6', topbar: '#1256a3', sidebar: '#5882fa', theme: '#b5cbf79c',
+    }, {
+      name: 'blackboard',  bg: '#223729', topbar: '#563E23', sidebar: '#7B5932', theme: '#DA8506',
     }];
     /* eslint-enable no-multi-spaces */
 

+ 8 - 1
packages/app/src/components/PageEditor/DrawioModal.jsx

@@ -135,7 +135,14 @@ class DrawioModal extends React.PureComponent {
 
   render() {
     return (
-      <Modal isOpen={this.state.show} toggle={this.cancel} className="drawio-modal" size="xl" keyboard={false}>
+      <Modal
+        isOpen={this.state.show}
+        toggle={this.cancel}
+        backdrop="static"
+        className="drawio-modal"
+        size="xl"
+        keyboard={false}
+      >
         <ModalBody className="p-0">
           {/* Loading spinner */}
           <div className="w-100 h-100 position-absolute d-flex">

+ 17 - 25
packages/app/src/components/PageList.jsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useCallback, useState } from 'react';
+import React, { useState } from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
@@ -8,39 +8,33 @@ import { withUnstatedContainers } from './UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 
+import { useSWRxPageList } from '~/stores/page';
+
 import PaginationWrapper from './PaginationWrapper';
 
 
 const PageList = (props) => {
   const { appContainer, pageContainer, t } = props;
   const { path } = pageContainer.state;
-  const [pages, setPages] = useState(null);
-  const [isLoading, setIsLoading] = useState(true);
 
   const [activePage, setActivePage] = useState(1);
-  const [totalPages, setTotalPages] = useState(0);
-  const [limit, setLimit] = useState(Infinity);
+
+  const { data: pagesListData, error: errors } = useSWRxPageList(path, activePage);
 
   function setPageNumber(selectedPageNumber) {
     setActivePage(selectedPageNumber);
   }
 
-  const updatePageList = useCallback(async() => {
-    const page = activePage;
-    const res = await appContainer.apiv3Get('/pages/list', { path, page });
-
-    setPages(res.data.pages);
-    setIsLoading(false);
-    setTotalPages(res.data.totalCount);
-    setLimit(res.data.limit);
-  }, [appContainer, path, activePage]);
-
-  useEffect(() => {
-    updatePageList();
-  }, [updatePageList]);
-
+  if (errors != null) {
+    return (
+      <div className="my-5">
+        {/* eslint-disable-next-line react/no-array-index-key */}
+        {errors.map((error, index) => <div key={index} className="text-danger">{error.message}</div>)}
+      </div>
+    );
+  }
 
-  if (isLoading) {
+  if (pagesListData == null) {
     return (
       <div className="wiki">
         <div className="text-muted text-center">
@@ -51,7 +45,7 @@ const PageList = (props) => {
   }
 
   const liClasses = props.liClasses.join(' ');
-  const pageList = pages.map(page => (
+  const pageList = pagesListData.items.map(page => (
     <li key={page._id} className={liClasses}>
       <Page page={page} />
     </li>
@@ -81,14 +75,12 @@ const PageList = (props) => {
       <PaginationWrapper
         activePage={activePage}
         changePage={setPageNumber}
-        totalItemsCount={totalPages}
-        pagingLimit={limit}
+        totalItemsCount={pagesListData.totalCount}
+        pagingLimit={pagesListData.limit}
         align="center"
       />
     </div>
   );
-
-
 };
 
 const PageListWrapper = withUnstatedContainers(PageList, [AppContainer, PageContainer]);

+ 2 - 2
packages/app/src/components/Sidebar/CustomSidebar.jsx

@@ -57,11 +57,11 @@ const CustomSidebar = (props) => {
   return (
     <>
       <div className="grw-sidebar-content-header p-3 d-flex">
-        <h3 className="mb-0">
+        <h3 className="mb-0 text-nowrap">
           Custom Sidebar
           <a className="h6 ml-2" href="/Sidebar"><i className="icon-pencil"></i></a>
         </h3>
-        <button type="button" className="btn btn-sm ml-auto grw-btn-reload-cs" onClick={fetchDataAndRenderHtml}>
+        <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={fetchDataAndRenderHtml}>
           <i className="icon icon-reload"></i>
         </button>
       </div>

+ 79 - 18
packages/app/src/components/Sidebar/RecentChanges.jsx

@@ -1,20 +1,23 @@
-import React from 'react';
+import React, {
+  useCallback, useEffect, useState,
+} from 'react';
 import PropTypes from 'prop-types';
 
-import { withTranslation } from 'react-i18next';
+import { useTranslation, withTranslation } from 'react-i18next';
 
 import { UserPicture } from '@growi/ui';
 import { DevidedPagePath } from '@growi/core';
+
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
+import { apiv3Get } from '~/client/util/apiv3-client';
+import { toastError } from '~/client/util/apiNotification';
+import { useSWRxRecentlyUpdated } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 
 import LinkedPagePath from '~/models/linked-page-path';
 
 import FootstampIcon from '../FootstampIcon';
 
-import { withUnstatedContainers } from '../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-import { toastError } from '~/client/util/apiNotification';
 
 import FormattedDistanceDate from '../FormattedDistanceDate';
 
@@ -119,17 +122,82 @@ function SmallPageItem({ page }) {
 SmallPageItem.propTypes = {
   page: PropTypes.any,
 };
-class RecentChanges extends React.Component {
+
+
+const RecentChanges = () => {
+
+  const { t } = useTranslation();
+  const { data: pages, error, mutate } = useSWRxRecentlyUpdated();
+
+  if (error != null) {
+    toastError(error, 'Error occurred in updating History');
+  }
+
+  const [isRecentChangesSidebarSmall, setIsRecentChangesSidebarSmall] = useState(false);
+
+  const retrieveSizePreferenceFromLocalStorage = useCallback(() => {
+    if (window.localStorage.isRecentChangesSidebarSmall === 'true') {
+      setIsRecentChangesSidebarSmall(true);
+    }
+  });
+
+  const changeSizeHandler = useCallback((e) => {
+    setIsRecentChangesSidebarSmall(e.target.checked);
+    window.localStorage.setItem('isRecentChangesSidebarSmall', e.target.checked);
+  }, []);
+
+  // componentDidMount
+  useEffect(() => {
+    retrieveSizePreferenceFromLocalStorage();
+  }, [retrieveSizePreferenceFromLocalStorage]);
+
+  return (
+    <>
+      <div className="grw-sidebar-content-header p-3 d-flex">
+        <h3 className="mb-0  text-nowrap">{t('Recent Changes')}</h3>
+        <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={() => mutate()}>
+          <i className="icon icon-reload"></i>
+        </button>
+        <div className="d-flex align-items-center">
+          <div className="grw-recent-changes-resize-button custom-control custom-switch ml-1">
+            <input
+              id="recentChangesResize"
+              className="custom-control-input"
+              type="checkbox"
+              checked={isRecentChangesSidebarSmall}
+              onChange={changeSizeHandler}
+            />
+            <label className="custom-control-label" htmlFor="recentChangesResize">
+            </label>
+          </div>
+        </div>
+      </div>
+      <div className="grw-sidebar-content-body grw-recent-changes p-3">
+        <ul className="list-group list-group-flush">
+          {(pages || []).map(page => (isRecentChangesSidebarSmall
+            ? <SmallPageItem key={page._id} page={page} />
+            : <LargePageItem key={page._id} page={page} />))}
+        </ul>
+      </div>
+    </>
+  );
+
+};
+
+// export default RecentChanges;
+
+
+class DeprecatedRecentChanges extends React.Component {
 
   static propTypes = {
     t: PropTypes.func.isRequired, // i18next
-    appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   };
 
   constructor(props) {
     super(props);
     this.state = {
       isRecentChangesSidebarSmall: false,
+      recentlyUpdatedPages: [],
     };
     this.reloadData = this.reloadData.bind(this);
   }
@@ -143,10 +211,9 @@ class RecentChanges extends React.Component {
   }
 
   async reloadData() {
-    const { appContainer } = this.props;
-
     try {
-      await appContainer.retrieveRecentlyUpdated();
+      const { data } = await apiv3Get('/pages/recent');
+      this.setState({ recentlyUpdatedPages: data.pages });
     }
     catch (error) {
       logger.error('failed to save', error);
@@ -171,7 +238,6 @@ class RecentChanges extends React.Component {
 
   render() {
     const { t } = this.props;
-    const { recentlyUpdatedPages } = this.props.appContainer.state;
 
     return (
       <>
@@ -195,7 +261,7 @@ class RecentChanges extends React.Component {
         </div>
         <div className="grw-sidebar-content-body grw-recent-changes p-3">
           <ul className="list-group list-group-flush">
-            {recentlyUpdatedPages.map(page => (this.state.isRecentChangesSidebarSmall
+            {this.state.recentlyUpdatedPages.map(page => (this.state.isRecentChangesSidebarSmall
               ? <SmallPageItem key={page._id} page={page} />
               : <LargePageItem key={page._id} page={page} />))}
           </ul>
@@ -206,10 +272,5 @@ class RecentChanges extends React.Component {
 
 }
 
-/**
- * Wrapper component for using unstated
- */
-const RecentChangesWrapper = withUnstatedContainers(RecentChanges, [AppContainer]);
-
 
-export default withTranslation()(RecentChangesWrapper);
+export default withTranslation()(DeprecatedRecentChanges);

+ 7 - 0
packages/app/src/interfaces/page-tag-relation.ts

@@ -0,0 +1,7 @@
+import { IPage } from './page';
+import { ITag } from './tag';
+
+export type IPageTagRelation = {
+  relatedPage: IPage,
+  relatedTag: ITag,
+}

+ 14 - 0
packages/app/src/interfaces/page.ts

@@ -0,0 +1,14 @@
+import { IUser } from './user';
+import { IRevision } from './revision';
+import { ITag } from './tag';
+
+export type IPage = {
+  path: string,
+  status: string,
+  revision: IRevision,
+  tags: ITag[],
+  creator: IUser,
+  createdAt: Date,
+  updatedAt: Date,
+  seenUsers: string[]
+}

+ 5 - 0
packages/app/src/interfaces/paging-result.ts

@@ -0,0 +1,5 @@
+export type IPagingResult<T> = {
+  items: T[],
+  totalCount: number,
+  limit: number,
+}

+ 9 - 0
packages/app/src/interfaces/revision.ts

@@ -0,0 +1,9 @@
+import { IUser } from './user';
+
+export type IRevision = {
+  body: string,
+  author: IUser,
+  hasDiffToPrev: boolean;
+  createdAt: Date,
+  updatedAt: Date,
+}

+ 4 - 0
packages/app/src/interfaces/tag.ts

@@ -0,0 +1,4 @@
+export type ITag = {
+  name: string,
+  createdAt: Date;
+}

+ 18 - 0
packages/app/src/interfaces/user.ts

@@ -0,0 +1,18 @@
+export type IUser = {
+  name: string;
+  username: string;
+  imageUrlCached: string;
+  admin: boolean;
+}
+
+export type IUserGroupRelation = {
+  relatedGroup: IUserGroup,
+  relatedUser: IUser,
+  createdAt: Date,
+}
+
+export type IUserGroup = {
+  userGroupId:string;
+  name: string;
+  createdAt: Date;
+}

+ 78 - 7
packages/app/src/server/routes/apiv3/page.js

@@ -112,6 +112,52 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  *          bool:
  *            type: boolean
  *            description: boolean for like status
+ *
+ *      PageInfo:
+ *        description: PageInfo
+ *        type: object
+ *        required:
+ *          - isSeen
+ *          - sumOfLikers
+ *          - likerIds
+ *          - sumOfSeenUsers
+ *          - seenUserIds
+ *        properties:
+ *          isSeen:
+ *            type: boolean
+ *            description: Whether the page has ever been seen
+ *          isLiked:
+ *            type: boolean
+ *            description: Whether the page is liked by the logged in user
+ *          sumOfLikers:
+ *            type: number
+ *            description: Number of users who have liked the page
+ *          likerIds:
+ *            type: array
+ *            items:
+ *              type: string
+ *            description: Ids of users who have liked the page
+ *            example: ["5e07345972560e001761fa63"]
+ *          sumOfSeenUsers:
+ *            type: number
+ *            description: Number of users who have seen the page
+ *          seenUserIds:
+ *            type: array
+ *            items:
+ *              type: string
+ *            description: Ids of users who have seen the page
+ *            example: ["5e07345972560e001761fa63"]
+ *
+ *      PageParams:
+ *        description: PageParams
+ *        type: object
+ *        required:
+ *          - pageId
+ *        properties:
+ *          pageId:
+ *            type: string
+ *            description: page ID
+ *            example: 5e07345972560e001761fa63
  */
 module.exports = (crowi) => {
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
@@ -130,6 +176,9 @@ module.exports = (crowi) => {
       body('pageId').isString(),
       body('bool').isBoolean(),
     ],
+    info: [
+      query('pageId').isMongoId().withMessage('pageId is required'),
+    ],
     export: [
       query('format').isString().isIn(['md', 'pdf']),
       query('revisionId').isString(),
@@ -208,10 +257,34 @@ module.exports = (crowi) => {
     }
   });
 
-  router.get(('/info', loginRequired), async(req, res) => {
+  /**
+   * @swagger
+   *
+   *    /page/info:
+   *      get:
+   *        tags: [Page]
+   *        summary: /page/info
+   *        description: Retrieve current page info
+   *        operationId: getPageInfo
+   *        requestBody:
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/PageParams'
+   *        responses:
+   *          200:
+   *            description: Successfully retrieved current page info.
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/PageInfo'
+   *          500:
+   *            description: Internal server error.
+   */
+  router.get('/info', loginRequired, validator.info, apiV3FormValidator, async(req, res) => {
+    const { pageId } = req.query;
 
     try {
-      const pageId = req.query._id;
       const page = await Page.findById(pageId);
 
       const guestUserResponse = {
@@ -222,11 +295,9 @@ module.exports = (crowi) => {
         isSeen: page.seenUsers.length > 0,
       };
 
-      {
-        const isGuestUser = !req.user;
-        if (isGuestUser) {
-          return res.apiv3(guestUserResponse);
-        }
+      const isGuestUser = !req.user;
+      if (isGuestUser) {
+        return res.apiv3(guestUserResponse);
       }
 
       const userResponse = { ...guestUserResponse, isLiked: page.isLiked(req.user) };

+ 33 - 0
packages/app/src/stores/page.tsx

@@ -0,0 +1,33 @@
+import useSWR, { SWRResponse } from 'swr';
+
+import { apiv3Get } from '~/client/util/apiv3-client';
+
+import { IPage } from '~/interfaces/page';
+import { IPagingResult } from '~/interfaces/paging-result';
+
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export const useSWRxRecentlyUpdated = <Data, Error>(): SWRResponse<IPage[], Error> => {
+  return useSWR(
+    '/pages/recent',
+    endpoint => apiv3Get<{ pages: IPage[] }>(endpoint).then(response => response.data?.pages),
+  );
+};
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export const useSWRxPageList = (
+    path: string,
+    pageNumber?: number,
+): SWRResponse<IPagingResult<IPage>, Error> => {
+  const page = pageNumber || 1;
+  return useSWR(
+    `/pages/list?path=${path}&page=${page}`,
+    endpoint => apiv3Get<{pages: IPage[], totalCount: number, limit: number}>(endpoint).then((response) => {
+      return {
+        items: response.data.pages,
+        totalCount: response.data.totalCount,
+        limit: response.data.limit,
+      };
+    }),
+  );
+};

+ 4 - 1
packages/app/src/styles/_layout.scss

@@ -99,11 +99,14 @@ body.growi-layout-fluid .grw-container-convertible {
 
 // printable style
 @media print {
-  padding: 30px;
+  body {
+    padding: 30px;
+  }
 
   a:after {
     display: none !important;
   }
+
   .main {
     header {
       border-bottom: solid 1px $secondary;

+ 1 - 5
packages/app/src/styles/_recent-changes.scss

@@ -1,12 +1,8 @@
 .grw-sidebar-content-header {
-  .grw-btn-reload-rc {
-    font-size: 18px;
-  }
-
   .grw-recent-changes-resize-button {
     font-size: 12px;
     line-height: normal;
-    transform: translateY(6px);
+    transform: translateY(-2px);
 
     .custom-control-label::before {
       padding-left: 16px;

+ 3 - 1
packages/app/src/styles/_sidebar.scss

@@ -135,7 +135,9 @@
   }
 
   .grw-sidebar-content-header {
-    min-width: $grw-sidebar-content-min-width + 20px;
+    .grw-btn-reload {
+      font-size: 18px;
+    }
   }
 }
 

+ 0 - 1
packages/app/src/styles/_variables.scss

@@ -25,7 +25,6 @@ $grw-navbar-bottom-height: 48px;
 $grw-editor-navbar-bottom-height: 48px;
 
 $grw-sidebar-nav-width: 64px; // !!DO NOT CHANGE!! 'margin-left' for '.css-teprsg' is hardcoded
-$grw-sidebar-content-min-width: 240px;
 
 $grw-logo-width: $grw-sidebar-nav-width;
 $grw-logomark-width: 36px;

+ 3 - 7
packages/app/src/styles/theme/_apply-colors.scss

@@ -15,8 +15,7 @@ $bordercolor-nav-tabs-hover: $gray-200 $gray-200 $bordercolor-nav-tabs !default;
 $color-nav-tabs-link-active: $gray-600 !default;
 $bordercolor-nav-tabs-active: $bordercolor-nav-tabs $bordercolor-nav-tabs $bgcolor-global !default;
 $color-seen-user: #549c79 !default;
-$reload-btn-rc-color: $gray-500;
-$reload-btn-cs-color: $gray-500;
+$color-btn-reload-in-sidebar: $gray-500;
 $bgcolor-keyword-highlighted: $grw-marker-yellow !default;
 
 // override bootstrap variables
@@ -268,11 +267,8 @@ ul.pagination {
   }
 
   .grw-sidebar-content-header {
-    .grw-btn-reload-rc {
-      color: $reload-btn-rc-color;
-    }
-    .grw-btn-reload-cs {
-      color: $reload-btn-cs-color;
+    .grw-btn-reload {
+      color: $color-btn-reload-in-sidebar;
     }
 
     .grw-recent-changes-resize-button {

+ 9 - 0
packages/app/src/utils/swr-utils.ts

@@ -0,0 +1,9 @@
+import { SWRConfiguration } from 'swr';
+
+import axios from './axios';
+
+export const swrGlobalConfiguration: SWRConfiguration = {
+  fetcher: url => axios.get(url).then(res => res.data),
+  revalidateOnFocus: false,
+  errorRetryCount: 1,
+};

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

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

+ 1 - 1
packages/core/package.json

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

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

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

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

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

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-pukiwiki-like-linker",
-  "version": "4.4.9",
+  "version": "4.4.10-RC.0",
   "description": "GROWI plugin to add PukiwikiLikeLinker",
   "license": "MIT",
   "keywords": [
@@ -22,7 +22,7 @@
   },
   "devDependencies": {
     "browser-bunyan": "^1.6.3",
-    "stylelint": "^13.2.0",
+    "stylelint": "^14.0.1",
     "stylelint-config-recess-order": "^2.0.1",
     "tsc-alias": "^1.2.9"
   }

+ 1 - 1
packages/slack/package.json

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

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "4.4.9",
+  "version": "4.4.10-slackbot-proxy.0",
   "license": "MIT",
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -25,7 +25,7 @@
   },
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^4.4.9",
+    "@growi/slack": "^4.4.10-RC.0",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",

+ 1 - 1
packages/ui/package.json

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

Разница между файлами не показана из-за своего большого размера
+ 152 - 390
yarn.lock


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