Parcourir la source

Merge remote-tracking branch 'origin/master' into feat/design-fix

Yuki Takei il y a 5 ans
Parent
commit
0402c1b178

+ 3 - 1
CHANGES.md

@@ -2,7 +2,9 @@
 
 ## v4.2.10-RC
 
-*
+* Feature: Staff Credits for apps on GROWI.cloud 
+* Improvement: Hackmd button behavior when disabled
+* Fix: Empty trash is not working
 
 ## v4.2.9
 

+ 3 - 0
package.json

@@ -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: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: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: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: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": "npm run build:dev:watch",
+    "build:poll": "npm run build:dev:watch:poll",
     "clean:app": "rimraf -- public/js public/styles",
     "clean:report": "rimraf -- report",
     "clean": "npm-run-all -p clean:*",

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

@@ -1,5 +1,6 @@
 const contributors = [
   {
+    order: 1,
     sectionName: 'GROWI VILLAGE',
     additionalClass: '',
     memberGroups: [
@@ -47,6 +48,7 @@ const contributors = [
     ],
   },
   {
+    order: 10,
     sectionName: 'CONTRIBUTER',
     additionalClass: '',
     memberGroups: [
@@ -92,6 +94,7 @@ const contributors = [
     ],
   },
   {
+    order: 100,
     sectionName: 'VULNERABILITY HUNTER',
     additionalClass: '',
     memberGroups: [
@@ -111,6 +114,7 @@ const contributors = [
     ],
   },
   {
+    order: 200,
     sectionName: 'SPECIAL THANKS',
     additionalClass: '',
     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 { UncontrolledTooltip } from 'reactstrap';
 
+import { withUnstatedContainers } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
+
 /* eslint-disable react/prop-types */
 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`];
   if (editorMode === targetMode) {
@@ -20,6 +23,7 @@ const PageEditorModeButtonWrapper = React.memo(({
       type="button"
       className={classNames.join(' ')}
       onClick={() => { onClick(targetMode) }}
+      id={id}
     >
       <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>
@@ -32,9 +36,14 @@ const PageEditorModeButtonWrapper = React.memo(({
 
 function PageEditorModeManager(props) {
   const {
-    t, editorMode, onPageEditorModeButtonClicked, isBtnDisabled, isDeviceSmallerThanMd,
+    t, appContainer,
+    editorMode, onPageEditorModeButtonClicked, isBtnDisabled, isDeviceSmallerThanMd,
   } = 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) => {
     if (isBtnDisabled) {
@@ -73,7 +82,7 @@ function PageEditorModeManager(props) {
             label={t('Edit')}
           />
         )}
-        {(!isDeviceSmallerThanMd || editorMode === 'view') && (
+        {(!isDeviceSmallerThanMd || editorMode === 'view') && showHackmdBtn && (
           <PageEditorModeButtonWrapper
             editorMode={editorMode}
             isBtnDisabled={isBtnDisabled}
@@ -81,6 +90,7 @@ function PageEditorModeManager(props) {
             targetMode="hackmd"
             icon={<i className="fa fa-file-text-o" />}
             label={t('hackmd.hack_md')}
+            id="grw-page-editor-mode-manager-hackmd-button"
           />
         )}
       </div>
@@ -89,6 +99,11 @@ function PageEditorModeManager(props) {
           {t('Not available for guest')}
         </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 = {
   t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
   onPageEditorModeButtonClicked: PropTypes.func,
   isBtnDisabled: PropTypes.bool,
   editorMode: PropTypes.string,
@@ -107,4 +124,9 @@ PageEditorModeManager.defaultProps = {
   isDeviceSmallerThanMd: false,
 };
 
-export default withTranslation()(PageEditorModeManager);
+/**
+ * Wrapper component for using unstated
+ */
+const PageEditorModeManagerWrapper = withUnstatedContainers(PageEditorModeManager, [AppContainer]);
+
+export default withTranslation()(PageEditorModeManagerWrapper);

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

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

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

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

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

@@ -436,7 +436,7 @@ module.exports = (crowi) => {
     const options = { socketClientId };
 
     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 });
     }
     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;
+
+};