Yuki Takei 3 лет назад
Родитель
Сommit
06938fd9f1

+ 50 - 0
packages/app/src/components/BasicLayout.tsx

@@ -0,0 +1,50 @@
+import React, { ReactNode } from 'react';
+
+// import dynamic from 'next/dynamic';
+
+// import GrowiNavbar from '~/client/js/components/Navbar/GrowiNavbar';
+// import GrowiNavbarBottom from '~/client/js/components/Navbar/GrowiNavbarBottom';
+
+import { RawLayout } from './RawLayout';
+
+
+type Props = {
+  title: string
+  className?: string,
+  children?: ReactNode
+}
+
+export const BasicLayout = ({ children, title, className }: Props): JSX.Element => {
+
+  // const Sidebar = dynamic(() => import('../client/js/components/Sidebar'), { ssr: false });
+  // const HotkeysManager = dynamic(() => import('../client/js/components/Hotkeys/HotkeysManager'), { ssr: false });
+  // const PageCreateModal = dynamic(() => import('../client/js/components/PageCreateModal'), { ssr: false });
+  // const SystemVersion = dynamic(() => import('./SystemVersion'), { ssr: false });
+
+  return (
+    <>
+      <RawLayout title={title} className={className}>
+        {/* <GrowiNavbar /> */}
+        GrowiNavbar
+
+        <div className="page-wrapper d-flex d-print-block">
+          <div className="grw-sidebar-wrapper">
+            {/* <Sidebar /> */}
+            Sidebar
+          </div>
+
+          <div className="flex-fill mw-0">
+            {children}
+          </div>
+        </div>
+
+        {/* <GrowiNavbarBottom /> */}
+        GrowiNavbarBottom
+      </RawLayout>
+
+      {/* <PageCreateModal /> */}
+      {/* <HotkeysManager /> */}
+      {/* <SystemVersion /> */}
+    </>
+  );
+};

+ 30 - 0
packages/app/src/components/RawLayout.tsx

@@ -0,0 +1,30 @@
+import React, { ReactNode } from 'react';
+
+import Head from 'next/head';
+
+type Props = {
+  title: string,
+  className?: string,
+  children?: ReactNode,
+}
+
+export const RawLayout = ({ children, title, className }: Props): JSX.Element => {
+
+  const classNames: string[] = ['wrapper'];
+  if (className != null) {
+    classNames.push(className);
+  }
+
+  return (
+    <>
+      <Head>
+        <title>{title}</title>
+        <meta charSet="utf-8" />
+        <meta name="viewport" content="initial-scale=1.0, width=device-width" />
+      </Head>
+      <div className={classNames.join(' ')}>
+        {children}
+      </div>
+    </>
+  );
+};

+ 12 - 0
packages/app/src/interfaces/crowi-request.ts

@@ -0,0 +1,12 @@
+import { Request } from 'express';
+
+import { IUserHasId } from './user';
+
+export interface CrowiRequest extends Request {
+
+  user?: IUserHasId,
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  crowi: any,
+
+}

+ 310 - 0
packages/app/src/pages/[[...path]].tsx

@@ -0,0 +1,310 @@
+import React, { useEffect } from 'react';
+
+import { pagePathUtils } from '@growi/core';
+import {
+  NextPage, GetServerSideProps, GetServerSidePropsContext,
+} from 'next';
+import Head from 'next/head';
+import { useRouter } from 'next/router';
+
+// import { PageAlerts } from '~/components/PageAlert/PageAlerts';
+// import { PageComments } from '~/components/PageComment/PageComments';
+// import { useTranslation } from '~/i18n';
+import { CrowiRequest } from '~/interfaces/crowi-request';
+// import { renderScriptTagByName, renderHighlightJsStyleTag } from '~/service/cdn-resources-loader';
+// import { useIndentSize } from '~/stores/editor';
+// import { useRendererSettings } from '~/stores/renderer';
+// import { EditorMode, useEditorMode, useIsMobile } from '~/stores/ui';
+import loggerFactory from '~/utils/logger';
+import { CommonProps, getServerSideCommonProps, useCustomTitle } from '~/utils/nextjs-page-utils';
+// import { isUserPage, isTrashPage, isSharedPage } from '~/utils/path-utils';
+
+
+// import GrowiSubNavigation from '../client/js/components/Navbar/GrowiSubNavigation';
+// import GrowiSubNavigationSwitcher from '../client/js/components/Navbar/GrowiSubNavigationSwitcher';
+// import DisplaySwitcher from '../client/js/components/Page/DisplaySwitcher';
+import { BasicLayout } from '../components/BasicLayout';
+
+// import { serializeUserSecurely } from '../server/models/serializers/user-serializer';
+// import PageStatusAlert from '../client/js/components/PageStatusAlert';
+
+
+import {
+  useCurrentUser, useCurrentPagePath,
+// useOwnerOfCurrentPage,
+//   useForbidden, useNotFound, useTrash, useShared, useShareLinkId, useIsSharedUser, useIsAbleToDeleteCompletely,
+//   useAppTitle, useSiteUrl, useConfidential, useIsEnabledStaleNotification,
+//   useSearchServiceConfigured, useSearchServiceReachable, useIsMailerSetup,
+//   useAclEnabled, useHasSlackConfig, useDrawioUri, useHackmdUri, useMathJax, useNoCdn, useEditorConfig,
+} from '../stores/context';
+// import { useCurrentPageSWR } from '../stores/page';
+
+
+const logger = loggerFactory('growi:pages:all');
+const { isUsersHomePage } = pagePathUtils;
+
+type Props = CommonProps & {
+  currentUser: string,
+
+  // page: any,
+  // pageUser?: any,
+  // redirectTo?: string;
+  // redirectFrom?: string;
+
+  // shareLinkId?: string;
+
+  // siteUrl: string,
+  // isForbidden: boolean,
+  // isNotFound: boolean,
+  // isAbleToDeleteCompletely: boolean,
+  // isSearchServiceConfigured: boolean,
+  // isSearchServiceReachable: boolean,
+  // isMailerSetup: boolean,
+  // isAclEnabled: boolean,
+  // hasSlackConfig: boolean,
+  // drawioUri: string,
+  // hackmdUri: string,
+  // mathJax: string,
+  // noCdn: string,
+  // highlightJsStyle: string,
+  // isAllReplyShown: boolean,
+  // isContainerFluid: boolean,
+  // editorConfig: any,
+  // isEnabledStaleNotification: boolean,
+  // isEnabledLinebreaks: boolean,
+  // isEnabledLinebreaksInComments: boolean,
+  // adminPreferredIndentSize: number,
+  // isIndentSizeForced: boolean,
+};
+
+const GrowiPage: NextPage<Props> = (props: Props) => {
+  // const { t } = useTranslation();
+  const router = useRouter();
+
+  const { data: currentUser } = useCurrentUser(props.currentUser != null ? JSON.parse(props.currentUser) : null);
+  useCurrentPagePath(props.currentPagePath);
+  // useOwnerOfCurrentPage(props.pageUser != null ? JSON.parse(props.pageUser) : null);
+  // useForbidden(props.isForbidden);
+  // useNotFound(props.isNotFound);
+  // useTrash(isTrashPage(props.currentPagePath));
+  // useShared(isSharedPage(props.currentPagePath));
+  // useShareLinkId(props.shareLinkId);
+  // useIsAbleToDeleteCompletely(props.isAbleToDeleteCompletely);
+  // useIsSharedUser(props.currentUser == null && isSharedPage(props.currentPagePath));
+  // useIsEnabledStaleNotification(props.isEnabledStaleNotification);
+
+  // useAppTitle(props.appTitle);
+  // useSiteUrl(props.siteUrl);
+  // useEditorConfig(props.editorConfig);
+  // useConfidential(props.confidential);
+  // useSearchServiceConfigured(props.isSearchServiceConfigured);
+  // useSearchServiceReachable(props.isSearchServiceReachable);
+  // useIsMailerSetup(props.isMailerSetup);
+  // useAclEnabled(props.isAclEnabled);
+  // useHasSlackConfig(props.hasSlackConfig);
+  // useDrawioUri(props.drawioUri);
+  // useHackmdUri(props.hackmdUri);
+  // useMathJax(props.mathJax);
+  // useNoCdn(props.noCdn);
+  // useIndentSize(props.adminPreferredIndentSize);
+
+  // useRendererSettings({
+  //   isEnabledLinebreaks: props.isEnabledLinebreaks,
+  //   isEnabledLinebreaksInComments: props.isEnabledLinebreaksInComments,
+  //   adminPreferredIndentSize: props.adminPreferredIndentSize,
+  //   isIndentSizeForced: props.isIndentSizeForced,
+  // });
+
+  // const { data: editorMode } = useEditorMode();
+
+  let page;
+  // if (props.page != null) {
+  //   page = JSON.parse(props.page);
+  // }
+  // useCurrentPageSWR(page);
+
+  const classNames: string[] = [];
+  // switch (editorMode) {
+  //   case EditorMode.Editor:
+  //     classNames.push('on-edit', 'builtin-editor');
+  //     break;
+  //   case EditorMode.HackMD:
+  //     classNames.push('on-edit', 'hackmd');
+  //     break;
+  // }
+  // if (props.isContainerFluid) {
+  //   classNames.push('growi-layout-fluid');
+  // }
+  // if (page == null) {
+  //   classNames.push('not-found-page');
+  // }
+
+
+  // // Rewrite browser url by Shallow Routing https://nextjs.org/docs/routing/shallow-routing
+  // useEffect(() => {
+  //   if (props.redirectTo != null) {
+  //     router.push('/[[...path]]', props.redirectTo, { shallow: true });
+  //   }
+  // // eslint-disable-next-line react-hooks/exhaustive-deps
+  // }, []);
+
+  return (
+    <>
+      <Head>
+        {/*
+        {renderScriptTagByName('drawio-viewer')}
+        {renderScriptTagByName('mathjax')}
+        {renderScriptTagByName('highlight-addons')}
+        {renderHighlightJsStyleTag(props.highlightJsStyle)}
+        */}
+      </Head>
+      {/* <BasicLayout title={useCustomTitle(props, t('GROWI'))} className={classNames.join(' ')}> */}
+      <BasicLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')}>
+        <header className="py-0">
+          {/* <GrowiSubNavigation /> */}
+          GrowiSubNavigation
+        </header>
+        <div className="d-edit-none">
+          {/* <GrowiSubNavigationSwitcher /> */}
+          GrowiSubNavigationSwitcher
+        </div>
+
+        <div id="grw-subnav-sticky-trigger" className="sticky-top"></div>
+        <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
+
+        <div id="main" className={`main ${isUsersHomePage(props.currentPagePath) && 'user-page'}`}>
+
+          <div className="row">
+            <div className="col grw-page-content-container">
+              <div id="content-main" className="content-main grw-container-convertible">
+                {/* <PageAlerts /> */}
+                PageAlerts
+                {/* <DisplaySwitcher /> */}
+                DisplaySwitcher
+                <div id="page-editor-navbar-bottom-container" className="d-none d-edit-block"></div>
+                {/* <PageStatusAlert /> */}
+                PageStatusAlert
+              </div>
+            </div>
+
+            {/* <div className="col-xl-2 col-lg-3 d-none d-lg-block revision-toc-container">
+              <div id="revision-toc" className="revision-toc mt-3 sps sps--abv" data-sps-offset="123">
+                <div id="revision-toc-content" className="revision-toc-content"></div>
+              </div>
+            </div> */}
+          </div>
+
+        </div>
+        <footer>
+          {/* <PageComments /> */}
+          PageComments
+        </footer>
+
+      </BasicLayout>
+    </>
+  );
+};
+
+// async function injectPageInformation(context: GetServerSidePropsContext, props: Props, specifiedPagePath?: string): Promise<void> {
+//   const req: CrowiRequest = context.req as CrowiRequest;
+//   const { crowi } = req;
+//   const { pageService } = crowi;
+
+//   const { user } = req;
+
+//   const pagePath = specifiedPagePath || props.currentPagePath;
+//   const result = await pageService.findPageAndMetaDataByViewer({ path: pagePath, user });
+//   const page = result.page;
+
+//   if (page == null) {
+//     // check the page is forbidden or just does not exist.
+//     props.isForbidden = result.isForbidden;
+//     props.isNotFound = result.isNotFound;
+//     logger.warn(`Page is ${props.isForbidden ? 'forbidden' : 'not found'}`, pagePath);
+//     return;
+//   }
+
+//   // get props recursively
+//   if (page.redirectTo) {
+//     // Pass to rewrite browser url
+//     props.redirectTo = page.redirectTo;
+//     props.redirectFrom = pagePath;
+//     logger.debug(`Redirect to '${page.redirectTo}'`);
+//     return injectPageInformation(context, props, page.redirectTo);
+//   }
+
+//   await page.populateDataToShowRevision();
+//   props.page = JSON.stringify(serializeUserSecurely(page));
+// }
+
+// async function injectPageUserInformation(context: GetServerSidePropsContext, props: Props): Promise<void> {
+//   const req: CrowiRequest = context.req as CrowiRequest;
+//   const { crowi } = req;
+//   const UserModel = crowi.model('User');
+
+//   if (isUserPage(props.currentPagePath)) {
+//     const user = await UserModel.findUserByUsername(UserModel.getUsernameByPath(props.currentPagePath));
+
+//     if (user != null) {
+//       props.pageUser = JSON.stringify(user.toObject());
+//     }
+//   }
+// }
+
+export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+  const req: CrowiRequest = context.req as CrowiRequest;
+  const { crowi } = req;
+  // const {
+  //   appService, searchService, configManager, aclService, slackNotificationService, mailService,
+  // } = crowi;
+
+  const { user } = req;
+
+  const result = await getServerSideCommonProps(context);
+
+  // check for presence
+  // see: https://github.com/vercel/next.js/issues/19271#issuecomment-730006862
+  if (!('props' in result)) {
+    throw new Error('invalid getSSP result');
+  }
+
+  const props: Props = result.props as Props;
+  // await injectPageInformation(context, props);
+  // await injectPageUserInformation(context, props);
+
+  if (user != null) {
+    props.currentUser = JSON.stringify(user);
+  }
+
+  // props.siteUrl = appService.getSiteUrl();
+  // props.confidential = appService.getAppConfidential();
+  // props.isSearchServiceConfigured = searchService.isConfigured;
+  // props.isSearchServiceReachable = searchService.isReachable;
+  // props.isMailerSetup = mailService.isMailerSetup;
+  // props.isAclEnabled = aclService.isAclEnabled();
+  // props.hasSlackConfig = slackNotificationService.hasSlackConfig();
+  // props.drawioUri = configManager.getConfig('crowi', 'app:drawioUri');
+  // props.hackmdUri = configManager.getConfig('crowi', 'app:hackmdUri');
+  // props.mathJax = configManager.getConfig('crowi', 'app:mathJax');
+  // props.noCdn = configManager.getConfig('crowi', 'app:noCdn');
+  // props.highlightJsStyle = configManager.getConfig('crowi', 'customize:highlightJsStyle');
+  // props.isAllReplyShown = configManager.getConfig('crowi', 'customize:isAllReplyShown');
+  // props.isContainerFluid = configManager.getConfig('crowi', 'customize:isContainerFluid');
+  // props.isEnabledStaleNotification = configManager.getConfig('crowi', 'customize:isEnabledStaleNotification');
+  // props.isEnabledLinebreaks = configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks');
+  // props.isEnabledLinebreaksInComments = configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments');
+  // props.editorConfig = {
+  //   upload: {
+  //     image: crowi.fileUploadService.getIsUploadable(),
+  //     file: crowi.fileUploadService.getFileUploadEnabled(),
+  //   },
+  // };
+  // props.adminPreferredIndentSize = configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize');
+  // props.isIndentSizeForced = configManager.getConfig('markdown', 'markdown:isIndentSizeForced');
+
+  return {
+    props,
+  };
+};
+
+export default GrowiPage;

+ 4 - 2
packages/app/src/pages/_app.tsx

@@ -1,3 +1,5 @@
+import React from 'react';
+
 import { AppProps } from 'next/app';
 
 // import { appWithTranslation } from '~/i18n';
@@ -9,7 +11,7 @@ import '~/styles/theme/default.scss';
 // import { useGrowiVersion } from '../stores/context';
 // import { useInterceptorManager } from '~/stores/interceptor';
 
-function GrowiApp({ Component, pageProps }: AppProps) {
+function GrowiApp({ Component, pageProps }: AppProps): JSX.Element {
   // useInterceptorManager(new InterceptorManager());
   // useGrowiVersion(pageProps.growiVersion);
 
@@ -20,4 +22,4 @@ function GrowiApp({ Component, pageProps }: AppProps) {
 
 // export default appWithTranslation(GrowiApp);
 
-export default GrowiApp
+export default GrowiApp;

+ 0 - 60
packages/app/src/pages/index.tsx

@@ -1,60 +0,0 @@
-import type { NextPage } from 'next';
-import Head from 'next/head';
-
-import styles from '../styles-next/Home.module.css';
-
-const Home: NextPage = () => {
-  return (
-    <div className={styles.container}>
-      <Head>
-        <title>Create Next App</title>
-        <meta name="description" content="Generated by create next app" />
-        <link rel="icon" href="/favicon.ico" />
-      </Head>
-
-      <main className={styles.main}>
-        <h1 className={styles.title}>
-          Welcome to <a href="https://nextjs.org">Next.js!</a>
-        </h1>
-
-        <p className={styles.description}>
-          Get started by editing{' '}
-          <code className={styles.code}>pages/index.tsx</code>
-        </p>
-
-        <div className={styles.grid}>
-          <a href="https://nextjs.org/docs" className={styles.card}>
-            <h2>Documentation &rarr;</h2>
-            <p>Find in-depth information about Next.js features and API.</p>
-          </a>
-
-          <a href="https://nextjs.org/learn" className={styles.card}>
-            <h2>Learn &rarr;</h2>
-            <p>Learn about Next.js in an interactive course with quizzes!</p>
-          </a>
-
-          <a
-            href="https://github.com/vercel/next.js/tree/canary/examples"
-            className={styles.card}
-          >
-            <h2>Examples &rarr;</h2>
-            <p>Discover and deploy boilerplate example Next.js projects.</p>
-          </a>
-
-          <a
-            href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
-            className={styles.card}
-          >
-            <h2>Deploy &rarr;</h2>
-            <p>
-              aaaInstantly deploy your Next.js site to a public URL with Vercel.
-            </p>
-          </a>
-        </div>
-      </main>
-
-    </div>
-  );
-};
-
-export default Home;

+ 0 - 116
packages/app/src/styles-next/Home.module.css

@@ -1,116 +0,0 @@
-.container {
-  padding: 0 2rem;
-}
-
-.main {
-  min-height: 100vh;
-  padding: 4rem 0;
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  align-items: center;
-}
-
-.footer {
-  display: flex;
-  flex: 1;
-  padding: 2rem 0;
-  border-top: 1px solid #eaeaea;
-  justify-content: center;
-  align-items: center;
-}
-
-.footer a {
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  flex-grow: 1;
-}
-
-.title a {
-  color: #0070f3;
-  text-decoration: none;
-}
-
-.title a:hover,
-.title a:focus,
-.title a:active {
-  text-decoration: underline;
-}
-
-.title {
-  margin: 0;
-  line-height: 1.15;
-  font-size: 4rem;
-}
-
-.title,
-.description {
-  text-align: center;
-}
-
-.description {
-  margin: 4rem 0;
-  line-height: 1.5;
-  font-size: 1.5rem;
-}
-
-.code {
-  background: #fafafa;
-  border-radius: 5px;
-  padding: 0.75rem;
-  font-size: 1.1rem;
-  font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
-    Bitstream Vera Sans Mono, Courier New, monospace;
-}
-
-.grid {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  flex-wrap: wrap;
-  max-width: 800px;
-}
-
-.card {
-  margin: 1rem;
-  padding: 1.5rem;
-  text-align: left;
-  color: inherit;
-  text-decoration: none;
-  border: 1px solid #eaeaea;
-  border-radius: 10px;
-  transition: color 0.15s ease, border-color 0.15s ease;
-  max-width: 300px;
-}
-
-.card:hover,
-.card:focus,
-.card:active {
-  color: #0070f3;
-  border-color: #0070f3;
-}
-
-.card h2 {
-  margin: 0 0 1rem 0;
-  font-size: 1.5rem;
-}
-
-.card p {
-  margin: 0;
-  font-size: 1.25rem;
-  line-height: 1.5;
-}
-
-.logo {
-  height: 1em;
-  margin-left: 0.5rem;
-}
-
-@media (max-width: 600px) {
-  .grid {
-    width: 100%;
-    flex-direction: column;
-  }
-}

+ 0 - 16
packages/app/src/styles-next/globals.css

@@ -1,16 +0,0 @@
-html,
-body {
-  padding: 0;
-  margin: 0;
-  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
-    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
-}
-
-a {
-  color: inherit;
-  text-decoration: none;
-}
-
-* {
-  box-sizing: border-box;
-}

+ 66 - 0
packages/app/src/utils/nextjs-page-utils.ts

@@ -0,0 +1,66 @@
+import { DevidedPagePath } from '@growi/core';
+import { GetServerSideProps, GetServerSidePropsContext } from 'next';
+
+
+import { CrowiRequest } from '~/interfaces/crowi-request';
+
+export type CommonProps = {
+  namespacesRequired: string[], // i18next
+  currentPagePath: string,
+  appTitle: string,
+  confidential: string,
+  customTitleTemplate: string,
+  growiVersion: string,
+}
+
+// eslint-disable-next-line max-len
+export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(context: GetServerSidePropsContext) => {
+
+  const req: CrowiRequest = context.req as CrowiRequest;
+  const { crowi } = req;
+  const {
+    appService, customizeService,
+  } = crowi;
+
+  const url = new URL(context.resolvedUrl, 'http://example.com');
+  const currentPagePath = decodeURI(url.pathname);
+
+  const props: CommonProps = {
+    namespacesRequired: ['translation'],
+    currentPagePath,
+    appTitle: appService.getAppTitle(),
+    confidential: appService.getAppConfidential() || '',
+    customTitleTemplate: customizeService.customTitleTemplate,
+    growiVersion: crowi.version,
+  };
+
+  return { props };
+};
+
+/**
+ * Generate whole title string for the specified title
+ * @param props
+ * @param title
+ */
+export const useCustomTitle = (props: CommonProps, title: string): string => {
+  return props.customTitleTemplate
+    .replace('{{sitename}}', props.appTitle)
+    .replace('{{page}}', title)
+    .replace('{{pagepath}}', title)
+    .replace('{{pagename}}', title);
+};
+
+/**
+ * Generate whole title string for the specified page path
+ * @param props
+ * @param pagePath
+ */
+export const useCustomTitleForPage = (props: CommonProps, pagePath: string): string => {
+  const dPagePath = new DevidedPagePath(pagePath, true, true);
+
+  return props.customTitleTemplate
+    .replace('{{sitename}}', props.appTitle)
+    .replace('{{pagepath}}', pagePath)
+    .replace('{{page}}', dPagePath.latter) // for backward compatibility
+    .replace('{{pagename}}', dPagePath.latter);
+};