Yuki Takei 7 месяцев назад
Родитель
Сommit
94fb9f9101

+ 1 - 1
apps/app/src/client/components/Hotkeys/Subscribers/EditPage.jsx

@@ -2,7 +2,7 @@ import { useEffect } from 'react';
 
 
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import { useIsEditable } from '~/states/context';
+import { useIsEditable } from '~/states/page';
 import { EditorMode, useEditorMode } from '~/states/ui/editor';
 import { EditorMode, useEditorMode } from '~/states/ui/editor';
 
 
 const EditPage = (props) => {
 const EditPage = (props) => {

+ 1 - 1
apps/app/src/client/components/Hotkeys/Subscribers/FocusToGlobalSearch.jsx

@@ -1,7 +1,7 @@
 import { useEffect } from 'react';
 import { useEffect } from 'react';
 
 
 import { useSearchModal } from '~/features/search/client/stores/search';
 import { useSearchModal } from '~/features/search/client/stores/search';
-import { useIsEditable } from '~/states/context';
+import { useIsEditable } from '~/states/page';
 
 
 
 
 const FocusToGlobalSearch = (props) => {
 const FocusToGlobalSearch = (props) => {

+ 1 - 2
apps/app/src/client/components/Page/DisplaySwitcher.tsx

@@ -3,8 +3,7 @@ import type { JSX } from 'react';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
 import { useHashChangedEffect } from '~/client/services/side-effects/hash-changed';
 import { useHashChangedEffect } from '~/client/services/side-effects/hash-changed';
-import { useIsEditable } from '~/states/context';
-import { useLatestRevision } from '~/states/page';
+import { useIsEditable, useLatestRevision } from '~/states/page';
 import { EditorMode, useEditorMode } from '~/states/ui/editor';
 import { EditorMode, useEditorMode } from '~/states/ui/editor';
 import { useReservedNextCaretLine } from '~/stores/editor';
 import { useReservedNextCaretLine } from '~/stores/editor';
 
 

+ 1 - 2
apps/app/src/client/components/PageEditor/EditorNavbarBottom/SavePageControls.tsx

@@ -14,8 +14,7 @@ import {
   DropdownToggle, DropdownMenu, DropdownItem, Modal,
   DropdownToggle, DropdownMenu, DropdownItem, Modal,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { useIsEditable } from '~/states/context';
-import { useCurrentPageData, useCurrentPagePath } from '~/states/page';
+import { useIsEditable, useCurrentPageData, useCurrentPagePath } from '~/states/page';
 import {
 import {
   isAclEnabledAtom,
   isAclEnabledAtom,
   isSlackConfiguredAtom,
   isSlackConfiguredAtom,

+ 1 - 1
apps/app/src/client/components/PageEditor/PageEditor.tsx

@@ -24,9 +24,9 @@ import { useUpdatePage, extractRemoteRevisionDataFromErrorObj } from '~/client/s
 import { uploadAttachments } from '~/client/services/upload-attachments';
 import { uploadAttachments } from '~/client/services/upload-attachments';
 import { toastError, toastSuccess, toastWarning } from '~/client/util/toastr';
 import { toastError, toastSuccess, toastWarning } from '~/client/util/toastr';
 import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
 import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
-import { useIsEditable } from '~/states/context';
 import { useCurrentPathname, useCurrentUser } from '~/states/global';
 import { useCurrentPathname, useCurrentUser } from '~/states/global';
 import {
 import {
+  useIsEditable,
   useCurrentPagePath,
   useCurrentPagePath,
   useCurrentPageData,
   useCurrentPageData,
   useCurrentPageId,
   useCurrentPageId,

+ 1 - 1
apps/app/src/client/services/side-effects/hash-changed.ts

@@ -2,7 +2,7 @@ import { useCallback, useEffect } from 'react';
 
 
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 
 
-import { useIsEditable } from '~/states/context';
+import { useIsEditable } from '~/states/page';
 import { useEditorMode, determineEditorModeByHash } from '~/states/ui/editor';
 import { useEditorMode, determineEditorModeByHash } from '~/states/ui/editor';
 
 
 /**
 /**

+ 3 - 2
apps/app/src/components/PageView/PageView.tsx

@@ -10,8 +10,9 @@ import { PagePathNavTitle } from '~/components/Common/PagePathNavTitle';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
 import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
 import { generateSSRViewOptions } from '~/services/renderer/renderer';
 import { generateSSRViewOptions } from '~/services/renderer/renderer';
-import { useIsForbidden, useIsIdenticalPath, useIsNotCreatable } from '~/states/context';
-import { useCurrentPageData, useCurrentPageId, usePageNotFound } from '~/states/page';
+import {
+  useIsForbidden, useIsIdenticalPath, useIsNotCreatable, useCurrentPageData, useCurrentPageId, usePageNotFound,
+} from '~/states/page';
 import { useViewOptions } from '~/stores/renderer';
 import { useViewOptions } from '~/stores/renderer';
 
 
 import { UserInfo } from '../User/UserInfo';
 import { UserInfo } from '../User/UserInfo';

+ 6 - 4
apps/app/src/pages/[[...path]]/index.page.tsx

@@ -24,10 +24,8 @@ import { useSetEditingMarkdown } from '~/states/ui/editor';
 import { useSWRMUTxCurrentPageYjsData } from '~/stores/yjs';
 import { useSWRMUTxCurrentPageYjsData } from '~/stores/yjs';
 
 
 import type { NextPageWithLayout } from '../_app.page';
 import type { NextPageWithLayout } from '../_app.page';
-import type { BasicLayoutConfigurationProps } from '../basic-layout-page';
 import { useHydrateBasicLayoutConfigurationAtoms } from '../basic-layout-page/hydrate';
 import { useHydrateBasicLayoutConfigurationAtoms } from '../basic-layout-page/hydrate';
 import { getServerSideCommonEachProps } from '../common-props';
 import { getServerSideCommonEachProps } from '../common-props';
-import type { GeneralPageInitialProps } from '../general-page';
 import { useInitialCSRFetch } from '../general-page';
 import { useInitialCSRFetch } from '../general-page';
 import { useHydrateGeneralPageConfigurationAtoms } from '../general-page/hydrate';
 import { useHydrateGeneralPageConfigurationAtoms } from '../general-page/hydrate';
 import { registerPageToShowRevisionWithMeta } from '../general-page/superjson';
 import { registerPageToShowRevisionWithMeta } from '../general-page/superjson';
@@ -37,7 +35,7 @@ import { mergeGetServerSidePropsResults } from '../utils/server-side-props';
 
 
 import { NEXT_JS_ROUTING_PAGE } from './consts';
 import { NEXT_JS_ROUTING_PAGE } from './consts';
 import { getServerSidePropsForInitial, getServerSidePropsForSameRoute } from './server-side-props';
 import { getServerSidePropsForInitial, getServerSidePropsForSameRoute } from './server-side-props';
-import type { EachProps } from './types';
+import type { EachProps, InitialProps } from './types';
 import { useSameRouteNavigation } from './use-same-route-navigation';
 import { useSameRouteNavigation } from './use-same-route-navigation';
 import { useShallowRouting } from './use-shallow-routing';
 import { useShallowRouting } from './use-shallow-routing';
 
 
@@ -70,7 +68,6 @@ const ConflictDiffModal = dynamic(() => import('~/client/components/PageEditor/C
 
 
 const EditablePageEffects = dynamic(() => import('~/client/components/Page/EditablePageEffects').then(mod => mod.EditablePageEffects), { ssr: false });
 const EditablePageEffects = dynamic(() => import('~/client/components/Page/EditablePageEffects').then(mod => mod.EditablePageEffects), { ssr: false });
 
 
-type InitialProps = EachProps & GeneralPageInitialProps & BasicLayoutConfigurationProps;
 type Props = EachProps | InitialProps;
 type Props = EachProps | InitialProps;
 
 
 const isInitialProps = (props: Props): props is InitialProps => {
 const isInitialProps = (props: Props): props is InitialProps => {
@@ -89,6 +86,11 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   const pageData = isInitialProps(props) ? props.pageWithMeta?.data : undefined;
   const pageData = isInitialProps(props) ? props.pageWithMeta?.data : undefined;
   useHydratePageAtoms(pageData, {
   useHydratePageAtoms(pageData, {
     redirectFrom: props.redirectFrom ?? undefined,
     redirectFrom: props.redirectFrom ?? undefined,
+    isNotFound: props.isNotFound,
+    isNotCreatable: props.isNotCreatable,
+    isForbidden: props.isForbidden,
+    templateTags: props.templateTagData,
+    templateBody: props.templateBodyData,
   });
   });
 
 
   const currentPage = useCurrentPageData();
   const currentPage = useCurrentPageData();

+ 66 - 36
apps/app/src/pages/[[...path]]/page-data-props.ts

@@ -9,13 +9,13 @@ import type { PageModel } from '~/server/models/page';
 import type { IPageRedirect, PageRedirectModel } from '~/server/models/page-redirect';
 import type { IPageRedirect, PageRedirectModel } from '~/server/models/page-redirect';
 
 
 import type { CommonEachProps } from '../common-props';
 import type { CommonEachProps } from '../common-props';
-import type { GeneralPageInitialProps } from '../general-page';
+import type { GeneralPageInitialProps, GeneralPageStatesProps } from '../general-page';
 
 
 import type { EachProps } from './types';
 import type { EachProps } from './types';
 
 
 // Utility to resolve path, redirect, and identical path page check
 // Utility to resolve path, redirect, and identical path page check
 type PathResolutionResult = {
 type PathResolutionResult = {
-  resolvedPathname: string;
+  resolvedPagePath: string;
   isIdenticalPathPage: boolean;
   isIdenticalPathPage: boolean;
   redirectFrom?: string;
   redirectFrom?: string;
 };
 };
@@ -24,10 +24,7 @@ let mongooseModel: typeof model;
 let Page: PageModel;
 let Page: PageModel;
 let PageRedirect: PageRedirectModel;
 let PageRedirect: PageRedirectModel;
 
 
-async function resolvePathAndCheckIdentical(
-    path: string,
-    user: IUser | undefined,
-): Promise<PathResolutionResult> {
+async function initModels(): Promise<void> {
   if (mongooseModel == null) {
   if (mongooseModel == null) {
     mongooseModel = (await import('mongoose')).model;
     mongooseModel = (await import('mongoose')).model;
   }
   }
@@ -37,29 +34,70 @@ async function resolvePathAndCheckIdentical(
   if (PageRedirect == null) {
   if (PageRedirect == null) {
     PageRedirect = mongooseModel<IPageRedirect, PageRedirectModel>('PageRedirect');
     PageRedirect = mongooseModel<IPageRedirect, PageRedirectModel>('PageRedirect');
   }
   }
+}
+
+async function resolvePathAndCheckIdentical(
+    path: string,
+    user: IUser | undefined,
+): Promise<PathResolutionResult> {
+  await initModels();
 
 
   const isPermalink = _isPermalink(path);
   const isPermalink = _isPermalink(path);
-  let resolvedPathname = path;
+  let resolvedPagePath = path;
   let redirectFrom: string | undefined;
   let redirectFrom: string | undefined;
   let isIdenticalPathPage = false;
   let isIdenticalPathPage = false;
 
 
   if (!isPermalink) {
   if (!isPermalink) {
     const chains = await PageRedirect.retrievePageRedirectEndpoints(path);
     const chains = await PageRedirect.retrievePageRedirectEndpoints(path);
     if (chains != null) {
     if (chains != null) {
-      resolvedPathname = chains.end.toPath;
+      resolvedPagePath = chains.end.toPath;
       redirectFrom = chains.start.fromPath;
       redirectFrom = chains.start.fromPath;
     }
     }
-    const multiplePagesCount = await Page.countByPathAndViewer(resolvedPathname, user, null, true);
+    const multiplePagesCount = await Page.countByPathAndViewer(resolvedPagePath, user, null, true);
     isIdenticalPathPage = multiplePagesCount > 1;
     isIdenticalPathPage = multiplePagesCount > 1;
   }
   }
-  return { resolvedPathname, isIdenticalPathPage, redirectFrom };
+  return { resolvedPagePath, isIdenticalPathPage, redirectFrom };
+}
+
+function getPageStatesPropsForIdenticalPathPage(): GeneralPageStatesProps {
+  return {
+    isNotFound: false,
+    isNotCreatable: true,
+    isForbidden: false,
+  };
+}
+
+async function getPageStatesProps(page: IPage | null, pagePath: string, pageId?: string | null): Promise<GeneralPageStatesProps> {
+  await initModels();
+
+  if (page != null) {
+    // Existing page
+    return {
+      isNotFound: page.isEmpty,
+      isNotCreatable: false,
+      isForbidden: false,
+    };
+  }
+
+  // Handle non-existent page
+  const isPermalink = _isPermalink(pagePath);
+  const count = isPermalink && pageId
+    ? await Page.count({ _id: pageId })
+    : await Page.count({ path: pagePath });
+
+  return {
+    isNotFound: true,
+    isNotCreatable: !isCreatablePage(pagePath),
+    isForbidden: count > 0,
+  };
 }
 }
 
 
 // Page data retrieval for initial load - returns GetServerSidePropsResult
 // Page data retrieval for initial load - returns GetServerSidePropsResult
 export async function getPageDataForInitial(
 export async function getPageDataForInitial(
     context: GetServerSidePropsContext,
     context: GetServerSidePropsContext,
 ): Promise<GetServerSidePropsResult<
 ): Promise<GetServerSidePropsResult<
-  Pick<GeneralPageInitialProps, 'pageWithMeta' | 'isNotFound' | 'isNotCreatable' | 'isForbidden' | 'skipSSR'> &
+  GeneralPageStatesProps &
+  Pick<GeneralPageInitialProps, 'pageWithMeta' | 'skipSSR'> &
   Pick<EachProps, 'currentPathname' | 'isIdenticalPathPage' | 'redirectFrom'>
   Pick<EachProps, 'currentPathname' | 'isIdenticalPathPage' | 'redirectFrom'>
 >> {
 >> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const req: CrowiRequest = context.req as CrowiRequest;
@@ -77,25 +115,23 @@ export async function getPageDataForInitial(
   const pageId = _isPermalink(pathFromUrl) ? removeHeadingSlash(pathFromUrl) : null;
   const pageId = _isPermalink(pathFromUrl) ? removeHeadingSlash(pathFromUrl) : null;
   const isPermalink = _isPermalink(pathFromUrl);
   const isPermalink = _isPermalink(pathFromUrl);
 
 
-  const { resolvedPathname, isIdenticalPathPage, redirectFrom } = await resolvePathAndCheckIdentical(pathFromUrl, user);
+  const { resolvedPagePath, isIdenticalPathPage, redirectFrom } = await resolvePathAndCheckIdentical(pathFromUrl, user);
 
 
   if (isIdenticalPathPage) {
   if (isIdenticalPathPage) {
     return {
     return {
       props: {
       props: {
-        currentPathname: resolvedPathname,
+        currentPathname: resolvedPagePath,
         isIdenticalPathPage: true,
         isIdenticalPathPage: true,
         pageWithMeta: null,
         pageWithMeta: null,
-        isNotFound: false,
-        isNotCreatable: true,
-        isForbidden: false,
         skipSSR: false,
         skipSSR: false,
         redirectFrom,
         redirectFrom,
+        ...getPageStatesPropsForIdenticalPathPage(),
       },
       },
     };
     };
   }
   }
 
 
   // Get full page data
   // Get full page data
-  const pageWithMeta = await pageService.findPageAndMetaDataByViewer(pageId, resolvedPathname, user, true);
+  const pageWithMeta = await pageService.findPageAndMetaDataByViewer(pageId, resolvedPagePath, user, true);
   const { data: page, meta } = pageWithMeta ?? {};
   const { data: page, meta } = pageWithMeta ?? {};
 
 
   // Add user to seen users
   // Add user to seen users
@@ -115,13 +151,13 @@ export async function getPageDataForInitial(
     const populatedPage = await page.populateDataToShowRevision(skipSSR);
     const populatedPage = await page.populateDataToShowRevision(skipSSR);
 
 
     // Handle URL conversion
     // Handle URL conversion
-    let finalPathname = resolvedPathname;
+    let finalPathname = resolvedPagePath;
     if (page != null && !page.isEmpty) {
     if (page != null && !page.isEmpty) {
       if (isPermalink) {
       if (isPermalink) {
         finalPathname = page.path;
         finalPathname = page.path;
       }
       }
       else {
       else {
-        const isToppage = isTopPage(resolvedPathname);
+        const isToppage = isTopPage(resolvedPagePath);
         if (!isToppage) {
         if (!isToppage) {
           finalPathname = `/${page._id}`;
           finalPathname = `/${page._id}`;
         }
         }
@@ -133,30 +169,21 @@ export async function getPageDataForInitial(
         currentPathname: finalPathname,
         currentPathname: finalPathname,
         isIdenticalPathPage: false,
         isIdenticalPathPage: false,
         pageWithMeta: { data: populatedPage, meta },
         pageWithMeta: { data: populatedPage, meta },
-        isNotFound: page.isEmpty,
-        isNotCreatable: false,
-        isForbidden: false,
         skipSSR,
         skipSSR,
         redirectFrom,
         redirectFrom,
+        ...await getPageStatesProps(page, resolvedPagePath, pageId),
       },
       },
     };
     };
   }
   }
 
 
-  // Handle non-existent page
-  const count = isPermalink
-    ? await Page.count({ _id: pageId })
-    : await Page.count({ path: resolvedPathname });
-
   return {
   return {
     props: {
     props: {
-      currentPathname: resolvedPathname,
+      currentPathname: resolvedPagePath,
       isIdenticalPathPage: false,
       isIdenticalPathPage: false,
       pageWithMeta: null,
       pageWithMeta: null,
-      isNotFound: true,
-      isNotCreatable: !isCreatablePage(resolvedPathname),
-      isForbidden: count > 0,
       skipSSR: false,
       skipSSR: false,
       redirectFrom,
       redirectFrom,
+      ...await getPageStatesProps(null, resolvedPagePath, pageId),
     },
     },
   };
   };
 }
 }
@@ -165,6 +192,7 @@ export async function getPageDataForInitial(
 export async function getPageDataForSameRoute(
 export async function getPageDataForSameRoute(
     context: GetServerSidePropsContext,
     context: GetServerSidePropsContext,
 ): Promise<GetServerSidePropsResult<
 ): Promise<GetServerSidePropsResult<
+    GeneralPageStatesProps &
     Pick<CommonEachProps, 'currentPathname'> &
     Pick<CommonEachProps, 'currentPathname'> &
     Pick<EachProps, 'currentPathname' | 'isIdenticalPathPage' | 'redirectFrom'>
     Pick<EachProps, 'currentPathname' | 'isIdenticalPathPage' | 'redirectFrom'>
 >> {
 >> {
@@ -175,30 +203,31 @@ export async function getPageDataForSameRoute(
   const pageId = _isPermalink(currentPathname) ? removeHeadingSlash(currentPathname) : null;
   const pageId = _isPermalink(currentPathname) ? removeHeadingSlash(currentPathname) : null;
   const isPermalink = _isPermalink(currentPathname);
   const isPermalink = _isPermalink(currentPathname);
 
 
-  const { resolvedPathname, isIdenticalPathPage, redirectFrom } = await resolvePathAndCheckIdentical(currentPathname, user);
+  const { resolvedPagePath, isIdenticalPathPage, redirectFrom } = await resolvePathAndCheckIdentical(currentPathname, user);
 
 
   if (isIdenticalPathPage) {
   if (isIdenticalPathPage) {
     return {
     return {
       props: {
       props: {
-        currentPathname: resolvedPathname,
+        currentPathname: resolvedPagePath,
         isIdenticalPathPage: true,
         isIdenticalPathPage: true,
         redirectFrom,
         redirectFrom,
+        ...getPageStatesPropsForIdenticalPathPage(),
       },
       },
     };
     };
   }
   }
 
 
   // For same route access, do minimal page lookup
   // For same route access, do minimal page lookup
   const basicPageInfo = await Page.findOne(
   const basicPageInfo = await Page.findOne(
-    isPermalink ? { _id: pageId } : { path: resolvedPathname },
+    isPermalink ? { _id: pageId } : { path: resolvedPagePath },
   ).exec();
   ).exec();
 
 
-  let finalPathname = resolvedPathname;
+  let finalPathname = resolvedPagePath;
   if (basicPageInfo != null && !basicPageInfo.isEmpty) {
   if (basicPageInfo != null && !basicPageInfo.isEmpty) {
     if (isPermalink) {
     if (isPermalink) {
       finalPathname = basicPageInfo.path;
       finalPathname = basicPageInfo.path;
     }
     }
     else {
     else {
-      const isToppage = isTopPage(resolvedPathname);
+      const isToppage = isTopPage(resolvedPagePath);
       if (!isToppage) {
       if (!isToppage) {
         finalPathname = `/${basicPageInfo._id}`;
         finalPathname = `/${basicPageInfo._id}`;
       }
       }
@@ -210,6 +239,7 @@ export async function getPageDataForSameRoute(
       currentPathname: finalPathname,
       currentPathname: finalPathname,
       isIdenticalPathPage: false,
       isIdenticalPathPage: false,
       redirectFrom,
       redirectFrom,
+      ...await getPageStatesProps(basicPageInfo, resolvedPagePath, pageId),
     },
     },
   };
   };
 }
 }

+ 3 - 4
apps/app/src/pages/[[...path]]/server-side-props.ts

@@ -4,7 +4,6 @@ import { getServerSideBasicLayoutProps } from '../basic-layout-page';
 import {
 import {
   getServerSideI18nProps, getServerSideCommonInitialProps,
   getServerSideI18nProps, getServerSideCommonInitialProps,
 } from '../common-props';
 } from '../common-props';
-import type { GeneralPageInitialProps } from '../general-page';
 import {
 import {
   getServerSideRendererConfigProps,
   getServerSideRendererConfigProps,
   getActivityAction,
   getActivityAction,
@@ -16,7 +15,7 @@ import { mergeGetServerSidePropsResults } from '../utils/server-side-props';
 
 
 import { NEXT_JS_ROUTING_PAGE } from './consts';
 import { NEXT_JS_ROUTING_PAGE } from './consts';
 import { getPageDataForInitial, getPageDataForSameRoute } from './page-data-props';
 import { getPageDataForInitial, getPageDataForSameRoute } from './page-data-props';
-import type { PageEachProps } from './types';
+import type { Stage2InitialProps, Stage2EachProps } from './types';
 
 
 
 
 const nextjsRoutingProps = {
 const nextjsRoutingProps = {
@@ -26,7 +25,7 @@ const nextjsRoutingProps = {
 };
 };
 
 
 export async function getServerSidePropsForInitial(context: GetServerSidePropsContext):
 export async function getServerSidePropsForInitial(context: GetServerSidePropsContext):
-    Promise<GetServerSidePropsResult<GeneralPageInitialProps & PageEachProps>> {
+    Promise<GetServerSidePropsResult<Stage2InitialProps>> {
   const [
   const [
     commonInitialResult,
     commonInitialResult,
     basicLayoutResult,
     basicLayoutResult,
@@ -67,7 +66,7 @@ export async function getServerSidePropsForInitial(context: GetServerSidePropsCo
   return mergedResult;
   return mergedResult;
 }
 }
 
 
-export async function getServerSidePropsForSameRoute(context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<PageEachProps>> {
+export async function getServerSidePropsForSameRoute(context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<Stage2EachProps>> {
   // Get page data
   // Get page data
   const result = await getPageDataForSameRoute(context);
   const result = await getPageDataForSameRoute(context);
 
 

+ 9 - 3
apps/app/src/pages/[[...path]]/types.ts

@@ -1,6 +1,8 @@
-import type { CommonEachProps } from '../common-props';
+import type { BasicLayoutConfigurationProps } from '../basic-layout-page';
+import type { CommonEachProps, CommonInitialProps } from '../common-props';
+import type { GeneralPageEachProps, GeneralPageInitialProps } from '../general-page';
 
 
-export type PageEachProps = {
+type PageEachProps = {
   redirectFrom?: string;
   redirectFrom?: string;
 
 
   isIdenticalPathPage: boolean,
   isIdenticalPathPage: boolean,
@@ -9,4 +11,8 @@ export type PageEachProps = {
   templateBodyData?: string,
   templateBodyData?: string,
 };
 };
 
 
-export type EachProps = CommonEachProps & PageEachProps;
+export type Stage2EachProps = GeneralPageEachProps & PageEachProps;
+export type Stage2InitialProps = Stage2EachProps & GeneralPageInitialProps & BasicLayoutConfigurationProps;
+
+export type EachProps = CommonEachProps & Stage2EachProps;
+export type InitialProps = CommonEachProps & CommonInitialProps & Stage2InitialProps;

+ 2 - 1
apps/app/src/pages/basic-layout-page/get-server-side-props/index.ts

@@ -1,12 +1,13 @@
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 
 
 import { mergeGetServerSidePropsResults } from '../../utils/server-side-props';
 import { mergeGetServerSidePropsResults } from '../../utils/server-side-props';
+import type { BasicLayoutConfigurationProps } from '../types';
 
 
 import { getServerSideSearchConfigurationProps } from './search-configurations';
 import { getServerSideSearchConfigurationProps } from './search-configurations';
 import { getServerSideSidebarConfigProps } from './sidebar-configurations';
 import { getServerSideSidebarConfigProps } from './sidebar-configurations';
 import { getServerSideUserUISettingsProps } from './user-ui-settings';
 import { getServerSideUserUISettingsProps } from './user-ui-settings';
 
 
-export const getServerSideBasicLayoutProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideBasicLayoutProps: GetServerSideProps<BasicLayoutConfigurationProps> = async(context: GetServerSidePropsContext) => {
   const [
   const [
     searchConfigResult,
     searchConfigResult,
     sidebarConfigResult,
     sidebarConfigResult,

+ 10 - 7
apps/app/src/pages/general-page/types.ts

@@ -5,8 +5,6 @@ import type {
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { PageDocument } from '~/server/models/page';
 import type { PageDocument } from '~/server/models/page';
 
 
-import type { CommonInitialProps } from '../common-props';
-
 export type IPageToShowRevisionWithMeta = IDataWithMeta<IPagePopulatedToShowRevision & PageDocument, IPageInfo>;
 export type IPageToShowRevisionWithMeta = IDataWithMeta<IPagePopulatedToShowRevision & PageDocument, IPageInfo>;
 
 
 export type RendererConfigProps = {
 export type RendererConfigProps = {
@@ -39,12 +37,17 @@ export type ServerConfigurationProps = {
   },
   },
 }
 }
 
 
-export type GeneralPageInitialProps = CommonInitialProps & RendererConfigProps & ServerConfigurationProps & {
-  pageWithMeta: IPageToShowRevisionWithMeta | null,
-  skipSSR?: boolean,
-
-  // Page state information determined on server-side
+export type GeneralPageStatesProps = {
   isNotFound: boolean,
   isNotFound: boolean,
   isForbidden: boolean,
   isForbidden: boolean,
   isNotCreatable: boolean,
   isNotCreatable: boolean,
 }
 }
+
+// Do not include CommonEachProps for multi stage
+export type GeneralPageEachProps = GeneralPageStatesProps;
+
+// Do not include CommonEachProps for multi stage
+export type GeneralPageInitialProps = GeneralPageStatesProps & RendererConfigProps & ServerConfigurationProps & {
+  pageWithMeta: IPageToShowRevisionWithMeta | null,
+  skipSSR?: boolean,
+}

+ 1 - 3
apps/app/src/pages/share/[[...path]]/index.page.tsx

@@ -19,14 +19,13 @@ import { useHydratePageAtoms } from '~/states/page/hydrate';
 import { disableLinkSharingAtom, useRendererConfig } from '~/states/server-configurations';
 import { disableLinkSharingAtom, useRendererConfig } from '~/states/server-configurations';
 
 
 import type { NextPageWithLayout } from '../../_app.page';
 import type { NextPageWithLayout } from '../../_app.page';
-import type { GeneralPageInitialProps } from '../../general-page';
 import { useInitialCSRFetch } from '../../general-page';
 import { useInitialCSRFetch } from '../../general-page';
 import { useHydrateGeneralPageConfigurationAtoms } from '../../general-page/hydrate';
 import { useHydrateGeneralPageConfigurationAtoms } from '../../general-page/hydrate';
 import { registerPageToShowRevisionWithMeta } from '../../general-page/superjson';
 import { registerPageToShowRevisionWithMeta } from '../../general-page/superjson';
 
 
 import { NEXT_JS_ROUTING_PAGE } from './consts';
 import { NEXT_JS_ROUTING_PAGE } from './consts';
 import { getServerSidePropsForInitial } from './server-side-props';
 import { getServerSidePropsForInitial } from './server-side-props';
-import type { ShareLinkInitialProps } from './types';
+import type { InitialProps } from './types';
 
 
 // call superjson custom register
 // call superjson custom register
 registerPageToShowRevisionWithMeta();
 registerPageToShowRevisionWithMeta();
@@ -35,7 +34,6 @@ registerPageToShowRevisionWithMeta();
 const GrowiContextualSubNavigation = dynamic(() => import('~/client/components/Navbar/GrowiContextualSubNavigation'), { ssr: false });
 const GrowiContextualSubNavigation = dynamic(() => import('~/client/components/Navbar/GrowiContextualSubNavigation'), { ssr: false });
 
 
 
 
-type InitialProps = CommonEachProps & GeneralPageInitialProps & ShareLinkInitialProps;
 type Props = CommonEachProps | InitialProps;
 type Props = CommonEachProps | InitialProps;
 
 
 const isInitialProps = (props: Props): props is InitialProps => {
 const isInitialProps = (props: Props): props is InitialProps => {

+ 2 - 2
apps/app/src/pages/share/[[...path]]/page-data-props.ts

@@ -8,7 +8,7 @@ import type { IShareLink } from '~/interfaces/share-link';
 import type { PageModel } from '~/server/models/page';
 import type { PageModel } from '~/server/models/page';
 import type { ShareLinkModel } from '~/server/models/share-link';
 import type { ShareLinkModel } from '~/server/models/share-link';
 
 
-import type { ShareLinkInitialProps } from './types';
+import type { ShareLinkPageStatesProps } from './types';
 
 
 
 
 let mongooseModel: typeof model;
 let mongooseModel: typeof model;
@@ -16,7 +16,7 @@ let Page: PageModel;
 let ShareLink: ShareLinkModel;
 let ShareLink: ShareLinkModel;
 
 
 export const getPageDataForInitial = async(context: GetServerSidePropsContext):
 export const getPageDataForInitial = async(context: GetServerSidePropsContext):
-    Promise<GetServerSidePropsResult<ShareLinkInitialProps>> => {
+    Promise<GetServerSidePropsResult<ShareLinkPageStatesProps>> => {
 
 
   const req = context.req as CrowiRequest;
   const req = context.req as CrowiRequest;
   const { crowi, params } = req;
   const { crowi, params } = req;

+ 2 - 3
apps/app/src/pages/share/[[...path]]/server-side-props.ts

@@ -3,7 +3,6 @@ import type { GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
 import {
 import {
   getServerSideI18nProps, getServerSideCommonInitialProps,
   getServerSideI18nProps, getServerSideCommonInitialProps,
 } from '../../common-props';
 } from '../../common-props';
-import type { GeneralPageInitialProps } from '../../general-page';
 import {
 import {
   getServerSideGeneralPageProps,
   getServerSideGeneralPageProps,
   getServerSideRendererConfigProps,
   getServerSideRendererConfigProps,
@@ -13,7 +12,7 @@ import { addActivity } from '../../utils/activity';
 import { mergeGetServerSidePropsResults } from '../../utils/server-side-props';
 import { mergeGetServerSidePropsResults } from '../../utils/server-side-props';
 
 
 import { getPageDataForInitial } from './page-data-props';
 import { getPageDataForInitial } from './page-data-props';
-import type { ShareLinkInitialProps } from './types';
+import type { Stage2InitialProps } from './types';
 
 
 
 
 const basisProps = {
 const basisProps = {
@@ -25,7 +24,7 @@ const basisProps = {
 };
 };
 
 
 export async function getServerSidePropsForInitial(context: GetServerSidePropsContext):
 export async function getServerSidePropsForInitial(context: GetServerSidePropsContext):
-    Promise<GetServerSidePropsResult<GeneralPageInitialProps & ShareLinkInitialProps>> {
+    Promise<GetServerSidePropsResult<Stage2InitialProps>> {
 
 
   const [
   const [
     commonInitialResult,
     commonInitialResult,

+ 8 - 1
apps/app/src/pages/share/[[...path]]/types.ts

@@ -1,7 +1,8 @@
 import type { IShareLinkHasId } from '~/interfaces/share-link';
 import type { IShareLinkHasId } from '~/interfaces/share-link';
+import type { CommonEachProps, CommonInitialProps } from '~/pages/common-props';
 import type { GeneralPageInitialProps } from '~/pages/general-page';
 import type { GeneralPageInitialProps } from '~/pages/general-page';
 
 
-export type ShareLinkInitialProps = Pick<GeneralPageInitialProps, 'pageWithMeta' | 'skipSSR'> & (
+export type ShareLinkPageStatesProps = Pick<GeneralPageInitialProps, 'pageWithMeta' | 'skipSSR'> & (
   {
   {
     isNotFound: true,
     isNotFound: true,
     isExpired: undefined,
     isExpired: undefined,
@@ -16,3 +17,9 @@ export type ShareLinkInitialProps = Pick<GeneralPageInitialProps, 'pageWithMeta'
     shareLink: IShareLinkHasId,
     shareLink: IShareLinkHasId,
   }
   }
 );
 );
+
+export type Stage2EachProps = ShareLinkPageStatesProps;
+export type Stage2InitialProps = Stage2EachProps & GeneralPageInitialProps;
+
+export type EachProps = CommonEachProps & Stage2EachProps;
+export type InitialProps = CommonEachProps & CommonInitialProps & Stage2InitialProps;

+ 0 - 43
apps/app/src/states/context.ts

@@ -1,27 +1,6 @@
 import { atom, useAtomValue, useSetAtom } from 'jotai';
 import { atom, useAtomValue, useSetAtom } from 'jotai';
 import { currentUserAtomGetter, growiCloudUriAtomGetter } from './global';
 import { currentUserAtomGetter, growiCloudUriAtomGetter } from './global';
 
 
-/**
- * Atom for checking if current path is identical
- */
-const isIdenticalPathAtom = atom<boolean>(false);
-
-export const useIsIdenticalPath = () => useAtomValue(isIdenticalPathAtom);
-
-/**
- * Atom for checking if current page is forbidden
- */
-const isForbiddenAtom = atom<boolean>(false);
-
-export const useIsForbidden = () => useAtomValue(isForbiddenAtom);
-
-/**
- * Atom for checking if current page is not creatable
- */
-const isNotCreatableAtom = atom<boolean>(false);
-
-export const useIsNotCreatable = () => useAtomValue(isNotCreatableAtom);
-
 /**
 /**
  * Computed atom for checking if current user is a guest user
  * Computed atom for checking if current user is a guest user
  * Depends on currentUser atom
  * Depends on currentUser atom
@@ -97,25 +76,3 @@ const growiDocumentationUrlAtom = atom((get) => {
 
 
 export const useGrowiDocumentationUrl = () =>
 export const useGrowiDocumentationUrl = () =>
   useAtomValue(growiDocumentationUrlAtom);
   useAtomValue(growiDocumentationUrlAtom);
-
-/**
- * Computed atom for checking if current page is editable
- * Depends on multiple atoms: isGuestUser, isReadOnlyUser, isForbidden, isNotCreatable, isIdenticalPath
- */
-const isEditableAtom = atom((get) => {
-  const isGuestUser = get(isGuestUserAtom);
-  const isReadOnlyUser = get(isReadOnlyUserAtom);
-  const isForbidden = get(isForbiddenAtom);
-  const isNotCreatable = get(isNotCreatableAtom);
-  const isIdenticalPath = get(isIdenticalPathAtom);
-
-  return (
-    !isForbidden &&
-    !isIdenticalPath &&
-    !isNotCreatable &&
-    !isGuestUser &&
-    !isReadOnlyUser
-  );
-});
-
-export const useIsEditable = () => useAtomValue(isEditableAtom);

+ 34 - 2
apps/app/src/states/page/hooks.ts

@@ -1,12 +1,16 @@
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { useAtomValue } from 'jotai';
 import { useAtomValue } from 'jotai';
-
+import { useAtomCallback } from 'jotai/utils';
+import { useCallback, useMemo } from 'react';
+import { useIsGuestUser, useIsReadOnlyUser } from '../context';
 import { useCurrentPathname } from '../global';
 import { useCurrentPathname } from '../global';
-
 import {
 import {
   currentPageDataAtom,
   currentPageDataAtom,
   currentPageIdAtom,
   currentPageIdAtom,
   currentPagePathAtom,
   currentPagePathAtom,
+  isForbiddenAtom,
+  isIdenticalPathAtom,
+  isNotCreatableAtom,
   isRevisionOutdatedAtom,
   isRevisionOutdatedAtom,
   isTrashPageAtom,
   isTrashPageAtom,
   latestRevisionAtom,
   latestRevisionAtom,
@@ -36,6 +40,12 @@ export const usePageNotFound = () => useAtomValue(pageNotFoundAtom);
 
 
 export const usePageNotCreatable = () => useAtomValue(pageNotCreatableAtom);
 export const usePageNotCreatable = () => useAtomValue(pageNotCreatableAtom);
 
 
+export const useIsIdenticalPath = () => useAtomValue(isIdenticalPathAtom);
+
+export const useIsForbidden = () => useAtomValue(isForbiddenAtom);
+
+export const useIsNotCreatable = () => useAtomValue(isNotCreatableAtom);
+
 export const useLatestRevision = () => useAtomValue(latestRevisionAtom);
 export const useLatestRevision = () => useAtomValue(latestRevisionAtom);
 
 
 export const useShareLinkId = () => useAtomValue(shareLinkIdAtom);
 export const useShareLinkId = () => useAtomValue(shareLinkIdAtom);
@@ -88,3 +98,25 @@ export const useIsTrashPage = (): boolean => useAtomValue(isTrashPageAtom);
  */
  */
 export const useIsRevisionOutdated = (): boolean =>
 export const useIsRevisionOutdated = (): boolean =>
   useAtomValue(isRevisionOutdatedAtom);
   useAtomValue(isRevisionOutdatedAtom);
+
+/**
+ * Computed hook for checking if current page is editable
+ */
+export const useIsEditable = () => {
+  const isGuestUser = useIsGuestUser();
+  const isReadOnlyUser = useIsReadOnlyUser();
+
+  const getCombinedConditions = useAtomCallback(
+    useCallback((get) => {
+      const isForbidden = get(isForbiddenAtom);
+      const isNotCreatable = get(isNotCreatableAtom);
+      const isIdenticalPath = get(isIdenticalPathAtom);
+
+      return !isForbidden && !isIdenticalPath && !isNotCreatable;
+    }, []),
+  );
+
+  return useMemo(() => {
+    return !isGuestUser && !isReadOnlyUser && getCombinedConditions();
+  }, [getCombinedConditions, isGuestUser, isReadOnlyUser]);
+};

+ 8 - 5
apps/app/src/states/page/hydrate.ts

@@ -4,6 +4,7 @@ import { useHydrateAtoms } from 'jotai/utils';
 import {
 import {
   currentPageDataAtom,
   currentPageDataAtom,
   currentPageIdAtom,
   currentPageIdAtom,
+  isForbiddenAtom,
   latestRevisionAtom,
   latestRevisionAtom,
   pageNotCreatableAtom,
   pageNotCreatableAtom,
   pageNotFoundAtom,
   pageNotFoundAtom,
@@ -41,10 +42,11 @@ import {
 export const useHydratePageAtoms = (
 export const useHydratePageAtoms = (
   page: IPagePopulatedToShowRevision | undefined,
   page: IPagePopulatedToShowRevision | undefined,
   options?: {
   options?: {
-    isNotFound?: boolean;
-    isNotCreatable?: boolean;
     isLatestRevision?: boolean;
     isLatestRevision?: boolean;
     shareLinkId?: string;
     shareLinkId?: string;
+    isNotFound?: boolean; // always overwrited
+    isNotCreatable?: boolean; // always overwrited
+    isForbidden?: boolean; // always overwrited
     redirectFrom?: string; // always overwrited
     redirectFrom?: string; // always overwrited
     templateTags?: string[]; // always overwrited
     templateTags?: string[]; // always overwrited
     templateBody?: string; // always overwrited
     templateBody?: string; // always overwrited
@@ -54,9 +56,6 @@ export const useHydratePageAtoms = (
     // Core page state - automatically extract from page object
     // Core page state - automatically extract from page object
     [currentPageIdAtom, page?._id],
     [currentPageIdAtom, page?._id],
     [currentPageDataAtom, page],
     [currentPageDataAtom, page],
-    [pageNotFoundAtom, options?.isNotFound ?? (page == null || page.isEmpty)],
-    [pageNotCreatableAtom, options?.isNotCreatable ?? false],
-    [latestRevisionAtom, options?.isLatestRevision ?? true],
 
 
     // ShareLink page state
     // ShareLink page state
     [shareLinkIdAtom, options?.shareLinkId],
     [shareLinkIdAtom, options?.shareLinkId],
@@ -69,6 +68,10 @@ export const useHydratePageAtoms = (
   // always overwrited
   // always overwrited
   useHydrateAtoms(
   useHydrateAtoms(
     [
     [
+      [pageNotFoundAtom, options?.isNotFound ?? (page == null || page.isEmpty)],
+      [pageNotCreatableAtom, options?.isNotCreatable ?? false],
+      [isForbiddenAtom, options?.isForbidden ?? false],
+      [latestRevisionAtom, options?.isLatestRevision ?? true],
       [redirectFromAtom, options?.redirectFrom ?? undefined],
       [redirectFromAtom, options?.redirectFrom ?? undefined],
       // Template data - from options (not auto-extracted from page)
       // Template data - from options (not auto-extracted from page)
       [templateTagsAtom, options?.templateTags ?? []],
       [templateTagsAtom, options?.templateTags ?? []],

+ 3 - 0
apps/app/src/states/page/internal-atoms.ts

@@ -12,6 +12,9 @@ export const currentPageIdAtom = atom<string>();
 export const currentPageDataAtom = atom<IPagePopulatedToShowRevision>();
 export const currentPageDataAtom = atom<IPagePopulatedToShowRevision>();
 export const pageNotFoundAtom = atom(false);
 export const pageNotFoundAtom = atom(false);
 export const pageNotCreatableAtom = atom(false);
 export const pageNotCreatableAtom = atom(false);
+export const isIdenticalPathAtom = atom<boolean>(false);
+export const isForbiddenAtom = atom<boolean>(false);
+export const isNotCreatableAtom = atom<boolean>(false);
 export const latestRevisionAtom = atom(true);
 export const latestRevisionAtom = atom(true);
 
 
 // ShareLink page state atoms (internal)
 // ShareLink page state atoms (internal)

+ 1 - 3
apps/app/src/states/ui/editor/hooks.ts

@@ -1,8 +1,6 @@
 import { useAtom, useAtomValue, useSetAtom } from 'jotai';
 import { useAtom, useAtomValue, useSetAtom } from 'jotai';
 import { useCallback } from 'react';
 import { useCallback } from 'react';
-
-import { useIsEditable } from '~/states/context';
-import { usePageNotFound } from '~/states/page';
+import { useIsEditable, usePageNotFound } from '~/states/page';
 
 
 import {
 import {
   editingMarkdownAtom,
   editingMarkdownAtom,

+ 2 - 2
apps/app/src/stores/ui.tsx

@@ -16,11 +16,11 @@ import useSWRImmutable from 'swr/immutable';
 
 
 import type { UpdateDescCountData } from '~/interfaces/websocket';
 import type { UpdateDescCountData } from '~/interfaces/websocket';
 import {
 import {
-  useIsEditable, useIsIdenticalPath, useIsReadOnlyUser, useIsSharedUser,
+  useIsReadOnlyUser, useIsSharedUser,
 } from '~/states/context';
 } from '~/states/context';
 import { useCurrentUser } from '~/states/global';
 import { useCurrentUser } from '~/states/global';
 import {
 import {
-  usePageNotFound, useCurrentPagePath, useIsTrashPage, useCurrentPageId,
+  useIsEditable, useIsIdenticalPath, usePageNotFound, useCurrentPagePath, useIsTrashPage, useCurrentPageId,
 } from '~/states/page';
 } from '~/states/page';
 import { useShareLinkId } from '~/states/page/hooks';
 import { useShareLinkId } from '~/states/page/hooks';
 import { EditorMode, useEditorMode } from '~/states/ui/editor';
 import { EditorMode, useEditorMode } from '~/states/ui/editor';