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

Moved unstated container into the root FC & Class component to FC

Taichi Masuyama 4 лет назад
Родитель
Сommit
559f57375b

+ 15 - 7
packages/app/src/client/services/AdminUserGroupDetailContainer.js

@@ -1,9 +1,17 @@
+/*
+ * TODO 85062: AdminUserGroupDetailContainer is under transplantation to UserGroupDetailPage.tsx
+ */
+
 import { Container } from 'unstated';
 
 import loggerFactory from '~/utils/logger';
 
 import { toastError } from '../util/apiNotification';
 
+import {
+  apiv3Get, apiv3Delete, apiv3Put, apiv3Post,
+} from '~/client/util/apiv3-client';
+
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:services:AdminUserGroupDetailContainer');
 
@@ -33,7 +41,7 @@ export default class AdminUserGroupDetailContainer extends Container {
       childUserGroups: [], // TODO 85062: fetch data on init (findChildGroupsByParentIds) For child group list
       grandChildUserGroups: [], // TODO 85062: fetch data on init (findChildGroupsByParentIds) For child group list
 
-      childUserGroupUsers: [], // TODO 85062: fetch data on init (findRelationsByGroupIds) For child group list
+      childUserGroupRelations: [], // TODO 85062: fetch data on init (findRelationsByGroupIds) For child group list users
       relatedPages: [], // For page list
       isUserGroupUserModalOpen: false,
       searchType: 'partial',
@@ -67,8 +75,8 @@ export default class AdminUserGroupDetailContainer extends Container {
         userGroupRelations,
         relatedPages,
       ] = await Promise.all([
-        this.appContainer.apiv3.get(`/user-groups/${this.state.userGroup._id}/user-group-relations`).then((res) => { return res.data.userGroupRelations }),
-        this.appContainer.apiv3.get(`/user-groups/${this.state.userGroup._id}/pages`).then((res) => { return res.data.pages }),
+        apiv3Get(`/user-groups/${this.state.userGroup._id}/user-group-relations`).then((res) => { return res.data.userGroupRelations }),
+        apiv3Get(`/user-groups/${this.state.userGroup._id}/pages`).then((res) => { return res.data.pages }),
       ]);
 
       await this.setState({
@@ -111,7 +119,7 @@ export default class AdminUserGroupDetailContainer extends Container {
    * @return {object} response object
    */
   async updateUserGroup(param) {
-    const res = await this.appContainer.apiv3.put(`/user-groups/${this.state.userGroup._id}`, param);
+    const res = await apiv3Put(`/user-groups/${this.state.userGroup._id}`, param);
     const { userGroup } = res.data;
 
     await this.setState({ userGroup });
@@ -142,7 +150,7 @@ export default class AdminUserGroupDetailContainer extends Container {
    * @param {string} username username of the user to be searched
    */
   async fetchApplicableUsers(searchWord) {
-    const res = await this.appContainer.apiv3.get(`/user-groups/${this.state.userGroup._id}/unrelated-users`, {
+    const res = await apiv3Get(`/user-groups/${this.state.userGroup._id}/unrelated-users`, {
       searchWord,
       searchType: this.state.searchType,
       isAlsoMailSearched: this.state.isAlsoMailSearched,
@@ -162,7 +170,7 @@ export default class AdminUserGroupDetailContainer extends Container {
    * @param {string} username username of the user to be added to the group
    */
   async addUserByUsername(username) {
-    const res = await this.appContainer.apiv3.post(`/user-groups/${this.state.userGroup._id}/users/${username}`);
+    const res = await apiv3Post(`/user-groups/${this.state.userGroup._id}/users/${username}`);
 
     // do not add users for ducaplicate
     if (res.data.userGroupRelation == null) { return }
@@ -177,7 +185,7 @@ export default class AdminUserGroupDetailContainer extends Container {
    * @param {string} username username of the user to be removed from the group
    */
   async removeUserByUsername(username) {
-    const res = await this.appContainer.apiv3.delete(`/user-groups/${this.state.userGroup._id}/users/${username}`);
+    const res = await apiv3Delete(`/user-groups/${this.state.userGroup._id}/users/${username}`);
 
     this.setState((prevState) => {
       return {

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

@@ -8,7 +8,7 @@ import {
 } from 'reactstrap';
 
 import AppContainer from '~/client/services/AppContainer';
-import { IUserGroupHasObjectId } from '~/interfaces/user';
+import { IUserGroupHasId } from '~/interfaces/user';
 import { CustomWindow } from '~/interfaces/global';
 import Xss from '~/services/xss';
 
@@ -22,11 +22,11 @@ import Xss from '~/services/xss';
 type Props = {
   appContainer: AppContainer,
 
-  userGroups: IUserGroupHasObjectId[],
-  deleteUserGroup?: IUserGroupHasObjectId,
+  userGroups: IUserGroupHasId[],
+  deleteUserGroup?: IUserGroupHasId,
   onDelete?: (deleteGroupId: string, actionName: string, transferToUserGroupId: string) => Promise<void> | void,
   isShow: boolean,
-  onShow?: (group: IUserGroupHasObjectId) => Promise<void> | void,
+  onShow?: (group: IUserGroupHasId) => Promise<void> | void,
   onHide?: () => Promise<void> | void,
 };
 

+ 63 - 87
packages/app/src/components/Admin/UserGroup/UserGroupForm.tsx

@@ -1,119 +1,95 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import React, { FC, useCallback, useState } from 'react';
+import { useTranslation } from 'react-i18next';
 import dateFnsFormat from 'date-fns/format';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
-import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { IUserGroup, IUserGroupHasId } from '~/interfaces/user';
+import { CustomWindow } from '~/interfaces/global';
+import Xss from '~/services/xss';
 
 type Props = {
-
+  userGroup: IUserGroupHasId,
+  onSubmit?: (userGroupData: Partial<IUserGroup>) => Promise<IUserGroupHasId>
 };
 
-const UserGroupForm: FC = () => {};
-
-class UserGroupEditForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    const { adminUserGroupDetailContainer } = props;
-    const { userGroup } = adminUserGroupDetailContainer.state;
+const UserGroupForm: FC<Props> = (props: Props) => {
+  const xss: Xss = (window as CustomWindow).xss;
+  const { t } = useTranslation();
 
-    this.state = {
-      name: userGroup.name,
-      nameCache: userGroup.name, // cache for name. update every submit
-    };
+  /*
+   * State
+   */
+  const [currentName, setName] = useState(props.userGroup.name);
+  const [nameCache, setNameCache] = useState(props.userGroup.name); // to validate the same name
 
-    this.xss = window.xss;
+  /*
+   * Function
+   */
+  const onChangeNameHandler = useCallback((e) => {
+    setName(e.target.value);
+  }, []);
 
-    this.changeUserGroupName = this.changeUserGroupName.bind(this);
-    this.handleSubmit = this.handleSubmit.bind(this);
-    this.validateForm = this.validateForm.bind(this);
-  }
+  const onSubmitHandler = useCallback(async(e) => {
+    e.preventDefault(); // no reload
 
-  changeUserGroupName(event) {
-    this.setState({
-      name: event.target.value,
-    });
-  }
-
-  // OUT
-  async handleSubmit(e) {
-    e.preventDefault();
+    if (props.onSubmit == null) {
+      return;
+    }
 
     try {
-      const res = await this.props.adminUserGroupDetailContainer.updateUserGroup({
-        name: this.state.name,
-      });
+      const newUserGroup = await props.onSubmit({ name: currentName });
 
-      toastSuccess(`Updated the group name to "${this.xss.process(res.data.userGroup.name)}"`);
-      this.setState({ nameCache: this.state.name });
+      toastSuccess(`Updated the group name to "${xss.process(newUserGroup.name)}"`);
+      setNameCache(currentName);
     }
     catch (err) {
       toastError(new Error('Unable to update the group name'));
     }
-  }
+  }, [currentName, props.onSubmit]);
 
-  // OUT
-  validateForm() {
-    return (
-      this.state.name !== this.state.nameCache
-      && this.state.name !== ''
-    );
-  }
+  const validateForm = useCallback(() => { return currentName !== nameCache && currentName !== '' }, [currentName, nameCache]);
 
-  render() {
-    const { t, adminUserGroupDetailContainer } = this.props;
 
-    return (
-      <form onSubmit={this.handleSubmit}>
-        <fieldset>
-          <h2 className="admin-setting-header">{t('admin:user_group_management.basic_info')}</h2>
-          <div className="form-group row">
-            <label htmlFor="name" className="col-md-2 col-form-label">
-              {t('Name')}
-            </label>
-            <div className="col-md-4">
-              <input className="form-control" type="text" name="name" value={this.state.name} onChange={this.changeUserGroupName} />
-            </div>
+  return (
+    <form onSubmit={onSubmitHandler}>
+      <fieldset>
+        <h2 className="admin-setting-header">{t('admin:user_group_management.basic_info')}</h2>
+        <div className="form-group row">
+          <label htmlFor="name" className="col-md-2 col-form-label">
+            {t('Name')}
+          </label>
+          <div className="col-md-4">
+            <input className="form-control" type="text" name="name" value={currentName} onChange={onChangeNameHandler} />
           </div>
-          <div className="form-group row">
-            <label className="col-md-2 col-form-label">{t('Created')}</label>
-            <div className="col-md-4">
-              <input
-                type="text"
-                className="form-control"
-                value={dateFnsFormat(new Date(adminUserGroupDetailContainer.state.userGroup.createdAt), 'yyyy-MM-dd')}
-                disabled
-              />
-            </div>
+        </div>
+        <div className="form-group row">
+          <label className="col-md-2 col-form-label">{t('Created')}</label>
+          <div className="col-md-4">
+            <input
+              type="text"
+              className="form-control"
+              value={dateFnsFormat(new Date(props.userGroup.createdAt), 'yyyy-MM-dd')}
+              disabled
+            />
           </div>
-          <div className="form-group row">
-            <div className="offset-md-2 col-md-10">
-              <button type="submit" className="btn btn-primary" disabled={!this.validateForm()}>
-                {t('Update')}
-              </button>
-            </div>
+        </div>
+        <div className="form-group row">
+          <div className="offset-md-2 col-md-10">
+            <button type="submit" className="btn btn-primary" disabled={!validateForm()}>
+              {t('Update')}
+            </button>
           </div>
-        </fieldset>
-      </form>
-    );
-  }
-
-}
-
-UserGroupEditForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
+        </div>
+      </fieldset>
+    </form>
+  );
 };
 
 /**
  * Wrapper component for using unstated
  */
-const UserGroupEditFormWrapper = withUnstatedContainers(UserGroupEditForm, [AppContainer, AdminUserGroupDetailContainer]);
+const UserGroupFormWrapper = withUnstatedContainers(UserGroupForm, [AppContainer]);
 
-export default withTranslation()(UserGroupEditFormWrapper);
+export default UserGroupFormWrapper;

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

@@ -9,7 +9,7 @@ import UserGroupDeleteModal from './UserGroupDeleteModal';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { IUserGroupHasObjectId, IUserGroupRelation } from '~/interfaces/user';
+import { IUserGroupHasId, IUserGroupRelation } from '~/interfaces/user';
 import Xss from '~/services/xss';
 import { CustomWindow } from '~/interfaces/global';
 import { apiv3Get, apiv3Delete } from '~/client/util/apiv3-client';
@@ -25,9 +25,9 @@ const UserGroupPage: FC<Props> = (props: Props) => {
   /*
    * State
    */
-  const [userGroups, setUserGroups] = useState<IUserGroupHasObjectId[]>([]);
+  const [userGroups, setUserGroups] = useState<IUserGroupHasId[]>([]);
   const [userGroupRelations, setUserGroupRelations] = useState<IUserGroupRelation[]>([]);
-  const [selectedUserGroup, setSelectedUserGroup] = useState<IUserGroupHasObjectId | undefined>(undefined); // not null but undefined (to use defaultProps in UserGroupDeleteModal)
+  const [selectedUserGroup, setSelectedUserGroup] = useState<IUserGroupHasId | undefined>(undefined); // not null but undefined (to use defaultProps in UserGroupDeleteModal)
   const [isDeleteModalShown, setDeleteModalShown] = useState<boolean>(false);
 
   /*
@@ -46,7 +46,7 @@ const UserGroupPage: FC<Props> = (props: Props) => {
     }
   }, []);
 
-  const showDeleteModal = useCallback(async(group: IUserGroupHasObjectId) => {
+  const showDeleteModal = useCallback(async(group: IUserGroupHasId) => {
     try {
       await syncUserGroupAndRelations();
 
@@ -63,7 +63,7 @@ const UserGroupPage: FC<Props> = (props: Props) => {
     setDeleteModalShown(false);
   }, []);
 
-  const addUserGroup = useCallback((userGroup: IUserGroupHasObjectId, userGroupRelations: IUserGroupRelation[]) => {
+  const addUserGroup = useCallback((userGroup: IUserGroupHasId, userGroupRelations: IUserGroupRelation[]) => {
     setUserGroups(prev => [...prev, userGroup]);
     setUserGroupRelations(prev => ([...prev, ...userGroupRelations]));
   }, []);

+ 119 - 1
packages/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx

@@ -1,4 +1,6 @@
-import React, { FC } from 'react';
+import React, {
+  FC, useState, useCallback, useEffect,
+} from 'react';
 import { useTranslation } from 'react-i18next';
 
 import UserGroupEditForm from './UserGroupEditForm';
@@ -7,10 +9,126 @@ import UserGroupUserModal from './UserGroupUserModal';
 import UserGroupPageList from './UserGroupPageList';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
+import { apiv3Get, apiv3Put, apiv3Delete, apiv3Post } from '~/client/util/apiv3-client';
+import { toastError } from '~/client/util/apiNotification';
+import { IPageHasId } from '~/interfaces/page';
+import { IUserGroup, IUserGroupHasId, IUserGroupRelation, IUserGroupRelationHasId } from '~/interfaces/user';
 
 const UserGroupDetailPage: FC = () => {
+  const rootElem = document.getElementById('admin-user-group-detail');
   const { t } = useTranslation();
 
+  /*
+   * State (from AdminUserGroupDetailContainer)
+   */
+  const [userGroup, setUserGroup] = useState<IUserGroupHasId>(JSON.parse(rootElem?.getAttribute('data-user-group') || 'null'));
+  const [userGroupRelations, setUserGroupRelations] = useState<IUserGroupRelationHasId[]>([]); // For user list
+
+  // TODO 85062: /_api/v3/user-groups/children?include_grand_child=boolean
+  const [childUserGroups, setChildUserGroups] = useState<IUserGroupHasId[]>([]); // TODO 85062: fetch data on init (findChildGroupsByParentIds) For child group list
+  const [grandChildUserGroups, setGrandChildUserGroups] = useState<IUserGroupHasId[]>([]); // TODO 85062: fetch data on init (findChildGroupsByParentIds) For child group list
+
+  const [childUserGroupRelations, setChildUserGroupRelations] = useState<IUserGroupRelation[]>([]); // TODO 85062: fetch data on init (findRelationsByGroupIds) For child group list
+  const [relatedPages, setRelatedPages] = useState<IPageHasId[]>([]); // For page list
+  const [isUserGroupUserModalOpen, setUserGroupUserModalOpen] = useState<boolean>(false);
+  const [searchType, setSearchType] = useState<string>('partial');
+  const [isAlsoMailSearched, setAlsoMailSearched] = useState<boolean>(false);
+  const [isAlsoNameSearched, setAlsoNameSearched] = useState<boolean>(false);
+
+  /*
+   * Function
+   */
+  const init = useCallback(async() => {
+    try {
+      const [
+        userGroupRelations,
+        relatedPages,
+      ] = await Promise.all([
+        apiv3Get(`/user-groups/${userGroup._id}/user-group-relations`).then(res => res.data.userGroupRelations),
+        apiv3Get(`/user-groups/${userGroup._id}/pages`).then(res => res.data.pages),
+      ]);
+
+      setUserGroup(userGroupRelations);
+      setUserGroupRelations(relatedPages);
+    }
+    catch (err) {
+      toastError(new Error('Failed to fetch data'));
+    }
+  }, []); // no deps
+
+  // TODO 85062: old name: switchIsAlsoMailSearched
+  const toggleIsAlsoMailSearched = useCallback(() => {
+    setAlsoMailSearched(prev => !prev);
+  }, []);
+
+  // TODO 85062: old name: switchIsAlsoNameSearched
+  const toggleAlsoNameSearched = useCallback(() => {
+    setAlsoNameSearched(prev => !prev);
+  }, []);
+
+  const switchSearchType = useCallback((searchType) => {
+    setSearchType(searchType);
+  }, []);
+
+  const updateUserGroup = useCallback(async(param: Partial<IUserGroup>) => {
+    const res = await apiv3Put<{ userGroup: IUserGroupHasId }>(`/user-groups/${userGroup._id}`, param);
+    const { userGroup: newUserGroup } = res.data;
+
+    setUserGroup(newUserGroup);
+
+    return newUserGroup;
+  }, [userGroup])
+
+  const openUserGroupUserModal = useCallback(() => {
+    setUserGroupUserModalOpen(true);
+  }, []);
+
+  const closeUserGroupUserModal = useCallback(() => {
+    setUserGroupUserModalOpen(false);
+  }, []);
+
+  const fetchApplicableUsers = useCallback(async(searchWord) => {
+    const res = await apiv3Get(`/user-groups/${userGroup._id}/unrelated-users`, {
+      searchWord,
+      searchType,
+      isAlsoMailSearched,
+      isAlsoNameSearched,
+    });
+
+    const { users } = res.data;
+
+    return users;
+  }, [searchType, isAlsoMailSearched, isAlsoNameSearched]);
+
+  const addUserByUsername = useCallback(async(username: string) => {
+    const res = await apiv3Post(`/user-groups/${userGroup._id}/users/${username}`);
+
+    // do not add users for ducaplicate
+    if (res.data.userGroupRelation == null) { return }
+
+    await init();
+  }, [userGroup]);
+
+  const removeUserByUsername = useCallback(async(username: string) => {
+    const res = await apiv3Delete(`/user-groups/${userGroup._id}/users/${username}`);
+
+    setUserGroupRelations(prev => prev.filter(u => u._id !== res.data.userGroupRelation._id))
+  }, [userGroup]);
+
+  /*
+   * componentDidMount
+   */
+  useEffect(() => {
+    init();
+  }, []);
+
+  /*
+   * Dependencies
+   */
+  if (userGroup == null) {
+    return <></>;
+  }
+
   return (
     <div>
       <a href="/admin/user-groups" className="btn btn-outline-secondary">

+ 2 - 1
packages/app/src/interfaces/user.ts

@@ -21,4 +21,5 @@ export type IUserGroup = {
   parent: Ref<IUserGroup>;
 }
 
-export type IUserGroupHasObjectId = IUserGroup & HasObjectId;
+export type IUserGroupHasId = IUserGroup & HasObjectId;
+export type IUserGroupRelationHasId = IUserGroupRelation & HasObjectId;

+ 0 - 4
packages/app/src/server/routes/apiv3/user-group.js

@@ -202,10 +202,6 @@ module.exports = (crowi) => {
     }
   });
 
-  // return one group with the id
-  // router.get('/:id', async(req, res) => {
-  // });
-
   validator.update = [
     body('name', 'Group name is required').trim().exists({ checkFalsy: true }),
   ];