فهرست منبع

Merge branch 'dev/5.0.x' into feat/80324-adjust-design-for-left-pane

* dev/5.0.x: (22 commits)
  fix page tree path color
  Renamed props
  change muted-color for dark theme
  apply secondary color to isEmptyPage
  fix sass style
  remove duplicate code
  apply active color
  change variable color
  clean code
  change colors for light and dark themes
  improve colors
  Not show dropdown item when guest user
  add grw-pagetree class
  using new svg file
  added a todo comment
  Not show control when guest user
  loginRequiredStrictly -> loginRequired
  add active color
  apply hover color to page-tree-item background
  apply color
  ...
Mao 4 سال پیش
والد
کامیت
bd3147f3fc

+ 38 - 20
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -6,13 +6,14 @@ import { useTranslation } from 'react-i18next';
 import { IPageHasId } from '~/interfaces/page';
 
 type PageItemControlProps = {
-  page: Partial<IPageHasId>,
-  onClickDeleteButton?: (pageId: string) => void,
+  page: Partial<IPageHasId>
+  isEnableActions: boolean
+  onClickDeleteButton?: (pageId: string) => void
 }
 
 const PageItemControl: FC<PageItemControlProps> = (props: PageItemControlProps) => {
 
-  const { page, onClickDeleteButton } = props;
+  const { page, isEnableActions, onClickDeleteButton } = props;
   const { t } = useTranslation('');
 
   const deleteButtonHandler = () => {
@@ -48,23 +49,40 @@ const PageItemControl: FC<PageItemControlProps> = (props: PageItemControlProps)
           TODO: add function to the following buttons like using modal or others
           ref: https://estoc.weseek.co.jp/redmine/issues/79026
         */}
-        <button className="dropdown-item" type="button" onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
-          <i className="icon-fw icon-star"></i>
-          {t('Add to bookmark')}
-        </button>
-        <button className="dropdown-item" type="button" onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
-          <i className="icon-fw icon-docs"></i>
-          {t('Duplicate')}
-        </button>
-        <button className="dropdown-item" type="button" onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
-          <i className="icon-fw  icon-action-redo"></i>
-          {t('Move/Rename')}
-        </button>
-        <div className="dropdown-divider"></div>
-        <button className="dropdown-item text-danger pt-2" type="button" onClick={deleteButtonHandler}>
-          <i className="icon-fw icon-trash"></i>
-          {t('Delete')}
-        </button>
+
+        {/* TODO: show dropdown when permalink section is implemented */}
+        {!isEnableActions && (
+          <p className="dropdown-item">
+            {t('search_result.currently_not_implemented')}
+          </p>
+        )}
+        {isEnableActions && (
+          <button className="dropdown-item" type="button" onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
+            <i className="icon-fw icon-star"></i>
+            {t('Add to bookmark')}
+          </button>
+        )}
+        {isEnableActions && (
+          <button className="dropdown-item" type="button" onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
+            <i className="icon-fw icon-docs"></i>
+            {t('Duplicate')}
+          </button>
+        )}
+        {isEnableActions && (
+          <button className="dropdown-item" type="button" onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
+            <i className="icon-fw  icon-action-redo"></i>
+            {t('Move/Rename')}
+          </button>
+        )}
+        {isEnableActions && (
+          <>
+            <div className="dropdown-divider"></div>
+            <button className="dropdown-item text-danger pt-2" type="button" onClick={deleteButtonHandler}>
+              <i className="icon-fw icon-trash"></i>
+              {t('Delete')}
+            </button>
+          </>
+        )}
       </div>
     </>
   );

+ 17 - 0
packages/app/src/components/Icons/TriangleIcon.tsx

@@ -0,0 +1,17 @@
+import React from 'react';
+
+const TriangleIcon = (): JSX.Element => (
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    width="12"
+    height="12"
+    viewBox="0 0 12 12"
+  >
+    <g transform="translate(18194 -6790)">
+      <rect width="12" height="12" transform="translate(-18194 6790)" fill="none" />
+      <path d="M5.2,1.067a1,1,0,0,1,1.6,0l4,5.333A1,1,0,0,1,10,8H2a1,1,0,0,1-.8-1.6Z" transform="translate(-18183 6790) rotate(90)" />
+    </g>
+  </svg>
+);
+
+export default TriangleIcon;

+ 18 - 2
packages/app/src/components/SearchPage.jsx

@@ -13,6 +13,7 @@ import SearchResultList from './SearchPage/SearchResultList';
 import SearchControl from './SearchPage/SearchControl';
 import { CheckboxType, SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
 import PageDeleteModal from './PageDeleteModal';
+import { useIsGuestUser } from '~/stores/context';
 
 export const specificPathNames = {
   user: '/user',
@@ -283,6 +284,7 @@ class SearchPage extends React.Component {
     return (
       <SearchResultList
         pages={this.state.searchResults || []}
+        isEnableActions={!this.props.isGuestUser}
         focusedSearchResultData={this.state.focusedSearchResultData}
         selectedPagesIdList={this.state.selectedPagesIdList || []}
         searchResultCount={this.state.searchResultCount}
@@ -346,16 +348,30 @@ class SearchPage extends React.Component {
 /**
  * Wrapper component for using unstated
  */
-const SearchPageWrapper = withUnstatedContainers(SearchPage, [AppContainer]);
+const SearchPageHOCWrapper = withTranslation()(withUnstatedContainers(SearchPage, [AppContainer]));
 
 SearchPage.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   query: PropTypes.object,
+  isGuestUser: PropTypes.bool.isRequired,
 };
 SearchPage.defaultProps = {
   // pollInterval: 1000,
   query: SearchPage.getQueryByLocation(window.location || {}),
 };
 
-export default withTranslation()(SearchPageWrapper);
+const SearchPageFCWrapper = (props) => {
+  const { data: isGuestUser } = useIsGuestUser();
+
+  /*
+   * dependencies
+   */
+  if (isGuestUser == null) {
+    return null;
+  }
+
+  return <SearchPageHOCWrapper {...props} isGuestUser={isGuestUser} />;
+};
+
+export default SearchPageFCWrapper;

+ 3 - 1
packages/app/src/components/SearchPage/SearchResultList.tsx

@@ -7,6 +7,7 @@ import { IPageSearchResultData } from '../../interfaces/search';
 type Props = {
   pages: IPageSearchResultData[],
   selectedPagesIdList: Set<string>
+  isEnableActions: boolean,
   searchResultCount?: number,
   activePage?: number,
   pagingLimit?: number,
@@ -19,7 +20,7 @@ type Props = {
 }
 
 const SearchResultList: FC<Props> = (props:Props) => {
-  const { focusedSearchResultData, selectedPagesIdList } = props;
+  const { focusedSearchResultData, selectedPagesIdList, isEnableActions } = props;
 
   const focusedPageId = (focusedSearchResultData != null && focusedSearchResultData.pageData != null) ? focusedSearchResultData.pageData._id : '';
   return (
@@ -31,6 +32,7 @@ const SearchResultList: FC<Props> = (props:Props) => {
           <SearchResultListItem
             key={page.pageData._id}
             page={page}
+            isEnableActions={isEnableActions}
             onClickSearchResultItem={props.onClickSearchResultItem}
             onClickCheckbox={props.onClickCheckbox}
             isChecked={isChecked}

+ 3 - 2
packages/app/src/components/SearchPage/SearchResultListItem.tsx

@@ -13,6 +13,7 @@ type Props = {
   page: IPageSearchResultData,
   isSelected: boolean,
   isChecked: boolean,
+  isEnableActions: boolean,
   onClickCheckbox?: (pageId: string) => void,
   onClickSearchResultItem?: (pageId: string) => void,
   onClickDeleteButton?: (pageId: string) => void,
@@ -21,7 +22,7 @@ type Props = {
 const SearchResultListItem: FC<Props> = (props:Props) => {
   const {
     // todo: refactoring variable name to clear what changed
-    page: { pageData, pageMeta }, isSelected, onClickSearchResultItem, onClickCheckbox, isChecked,
+    page: { pageData, pageMeta }, isSelected, onClickSearchResultItem, onClickCheckbox, isChecked, isEnableActions,
   } = props;
 
   // Add prefix 'id_' in pageId, because scrollspy of bootstrap doesn't work when the first letter of id attr of target component is numeral.
@@ -77,7 +78,7 @@ const SearchResultListItem: FC<Props> = (props:Props) => {
               </div>
               {/* doropdown icon includes page control buttons */}
               <div className="ml-auto">
-                <PageItemControl page={pageData} onClickDeleteButton={props.onClickDeleteButton} />
+                <PageItemControl page={pageData} onClickDeleteButton={props.onClickDeleteButton} isEnableActions={isEnableActions} />
               </div>
             </div>
             <div className="my-2 search-result-list-snippet">

+ 14 - 3
packages/app/src/components/Sidebar/PageTree.tsx

@@ -2,7 +2,9 @@ import React, { FC, memo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
-import { useCurrentPagePath, useCurrentPageId, useTargetAndAncestors } from '~/stores/context';
+import {
+  useCurrentPagePath, useCurrentPageId, useTargetAndAncestors, useIsGuestUser,
+} from '~/stores/context';
 
 import ItemsTree from './PageTree/ItemsTree';
 import PrivateLegacyPages from './PageTree/PrivateLegacyPages';
@@ -12,16 +14,24 @@ import { IPageForPageDeleteModal } from '../PageDeleteModal';
 const PageTree: FC = memo(() => {
   const { t } = useTranslation();
 
+  const { data: isGuestUser } = useIsGuestUser();
   const { data: currentPath } = useCurrentPagePath();
   const { data: targetId } = useCurrentPageId();
   const { data: targetAndAncestorsData } = useTargetAndAncestors();
 
-  const { data: migrationStatus } = useSWRxV5MigrationStatus();
+  const { data: migrationStatus } = useSWRxV5MigrationStatus(!isGuestUser);
 
   // for delete modal
   const [isDeleteModalOpen, setDeleteModalOpen] = useState(false);
   const [pagesToDelete, setPagesToDelete] = useState<IPageForPageDeleteModal[]>([]);
 
+  /*
+   * dependencies
+   */
+  if (isGuestUser == null) {
+    return null;
+  }
+
   const onClickDeleteByPage = (page: IPageForPageDeleteModal) => {
     setDeleteModalOpen(true);
     setPagesToDelete([page]);
@@ -41,6 +51,7 @@ const PageTree: FC = memo(() => {
 
       <div className="grw-sidebar-content-body">
         <ItemsTree
+          isEnableActions={!isGuestUser}
           targetPath={path}
           targetId={targetId}
           targetAndAncestorsData={targetAndAncestorsData}
@@ -55,7 +66,7 @@ const PageTree: FC = memo(() => {
 
       <div className="grw-sidebar-content-footer">
         {
-          migrationStatus?.migratablePagesCount != null && migrationStatus.migratablePagesCount !== 0 && (
+          !isGuestUser && migrationStatus?.migratablePagesCount != null && migrationStatus.migratablePagesCount !== 0 && (
             <PrivateLegacyPages />
           )
         }

+ 21 - 11
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -11,8 +11,11 @@ import ClosableTextInput, { AlertInfo, AlertType } from '../../Common/ClosableTe
 import PageItemControl from '../../Common/Dropdown/PageItemControl';
 import { IPageForPageDeleteModal } from '~/components/PageDeleteModal';
 
+import TriangleIcon from '~/components/Icons/TriangleIcon';
+
 
 interface ItemProps {
+  isEnableActions: boolean
   itemNode: ItemNode
   targetId?: string
   isOpen?: boolean
@@ -35,6 +38,7 @@ const markTarget = (children: ItemNode[], targetId?: string): void => {
 
 type ItemControlProps = {
   page: Partial<IPageHasId>
+  isEnableActions: boolean
   onClickDeleteButtonHandler?(): void
   onClickPlusButtonHandler?(): void
 }
@@ -62,7 +66,7 @@ const ItemControl: FC<ItemControlProps> = memo((props: ItemControlProps) => {
 
   return (
     <>
-      <PageItemControl page={props.page} onClickDeleteButton={onClickDeleteButton} />
+      <PageItemControl page={props.page} onClickDeleteButton={onClickDeleteButton} isEnableActions={props.isEnableActions} />
       <button
         type="button"
         className="btn-link nav-link border-0 rounded grw-btn-page-management py-0"
@@ -87,7 +91,7 @@ const ItemCount: FC = () => {
 const Item: FC<ItemProps> = (props: ItemProps) => {
   const { t } = useTranslation();
   const {
-    itemNode, targetId, isOpen: _isOpen = false, onClickDeleteByPage,
+    itemNode, targetId, isOpen: _isOpen = false, onClickDeleteByPage, isEnableActions,
   } = props;
 
   const { page, children } = itemNode;
@@ -183,10 +187,12 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
           className={`grw-pagetree-button btn ${buttonClass}`}
           onClick={onClickLoadChildren}
         >
-          <i className="icon-control-play"></i>
+          <div className="grw-triangle-icon">
+            <TriangleIcon />
+          </div>
         </button>
         <a href={page._id} className="grw-pagetree-title-anchor flex-grow-1">
-          <p className="grw-pagetree-title m-auto">{nodePath.basename(page.path as string) || '/'}</p>
+          <p className={`grw-pagetree-title m-auto ${page.isEmpty && 'text-muted'}`}>{nodePath.basename(page.path as string) || '/'}</p>
         </a>
         <div className="grw-pagetree-count-wrapper">
           <ItemCount />
@@ -196,21 +202,25 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
             page={page}
             onClickDeleteButtonHandler={onClickDeleteButtonHandler}
             onClickPlusButtonHandler={() => { setNewPageInputShown(true) }}
+            isEnableActions={isEnableActions}
           />
         </div>
       </div>
 
-      <ClosableTextInput
-        isShown={isNewPageInputShown}
-        placeholder={t('Input title')}
-        onClickOutside={() => { setNewPageInputShown(false) }}
-        onPressEnter={onPressEnterHandler}
-        inputValidator={inputValidator}
-      />
+      {!isEnableActions && (
+        <ClosableTextInput
+          isShown={isNewPageInputShown}
+          placeholder={t('Input title')}
+          onClickOutside={() => { setNewPageInputShown(false) }}
+          onPressEnter={onPressEnterHandler}
+          inputValidator={inputValidator}
+        />
+      )}
       {
         isOpen && hasChildren() && currentChildren.map(node => (
           <Item
             key={node.page._id}
+            isEnableActions={isEnableActions}
             itemNode={node}
             isOpen={false}
             onClickDeleteByPage={onClickDeleteByPage}

+ 13 - 5
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -43,6 +43,7 @@ const generateInitialNodeAfterResponse = (ancestorsChildren: Record<string, Part
 };
 
 type ItemsTreeProps = {
+  isEnableActions: boolean
   targetPath: string
   targetId?: string
   targetAndAncestorsData?: TargetAndAncestors
@@ -57,11 +58,18 @@ type ItemsTreeProps = {
 }
 
 const renderByInitialNode = (
-    initialNode: ItemNode, DeleteModal: JSX.Element, targetId?: string, onClickDeleteByPage?: (page: IPageForPageDeleteModal) => void,
+    initialNode: ItemNode, DeleteModal: JSX.Element, isEnableActions: boolean, targetId?: string, onClickDeleteByPage?: (page: IPageForPageDeleteModal) => void,
 ): JSX.Element => {
   return (
     <div className="grw-pagetree p-3">
-      <Item key={initialNode.page.path} targetId={targetId} itemNode={initialNode} isOpen onClickDeleteByPage={onClickDeleteByPage} />
+      <Item
+        key={initialNode.page.path}
+        targetId={targetId}
+        itemNode={initialNode}
+        isOpen
+        isEnableActions={isEnableActions}
+        onClickDeleteByPage={onClickDeleteByPage}
+      />
       {DeleteModal}
     </div>
   );
@@ -74,7 +82,7 @@ const renderByInitialNode = (
 const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
   const {
     targetPath, targetId, targetAndAncestorsData, isDeleteModalOpen, pagesToDelete, isAbleToDeleteCompletely, isDeleteCompletelyModal, onCloseDelete,
-    onClickDeleteByPage,
+    onClickDeleteByPage, isEnableActions,
   } = props;
 
   const { data: ancestorsChildrenData, error: error1 } = useSWRxPageAncestorsChildren(targetPath);
@@ -101,7 +109,7 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
    */
   if (ancestorsChildrenData != null && rootPageData != null) {
     const initialNode = generateInitialNodeAfterResponse(ancestorsChildrenData.ancestorsChildren, new ItemNode(rootPageData.rootPage));
-    return renderByInitialNode(initialNode, DeleteModal, targetId, onClickDeleteByPage);
+    return renderByInitialNode(initialNode, DeleteModal, isEnableActions, targetId, onClickDeleteByPage);
   }
 
   /*
@@ -109,7 +117,7 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
    */
   if (targetAndAncestorsData != null) {
     const initialNode = generateInitialNodeBeforeResponse(targetAndAncestorsData.targetAndAncestors);
-    return renderByInitialNode(initialNode, DeleteModal, targetId, onClickDeleteByPage);
+    return renderByInitialNode(initialNode, DeleteModal, isEnableActions, targetId, onClickDeleteByPage);
   }
 
   return null;

+ 4 - 5
packages/app/src/server/routes/apiv3/page-listing.ts

@@ -34,14 +34,13 @@ const validator = {
  */
 export default (crowi: Crowi): Router => {
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
-  // Do not use loginRequired with isGuestAllowed true since page tree may show private page titles
-  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
+  const loginRequired = require('../../middlewares/login-required')(crowi, true);
   const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
 
   const router = express.Router();
 
 
-  router.get('/root', accessTokenParser, loginRequiredStrictly, async(req: AuthorizedRequest, res: ApiV3Response) => {
+  router.get('/root', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
     const Page: PageModel = crowi.model('Page');
 
     let rootPage;
@@ -56,7 +55,7 @@ export default (crowi: Crowi): Router => {
   });
 
   // eslint-disable-next-line max-len
-  router.get('/ancestors-children', accessTokenParser, loginRequiredStrictly, ...validator.pagePathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response): Promise<any> => {
+  router.get('/ancestors-children', accessTokenParser, loginRequired, ...validator.pagePathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response): Promise<any> => {
     const { path } = req.query;
 
     const Page: PageModel = crowi.model('Page');
@@ -76,7 +75,7 @@ export default (crowi: Crowi): Router => {
    * In most cases, using id should be prioritized
    */
   // eslint-disable-next-line max-len
-  router.get('/children', accessTokenParser, loginRequiredStrictly, validator.pageIdOrPathRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
+  router.get('/children', accessTokenParser, loginRequired, validator.pageIdOrPathRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
     const { id, path } = req.query;
 
     const Page: PageModel = crowi.model('Page');

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

@@ -45,9 +45,11 @@ export const useSWRxPageChildren = (
   );
 };
 
-export const useSWRxV5MigrationStatus = (): SWRResponse<V5MigrationStatus, Error> => {
+export const useSWRxV5MigrationStatus = (
+    shouldFetch = true,
+): SWRResponse<V5MigrationStatus, Error> => {
   return useSWR(
-    '/pages/v5-migration-status',
+    shouldFetch ? '/pages/v5-migration-status' : null,
     endpoint => apiv3Get(endpoint).then((response) => {
       return {
         migratablePagesCount: response.data.migratablePagesCount,

+ 1 - 1
packages/app/src/styles/_override-bootstrap-variables.scss

@@ -18,6 +18,7 @@ $gray-200: $light !default;
 $gray-300: darken($light, 5%) !default;
 $gray-400: darken($light, 20%) !default;
 $gray-500: darken($light, 30%) !default;
+$gray-550: lighten($dark, 15%) !default;
 $gray-600: lighten($dark, 10%) !default;
 $gray-700: lighten($dark, 5%) !default;
 $gray-800: $dark !default;
@@ -64,7 +65,6 @@ $font-family-base:        $font-family-sans-serif;
 $font-size-root: 14px;
 $line-height-base: 1.42857;
 
-$text-muted: $gray-500;
 $blockquote-small-color: $gray-500;
 
 

+ 2 - 0
packages/app/src/styles/_page-tree.scss

@@ -28,6 +28,8 @@
     .grw-pagetree-title-anchor {
       width: 100%;
       overflow: hidden;
+      color: inherit;
+      text-decoration: none;
 
       .grw-pagetree-title {
         overflow: hidden;

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

@@ -18,6 +18,7 @@ $border-color-global: $gray-500 !default;
 $border-color-toc: $border-color-global !default;
 
 // override bootstrap variables
+$text-muted: $gray-550;
 $table-dark-color: $color-table;
 $table-dark-bg: $bgcolor-table;
 $table-dark-border-color: $border-color-table;
@@ -25,6 +26,7 @@ $table-dark-hover-color: $color-table-hover;
 $table-dark-hover-bg: $bgcolor-table-hover;
 $border-color: $border-color-global;
 
+@import 'reboot-bootstrap-text';
 @import 'reboot-bootstrap-border-colors';
 @import 'reboot-bootstrap-tables';
 
@@ -249,6 +251,23 @@ ul.pagination {
 .grw-sidebar {
   // List
   @include override-list-group-item($color-list, $bgcolor-sidebar-list-group, $color-list-hover, $bgcolor-list-hover, $color-list-active, $bgcolor-list-active);
+
+  // Pagetree
+  .grw-pagetree {
+    .grw-pagetree-item {
+      .grw-triangle-icon {
+        svg {
+          fill: $gray-500;
+        }
+      }
+      &:hover {
+        background: $bgcolor-list-hover;
+      }
+      &:active {
+        background: lighten($bgcolor-list-hover, 5%);
+      }
+    }
+  }
 }
 
 /*

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

@@ -2,8 +2,8 @@
 $color-list: $color-global !default;
 $bgcolor-list: $bgcolor-global !default;
 $color-list-hover: $color-global !default;
-$bgcolor-list-hover: darken($bgcolor-global, 3%) !default;
-$bgcolor-list-active: $primary !default;
+$bgcolor-list-hover: lighten($primary, 72%) !default;
+$bgcolor-list-active: lighten($primary, 65%) !default;
 $color-list-active: color-yiq($bgcolor-list-active) !default;
 $bgcolor-subnav: darken($bgcolor-global, 3%) !default;
 $color-table: $color-global !default;
@@ -18,6 +18,7 @@ $border-color-global: $gray-300 !default;
 $border-color-toc: $border-color-global !default;
 
 // override bootstrap variables
+$text-muted: $gray-500;
 $table-color: $color-table;
 $table-bg: $bgcolor-table;
 $table-border-color: $border-color-table;
@@ -25,6 +26,7 @@ $table-hover-color: $color-table-hover;
 $table-hover-bg: $bgcolor-table-hover;
 $border-color: $border-color-global;
 
+@import 'reboot-bootstrap-text';
 @import 'reboot-bootstrap-border-colors';
 @import 'reboot-bootstrap-tables';
 
@@ -166,6 +168,23 @@ $border-color: $border-color-global;
 .grw-sidebar {
   // List
   @include override-list-group-item($color-list, $bgcolor-sidebar-list-group, $color-list-hover, $bgcolor-list-hover, $color-list-active, $bgcolor-list-active);
+
+  // Pagetree
+  .grw-pagetree {
+    .grw-pagetree-item {
+      .grw-triangle-icon {
+        svg {
+          fill: $gray-400;
+        }
+      }
+      &:hover {
+        background: $bgcolor-list-hover;
+      }
+      &:active {
+        background: $bgcolor-list-active;
+      }
+    }
+  }
 }
 
 /*

+ 3 - 0
packages/app/src/styles/theme/_reboot-bootstrap-text.scss

@@ -0,0 +1,3 @@
+.text-muted {
+  color: $text-muted !important;
+}