|
|
@@ -1,129 +1,88 @@
|
|
|
-import React, { useEffect, type JSX } from 'react';
|
|
|
+import type { ReactNode, JSX } from 'react';
|
|
|
+import React, { useEffect } from 'react';
|
|
|
|
|
|
-import { type IPagePopulatedToShowRevision, getIdForRef } from '@growi/core';
|
|
|
import type {
|
|
|
GetServerSideProps, GetServerSidePropsContext,
|
|
|
} from 'next';
|
|
|
-import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
|
|
import dynamic from 'next/dynamic';
|
|
|
import Head from 'next/head';
|
|
|
-import superjson from 'superjson';
|
|
|
|
|
|
import { ShareLinkLayout } from '~/components/Layout/ShareLinkLayout';
|
|
|
import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
|
|
|
import { ShareLinkPageView } from '~/components/ShareLinkPageView';
|
|
|
import type { SupportedActionType } from '~/interfaces/activity';
|
|
|
import { SupportedAction } from '~/interfaces/activity';
|
|
|
-import type { CrowiRequest } from '~/interfaces/crowi-request';
|
|
|
-import { RegistrationMode } from '~/interfaces/registration-mode';
|
|
|
-import type { RendererConfig } from '~/interfaces/services/renderer';
|
|
|
-import type { IShareLinkHasId } from '~/interfaces/share-link';
|
|
|
-import type { PageDocument, PageModel } from '~/server/models/page';
|
|
|
-import ShareLink from '~/server/models/share-link';
|
|
|
-import { useHydrateSharedPageAtoms } from '~/states/hydrate/page';
|
|
|
-import { useCurrentPageData, useFetchCurrentPage } from '~/states/page';
|
|
|
+import type { CommonEachProps } from '~/pages/common-props';
|
|
|
+import { NextjsRoutingType, detectNextjsRoutingType } from '~/pages/utils/nextjs-routing-utils';
|
|
|
+import { useCustomTitleForPage } from '~/pages/utils/page-title-customization';
|
|
|
+import { useIsSearchPage, useIsSharedUser } from '~/states/context';
|
|
|
import {
|
|
|
- useCurrentUser, useRendererConfig, useIsSearchPage, useCurrentPathname,
|
|
|
- useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault, useIsContainerFluid, useIsEnabledMarp,
|
|
|
- useIsLocalAccountRegistrationEnabled, useShowPageSideAuthors,
|
|
|
-} from '~/stores-universal/context';
|
|
|
+ useCurrentPageData, useCurrentPagePath,
|
|
|
+} from '~/states/page';
|
|
|
+import { useHydratePageAtoms } from '~/states/page/hydrate';
|
|
|
+import { useDisableLinkSharing, useRendererConfig } from '~/states/server-configurations';
|
|
|
+import { useHydrateServerConfigurationAtoms } from '~/states/server-configurations/hydrate';
|
|
|
import loggerFactory from '~/utils/logger';
|
|
|
|
|
|
import type { NextPageWithLayout } from '../../_app.page';
|
|
|
-import type { CommonProps } from '../../common-props';
|
|
|
-import {
|
|
|
- getServerSideCommonProps, generateCustomTitleForPage, getNextI18NextConfig, skipSSR, addActivity,
|
|
|
-} from '../../common-props';
|
|
|
+import type { InitialProps } from '../../general-page';
|
|
|
+import { useInitialCSRFetch } from '../../general-page';
|
|
|
+import { registerPageToShowRevisionWithMeta } from '../../general-page/superjson';
|
|
|
|
|
|
+import { NEXT_JS_ROUTING_PAGE } from './consts';
|
|
|
+import { getServerSidePropsForInitial, getServerSidePropsForSameRoute } from './server-side-props';
|
|
|
+import type { ShareLinkPageProps } from './types';
|
|
|
|
|
|
-const GrowiContextualSubNavigationSubstance = dynamic(() => import('~/client/components/Navbar/GrowiContextualSubNavigation'), { ssr: false });
|
|
|
+// call superjson custom register
|
|
|
+registerPageToShowRevisionWithMeta();
|
|
|
|
|
|
|
|
|
-const logger = loggerFactory('growi:next-page:share');
|
|
|
+const GrowiContextualSubNavigation = dynamic(() => import('~/client/components/Navbar/GrowiContextualSubNavigation'), { ssr: false });
|
|
|
|
|
|
-type Props = CommonProps & {
|
|
|
- shareLinkRelatedPage?: IShareLinkRelatedPage,
|
|
|
- shareLink?: IShareLinkHasId,
|
|
|
- isNotFound: boolean,
|
|
|
- isExpired: boolean,
|
|
|
- disableLinkSharing: boolean,
|
|
|
- isSearchServiceConfigured: boolean,
|
|
|
- isSearchServiceReachable: boolean,
|
|
|
- isSearchScopeChildrenAsDefault: boolean,
|
|
|
- showPageSideAuthors: boolean,
|
|
|
- isEnabledMarp: boolean,
|
|
|
- isLocalAccountRegistrationEnabled: boolean,
|
|
|
- drawioUri: string | null,
|
|
|
- rendererConfig: RendererConfig,
|
|
|
- skipSSR: boolean,
|
|
|
- ssrMaxRevisionBodyLength: number,
|
|
|
-};
|
|
|
|
|
|
-type IShareLinkRelatedPage = IPagePopulatedToShowRevision & PageDocument;
|
|
|
-
|
|
|
-superjson.registerCustom<IShareLinkRelatedPage, string>(
|
|
|
- {
|
|
|
- isApplicable: (v): v is IShareLinkRelatedPage => {
|
|
|
- return v != null
|
|
|
- && v.toObject != null;
|
|
|
- },
|
|
|
- serialize: (v) => { return superjson.stringify(v.toObject()) },
|
|
|
- deserialize: (v) => { return superjson.parse(v) },
|
|
|
- },
|
|
|
- 'IShareLinkRelatedPageTransformer',
|
|
|
-);
|
|
|
-
|
|
|
-// GrowiContextualSubNavigation for shared page
|
|
|
-// get page info from props not to send request 'GET /page' from client
|
|
|
-type GrowiContextualSubNavigationForSharedPageProps = {
|
|
|
- page?: IPagePopulatedToShowRevision,
|
|
|
- isLinkSharingDisabled: boolean,
|
|
|
-}
|
|
|
+const logger = loggerFactory('growi:next-page:share');
|
|
|
|
|
|
-const GrowiContextualSubNavigationForSharedPage = (props: GrowiContextualSubNavigationForSharedPageProps): JSX.Element => {
|
|
|
- const { page, isLinkSharingDisabled } = props;
|
|
|
+type Props = ShareLinkPageProps &
|
|
|
+ (CommonEachProps | (CommonEachProps & InitialProps));
|
|
|
|
|
|
- return (
|
|
|
- <GrowiContextualSubNavigationSubstance currentPage={page} isLinkSharingDisabled={isLinkSharingDisabled} />
|
|
|
- );
|
|
|
+const isInitialProps = (props: Props): props is (ShareLinkPageProps & InitialProps & CommonEachProps) => {
|
|
|
+ return 'isNextjsRoutingTypeInitial' in props && props.isNextjsRoutingTypeInitial;
|
|
|
};
|
|
|
|
|
|
const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
|
|
|
- useHydrateSharedPageAtoms({
|
|
|
- pageId: props.shareLinkRelatedPage?._id,
|
|
|
- isNotFound: props.isNotFound,
|
|
|
+
|
|
|
+ const { shareLink, isExpired } = props;
|
|
|
+
|
|
|
+ // Initialize Jotai atoms with initial data - must be called unconditionally
|
|
|
+ const pageData = isInitialProps(props) ? props.pageWithMeta?.data : undefined;
|
|
|
+ useHydratePageAtoms(pageData, {
|
|
|
+ shareLinkId: props.shareLink?._id,
|
|
|
});
|
|
|
|
|
|
const [currentPage] = useCurrentPageData();
|
|
|
- useCurrentPathname(props.shareLink?.relatedPage.path);
|
|
|
- useIsSearchPage(false);
|
|
|
- useShareLinkId(props.shareLink?._id);
|
|
|
- useCurrentUser(props.currentUser);
|
|
|
- useRendererConfig(props.rendererConfig);
|
|
|
- useIsSearchServiceConfigured(props.isSearchServiceConfigured);
|
|
|
- useIsSearchServiceReachable(props.isSearchServiceReachable);
|
|
|
- useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
|
|
|
- useIsEnabledMarp(props.rendererConfig.isEnabledMarp);
|
|
|
- useIsLocalAccountRegistrationEnabled(props.isLocalAccountRegistrationEnabled);
|
|
|
- useShowPageSideAuthors(props.showPageSideAuthors);
|
|
|
- useIsContainerFluid(props.isContainerFluid);
|
|
|
-
|
|
|
- const { fetchCurrentPage } = useFetchCurrentPage();
|
|
|
+ const [currentPagePath] = useCurrentPagePath();
|
|
|
+ const [rendererConfig] = useRendererConfig();
|
|
|
+ const [, setIsSharedUser] = useIsSharedUser();
|
|
|
+ const [, setIsSearchPage] = useIsSearchPage();
|
|
|
+ const [isLinkSharingDisabled] = useDisableLinkSharing();
|
|
|
|
|
|
- useEffect(() => {
|
|
|
- if (!props.skipSSR) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ // Use custom hooks for navigation and routing
|
|
|
+ // useSameRouteNavigation();
|
|
|
|
|
|
- if (props.shareLink?.relatedPage._id != null && !props.isNotFound) {
|
|
|
- fetchCurrentPage();
|
|
|
- }
|
|
|
- }, [fetchCurrentPage, props.isNotFound, props.shareLink?.relatedPage._id, props.skipSSR]);
|
|
|
+ // If initial props and skipSSR, fetch page data on client-side
|
|
|
+ useInitialCSRFetch(isInitialProps(props) && props.skipSSR);
|
|
|
|
|
|
+ // Initialize atom values
|
|
|
+ useEffect(() => {
|
|
|
+ setIsSharedUser(true);
|
|
|
+ setIsSearchPage(false);
|
|
|
+ }, [setIsSharedUser, setIsSearchPage]);
|
|
|
|
|
|
- const pagePath = props.shareLinkRelatedPage?.path ?? '';
|
|
|
+ // If the data on the page changes without router.push, pageWithMeta remains old because getServerSideProps() is not executed
|
|
|
+ // So preferentially take page data from useSWRxCurrentPage
|
|
|
+ const pagePath = currentPagePath ?? props.currentPathname;
|
|
|
|
|
|
- const title = generateCustomTitleForPage(props, pagePath);
|
|
|
+ const title = useCustomTitleForPage(pagePath);
|
|
|
|
|
|
return (
|
|
|
<>
|
|
|
@@ -133,15 +92,14 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
|
|
|
|
|
|
<div className="dynamic-layout-root justify-content-between">
|
|
|
|
|
|
- <GrowiContextualSubNavigationForSharedPage page={currentPage ?? props.shareLinkRelatedPage} isLinkSharingDisabled={props.disableLinkSharing} />
|
|
|
+ <GrowiContextualSubNavigation currentPage={currentPage} />
|
|
|
|
|
|
<ShareLinkPageView
|
|
|
pagePath={pagePath}
|
|
|
- rendererConfig={props.rendererConfig}
|
|
|
- page={currentPage ?? props.shareLinkRelatedPage}
|
|
|
- shareLink={props.shareLink}
|
|
|
- isExpired={props.isExpired}
|
|
|
- disableLinkSharing={props.disableLinkSharing}
|
|
|
+ rendererConfig={rendererConfig}
|
|
|
+ shareLink={shareLink}
|
|
|
+ isExpired={isExpired}
|
|
|
+ disableLinkSharing={isLinkSharingDisabled}
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
@@ -149,62 +107,29 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
|
|
|
);
|
|
|
};
|
|
|
|
|
|
+type LayoutProps = Props & {
|
|
|
+ children?: ReactNode
|
|
|
+}
|
|
|
+
|
|
|
+const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
|
|
|
+ // Hydrate sidebar atoms with server-side data - must be called unconditionally
|
|
|
+ const initialProps = isInitialProps(props) ? props : undefined;
|
|
|
+ useHydrateServerConfigurationAtoms(initialProps?.serverConfig, initialProps?.rendererConfig);
|
|
|
+
|
|
|
+ return <ShareLinkLayout>{children}</ShareLinkLayout>;
|
|
|
+};
|
|
|
+
|
|
|
SharedPage.getLayout = function getLayout(page) {
|
|
|
return (
|
|
|
<>
|
|
|
<DrawioViewerScript drawioUri={page.props.rendererConfig.drawioUri} />
|
|
|
- <ShareLinkLayout>{page}</ShareLinkLayout>
|
|
|
+ <Layout {...page.props}>
|
|
|
+ {page}
|
|
|
+ </Layout>
|
|
|
</>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
-function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
|
|
|
- const req: CrowiRequest = context.req as CrowiRequest;
|
|
|
- const { crowi } = req;
|
|
|
- const { configManager, searchService } = crowi;
|
|
|
-
|
|
|
- props.disableLinkSharing = configManager.getConfig('security:disableLinkSharing');
|
|
|
-
|
|
|
- props.isSearchServiceConfigured = searchService.isConfigured;
|
|
|
- props.isSearchServiceReachable = searchService.isReachable;
|
|
|
- props.isSearchScopeChildrenAsDefault = configManager.getConfig('customize:isSearchScopeChildrenAsDefault');
|
|
|
-
|
|
|
- props.drawioUri = configManager.getConfig('app:drawioUri');
|
|
|
-
|
|
|
- props.showPageSideAuthors = configManager.getConfig('customize:showPageSideAuthors');
|
|
|
-
|
|
|
- props.isLocalAccountRegistrationEnabled = crowi.passportService.isLocalStrategySetup
|
|
|
- && configManager.getConfig('security:registrationMode') !== RegistrationMode.CLOSED;
|
|
|
-
|
|
|
- props.rendererConfig = {
|
|
|
- isSharedPage: true,
|
|
|
- isEnabledLinebreaks: configManager.getConfig('markdown:isEnabledLinebreaks'),
|
|
|
- isEnabledLinebreaksInComments: configManager.getConfig('markdown:isEnabledLinebreaksInComments'),
|
|
|
- isEnabledMarp: configManager.getConfig('customize:isEnabledMarp'),
|
|
|
- adminPreferredIndentSize: configManager.getConfig('markdown:adminPreferredIndentSize'),
|
|
|
- isIndentSizeForced: configManager.getConfig('markdown:isIndentSizeForced'),
|
|
|
-
|
|
|
- drawioUri: configManager.getConfig('app:drawioUri'),
|
|
|
- plantumlUri: configManager.getConfig('app:plantumlUri'),
|
|
|
-
|
|
|
- // XSS Options
|
|
|
- isEnabledXssPrevention: configManager.getConfig('markdown:rehypeSanitize:isEnabledPrevention'),
|
|
|
- sanitizeType: configManager.getConfig('markdown:rehypeSanitize:option'),
|
|
|
- customTagWhitelist: crowi.configManager.getConfig('markdown:rehypeSanitize:tagNames'),
|
|
|
- customAttrWhitelist: configManager.getConfig('markdown:rehypeSanitize:attributes') != null
|
|
|
- ? JSON.parse(configManager.getConfig('markdown:rehypeSanitize:attributes'))
|
|
|
- : undefined,
|
|
|
- highlightJsStyleBorder: crowi.configManager.getConfig('customize:highlightJsStyleBorder'),
|
|
|
- };
|
|
|
-
|
|
|
- props.ssrMaxRevisionBodyLength = configManager.getConfig('app:ssrMaxRevisionBodyLength');
|
|
|
-}
|
|
|
-
|
|
|
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
|
|
|
- const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
|
|
|
- props._nextI18Next = nextI18NextConfig._nextI18Next;
|
|
|
-}
|
|
|
-
|
|
|
function getAction(props: Props): SupportedActionType {
|
|
|
let action: SupportedActionType;
|
|
|
if (props.isExpired) {
|
|
|
@@ -219,50 +144,17 @@ function getAction(props: Props): SupportedActionType {
|
|
|
|
|
|
return action;
|
|
|
}
|
|
|
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
|
|
|
- const req = context.req as CrowiRequest;
|
|
|
- const { crowi, params } = req;
|
|
|
- const result = await getServerSideCommonProps(context);
|
|
|
|
|
|
- if (!('props' in result)) {
|
|
|
- throw new Error('invalid getSSP result');
|
|
|
- }
|
|
|
- const props: Props = result.props as Props;
|
|
|
-
|
|
|
- try {
|
|
|
- const shareLink = await ShareLink.findOne({ _id: params.linkId }).populate('relatedPage');
|
|
|
- if (shareLink == null) {
|
|
|
- props.isNotFound = true;
|
|
|
- }
|
|
|
- else {
|
|
|
- props.isNotFound = false;
|
|
|
- props.isExpired = shareLink.isExpired();
|
|
|
- props.shareLink = shareLink.toObject();
|
|
|
-
|
|
|
- // retrieve Page
|
|
|
- const Page = crowi.model('Page') as PageModel;
|
|
|
- const relatedPage = await Page.findOne({ _id: getIdForRef(shareLink.relatedPage) });
|
|
|
- // determine whether skip SSR
|
|
|
- const ssrMaxRevisionBodyLength = crowi.configManager.getConfig('app:ssrMaxRevisionBodyLength');
|
|
|
-
|
|
|
- if (relatedPage != null) {
|
|
|
- props.skipSSR = await skipSSR(relatedPage, ssrMaxRevisionBodyLength);
|
|
|
- // populate
|
|
|
- props.shareLinkRelatedPage = await relatedPage.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- logger.error(err);
|
|
|
- }
|
|
|
+export const getServerSideProps: GetServerSideProps<ShareLinkPageProps> = async(context: GetServerSidePropsContext) => {
|
|
|
+ // detect Next.js routing type
|
|
|
+ const nextjsRoutingType = detectNextjsRoutingType(context, NEXT_JS_ROUTING_PAGE);
|
|
|
|
|
|
- injectServerConfigurations(context, props);
|
|
|
- await injectNextI18NextConfigurations(context, props);
|
|
|
- await addActivity(context, getAction(props));
|
|
|
+ if (nextjsRoutingType === NextjsRoutingType.INITIAL) {
|
|
|
+ return getServerSidePropsForInitial(context);
|
|
|
+ }
|
|
|
|
|
|
- return {
|
|
|
- props,
|
|
|
- };
|
|
|
+ // Lightweight props for same-route navigation
|
|
|
+ return getServerSidePropsForSameRoute(context);
|
|
|
};
|
|
|
|
|
|
export default SharedPage;
|