Przeglądaj źródła

Merge pull request #6441 from weseek/imprv/101883-show-user-management-detail-page-new

Imprv/101883 show user management detail page new
cao 3 lat temu
rodzic
commit
a9179e4f19

+ 11 - 0
packages/app/src/components/Admin/NotFoundPage.tsx

@@ -0,0 +1,11 @@
+import React from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+export const AdminNotFoundPage = (): JSX.Element => {
+  const { t } = useTranslation();
+
+  return (
+    <h1 className="title">{t('not_found_page.page_not_exist')}</h1>
+  );
+};

+ 1 - 3
packages/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx

@@ -40,7 +40,7 @@ const actionForPages = {
   transfer: 'transfer',
 };
 
-const UserGroupDeleteModal: FC<Props> = (props: Props) => {
+export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
 
   const { t } = useTranslation();
 
@@ -209,5 +209,3 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
     </Modal>
   );
 };
-
-export default UserGroupDeleteModal;

+ 2 - 3
packages/app/src/components/Admin/UserGroup/UserGroupDropdown.tsx

@@ -1,4 +1,5 @@
 import React, { FC, useCallback } from 'react';
+
 import { useTranslation } from 'next-i18next';
 
 import { IUserGroupHasId } from '~/interfaces/user';
@@ -9,7 +10,7 @@ type Props = {
   onClickCreateUserGroupButton?(): void
 };
 
-const UserGroupDropdown: FC<Props> = (props: Props) => {
+export const UserGroupDropdown: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
 
   const { selectableUserGroups, onClickAddExistingUserGroupButton, onClickCreateUserGroupButton } = props;
@@ -66,5 +67,3 @@ const UserGroupDropdown: FC<Props> = (props: Props) => {
     </>
   );
 };
-
-export default UserGroupDropdown;

+ 1 - 3
packages/app/src/components/Admin/UserGroup/UserGroupForm.tsx

@@ -13,7 +13,7 @@ type Props = {
   onSubmit?: (targetGroup: IUserGroupHasId, userGroupData: Partial<IUserGroupHasId>) => Promise<void> | void
 };
 
-const UserGroupForm: FC<Props> = (props: Props) => {
+export const UserGroupForm: FC<Props> = (props: Props) => {
 
   const { t } = useTranslation();
 
@@ -152,5 +152,3 @@ const UserGroupForm: FC<Props> = (props: Props) => {
     </form>
   );
 };
-
-export default UserGroupForm;

+ 1 - 3
packages/app/src/components/Admin/UserGroup/UserGroupModal.tsx

@@ -19,7 +19,7 @@ type Props = {
   onHide?: () => Promise<void> | void
 };
 
-const UserGroupModal: FC<Props> = (props: Props) => {
+export const UserGroupModal: FC<Props> = (props: Props) => {
 
   const { t } = useTranslation();
 
@@ -116,5 +116,3 @@ const UserGroupModal: FC<Props> = (props: Props) => {
     </Modal>
   );
 };
-
-export default UserGroupModal;

+ 5 - 7
packages/app/src/components/Admin/UserGroup/UserGroupPage.tsx

@@ -1,19 +1,19 @@
 import React, { FC, useState, useCallback } from 'react';
 
+import dynamic from 'next/dynamic';
 import { useTranslation } from 'react-i18next';
 
-
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Delete, apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
 import { IUserGroup, IUserGroupHasId } from '~/interfaces/user';
 import { useIsAclEnabled } from '~/stores/context';
 import { useSWRxUserGroupList, useSWRxChildUserGroupList, useSWRxUserGroupRelationList } from '~/stores/user-group';
 
-import UserGroupDeleteModal from './UserGroupDeleteModal';
-import UserGroupModal from './UserGroupModal';
-import UserGroupTable from './UserGroupTable';
+const UserGroupDeleteModal = dynamic(() => import('./UserGroupDeleteModal').then(mod => mod.UserGroupDeleteModal), { ssr: false });
+const UserGroupModal = dynamic(() => import('./UserGroupModal').then(mod => mod.UserGroupModal), { ssr: false });
+const UserGroupTable = dynamic(() => import('./UserGroupTable').then(mod => mod.UserGroupTable), { ssr: false });
 
-const UserGroupPage: FC = () => {
+export const UserGroupPage: FC = () => {
   const { t } = useTranslation();
 
   const { data: isAclEnabled } = useIsAclEnabled();
@@ -193,5 +193,3 @@ const UserGroupPage: FC = () => {
     </div>
   );
 };
-
-export default UserGroupPage;

+ 1 - 3
packages/app/src/components/Admin/UserGroup/UserGroupTable.tsx

@@ -53,7 +53,7 @@ const generateGroupIdToChildGroupsMap = (childUserGroups: IUserGroupHasId[]): Re
 };
 
 
-const UserGroupTable: FC<Props> = (props: Props) => {
+export const UserGroupTable: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
 
   /*
@@ -219,5 +219,3 @@ const UserGroupTable: FC<Props> = (props: Props) => {
     </>
   );
 };
-
-export default UserGroupTable;

+ 2 - 3
packages/app/src/components/Admin/UserGroupDetail/UpdateParentConfirmModal.tsx

@@ -1,4 +1,5 @@
 import React, { FC, useState } from 'react';
+
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
@@ -7,7 +8,7 @@ import {
 import { useUpdateUserGroupConfirmModal } from '~/stores/modal';
 
 
-const UpdateParentConfirmModal: FC = () => {
+export const UpdateParentConfirmModal: FC = () => {
   const { t } = useTranslation();
 
   const [isForceUpdate, setForceUpdate] = useState(false);
@@ -88,5 +89,3 @@ const UpdateParentConfirmModal: FC = () => {
     </Modal>
   );
 };
-
-export default UpdateParentConfirmModal;

+ 68 - 44
packages/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx

@@ -1,41 +1,50 @@
 import React, {
-  FC, useState, useCallback,
+  FC, useState, useCallback, useEffect,
 } from 'react';
-import { useTranslation } from 'next-i18next';
 
-import UserGroupForm from '../UserGroup/UserGroupForm';
-import UserGroupTable from '../UserGroup/UserGroupTable';
-import UserGroupModal from '../UserGroup/UserGroupModal';
-import UserGroupDeleteModal from '../UserGroup/UserGroupDeleteModal';
-import UpdateParentConfirmModal from './UpdateParentConfirmModal';
-import UserGroupDropdown from '../UserGroup/UserGroupDropdown';
-import UserGroupUserTable from './UserGroupUserTable';
-import UserGroupUserModal from './UserGroupUserModal';
-import UserGroupPageList from './UserGroupPageList';
+import { useTranslation } from 'next-i18next';
+import dynamic from 'next/dynamic';
+import { useRouter } from 'next/router';
 
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import {
   apiv3Get, apiv3Put, apiv3Delete, apiv3Post,
 } from '~/client/util/apiv3-client';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { IPageHasId } from '~/interfaces/page';
+import { IUserGroup, IUserGroupHasId } from '~/interfaces/user';
+import { useIsAclEnabled } from '~/stores/context';
+import { useUpdateUserGroupConfirmModal } from '~/stores/modal';
 import {
-  IUserGroup, IUserGroupHasId,
-} from '~/interfaces/user';
-import {
-  useSWRxUserGroupPages, useSWRxUserGroupRelationList, useSWRxChildUserGroupList,
+  useSWRxUserGroupPages, useSWRxUserGroupRelationList, useSWRxChildUserGroupList, useSWRxUserGroup,
   useSWRxSelectableParentUserGroups, useSWRxSelectableChildUserGroups, useSWRxAncestorUserGroups,
 } from '~/stores/user-group';
-import { useIsAclEnabled } from '~/stores/context';
-import { useUpdateUserGroupConfirmModal } from '~/stores/modal';
 
-const UserGroupDetailPage: FC = () => {
+import { isValidObjectId } from '../../../../../core/src/utils/objectid-utils';
+
+const UserGroupDeleteModal = dynamic(() => import('../UserGroup/UserGroupDeleteModal').then(mod => mod.UserGroupDeleteModal), { ssr: false });
+const UserGroupDropdown = dynamic(() => import('../UserGroup/UserGroupDropdown').then(mod => mod.UserGroupDropdown), { ssr: false });
+const UserGroupForm = dynamic(() => import('../UserGroup/UserGroupForm').then(mod => mod.UserGroupForm), { ssr: false });
+const UserGroupModal = dynamic(() => import('../UserGroup/UserGroupModal').then(mod => mod.UserGroupModal), { ssr: false });
+const UserGroupTable = dynamic(() => import('../UserGroup/UserGroupTable').then(mod => mod.UserGroupTable), { ssr: false });
+const UpdateParentConfirmModal = dynamic(() => import('./UpdateParentConfirmModal').then(mod => mod.UpdateParentConfirmModal), { ssr: false });
+// import UserGroupPageList from './UserGroupPageList';
+// import UserGroupUserModal from './UserGroupUserModal';
+// import UserGroupUserTable from './UserGroupUserTable';
+
+
+type Props = {
+  userGroupId?: string,
+}
+
+const UserGroupDetailPage = (props: Props) => {
   const { t } = useTranslation();
-  const adminUserGroupDetailElem = document.getElementById('admin-user-group-detail');
+  const router = useRouter();
+  const { userGroupId: currentUserGroupId } = props;
 
   /*
    * State (from AdminUserGroupDetailContainer)
    */
-  const [currentUserGroup, setUserGroup] = useState<IUserGroupHasId>(JSON.parse(adminUserGroupDetailElem?.getAttribute('data-user-group') || 'null'));
+  const { data: currentUserGroup } = useSWRxUserGroup(currentUserGroupId);
   const [relatedPages, setRelatedPages] = useState<IPageHasId[]>([]); // For page list
   const [searchType, setSearchType] = useState<string>('partial');
   const [isAlsoMailSearched, setAlsoMailSearched] = useState<boolean>(false);
@@ -45,12 +54,23 @@ const UserGroupDetailPage: FC = () => {
   const [isUpdateModalShown, setUpdateModalShown] = useState<boolean>(false);
   const [isDeleteModalShown, setDeleteModalShown] = useState<boolean>(false);
 
+  const isLoading = currentUserGroup === undefined;
+  const notExistsUerGroup = !isLoading && currentUserGroup == null;
+
+  useEffect(() => {
+    if (!isValidObjectId(currentUserGroupId) || notExistsUerGroup) {
+      router.push('/admin/user-groups');
+    }
+  }, [currentUserGroup, currentUserGroupId, notExistsUerGroup, router]);
+
+
   /*
    * Fetch
    */
-  const { data: userGroupPages } = useSWRxUserGroupPages(currentUserGroup._id, 10, 0);
+  const { data: userGroupPages } = useSWRxUserGroupPages(currentUserGroupId, 10, 0);
+
 
-  const { data: childUserGroupsList, mutate: mutateChildUserGroups } = useSWRxChildUserGroupList([currentUserGroup._id], true);
+  const { data: childUserGroupsList, mutate: mutateChildUserGroups } = useSWRxChildUserGroupList(currentUserGroupId ? [currentUserGroupId] : [], true);
   const childUserGroups = childUserGroupsList != null ? childUserGroupsList.childUserGroups : [];
   const grandChildUserGroups = childUserGroupsList != null ? childUserGroupsList.grandChildUserGroups : [];
   const childUserGroupIds = childUserGroups.map(group => group._id);
@@ -58,15 +78,16 @@ const UserGroupDetailPage: FC = () => {
   const { data: userGroupRelationList, mutate: mutateUserGroupRelations } = useSWRxUserGroupRelationList(childUserGroupIds);
   const childUserGroupRelations = userGroupRelationList != null ? userGroupRelationList : [];
 
-  const { data: selectableParentUserGroups, mutate: mutateSelectableParentUserGroups } = useSWRxSelectableParentUserGroups(currentUserGroup._id);
-  const { data: selectableChildUserGroups, mutate: mutateSelectableChildUserGroups } = useSWRxSelectableChildUserGroups(currentUserGroup._id);
+  const { data: selectableParentUserGroups, mutate: mutateSelectableParentUserGroups } = useSWRxSelectableParentUserGroups(currentUserGroupId);
+  const { data: selectableChildUserGroups, mutate: mutateSelectableChildUserGroups } = useSWRxSelectableChildUserGroups(currentUserGroupId);
 
-  const { data: ancestorUserGroups, mutate: mutateAncestorUserGroups } = useSWRxAncestorUserGroups(currentUserGroup._id);
+  const { data: ancestorUserGroups, mutate: mutateAncestorUserGroups } = useSWRxAncestorUserGroups(currentUserGroupId);
 
   const { data: isAclEnabled } = useIsAclEnabled();
 
   const { open: openUpdateParentConfirmModal } = useUpdateUserGroupConfirmModal();
 
+
   /*
    * Function
    */
@@ -98,13 +119,11 @@ const UserGroupDetailPage: FC = () => {
     });
     const { userGroup: updatedUserGroup } = res.data;
 
-    setUserGroup(updatedUserGroup);
-
     // mutate
     mutateAncestorUserGroups();
     mutateSelectableChildUserGroups();
     mutateSelectableParentUserGroups();
-  }, [setUserGroup, mutateAncestorUserGroups, mutateSelectableChildUserGroups, mutateSelectableParentUserGroups]);
+  }, [mutateAncestorUserGroups, mutateSelectableChildUserGroups, mutateSelectableParentUserGroups]);
 
   const onSubmitUpdateGroup = useCallback(
     async(targetGroup: IUserGroupHasId, userGroupData: Partial<IUserGroupHasId>, forceUpdateParents: boolean): Promise<void> => {
@@ -143,7 +162,7 @@ const UserGroupDetailPage: FC = () => {
   }, [t, openUpdateParentConfirmModal, onSubmitUpdateGroup]);
 
   const fetchApplicableUsers = useCallback(async(searchWord) => {
-    const res = await apiv3Get(`/user-groups/${currentUserGroup._id}/unrelated-users`, {
+    const res = await apiv3Get(`/user-groups/${currentUserGroupId}/unrelated-users`, {
       searchWord,
       searchType,
       isAlsoMailSearched,
@@ -153,18 +172,18 @@ const UserGroupDetailPage: FC = () => {
     const { users } = res.data;
 
     return users;
-  }, [searchType, isAlsoMailSearched, isAlsoNameSearched]);
+  }, [currentUserGroupId, searchType, isAlsoMailSearched, isAlsoNameSearched]);
 
   // TODO 85062: will be used in UserGroupUserFormByInput
   const addUserByUsername = useCallback(async(username: string) => {
-    await apiv3Post(`/user-groups/${currentUserGroup._id}/users/${username}`);
+    await apiv3Post(`/user-groups/${currentUserGroupId}/users/${username}`);
     mutateUserGroupRelations();
-  }, [currentUserGroup, mutateUserGroupRelations]);
+  }, [currentUserGroupId, mutateUserGroupRelations]);
 
   const removeUserByUsername = useCallback(async(username: string) => {
-    await apiv3Delete(`/user-groups/${currentUserGroup._id}/users/${username}`);
+    await apiv3Delete(`/user-groups/${currentUserGroupId}/users/${username}`);
     mutateUserGroupRelations();
-  }, [currentUserGroup, mutateUserGroupRelations]);
+  }, [currentUserGroupId, mutateUserGroupRelations]);
 
   const showUpdateModal = useCallback((group: IUserGroupHasId) => {
     setUpdateModalShown(true);
@@ -201,11 +220,11 @@ const UserGroupDetailPage: FC = () => {
     await openUpdateParentConfirmModal(
       selectedChild,
       {
-        parent: currentUserGroup._id,
+        parent: currentUserGroupId,
       },
       onSubmitUpdateGroup,
     );
-  }, [openUpdateParentConfirmModal, onSubmitUpdateGroup, currentUserGroup]);
+  }, [openUpdateParentConfirmModal, currentUserGroupId, onSubmitUpdateGroup]);
 
   const showCreateModal = useCallback(() => {
     setCreateModalShown(true);
@@ -220,7 +239,7 @@ const UserGroupDetailPage: FC = () => {
       await apiv3Post('/user-groups', {
         name: userGroupData.name,
         description: userGroupData.description,
-        parentId: currentUserGroup._id,
+        parentId: currentUserGroupId,
       });
 
       toastSuccess(t('toaster.update_successed', { target: t('UserGroup') }));
@@ -235,7 +254,7 @@ const UserGroupDetailPage: FC = () => {
     catch (err) {
       toastError(err);
     }
-  }, [t, currentUserGroup, mutateChildUserGroups, mutateSelectableChildUserGroups, mutateSelectableParentUserGroups, hideCreateModal]);
+  }, [currentUserGroupId, t, mutateChildUserGroups, mutateSelectableChildUserGroups, mutateSelectableParentUserGroups, hideCreateModal]);
 
   const showDeleteModal = useCallback(async(group: IUserGroupHasId) => {
     setSelectedUserGroup(group);
@@ -303,8 +322,8 @@ const UserGroupDetailPage: FC = () => {
             ancestorUserGroups != null && ancestorUserGroups.length > 0 && (
               ancestorUserGroups.map((ancestorUserGroup: IUserGroupHasId) => (
                 // eslint-disable-next-line max-len
-                <li key={ancestorUserGroup._id} className={`breadcrumb-item ${ancestorUserGroup._id === currentUserGroup._id ? 'active' : ''}`} aria-current="page">
-                  { ancestorUserGroup._id === currentUserGroup._id ? (
+                <li key={ancestorUserGroup._id} className={`breadcrumb-item ${ancestorUserGroup._id === currentUserGroupId ? 'active' : ''}`} aria-current="page">
+                  { ancestorUserGroup._id === currentUserGroupId ? (
                     <>{ancestorUserGroup.name}</>
                   ) : (
                     <a href={`/admin/user-group-detail/${ancestorUserGroup._id}`}>{ancestorUserGroup.name}</a>
@@ -325,8 +344,11 @@ const UserGroupDetailPage: FC = () => {
         />
       </div>
       <h2 className="admin-setting-header mt-4">{t('admin:user_group_management.user_list')}</h2>
-      <UserGroupUserTable />
-      <UserGroupUserModal />
+      {/* These compoents will be successfully shown in https://redmine.weseek.co.jp/issues/102159 */}
+      {/* <UserGroupUserTable /> */}
+      UserGroupUserTable
+      {/* <UserGroupUserModal /> */}
+      UserGroupUserModal
 
       <h2 className="admin-setting-header mt-4">{t('admin:user_group_management.child_group_list')}</h2>
       <UserGroupDropdown
@@ -372,7 +394,9 @@ const UserGroupDetailPage: FC = () => {
 
       <h2 className="admin-setting-header mt-4">{t('Page')}</h2>
       <div className="page-list">
-        <UserGroupPageList />
+        {/* This compoent will be successfully shown in https://redmine.weseek.co.jp/issues/102159 */}
+        {/* <UserGroupPageList /> */}
+        UserGroupPageList
       </div>
     </div>
   );

+ 2 - 4
packages/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx

@@ -1,13 +1,12 @@
 import React from 'react';
 
 import { UserPicture } from '@growi/ui';
+import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
-import { useTranslation } from 'next-i18next';
 import { debounce } from 'throttle-debounce';
 
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import Xss from '~/services/xss';
 
@@ -162,7 +161,6 @@ class UserGroupUserFormByInput extends React.Component {
 
 UserGroupUserFormByInput.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 
@@ -174,6 +172,6 @@ const UserGroupUserFormByInputWrapperFC = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const UserGroupUserFormByInputWrapper = withUnstatedContainers(UserGroupUserFormByInputWrapperFC, [AppContainer, AdminUserGroupDetailContainer]);
+const UserGroupUserFormByInputWrapper = withUnstatedContainers(UserGroupUserFormByInputWrapperFC, [AdminUserGroupDetailContainer]);
 
 export default UserGroupUserFormByInputWrapper;

+ 2 - 4
packages/app/src/components/Admin/UserGroupDetail/UserGroupUserModal.jsx

@@ -1,13 +1,12 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import {
   Modal, ModalHeader, ModalBody,
 } from 'reactstrap';
 
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
-import AppContainer from '~/client/services/AppContainer';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -81,7 +80,6 @@ class UserGroupUserModal extends React.Component {
 
 UserGroupUserModal.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 
@@ -92,6 +90,6 @@ const UserGroupUserModalWrapperFC = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const UserGroupUserModalWrapper = withUnstatedContainers(UserGroupUserModalWrapperFC, [AppContainer, AdminUserGroupDetailContainer]);
+const UserGroupUserModalWrapper = withUnstatedContainers(UserGroupUserModalWrapperFC, [AdminUserGroupDetailContainer]);
 
 export default UserGroupUserModalWrapper;

+ 1 - 3
packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.jsx

@@ -6,7 +6,6 @@ import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import Xss from '~/services/xss';
 
@@ -114,7 +113,6 @@ class UserGroupUserTable extends React.Component {
 
 UserGroupUserTable.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 
@@ -126,6 +124,6 @@ const UserGroupUserTableWrapperFC = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const UserGroupUserTableWrapper = withUnstatedContainers(UserGroupUserTableWrapperFC, [AppContainer, AdminUserGroupDetailContainer]);
+const UserGroupUserTableWrapper = withUnstatedContainers(UserGroupUserTableWrapperFC, [AdminUserGroupDetailContainer]);
 
 export default UserGroupUserTableWrapper;

+ 3 - 1
packages/app/src/components/Layout/AdminLayout.tsx

@@ -8,6 +8,8 @@ import { RawLayout } from './RawLayout';
 
 import styles from './Admin.module.scss';
 
+const AdminNotFoundPage = dynamic(() => import('../Admin/NotFoundPage').then(mod => mod.AdminNotFoundPage), { ssr: false });
+
 
 type Props = {
   title: string
@@ -43,7 +45,7 @@ const AdminLayout = ({
                 <AdminNavigation selected={selectedNavOpt} />
               </div>
               <div className="col-lg-9">
-                {children}
+                {children || <AdminNotFoundPage />}
               </div>
             </div>
           </div>

+ 1 - 1
packages/app/src/components/Layout/RawLayout.tsx

@@ -12,7 +12,7 @@ import { getBackgroundImageSrc } from '../Theme/utils/ThemeImageProvider';
 import { ThemeProvider } from '../Theme/utils/ThemeProvider';
 
 type Props = {
-  title: string,
+  title?: string,
   className?: string,
   children?: ReactNode,
 }

+ 26 - 12
packages/app/src/pages/admin/[[...path]].page.tsx

@@ -1,6 +1,6 @@
-import React from 'react';
+import React, { useCallback } from 'react';
 
-import { isClient } from '@growi/core';
+import { isClient, objectIdUtils } from '@growi/core';
 import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
@@ -55,13 +55,12 @@ const SlackIntegration = dynamic(() => import('../../components/Admin/SlackInteg
 const LegacySlackIntegration = dynamic(() => import('../../components/Admin/LegacySlackIntegration/LegacySlackIntegration'), { ssr: false });
 const UserManagement = dynamic(() => import('../../components/Admin/UserManagement'), { ssr: false });
 const ManageExternalAccount = dynamic(() => import('../../components/Admin/ManageExternalAccount'), { ssr: false });
-const UserGroupPage = dynamic(() => import('../../components/Admin/UserGroup/UserGroupPage'), { ssr: false });
 const ElasticsearchManagement = dynamic(() => import('../../components/Admin/ElasticsearchManagement/ElasticsearchManagement'), { ssr: false });
-// named export
-const AuditLogManagement = dynamic(() => import('../../components/Admin/AuditLogManagement').then(module => module.AuditLogManagement));
-
-
+const UserGroupDetailPage = dynamic(() => import('../../components/Admin/UserGroupDetail/UserGroupDetailPage'), { ssr: false });
 const AdminLayout = dynamic(() => import('../../components/Layout/AdminLayout'), { ssr: false });
+// named export
+const UserGroupPage = dynamic(() => import('../../components/Admin/UserGroup/UserGroupPage').then(mod => mod.UserGroupPage), { ssr: false });
+const AuditLogManagement = dynamic(() => import('../../components/Admin/AuditLogManagement').then(mod => mod.AuditLogManagement), { ssr: false });
 
 const pluginUtils = new PluginUtils();
 
@@ -88,6 +87,17 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
   const { path } = router.query;
   const pagePathKeys: string[] = Array.isArray(path) ? path : ['home'];
 
+  /*
+  * Set userGroupId as a adminPagesMap key
+  * eg) In case that url is `/user-group-detail/62e8388a9a649bea5e703ef7`, userGroupId will be 62e8388a9a649bea5e703ef7
+  */
+  let userGroupId;
+  const [firstPath, secondPath] = pagePathKeys;
+  if (firstPath === 'user-group-detail') {
+    userGroupId = objectIdUtils.isValidObjectId(secondPath) ? secondPath : undefined;
+  }
+
+  // TODO: refactoring adminPagesMap => https://redmine.weseek.co.jp/issues/102694
   const adminPagesMap = {
     home: {
       title: useCustomTitle(props, t('Wiki Management Home Page')),
@@ -150,6 +160,12 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
       title: useCustomTitle(props, t('UserGroup Management')),
       component: <UserGroupPage />,
     },
+    'user-group-detail': {
+      [userGroupId]: {
+        title: t('UserGroup Management'),
+        component: <UserGroupDetailPage userGroupId={userGroupId} />,
+      },
+    },
     search: {
       title: useCustomTitle(props, t('Full Text Search Management')),
       component: <ElasticsearchManagement />,
@@ -160,14 +176,13 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
     },
   };
 
-  const getTargetPageToRender = (pagesMap, keys) => {
+  const getTargetPageToRender = (pagesMap, keys): {title: string, component: JSX.Element} => {
     return keys.reduce((pagesMap, key) => {
       return pagesMap[key];
     }, pagesMap);
   };
 
-  const targetPage: {title: string, component: JSX.Element} = getTargetPageToRender(adminPagesMap, pagePathKeys);
-  const title = targetPage.title;
+  const targetPage = getTargetPageToRender(adminPagesMap, pagePathKeys);
 
   useCurrentUser(props.currentUser != null ? JSON.parse(props.currentUser) : null);
   // useIsMailerSetup(props.isMailerSetup);
@@ -239,13 +254,12 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
         adminTwitterSecurityContainer,
       );
     }
-
   }
 
 
   return (
     <Provider inject={[...injectableContainers, ...adminSecurityContainers]}>
-      <AdminLayout title={title} selectedNavOpt={pagePathKeys[0]}>
+      <AdminLayout title={targetPage.title} selectedNavOpt={firstPath}>
         {targetPage.component}
       </AdminLayout>
     </Provider>

+ 7 - 4
packages/app/src/server/routes/admin.js

@@ -524,10 +524,13 @@ module.exports = function(crowi, app) {
     return res.json(ApiResponse.success());
   };
 
-  actions.notFound = {};
-  actions.notFound.index = function(req, res) {
-    return res.render('admin/not_found');
-  };
+  /*
+  * Use AdminNotFoundPage component instead
+  */
+  // actions.notFound = {};
+  // actions.notFound.index = function(req, res) {
+  //   return res.render('admin/not_found');
+  // };
 
   return actions;
 };

+ 3 - 2
packages/app/src/server/views/admin/not_found.html

@@ -1,7 +1,8 @@
-{% extends '../layout/admin.html' %}
+<!-- Use AdminNotFoundPage component instead -->
+<!-- {% extends '../layout/admin.html' %}
 
 {% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('not_found_page.page_not_exist')) }}{% endblock %}
 
 {% block content_main %}
 <h1 class="title">{{ t('not_found_page.page_not_exist') }}</h1>
-{% endblock content_main %}
+{% endblock content_main %} -->