浏览代码

Merge pull request #3485 from weseek/master

release v4.2.10
Yuki Takei 5 年之前
父节点
当前提交
a791ddb760

+ 8 - 1
CHANGES.md

@@ -1,6 +1,13 @@
 # CHANGES
 # CHANGES
 
 
-## v4.2.9-RC
+## v4.2.10-RC
+
+* Feature: Staff Credits for apps on GROWI.cloud 
+* Improvement: Hackmd button behavior when disabled
+* Improvement: Layout of comparing revisions
+* Fix: Empty trash is not working
+
+## v4.2.9
 
 
 * Feature: Comparing revisions
 * Feature: Comparing revisions
 * Improvement: Memory consumption when re-indexing for full text searching
 * Improvement: Memory consumption when re-indexing for full text searching

+ 4 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "4.2.9-RC",
+  "version": "4.2.10-RC",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -24,12 +24,15 @@
     "build:apiv3:jsdoc": "cross-env API_VERSION=3 npm run build:api:jsdoc -- \"src/server/routes/apiv3/**/*.js\" \"src/server/models/**/*.js\"",
     "build:apiv3:jsdoc": "cross-env API_VERSION=3 npm run build:api:jsdoc -- \"src/server/routes/apiv3/**/*.js\" \"src/server/models/**/*.js\"",
     "build:apiv1:jsdoc": "cross-env API_VERSION=1 npm run build:api:jsdoc -- \"src/server/*/*.js\" \"src/server/models/**/*.js\"",
     "build:apiv1:jsdoc": "cross-env API_VERSION=1 npm run build:api:jsdoc -- \"src/server/*/*.js\" \"src/server/models/**/*.js\"",
     "build:dev:app:watch": "npm run build:dev:app -- --watch",
     "build:dev:app:watch": "npm run build:dev:app -- --watch",
+    "build:dev:app:watch:poll": "npm run build:dev:app -- --watch --watch-poll",
     "build:dev:app": "env-cmd -f config/env.dev.js webpack --config config/webpack.dev.js --progress",
     "build:dev:app": "env-cmd -f config/env.dev.js webpack --config config/webpack.dev.js --progress",
     "build:dev:watch": "npm run build:dev:app:watch",
     "build:dev:watch": "npm run build:dev:app:watch",
+    "build:dev:watch:poll": "npm run build:dev:app:watch:poll",
     "build:dev": "npm run build:dev:app",
     "build:dev": "npm run build:dev:app",
     "build:prod:analyze": "cross-env ANALYZE=1 npm run build:prod",
     "build:prod:analyze": "cross-env ANALYZE=1 npm run build:prod",
     "build:prod": "env-cmd -f config/env.prod.js webpack --config config/webpack.prod.js --profile --bail",
     "build:prod": "env-cmd -f config/env.prod.js webpack --config config/webpack.prod.js --profile --bail",
     "build": "npm run build:dev:watch",
     "build": "npm run build:dev:watch",
+    "build:poll": "npm run build:dev:watch:poll",
     "clean:app": "rimraf -- public/js public/styles",
     "clean:app": "rimraf -- public/js public/styles",
     "clean:report": "rimraf -- report",
     "clean:report": "rimraf -- report",
     "clean": "npm-run-all -p clean:*",
     "clean": "npm-run-all -p clean:*",

+ 4 - 0
src/client/js/components/StaffCredit/Contributor.js → resource/Contributor.js

@@ -1,5 +1,6 @@
 const contributors = [
 const contributors = [
   {
   {
+    order: 1,
     sectionName: 'GROWI VILLAGE',
     sectionName: 'GROWI VILLAGE',
     additionalClass: '',
     additionalClass: '',
     memberGroups: [
     memberGroups: [
@@ -47,6 +48,7 @@ const contributors = [
     ],
     ],
   },
   },
   {
   {
+    order: 10,
     sectionName: 'CONTRIBUTER',
     sectionName: 'CONTRIBUTER',
     additionalClass: '',
     additionalClass: '',
     memberGroups: [
     memberGroups: [
@@ -92,6 +94,7 @@ const contributors = [
     ],
     ],
   },
   },
   {
   {
+    order: 100,
     sectionName: 'VULNERABILITY HUNTER',
     sectionName: 'VULNERABILITY HUNTER',
     additionalClass: '',
     additionalClass: '',
     memberGroups: [
     memberGroups: [
@@ -111,6 +114,7 @@ const contributors = [
     ],
     ],
   },
   },
   {
   {
+    order: 200,
     sectionName: 'SPECIAL THANKS',
     sectionName: 'SPECIAL THANKS',
     additionalClass: '',
     additionalClass: '',
     memberGroups: [
     memberGroups: [

+ 26 - 4
src/client/js/components/Navbar/PageEditorModeManager.jsx

@@ -3,9 +3,12 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
+import { withUnstatedContainers } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
+
 /* eslint-disable react/prop-types */
 /* eslint-disable react/prop-types */
 const PageEditorModeButtonWrapper = React.memo(({
 const PageEditorModeButtonWrapper = React.memo(({
-  editorMode, isBtnDisabled, onClick, targetMode, icon, label,
+  editorMode, isBtnDisabled, onClick, targetMode, icon, label, id,
 }) => {
 }) => {
   const classNames = [`btn btn-outline-primary ${targetMode}-button px-1`];
   const classNames = [`btn btn-outline-primary ${targetMode}-button px-1`];
   if (editorMode === targetMode) {
   if (editorMode === targetMode) {
@@ -20,6 +23,7 @@ const PageEditorModeButtonWrapper = React.memo(({
       type="button"
       type="button"
       className={classNames.join(' ')}
       className={classNames.join(' ')}
       onClick={() => { onClick(targetMode) }}
       onClick={() => { onClick(targetMode) }}
+      id={id}
     >
     >
       <span className="d-flex flex-column flex-md-row justify-content-center">
       <span className="d-flex flex-column flex-md-row justify-content-center">
         <span className="grw-page-editor-mode-manager-icon mr-md-1">{icon}</span>
         <span className="grw-page-editor-mode-manager-icon mr-md-1">{icon}</span>
@@ -32,9 +36,14 @@ const PageEditorModeButtonWrapper = React.memo(({
 
 
 function PageEditorModeManager(props) {
 function PageEditorModeManager(props) {
   const {
   const {
-    t, editorMode, onPageEditorModeButtonClicked, isBtnDisabled, isDeviceSmallerThanMd,
+    t, appContainer,
+    editorMode, onPageEditorModeButtonClicked, isBtnDisabled, isDeviceSmallerThanMd,
   } = props;
   } = props;
 
 
+  const isAdmin = appContainer.isAdmin;
+  const isHackmdEnabled = appContainer.config.env.HACKMD_URI != null;
+  const showHackmdBtn = isHackmdEnabled || isAdmin;
+  const showHackmdDisabledTooltip = isAdmin && !isHackmdEnabled;
 
 
   const pageEditorModeButtonClickedHandler = useCallback((viewType) => {
   const pageEditorModeButtonClickedHandler = useCallback((viewType) => {
     if (isBtnDisabled) {
     if (isBtnDisabled) {
@@ -73,7 +82,7 @@ function PageEditorModeManager(props) {
             label={t('Edit')}
             label={t('Edit')}
           />
           />
         )}
         )}
-        {(!isDeviceSmallerThanMd || editorMode === 'view') && (
+        {(!isDeviceSmallerThanMd || editorMode === 'view') && showHackmdBtn && (
           <PageEditorModeButtonWrapper
           <PageEditorModeButtonWrapper
             editorMode={editorMode}
             editorMode={editorMode}
             isBtnDisabled={isBtnDisabled}
             isBtnDisabled={isBtnDisabled}
@@ -81,6 +90,7 @@ function PageEditorModeManager(props) {
             targetMode="hackmd"
             targetMode="hackmd"
             icon={<i className="fa fa-file-text-o" />}
             icon={<i className="fa fa-file-text-o" />}
             label={t('hackmd.hack_md')}
             label={t('hackmd.hack_md')}
+            id="grw-page-editor-mode-manager-hackmd-button"
           />
           />
         )}
         )}
       </div>
       </div>
@@ -89,6 +99,11 @@ function PageEditorModeManager(props) {
           {t('Not available for guest')}
           {t('Not available for guest')}
         </UncontrolledTooltip>
         </UncontrolledTooltip>
       )}
       )}
+      {!isBtnDisabled && showHackmdDisabledTooltip && (
+        <UncontrolledTooltip placement="top" target="grw-page-editor-mode-manager-hackmd-button" fade={false}>
+          {t('HackMD editor is not available')}
+        </UncontrolledTooltip>
+      )}
     </>
     </>
   );
   );
 
 
@@ -96,6 +111,8 @@ function PageEditorModeManager(props) {
 
 
 PageEditorModeManager.propTypes = {
 PageEditorModeManager.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
   onPageEditorModeButtonClicked: PropTypes.func,
   onPageEditorModeButtonClicked: PropTypes.func,
   isBtnDisabled: PropTypes.bool,
   isBtnDisabled: PropTypes.bool,
   editorMode: PropTypes.string,
   editorMode: PropTypes.string,
@@ -107,4 +124,9 @@ PageEditorModeManager.defaultProps = {
   isDeviceSmallerThanMd: false,
   isDeviceSmallerThanMd: false,
 };
 };
 
 
-export default withTranslation()(PageEditorModeManager);
+/**
+ * Wrapper component for using unstated
+ */
+const PageEditorModeManagerWrapper = withUnstatedContainers(PageEditorModeManager, [AppContainer]);
+
+export default withTranslation()(PageEditorModeManagerWrapper);

+ 2 - 6
src/client/js/components/PageHistory.jsx

@@ -2,7 +2,6 @@ import React, { useCallback } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
 
 
-import { withTranslation } from 'react-i18next';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { toastError } from '../util/apiNotification';
 import { toastError } from '../util/apiNotification';
 
 
@@ -17,7 +16,7 @@ import RevisionComparerContainer from '../services/RevisionComparerContainer';
 const logger = loggerFactory('growi:PageHistory');
 const logger = loggerFactory('growi:PageHistory');
 
 
 function PageHistory(props) {
 function PageHistory(props) {
-  const { pageHistoryContainer, revisionComparerContainer, t } = props;
+  const { pageHistoryContainer, revisionComparerContainer } = props;
   const { getPreviousRevision } = pageHistoryContainer;
   const { getPreviousRevision } = pageHistoryContainer;
   const {
   const {
     activePage, totalPages, pagingLimit, revisions, diffOpened,
     activePage, totalPages, pagingLimit, revisions, diffOpened,
@@ -70,7 +69,6 @@ function PageHistory(props) {
 
 
   return (
   return (
     <div className="revision-history">
     <div className="revision-history">
-      <h3 className="pb-3">{t('page_history.revision_list')}</h3>
       <PageRevisionTable
       <PageRevisionTable
         pageHistoryContainer={pageHistoryContainer}
         pageHistoryContainer={pageHistoryContainer}
         revisionComparerContainer={revisionComparerContainer}
         revisionComparerContainer={revisionComparerContainer}
@@ -90,10 +88,8 @@ function PageHistory(props) {
 const RenderPageHistoryWrapper = withUnstatedContainers(withLoadingSppiner(PageHistory), [PageHistroyContainer, RevisionComparerContainer]);
 const RenderPageHistoryWrapper = withUnstatedContainers(withLoadingSppiner(PageHistory), [PageHistroyContainer, RevisionComparerContainer]);
 
 
 PageHistory.propTypes = {
 PageHistory.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
   pageHistoryContainer: PropTypes.instanceOf(PageHistroyContainer).isRequired,
   pageHistoryContainer: PropTypes.instanceOf(PageHistroyContainer).isRequired,
   revisionComparerContainer: PropTypes.instanceOf(RevisionComparerContainer).isRequired,
   revisionComparerContainer: PropTypes.instanceOf(RevisionComparerContainer).isRequired,
 };
 };
 
 
-export default withTranslation()(RenderPageHistoryWrapper);
+export default RenderPageHistoryWrapper;

+ 1 - 1
src/client/js/components/PageHistory/RevisionDiff.jsx

@@ -63,7 +63,7 @@ class RevisionDiff extends React.Component {
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
-        <div className="revision-history-diff" dangerouslySetInnerHTML={diffView} />
+        <div className="revision-history-diff pb-1" dangerouslySetInnerHTML={diffView} />
       </>
       </>
     );
     );
   }
   }

+ 16 - 9
src/client/js/components/RevisionComparer/RevisionComparer.jsx

@@ -61,6 +61,8 @@ const RevisionComparer = (props) => {
     return null;
     return null;
   }
   }
 
 
+  const isNodiff = sourceRevision._id === targetRevision._id;
+
   return (
   return (
     <div className="revision-compare">
     <div className="revision-compare">
       <div className="d-flex">
       <div className="d-flex">
@@ -76,7 +78,7 @@ const RevisionComparer = (props) => {
           >
           >
             <i className="ti-clipboard"></i>
             <i className="ti-clipboard"></i>
           </DropdownToggle>
           </DropdownToggle>
-          <DropdownMenu positionFixed modifiers={{ preventOverflow: { boundariesElement: null } }}>
+          <DropdownMenu positionFixed right modifiers={{ preventOverflow: { boundariesElement: null } }}>
             {/* Page path URL */}
             {/* Page path URL */}
             <CopyToClipboard text={pagePathUrl()}>
             <CopyToClipboard text={pagePathUrl()}>
               <DropdownItem className="px-3">
               <DropdownItem className="px-3">
@@ -88,14 +90,19 @@ const RevisionComparer = (props) => {
         </Dropdown>
         </Dropdown>
       </div>
       </div>
 
 
-      <div className="revision-compare-outer">
-        {sourceRevision._id === targetRevision._id ? t('No diff') : (
-          <RevisionDiff
-            revisionDiffOpened
-            previousRevision={sourceRevision}
-            currentRevision={targetRevision}
-          />
-        )}
+      <div className={`revision-compare-container ${isNodiff ? 'nodiff' : ''}`}>
+        { isNodiff
+          ? (
+            <span className="h3 text-muted">{t('No diff')}</span>
+          )
+          : (
+            <RevisionDiff
+              revisionDiffOpened
+              previousRevision={sourceRevision}
+              currentRevision={targetRevision}
+            />
+          )
+        }
       </div>
       </div>
     </div>
     </div>
   );
   );

+ 20 - 4
src/client/js/components/StaffCredit/StaffCredit.jsx

@@ -4,7 +4,8 @@ import loggerFactory from '@alias/logger';
 import {
 import {
   Modal, ModalBody,
   Modal, ModalBody,
 } from 'reactstrap';
 } from 'reactstrap';
-import contributors from './Contributor';
+import AppContainer from '../../services/AppContainer';
+import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 /**
 /**
  * Page staff credit component
  * Page staff credit component
@@ -17,13 +18,14 @@ import contributors from './Contributor';
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:cli:StaffCredit');
 const logger = loggerFactory('growi:cli:StaffCredit');
 
 
-export default class StaffCredit extends React.Component {
+class StaffCredit extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
 
 
     super(props);
     super(props);
     this.state = {
     this.state = {
       isShown: true,
       isShown: true,
+      contributors: null,
     };
     };
     this.deleteCredit = this.deleteCredit.bind(this);
     this.deleteCredit = this.deleteCredit.bind(this);
   }
   }
@@ -57,7 +59,7 @@ export default class StaffCredit extends React.Component {
 
 
   renderContributors() {
   renderContributors() {
     if (this.state.isShown) {
     if (this.state.isShown) {
-      const credit = contributors.map((contributor) => {
+      const credit = this.state.contributors.map((contributor) => {
         // construct members elements
         // construct members elements
         const memberGroups = contributor.memberGroups.map((memberGroup, idx) => {
         const memberGroups = contributor.memberGroups.map((memberGroup, idx) => {
           return this.renderMembers(memberGroup, `${contributor.sectionName}-group${idx}`);
           return this.renderMembers(memberGroup, `${contributor.sectionName}-group${idx}`);
@@ -83,7 +85,11 @@ export default class StaffCredit extends React.Component {
     return null;
     return null;
   }
   }
 
 
-  componentDidMount() {
+  async componentDidMount() {
+    const res = await this.props.appContainer.apiv3Get('/staffs');
+    const contributors = res.data.contributors;
+    this.setState({ contributors });
+
     setTimeout(() => {
     setTimeout(() => {
       // px / sec
       // px / sec
       const scrollSpeed = 200;
       const scrollSpeed = 200;
@@ -103,6 +109,10 @@ export default class StaffCredit extends React.Component {
   render() {
   render() {
     const { onClosed } = this.props;
     const { onClosed } = this.props;
 
 
+    if (this.state.contributors === null) {
+      return <></>;
+    }
+
     return (
     return (
       <Modal
       <Modal
         isOpen={this.state.isShown}
         isOpen={this.state.isShown}
@@ -123,6 +133,12 @@ export default class StaffCredit extends React.Component {
   }
   }
 
 
 }
 }
+
+const StaffCreditWrapper = withUnstatedContainers(StaffCredit, [AppContainer]);
+
 StaffCredit.propTypes = {
 StaffCredit.propTypes = {
   onClosed: PropTypes.func,
   onClosed: PropTypes.func,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 };
 };
+
+export default StaffCreditWrapper;

+ 7 - 3
src/client/styles/scss/_page-history.scss

@@ -47,10 +47,14 @@
 }
 }
 
 
 .revision-compare {
 .revision-compare {
-  .revision-compare-outer {
+  .revision-compare-container {
     min-height: 100px;
     min-height: 100px;
-    max-height: 250px;
-    overflow: auto;
+
+    &.nodiff {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
   }
   }
   .d2h-file-header {
   .d2h-file-header {
     display: none;
     display: none;

+ 2 - 0
src/server/routes/apiv3/index.js

@@ -46,5 +46,7 @@ module.exports = (crowi) => {
   router.use('/bookmarks', require('./bookmarks')(crowi));
   router.use('/bookmarks', require('./bookmarks')(crowi));
   router.use('/attachment', require('./attachment')(crowi));
   router.use('/attachment', require('./attachment')(crowi));
 
 
+  router.use('/staffs', require('./staffs')(crowi));
+
   return router;
   return router;
 };
 };

+ 1 - 1
src/server/routes/apiv3/pages.js

@@ -436,7 +436,7 @@ module.exports = (crowi) => {
     const options = { socketClientId };
     const options = { socketClientId };
 
 
     try {
     try {
-      const pages = await crowi.pageService.deletePageRecursivelyCompletely({ path: '/trash' }, req.user, options);
+      const pages = await crowi.pageService.deleteCompletelyDescendantsWithStream({ path: '/trash' }, req.user, options);
       return res.apiv3({ pages });
       return res.apiv3({ pages });
     }
     }
     catch (err) {
     catch (err) {

+ 52 - 0
src/server/routes/apiv3/staffs.js

@@ -0,0 +1,52 @@
+const loggerFactory = require('@alias/logger');
+
+const logger = loggerFactory('growi:routes:apiv3:staffs'); // eslint-disable-line no-unused-vars
+
+const express = require('express');
+
+const axios = require('axios');
+
+const router = express.Router();
+const { isAfter, addHours } = require('date-fns');
+
+const contributors = require('../../../../resource/Contributor');
+
+let expiredAt;
+const contributorsCache = contributors;
+let gcContributors;
+
+// Sorting contributors by this method
+const compareFunction = function(a, b) {
+  return a.order - b.order;
+};
+
+module.exports = (crowi) => {
+
+  router.get('/', async(req, res) => {
+    const now = new Date();
+    const growiCloudUri = await crowi.configManager.getConfig('crowi', 'app:growiCloudUri');
+
+    if (growiCloudUri != null && (expiredAt == null || isAfter(now, expiredAt))) {
+      const url = new URL('_api/staffCredit', growiCloudUri);
+      try {
+        const gcContributorsRes = await axios.get(url.toString());
+        if (gcContributors == null) {
+          gcContributors = gcContributorsRes.data;
+          // merging contributors
+          contributorsCache.push(gcContributors);
+        }
+        // Change the order of section
+        contributorsCache.sort(compareFunction);
+        // caching 'expiredAt' for 1 hour
+        expiredAt = addHours(now, 1);
+      }
+      catch (err) {
+        logger.warn('Getting GROWI.cloud staffcredit is failed');
+      }
+    }
+    return res.apiv3({ contributors: contributorsCache });
+  });
+
+  return router;
+
+};

+ 1 - 1
src/server/routes/attachment.js

@@ -188,7 +188,7 @@ module.exports = function(crowi, app) {
     const user = req.user;
     const user = req.user;
     const isAccessible = await isAccessibleByViewer(user, attachment);
     const isAccessible = await isAccessibleByViewer(user, attachment);
     if (!isAccessible) {
     if (!isAccessible) {
-      return res.json(ApiResponse.error(`Forbidden to access to the attachment '${attachment.id}'`));
+      return res.json(ApiResponse.error(`Forbidden to access to the attachment '${attachment.id}'. This attachment might belong to other pages.`));
     }
     }
 
 
     // add headers before evaluating 'req.fresh'
     // add headers before evaluating 'req.fresh'