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

Merge branch 'master' into feat/6982-textlint

Steven Fukase 4 лет назад
Родитель
Сommit
29e4df55c2

+ 1 - 1
packages/app/src/components/PagePathHierarchicalLink.jsx

@@ -46,7 +46,7 @@ const PagePathHierarchicalLink = (props) => {
   const RootElm = ({ children }) => {
     return props.isInnerElem
       ? <>{children}</>
-      : <span className="grw-page-path-hierarchical-link d-inline-block text-break">{children}</span>;
+      : <span className="grw-page-path-hierarchical-link text-break">{children}</span>;
   };
 
   return (

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

@@ -61,7 +61,7 @@ const CustomSidebar = (props) => {
           Custom Sidebar
           <a className="h6 ml-2" href="/Sidebar"><i className="icon-pencil"></i></a>
         </h3>
-        <button type="button" className="btn btn-sm btn-outline-secondary ml-auto" onClick={fetchDataAndRenderHtml}>
+        <button type="button" className="btn btn-sm ml-auto grw-btn-reload-cs" onClick={fetchDataAndRenderHtml}>
           <i className="icon icon-reload"></i>
         </button>
       </div>

+ 138 - 30
packages/app/src/components/Sidebar/RecentChanges.jsx

@@ -10,6 +10,8 @@ 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';
@@ -17,6 +19,106 @@ import { toastError } from '~/client/util/apiNotification';
 import FormattedDistanceDate from '../FormattedDistanceDate';
 
 const logger = loggerFactory('growi:History');
+
+function PageItemLower({ page }) {
+  return (
+    <div className="d-flex justify-content-between grw-recent-changes-item-lower pt-1">
+      <div className="d-flex">
+        <div className="footstamp-icon mr-1 d-inline-block"><FootstampIcon /></div>
+        <div className="mr-2 grw-list-counts d-inline-block">{page.seenUsers.length}</div>
+        <div className="icon-bubble mr-1 d-inline-block"></div>
+        <div className="mr-2 grw-list-counts d-inline-block">{page.commentCount}</div>
+      </div>
+      <div className="grw-formatted-distance-date small mt-auto">
+        <FormattedDistanceDate id={page._id} date={page.updatedAt} />
+      </div>
+    </div>
+  );
+}
+PageItemLower.propTypes = {
+  page: PropTypes.any,
+};
+function LargePageItem({ page }) {
+  const dPagePath = new DevidedPagePath(page.path, false, true);
+  const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
+  const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
+  const FormerLink = () => (
+    <div className="grw-page-path-text-muted-container small">
+      <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} />
+    </div>
+  );
+
+  let locked;
+  if (page.grant !== 1) {
+    locked = <span><i className="icon-lock ml-2" /></span>;
+  }
+
+  const tags = page.tags;
+  const tagElements = tags.map((tag) => {
+    return (
+      <a key={tag.name} href={`/_search?q=tag:${tag.name}`} className="grw-tag-label badge badge-secondary mr-2 small">
+        {tag.name}
+      </a>
+    );
+  });
+
+  return (
+    <li className="list-group-item py-3 px-0">
+      <div className="d-flex w-100">
+        <UserPicture user={page.lastUpdateUser} size="md" noTooltip />
+        <div className="flex-grow-1 ml-2">
+          { !dPagePath.isRoot && <FormerLink /> }
+          <h5 className="my-2">
+            <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
+            {locked}
+          </h5>
+          <div className="grw-tag-labels mt-1 mb-2">
+            { tagElements }
+          </div>
+          <PageItemLower page={page} />
+        </div>
+      </div>
+    </li>
+  );
+}
+LargePageItem.propTypes = {
+  page: PropTypes.any,
+};
+
+function SmallPageItem({ page }) {
+  const dPagePath = new DevidedPagePath(page.path, false, true);
+  const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
+  const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
+  const FormerLink = () => (
+    <div className="grw-page-path-text-muted-container small">
+      <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} />
+    </div>
+  );
+
+  let locked;
+  if (page.grant !== 1) {
+    locked = <span><i className="icon-lock ml-2" /></span>;
+  }
+
+  return (
+    <li className="list-group-item py-2 px-0">
+      <div className="d-flex w-100">
+        <UserPicture user={page.lastUpdateUser} size="md" noTooltip />
+        <div className="flex-grow-1 ml-2">
+          { !dPagePath.isRoot && <FormerLink /> }
+          <h5 className="my-0">
+            <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
+            {locked}
+          </h5>
+          <PageItemLower page={page} />
+        </div>
+      </div>
+    </li>
+  );
+}
+SmallPageItem.propTypes = {
+  page: PropTypes.any,
+};
 class RecentChanges extends React.Component {
 
   static propTypes = {
@@ -26,10 +128,16 @@ class RecentChanges extends React.Component {
 
   constructor(props) {
     super(props);
-
+    this.state = {
+      isRecentChangesSidebarSmall: false,
+    };
     this.reloadData = this.reloadData.bind(this);
   }
 
+  componentWillMount() {
+    this.retrieveSizePreferenceFromLocalStorage();
+  }
+
   async componentDidMount() {
     this.reloadData();
   }
@@ -46,36 +154,22 @@ class RecentChanges extends React.Component {
     }
   }
 
-  PageItem = ({ page }) => {
-    const dPagePath = new DevidedPagePath(page.path, false, true);
-    const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
-    const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
-    const FormerLink = () => (
-      <div className="grw-page-path-text-muted-container small">
-        <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} />
-      </div>
-    );
+  retrieveSizePreferenceFromLocalStorage() {
+    if (window.localStorage.isRecentChangesSidebarSmall === 'true') {
+      this.setState({
+        isRecentChangesSidebarSmall: true,
+      });
+    }
+  }
 
-    return (
-      <li className="list-group-item p-2">
-        <div className="d-flex w-100">
-          <UserPicture user={page.lastUpdateUser} size="md" noTooltip />
-          <div className="flex-grow-1 ml-2">
-            { !dPagePath.isRoot && <FormerLink /> }
-            <h5 className="mb-1">
-              <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
-            </h5>
-            <div className="text-right small">
-              <FormattedDistanceDate id={page.id} date={page.updatedAt} />
-            </div>
-          </div>
-        </div>
-      </li>
-    );
+  changeSizeHandler = (e) => {
+    this.setState({
+      isRecentChangesSidebarSmall: e.target.checked,
+    });
+    window.localStorage.setItem('isRecentChangesSidebarSmall', e.target.checked);
   }
 
   render() {
-    const { PageItem } = this;
     const { t } = this.props;
     const { recentlyUpdatedPages } = this.props.appContainer.state;
 
@@ -84,13 +178,26 @@ class RecentChanges extends React.Component {
         <div className="grw-sidebar-content-header p-3 d-flex">
           <h3 className="mb-0">{t('Recent Changes')}</h3>
           {/* <h3 className="mb-0">{t('Recent Created')}</h3> */} {/* TODO: impl switching */}
-          <button type="button" className="btn btn-sm btn-outline-secondary ml-auto" onClick={this.reloadData}>
+          <button type="button" className="btn btn-sm ml-auto grw-btn-reload-rc" onClick={this.reloadData}>
             <i className="icon icon-reload"></i>
           </button>
+          <div className="grw-recent-changes-resize-button custom-control custom-switch ml-2">
+            <input
+              id="recentChangesResize"
+              className="custom-control-input"
+              type="checkbox"
+              checked={this.state.isRecentChangesSidebarSmall}
+              onChange={e => this.setState({ isRecentChangesSidebarSmall: e.target.checked })}
+            />
+            <label className="custom-control-label" htmlFor="recentChangesResize">
+            </label>
+          </div>
         </div>
-        <div className="grw-sidebar-content-body p-3">
+        <div className="grw-sidebar-content-body grw-recent-changes p-3">
           <ul className="list-group list-group-flush">
-            { recentlyUpdatedPages.map(page => <PageItem key={page.id} page={page} />) }
+            {recentlyUpdatedPages.map(page => (this.state.isRecentChangesSidebarSmall
+              ? <SmallPageItem key={page._id} page={page} />
+              : <LargePageItem key={page._id} page={page} />))}
           </ul>
         </div>
       </>
@@ -104,4 +211,5 @@ class RecentChanges extends React.Component {
  */
 const RecentChangesWrapper = withUnstatedContainers(RecentChanges, [AppContainer]);
 
+
 export default withTranslation()(RecentChangesWrapper);

+ 1 - 1
packages/app/src/server/models/page.js

@@ -772,7 +772,7 @@ module.exports = function(crowi) {
     // find
     builder.addConditionToPagenate(opt.offset, opt.limit, sortOpt);
     builder.populateDataToList(User.USER_FIELDS_EXCEPT_CONFIDENTIAL);
-    const pages = await builder.query.exec('find');
+    const pages = await builder.query.lean().exec('find');
 
     const result = {
       pages, totalCount, offset: opt.offset, limit: opt.limit,

+ 21 - 0
packages/app/src/server/routes/apiv3/pages.js

@@ -4,6 +4,7 @@ import loggerFactory from '~/utils/logger';
 const logger = loggerFactory('growi:routes:apiv3:pages'); // eslint-disable-line no-unused-vars
 const express = require('express');
 const pathUtils = require('growi-commons').pathUtils;
+const mongoose = require('mongoose');
 
 const { body } = require('express-validator');
 const { query } = require('express-validator');
@@ -358,6 +359,26 @@ module.exports = (crowi) => {
         }
       });
 
+      const PageTagRelation = mongoose.model('PageTagRelation');
+      const ids = result.pages.map((page) => { return page._id });
+      const relations = await PageTagRelation.find({ relatedPage: { $in: ids } }).populate('relatedTag');
+
+      // { pageId: [{ tag }, ...] }
+      const relationsMap = new Map();
+      // increment relationsMap
+      relations.forEach((relation) => {
+        const pageId = relation.relatedPage.toString();
+        if (!relationsMap.has(pageId)) {
+          relationsMap.set(pageId, []);
+        }
+        relationsMap.get(pageId).push(relation.relatedTag);
+      });
+      // add tags to each page
+      result.pages.forEach((page) => {
+        const pageId = page._id.toString();
+        page.tags = relationsMap.has(pageId) ? relationsMap.get(pageId) : [];
+      });
+
       return res.apiv3(result);
     }
     catch (err) {

+ 49 - 0
packages/app/src/styles/_recent-changes.scss

@@ -0,0 +1,49 @@
+.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);
+
+    .custom-control-label::before {
+      padding-left: 16px;
+      content: 'L';
+    }
+
+    .custom-control-input:checked + .custom-control-label::before {
+      padding-left: 5px;
+      content: 'S';
+    }
+  }
+}
+
+.list-group {
+  .list-group-item {
+    .grw-recent-changes-item-lower {
+      height: 17.5px;
+    }
+    .footstamp-icon {
+      svg {
+        width: 14px;
+        height: 14px;
+        transform: translateY(-3.5px);
+      }
+    }
+
+    .grw-list-counts {
+      height: 14px;
+      font-size: 12px;
+    }
+
+    .grw-formatted-distance-date {
+      font-size: 10px;
+    }
+
+    .icon-lock {
+      font-size: 14px;
+    }
+  }
+}

+ 4 - 0
packages/app/src/styles/_sidebar.scss

@@ -133,6 +133,10 @@
   .grw-drawer-toggler {
     display: none; // invisible in default
   }
+
+  .grw-sidebar-content-header {
+    min-width: $grw-sidebar-content-min-width + 20px;
+  }
 }
 
 // Dock Mode

+ 8 - 2
packages/app/src/styles/_tag.scss

@@ -6,9 +6,9 @@
 
 .grw-tag-labels {
   .grw-tag-label {
-    margin-left: 1px;
     font-size: 12px;
-    border-radius: $border-radius-xl;
+    font-weight: normal;
+    border-radius: $border-radius-sm;
   }
 }
 
@@ -17,3 +17,9 @@
     height: auto;
   }
 }
+
+.grw-recent-changes {
+  .grw-tag-label {
+    font-size: 10px;
+  }
+}

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

@@ -59,6 +59,7 @@
 @import 'page';
 @import 'page-presentation';
 @import 'page-history';
+@import 'recent-changes';
 @import 'search';
 @import 'shortcuts';
 @import 'sidebar';

+ 1 - 1
packages/app/src/styles/theme/_apply-colors-light.scss

@@ -12,7 +12,7 @@ $border-color-table: $gray-200 !default;
 $color-table-hover: $color-table !default;
 $bgcolor-table-hover: rgba(black, 0.075) !default;
 $bgcolor-sidebar-list-group: $bgcolor-list !default;
-$color-tags: $gray-500 !default;
+$color-tags: $secondary !default;
 $bgcolor-tags: $gray-200 !default;
 $border-color-global: $gray-300 !default;
 $border-color-toc: $border-color-global !default;

+ 54 - 0
packages/app/src/styles/theme/_apply-colors.scss

@@ -15,6 +15,8 @@ $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;
 
 // override bootstrap variables
 $body-bg: $bgcolor-global;
@@ -263,6 +265,58 @@ 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-recent-changes-resize-button {
+      .custom-control-label::before {
+        background-color: $primary;
+      }
+
+      .custom-control-label::after {
+        background-color: $bgcolor-global;
+      }
+
+      .custom-control-input:not(:checked) + .custom-control-label::before {
+        color: $bgcolor-global;
+      }
+
+      .custom-control-input:checked + .custom-control-label::before {
+        color: $bgcolor-global;
+        background-color: $primary;
+        // border-color: $primary !important;
+      }
+      .custom-control-input:checked + .custom-control-label::after {
+        color: $bgcolor-global;
+      }
+    }
+  }
+
+  .grw-recent-changes {
+    .list-group {
+      .list-group-item {
+        background-color: transparent;
+
+        .icon-lock {
+          color: $color-link;
+        }
+
+        .grw-recent-changes-item-lower {
+          color: $gray-500;
+
+          svg {
+            fill: $gray-500;
+          }
+        }
+      }
+    }
+  }
 }
 
 /*