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

Merge pull request #6425 from weseek/feat/99076-next-me-page

feat: render /me page
Yuki Takei 3 лет назад
Родитель
Сommit
f62cadf91d

+ 4 - 21
packages/app/src/components/Me/BasicInfoSettings.tsx

@@ -2,20 +2,14 @@ import React from 'react';
 
 import { useTranslation } from 'next-i18next';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 // import { localeMetadatas } from '~/client/util/i18n';
+import { useRegistrationWhiteList } from '~/stores/context';
 import { usePersonalSettings } from '~/stores/personal-settings';
 
-import { withUnstatedContainers } from '../UnstatedUtils';
-
-type Props = {
-  appContainer: AppContainer,
-}
-
-const BasicInfoSettings = (props: Props) => {
+export const BasicInfoSettings = (): JSX.Element => {
   const { t } = useTranslation();
-  const { appContainer } = props;
+  const { data: registrationWhiteList } = useRegistrationWhiteList();
 
   const {
     data: personalSettingsInfo, mutate: mutatePersonalSettings, sync, updateBasicInfo, error,
@@ -34,9 +28,6 @@ const BasicInfoSettings = (props: Props) => {
     }
   };
 
-
-  const { registrationWhiteList } = appContainer.getConfig();
-
   const changePersonalSettingsHandler = (updateData) => {
     if (personalSettingsInfo == null) {
       return;
@@ -71,7 +62,7 @@ const BasicInfoSettings = (props: Props) => {
             defaultValue={personalSettingsInfo?.email || ''}
             onChange={e => changePersonalSettingsHandler({ email: e.target.value })}
           />
-          {registrationWhiteList.length !== 0 && (
+          {registrationWhiteList != null && registrationWhiteList.length !== 0 && (
             <div className="form-text text-muted">
               {t('page_register.form_help.email')}
               <ul>
@@ -163,11 +154,3 @@ const BasicInfoSettings = (props: Props) => {
     </>
   );
 };
-
-
-/**
- * Wrapper component for using unstated
- */
-const BasicInfoSettingsWrapper = withUnstatedContainers(BasicInfoSettings, [AppContainer]);
-
-export default BasicInfoSettingsWrapper;

+ 2 - 9
packages/app/src/components/Me/ExternalAccountLinkedMe.jsx

@@ -1,10 +1,9 @@
 import React, { Fragment } from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
-import AppContainer from '~/client/services/AppContainer';
 import { useSWRxPersonalExternalAccounts } from '~/stores/personal-settings';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
@@ -115,7 +114,6 @@ class ExternalAccountLinkedMe extends React.Component {
 
 ExternalAccountLinkedMe.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   personalExternalAccounts: PropTypes.arrayOf(PropTypes.object),
 };
 
@@ -126,9 +124,4 @@ const ExternalAccountLinkedMeWrapperFC = (props) => {
   return <ExternalAccountLinkedMe t={t} personalExternalAccounts={personalExternalAccountsData} {...props} />;
 };
 
-/**
- * Wrapper component for using unstated
- */
-const ExternalAccountLinkedMeWrapper = withUnstatedContainers(ExternalAccountLinkedMeWrapperFC, [AppContainer]);
-
-export default ExternalAccountLinkedMeWrapper;
+export default ExternalAccountLinkedMeWrapperFC;

+ 1 - 1
packages/app/src/components/Me/UserSettings.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 
 import { useTranslation } from 'next-i18next';
 
-import BasicInfoSettings from './BasicInfoSettings';
+import { BasicInfoSettings } from './BasicInfoSettings';
 import ProfileImageSettings from './ProfileImageSettings';
 
 const UserSettings = React.memo((): JSX.Element => {

+ 166 - 0
packages/app/src/pages/me.page.tsx

@@ -0,0 +1,166 @@
+import React from 'react';
+
+import {
+  IUser, IUserHasId,
+} from '@growi/core';
+import { model as mongooseModel } from 'mongoose';
+import {
+  NextPage, GetServerSideProps, GetServerSidePropsContext,
+} from 'next';
+import { useTranslation } from 'next-i18next';
+import dynamic from 'next/dynamic';
+import { useRouter } from 'next/router';
+
+import { CrowiRequest } from '~/interfaces/crowi-request';
+import { ISidebarConfig } from '~/interfaces/sidebar-config';
+import { IUserUISettings } from '~/interfaces/user-ui-settings';
+import { UserUISettingsModel } from '~/server/models/user-ui-settings';
+import {
+  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
+} from '~/stores/ui';
+import loggerFactory from '~/utils/logger';
+
+
+import { BasicLayout } from '../components/Layout/BasicLayout';
+import {
+  useCurrentUser,
+  useIsSearchServiceConfigured, useIsSearchServiceReachable,
+  useCsrfToken, useIsSearchScopeChildrenAsDefault,
+  useRegistrationWhiteList,
+} from '../stores/context';
+
+import {
+  CommonProps, getNextI18NextConfig, getServerSideCommonProps, useCustomTitle,
+} from './utils/commons';
+
+
+const logger = loggerFactory('growi:pages:me');
+
+type Props = CommonProps & {
+  currentUser: IUser,
+  isSearchServiceConfigured: boolean,
+  isSearchServiceReachable: boolean,
+  isSearchScopeChildrenAsDefault: boolean,
+  userUISettings?: IUserUISettings
+  sidebarConfig: ISidebarConfig,
+
+  // config
+  registrationWhiteList: string[],
+};
+
+const MePage: NextPage<Props> = (props: Props) => {
+  useCurrentUser(props.currentUser ?? null);
+
+  useRegistrationWhiteList(props.registrationWhiteList);
+
+  // commons
+  useCsrfToken(props.csrfToken);
+
+  // // UserUISettings
+  usePreferDrawerModeByUser(props.userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);
+  usePreferDrawerModeOnEditByUser(props.userUISettings?.preferDrawerModeOnEditByUser);
+  useSidebarCollapsed(props.userUISettings?.isSidebarCollapsed ?? props.sidebarConfig.isSidebarClosedAtDockMode);
+  useCurrentSidebarContents(props.userUISettings?.currentSidebarContents);
+  useCurrentProductNavWidth(props.userUISettings?.currentProductNavWidth);
+
+  // // page
+  useIsSearchServiceConfigured(props.isSearchServiceConfigured);
+  useIsSearchServiceReachable(props.isSearchServiceReachable);
+  useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
+  const { t } = useTranslation();
+
+  const PersonalSettings = dynamic(() => import('~/components/Me/PersonalSettings'), { ssr: false });
+
+  return (
+    <>
+      <BasicLayout title={useCustomTitle(props, 'GROWI')}>
+
+        <header className="py-3">
+          <div className="container-fluid">
+            <h1 className="title">{t('User Settings')}</h1>
+          </div>
+        </header>
+
+        <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
+
+        <div id="main" className='main'>
+          <div id="content-main" className="content-main grw-container-convertible">
+            <PersonalSettings />
+          </div>
+        </div>
+
+      </BasicLayout>
+    </>
+  );
+};
+
+async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
+  const req = context.req as CrowiRequest<IUserHasId & any>;
+  const { user } = req;
+  const UserUISettings = mongooseModel('UserUISettings') as UserUISettingsModel;
+
+  const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
+  if (userUISettings != null) {
+    props.userUISettings = userUISettings.toObject();
+  }
+}
+
+async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
+  const req: CrowiRequest = context.req as CrowiRequest;
+  const { crowi } = req;
+  const {
+    searchService,
+    configManager,
+  } = crowi;
+
+  props.isSearchServiceConfigured = searchService.isConfigured;
+  props.isSearchServiceReachable = searchService.isReachable;
+  props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
+
+  props.registrationWhiteList = configManager.getConfig('crowi', 'security:registrationWhiteList');
+
+  props.sidebarConfig = {
+    isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+  };
+}
+
+// /**
+//  * for Server Side Translations
+//  * @param context
+//  * @param props
+//  * @param namespacesRequired
+//  */
+// async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
+//   const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+//   props._nextI18Next = nextI18NextConfig._nextI18Next;
+// }
+
+export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+  const req = context.req as CrowiRequest<IUserHasId & any>;
+  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;
+
+  if (user != null) {
+    props.currentUser = user.toObject();
+  }
+
+  await injectUserUISettings(context, props);
+  await injectServerConfigurations(context, props);
+
+  return {
+    props,
+  };
+};
+
+export default MePage;

+ 1 - 1
packages/app/src/server/routes/index.js

@@ -207,7 +207,7 @@ module.exports = function(crowi, app) {
   // app.get('/tags'                     , loginRequired, tag.showPage);
   app.get('/tags', loginRequired, next.delegateToNext);
 
-  app.get('/me'                                 , loginRequiredStrictly, injectUserUISettings, me.index);
+  app.get('/me'                                 , loginRequiredStrictly, injectUserUISettings, next.delegateToNext);
   // external-accounts
   // my in-app-notifications
   app.get('/me/all-in-app-notifications'   , loginRequiredStrictly, allInAppNotifications.list);

+ 0 - 8
packages/app/src/server/routes/me.js

@@ -100,14 +100,6 @@ module.exports = function(crowi, app) {
       });
   };
 
-  actions.index = async function(req, res) {
-    const User = crowi.model('User');
-    const userData = await User.findById(req.user.id).populate({ path: 'imageAttachment', select: 'filePathProxied' });
-    const renderVars = {};
-    renderVars.user = userData;
-    return res.render('me/index', renderVars);
-  };
-
   actions.externalAccounts = {};
   actions.externalAccounts.list = function(req, res) {
     const userData = req.user;

+ 4 - 0
packages/app/src/stores/context.tsx

@@ -128,6 +128,10 @@ export const useDisableLinkSharing = (initialData?: Nullable<boolean>): SWRRespo
   return useStaticSWR<Nullable<boolean>, Error>('disableLinkSharing', initialData);
 };
 
+export const useRegistrationWhiteList = (initialData?: Nullable<string[]>): SWRResponse<Nullable<string[]>, Error> => {
+  return useStaticSWR<Nullable<string[]>, Error>('registrationWhiteList', initialData);
+};
+
 export const useRevisionIdHackmdSynced = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {
   return useStaticSWR<Nullable<any>, Error>('revisionIdHackmdSynced', initialData);
 };