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

Highlight target page even when empty

Taichi Masuyama 4 лет назад
Родитель
Сommit
ddd445ac43

+ 5 - 1
packages/app/src/client/services/ContextExtractor.tsx

@@ -5,7 +5,8 @@ import {
   useCreatedAt, useDeleteUsername, useDeletedAt, useHasChildren, useHasDraftOnHackmd, useIsAbleToDeleteCompletely,
   useIsDeletable, useIsDeleted, useIsNotCreatable, useIsPageExist, useIsTrashPage, useIsUserPage, useLastUpdateUsername,
   useCurrentPageId, usePageIdOnHackmd, usePageUser, useCurrentPagePath, useRevisionCreatedAt, useRevisionId, useRevisionIdHackmdSynced,
-  useShareLinkId, useShareLinksNumber, useTemplateTagData, useUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser, useTargetAndAncestors, useSlackChannels,
+  useShareLinkId, useShareLinksNumber, useTemplateTagData, useUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser, useTargetAndAncestors,
+  useSlackChannels, useNotFoundTargetPathOrId,
 } from '../../stores/context';
 import {
   useIsDeviceSmallerThanMd,
@@ -20,6 +21,7 @@ const jsonNull = 'null';
 const ContextExtractorOnce: FC = () => {
 
   const mainContent = document.querySelector('#content-main');
+  const notFoundContent = document.getElementById('growi-pagetree-not-found-context');
 
   /*
    * App Context from DOM
@@ -61,6 +63,7 @@ const ContextExtractorOnce: FC = () => {
   const creator = JSON.parse(mainContent?.getAttribute('data-page-creator') || jsonNull);
   const revisionAuthor = JSON.parse(mainContent?.getAttribute('data-page-revision-author') || jsonNull);
   const targetAndAncestors = JSON.parse(document.getElementById('growi-pagetree-target-and-ancestors')?.textContent || jsonNull);
+  const notFoundTargetPathOrId = JSON.parse(notFoundContent?.getAttribute('data-not-found-target-path-or-id') || jsonNull);
   const slackChannels = mainContent?.getAttribute('data-slack-channels') || '';
 
   /*
@@ -104,6 +107,7 @@ const ContextExtractorOnce: FC = () => {
   useCreator(creator);
   useRevisionAuthor(revisionAuthor);
   useTargetAndAncestors(targetAndAncestors);
+  useNotFoundTargetPathOrId(notFoundTargetPathOrId);
 
   // Navigation
   usePreferDrawerModeByUser();

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

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
 
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 import {
-  useCurrentPagePath, useCurrentPageId, useTargetAndAncestors, useIsGuestUser,
+  useCurrentPagePath, useCurrentPageId, useTargetAndAncestors, useIsGuestUser, useNotFoundTargetPathOrId,
 } from '~/stores/context';
 
 import ItemsTree from './PageTree/ItemsTree';
@@ -18,6 +18,7 @@ const PageTree: FC = memo(() => {
   const { data: currentPath } = useCurrentPagePath();
   const { data: targetId } = useCurrentPageId();
   const { data: targetAndAncestorsData } = useTargetAndAncestors();
+  const { data: notFoundTargetPathOrId } = useNotFoundTargetPathOrId();
 
   const { data: migrationStatus } = useSWRxV5MigrationStatus();
 
@@ -25,6 +26,8 @@ const PageTree: FC = memo(() => {
   const [isDeleteModalOpen, setDeleteModalOpen] = useState(false);
   const [pagesToDelete, setPagesToDelete] = useState<IPageForPageDeleteModal[]>([]);
 
+  const targetPathOrId = targetId || notFoundTargetPathOrId;
+
   if (migrationStatus == null) {
     return (
       <>
@@ -81,7 +84,7 @@ const PageTree: FC = memo(() => {
         <ItemsTree
           isEnableActions={!isGuestUser}
           targetPath={path}
-          targetId={targetId}
+          targetPathOrId={targetPathOrId}
           targetAndAncestorsData={targetAndAncestorsData}
           isDeleteModalOpen={isDeleteModalOpen}
           pagesToDelete={pagesToDelete}

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

@@ -21,19 +21,19 @@ const { isTopPage } = pagePathUtils;
 interface ItemProps {
   isEnableActions: boolean
   itemNode: ItemNode
-  targetId?: string
+  targetPathOrId?: string
   isOpen?: boolean
   onClickDeleteByPage?(page: IPageForPageDeleteModal): void
 }
 
 // Utility to mark target
-const markTarget = (children: ItemNode[], targetId?: string): void => {
-  if (targetId == null) {
+const markTarget = (children: ItemNode[], targetPathOrId?: string): void => {
+  if (targetPathOrId == null) {
     return;
   }
 
   children.forEach((node) => {
-    if (node.page._id === targetId) {
+    if (node.page._id === targetPathOrId || node.page.path === targetPathOrId) {
       node.page.isTarget = true;
     }
     return node;
@@ -96,7 +96,7 @@ const ItemCount: FC = () => {
 const Item: FC<ItemProps> = (props: ItemProps) => {
   const { t } = useTranslation();
   const {
-    itemNode, targetId, isOpen: _isOpen = false, onClickDeleteByPage, isEnableActions,
+    itemNode, targetPathOrId, isOpen: _isOpen = false, onClickDeleteByPage, isEnableActions,
   } = props;
 
   const { page, children } = itemNode;
@@ -162,7 +162,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
    */
   useEffect(() => {
     if (children.length > currentChildren.length) {
-      markTarget(children, targetId);
+      markTarget(children, targetPathOrId);
       setCurrentChildren(children);
     }
   }, []);
@@ -173,7 +173,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   useEffect(() => {
     if (isOpen && error == null && data != null) {
       const newChildren = ItemNode.generateNodesFromPages(data.children);
-      markTarget(newChildren, targetId);
+      markTarget(newChildren, targetPathOrId);
       setCurrentChildren(newChildren);
     }
   }, [data, isOpen]);
@@ -223,7 +223,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
               isEnableActions={isEnableActions}
               itemNode={node}
               isOpen={false}
-              targetId={targetId}
+              targetPathOrId={targetPathOrId}
               onClickDeleteByPage={onClickDeleteByPage}
             />
           </div>

+ 7 - 6
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -50,7 +50,7 @@ const generateInitialNodeAfterResponse = (ancestorsChildren: Record<string, Part
 type ItemsTreeProps = {
   isEnableActions: boolean
   targetPath: string
-  targetId?: string
+  targetPathOrId?: string
   targetAndAncestorsData?: TargetAndAncestors
 
   // for deleteModal
@@ -63,13 +63,14 @@ type ItemsTreeProps = {
 }
 
 const renderByInitialNode = (
-    initialNode: ItemNode, DeleteModal: JSX.Element, isEnableActions: boolean, targetId?: string, onClickDeleteByPage?: (page: IPageForPageDeleteModal) => void,
+    // eslint-disable-next-line max-len
+    initialNode: ItemNode, DeleteModal: JSX.Element, isEnableActions: boolean, targetPathOrId?: string, onClickDeleteByPage?: (page: IPageForPageDeleteModal) => void,
 ): JSX.Element => {
   return (
     <div className="grw-pagetree p-3">
       <Item
         key={initialNode.page.path}
-        targetId={targetId}
+        targetPathOrId={targetPathOrId}
         itemNode={initialNode}
         isOpen
         isEnableActions={isEnableActions}
@@ -86,7 +87,7 @@ const renderByInitialNode = (
  */
 const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
   const {
-    targetPath, targetId, targetAndAncestorsData, isDeleteModalOpen, pagesToDelete, isAbleToDeleteCompletely, isDeleteCompletelyModal, onCloseDelete,
+    targetPath, targetPathOrId, targetAndAncestorsData, isDeleteModalOpen, pagesToDelete, isAbleToDeleteCompletely, isDeleteCompletelyModal, onCloseDelete,
     onClickDeleteByPage, isEnableActions,
   } = props;
 
@@ -114,7 +115,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, isEnableActions, targetId, onClickDeleteByPage);
+    return renderByInitialNode(initialNode, DeleteModal, isEnableActions, targetPathOrId, onClickDeleteByPage);
   }
 
   /*
@@ -122,7 +123,7 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
    */
   if (targetAndAncestorsData != null) {
     const initialNode = generateInitialNodeBeforeResponse(targetAndAncestorsData.targetAndAncestors);
-    return renderByInitialNode(initialNode, DeleteModal, isEnableActions, targetId, onClickDeleteByPage);
+    return renderByInitialNode(initialNode, DeleteModal, isEnableActions, targetPathOrId, onClickDeleteByPage);
   }
 
   return null;

+ 5 - 0
packages/app/src/interfaces/page-listing-results.ts

@@ -23,6 +23,11 @@ export interface TargetAndAncestors {
 }
 
 
+export interface NotFoundTargetPathOrId {
+  notFoundTargetPathOrId: string
+}
+
+
 export interface V5MigrationStatus {
   isV5Compatible : boolean,
   migratablePagesCount: number

+ 9 - 0
packages/app/src/server/routes/page.js

@@ -274,6 +274,14 @@ module.exports = function(crowi, app) {
     renderVars.targetAndAncestors = { targetAndAncestors, rootPage };
   }
 
+  function addRenderVarsWhenNotFound(renderVars, pathOrId) {
+    if (pathOrId == null) {
+      return;
+    }
+
+    renderVars.notFoundTargetPathOrId = pathOrId;
+  }
+
   function replacePlaceholdersOfTemplate(template, req) {
     if (req.user == null) {
       return '';
@@ -328,6 +336,7 @@ module.exports = function(crowi, app) {
     const offset = parseInt(req.query.offset) || 0;
     await addRenderVarsForDescendants(renderVars, path, req.user, offset, limit, true);
     await addRenderVarsForPageTree(renderVars, pathOrId, req.user);
+    addRenderVarsWhenNotFound(renderVars, pathOrId);
 
     return res.render(view, renderVars);
   }

+ 5 - 0
packages/app/src/server/views/layout-growi/not_found.html

@@ -3,6 +3,11 @@
 {% block html_base_css %}not-found-page{% endblock %}
 
 {% block content_main_before %}
+  <div
+    id="growi-pagetree-not-found-context"
+    data-not-found-target-path-or-id="{% if notFoundTargetPathOrId %}{{notFoundTargetPathOrId|json}}{% endif %}"
+  >
+  </div>
   <div class="grw-container-convertible">
     {% include '../widget/page_alerts.html' %}
   </div>

+ 5 - 1
packages/app/src/stores/context.tsx

@@ -7,7 +7,7 @@ import { IUser } from '../interfaces/user';
 
 import { useStaticSWR } from './use-static-swr';
 
-import { TargetAndAncestors } from '../interfaces/page-listing-results';
+import { TargetAndAncestors, NotFoundTargetPathOrId } from '../interfaces/page-listing-results';
 
 type Nullable<T> = T | null;
 
@@ -165,3 +165,7 @@ export const useIsSharedUser = (): SWRResponse<boolean, Error> => {
 export const useTargetAndAncestors = (initialData?: TargetAndAncestors): SWRResponse<TargetAndAncestors, Error> => {
   return useStaticSWR<TargetAndAncestors, Error>('targetAndAncestors', initialData || null);
 };
+
+export const useNotFoundTargetPathOrId = (initialData?: NotFoundTargetPathOrId): SWRResponse<NotFoundTargetPathOrId, Error> => {
+  return useStaticSWR<NotFoundTargetPathOrId, Error>('notFoundTargetPathOrId', initialData || null);
+};