Forráskód Böngészése

Merge branch 'imprv/user-group-detail-page-clean-code' into imprv/commonize-user-group-form

Taichi Masuyama 4 éve
szülő
commit
aaf2570a7c

+ 2 - 2
packages/app/src/components/Admin/UserGroup/UserGroupCreateForm.jsx

@@ -43,11 +43,11 @@ class UserGroupCreateForm extends React.Component {
       const userGroup = res.data.userGroup;
       const userGroupId = userGroup._id;
 
-      const res2 = await this.props.appContainer.apiv3.get(`/user-groups/${userGroupId}/users`);
+      const res2 = await this.props.appContainer.apiv3.get(`/user-groups/${userGroupId}/users`); // TODO 85062: fetch userGroupRelationsById instead
 
       const { users } = res2.data;
 
-      this.props.onCreate(userGroup, users);
+      this.props.onCreate(userGroup, users); // TODO 85062: pass userGroupRelations instead of users
 
       this.setState({ name: '' });
 

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

@@ -1,11 +1,14 @@
-import React from 'react';
-import { WithTranslation, withTranslation } from 'react-i18next';
+import React, {
+  FC, useCallback, useState, useMemo,
+} from 'react';
+import { TFunction } from 'i18next';
+import { useTranslation } from 'react-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
 import AppContainer from '~/client/services/AppContainer';
-import { IUserGroup } from '~/interfaces/user';
+import { IUserGroupHasObjectId } from '~/interfaces/user';
 import { CustomWindow } from '~/interfaces/global';
 import Xss from '~/services/xss';
 
@@ -16,125 +19,112 @@ import Xss from '~/services/xss';
  * @class GrantSelector
  * @extends {React.Component}
  */
-interface Props extends WithTranslation {
+type Props = {
   appContainer: AppContainer,
 
-  userGroups: IUserGroup[],
-  deleteUserGroup?: IUserGroup,
+  userGroups: IUserGroupHasObjectId[],
+  deleteUserGroup?: IUserGroupHasObjectId,
   onDelete?: (deleteGroupId: string, actionName: string, transferToUserGroupId: string) => Promise<void> | void,
   isShow: boolean,
-  onShow?: (group: IUserGroup) => Promise<void> | void,
+  onShow?: (group: IUserGroupHasObjectId) => Promise<void> | void,
   onHide?: () => Promise<void> | void,
-}
-
-type State = {
-  actionName: string,
-  transferToUserGroupId: string,
 };
 
-class UserGroupDeleteModal extends React.Component<Props, State> {
-
-  actionForPages: any;
-
-  availableOptions: any;
-
-  xss: Xss;
-
-  state: State;
-
-  private initialState: State;
+type AvailableOption = {
+  id: number,
+  actionForPages: string,
+  iconClass: string,
+  styleClass: string,
+  label: ReturnType<TFunction>,
+};
 
-  constructor(props) {
-    super(props);
+// actionName master constants
+const actionForPages = {
+  public: 'public',
+  delete: 'delete',
+  transfer: 'transfer',
+};
 
-    const { t } = this.props;
+const UserGroupDeleteModal: FC<Props> = (props: Props) => {
+  const xss: Xss = (window as CustomWindow).xss;
 
-    // actionName master constants
-    this.actionForPages = {
-      public: 'public',
-      delete: 'delete',
-      transfer: 'transfer',
-    };
+  const { t } = useTranslation();
 
-    this.availableOptions = [
+  const availableOptions = useMemo<AvailableOption[]>(() => {
+    return [
       {
         id: 1,
-        actionForPages: this.actionForPages.public,
+        actionForPages: actionForPages.public,
         iconClass: 'icon-people',
         styleClass: '',
         label: t('admin:user_group_management.delete_modal.publish_pages'),
       },
       {
         id: 2,
-        actionForPages: this.actionForPages.delete,
+        actionForPages: actionForPages.delete,
         iconClass: 'icon-trash',
         styleClass: 'text-danger',
         label: t('admin:user_group_management.delete_modal.delete_pages'),
       },
       {
         id: 3,
-        actionForPages: this.actionForPages.transfer,
+        actionForPages: actionForPages.transfer,
         iconClass: 'icon-options',
         styleClass: '',
         label: t('admin:user_group_management.delete_modal.transfer_pages'),
       },
     ];
-
-    this.initialState = {
-      actionName: '',
-      transferToUserGroupId: '',
-    };
-
-    this.state = this.initialState;
-
-    this.xss = (window as CustomWindow).xss;
-
-    this.onHide = this.onHide.bind(this);
-    this.handleActionChange = this.handleActionChange.bind(this);
-    this.handleGroupChange = this.handleGroupChange.bind(this);
-    this.handleSubmit = this.handleSubmit.bind(this);
-    this.renderPageActionSelector = this.renderPageActionSelector.bind(this);
-    this.renderGroupSelector = this.renderGroupSelector.bind(this);
-    this.validateForm = this.validateForm.bind(this);
-  }
-
-  onHide() {
-    if (this.props.onHide == null) {
+  }, []);
+
+  /*
+   * State
+   */
+  const [actionName, setActionName] = useState<string>('');
+  const [transferToUserGroupId, setTransferToUserGroupId] = useState<string>('');
+
+  /*
+   * Function
+   */
+  const resetStates = useCallback(() => {
+    setActionName('');
+    setTransferToUserGroupId('');
+  }, []);
+
+  const onHide = useCallback(() => {
+    if (props.onHide == null) {
       return;
     }
 
-    this.setState(this.initialState);
-    this.props.onHide();
-  }
+    resetStates();
+    props.onHide();
+  }, [props.onHide]);
 
-  handleActionChange(e) {
+  const handleActionChange = useCallback((e) => {
     const actionName = e.target.value;
-    this.setState({ actionName });
-  }
+    setActionName(actionName);
+  }, []);
 
-  handleGroupChange(e) {
+  const handleGroupChange = useCallback((e) => {
     const transferToUserGroupId = e.target.value;
-    this.setState({ transferToUserGroupId });
-  }
+    setTransferToUserGroupId(transferToUserGroupId);
+  }, []);
 
-  handleSubmit(e) {
-    if (this.props.onDelete == null || this.props.deleteUserGroup == null) {
+  const handleSubmit = useCallback((e) => {
+    if (props.onDelete == null || props.deleteUserGroup == null) {
       return;
     }
 
     e.preventDefault();
 
-    this.props.onDelete(
-      this.props.deleteUserGroup._id,
-      this.state.actionName,
-      this.state.transferToUserGroupId,
+    props.onDelete(
+      props.deleteUserGroup._id,
+      actionName,
+      transferToUserGroupId,
     );
-  }
-
-  renderPageActionSelector() {
-    const { t } = this.props;
+  }, [props.onDelete, props.deleteUserGroup]);
 
-    const optoins = this.availableOptions.map((opt) => {
+  const renderPageActionSelector = useCallback(() => {
+    const optoins = availableOptions.map((opt) => {
       const dataContent = `<i class="icon icon-fw ${opt.iconClass} ${opt.styleClass}"></i> <span class="action-name ${opt.styleClass}">${opt.label}</span>`;
       return <option key={opt.id} value={opt.actionForPages} data-content={dataContent}>{opt.label}</option>;
     });
@@ -144,29 +134,29 @@ class UserGroupDeleteModal extends React.Component<Props, State> {
         name="actionName"
         className="form-control"
         placeholder="select"
-        value={this.state.actionName}
-        onChange={this.handleActionChange}
+        value={actionName}
+        onChange={handleActionChange}
       >
         <option value="" disabled>{t('admin:user_group_management.delete_modal.dropdown_desc')}</option>
         {optoins}
       </select>
     );
-  }
+  }, [handleActionChange]);
 
-  renderGroupSelector() {
-    const { t, deleteUserGroup } = this.props;
+  const renderGroupSelector = useCallback(() => {
+    const { deleteUserGroup } = props;
 
     if (deleteUserGroup == null) {
       return;
     }
 
-    const groups = this.props.userGroups.filter((group) => {
+    const groups = props.userGroups.filter((group) => {
       return group._id !== deleteUserGroup._id;
     });
 
     const options = groups.map((group) => {
-      const dataContent = `<i class="icon icon-fw icon-organization"></i> ${this.xss.process(group.name)}`;
-      return <option key={group._id} value={group._id} data-content={dataContent}>{this.xss.process(group.name)}</option>;
+      const dataContent = `<i class="icon icon-fw icon-organization"></i> ${xss.process(group.name)}`;
+      return <option key={group._id} value={group._id} data-content={dataContent}>{xss.process(group.name)}</option>;
     });
 
     const defaultOptionText = groups.length === 0 ? t('admin:user_group_management.delete_modal.no_groups')
@@ -175,60 +165,55 @@ class UserGroupDeleteModal extends React.Component<Props, State> {
     return (
       <select
         name="transferToUserGroupId"
-        className={`form-control ${this.state.actionName === this.actionForPages.transfer ? '' : 'd-none'}`}
-        value={this.state.transferToUserGroupId}
-        onChange={this.handleGroupChange}
+        className={`form-control ${actionName === actionForPages.transfer ? '' : 'd-none'}`}
+        value={transferToUserGroupId}
+        onChange={handleGroupChange}
       >
         <option value="" disabled>{defaultOptionText}</option>
         {options}
       </select>
     );
-  }
+  }, [actionName, transferToUserGroupId, props.userGroups, props.deleteUserGroup]);
 
-  validateForm() {
+  const validateForm = useCallback(() => {
     let isValid = true;
 
-    if (this.state.actionName === '') {
+    if (actionName === '') {
       isValid = false;
     }
-    else if (this.state.actionName === this.actionForPages.transfer) {
-      isValid = this.state.transferToUserGroupId !== '';
+    else if (actionName === actionForPages.transfer) {
+      isValid = transferToUserGroupId !== '';
     }
 
     return isValid;
-  }
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <Modal className="modal-md" isOpen={this.props.isShow} toggle={this.props.onHide}>
-        <ModalHeader tag="h4" toggle={this.props.onHide} className="bg-danger text-light">
-          <i className="icon icon-fire"></i> {t('admin:user_group_management.delete_modal.header')}
-        </ModalHeader>
-        <ModalBody>
-          <div>
-            <span className="font-weight-bold">{t('admin:user_group_management.group_name')}</span> : &quot;{this.props?.deleteUserGroup?.name || ''}&quot;
-          </div>
-          <div className="text-danger mt-5">
-            {t('admin:user_group_management.delete_modal.desc')}
+  }, [actionName, transferToUserGroupId]);
+
+  return (
+    <Modal className="modal-md" isOpen={props.isShow} toggle={onHide}>
+      <ModalHeader tag="h4" toggle={onHide} className="bg-danger text-light">
+        <i className="icon icon-fire"></i> {t('admin:user_group_management.delete_modal.header')}
+      </ModalHeader>
+      <ModalBody>
+        <div>
+          <span className="font-weight-bold">{t('admin:user_group_management.group_name')}</span> : &quot;{props?.deleteUserGroup?.name || ''}&quot;
+        </div>
+        <div className="text-danger mt-5">
+          {t('admin:user_group_management.delete_modal.desc')}
+        </div>
+      </ModalBody>
+      <ModalFooter>
+        <form className="d-flex justify-content-between w-100" onSubmit={handleSubmit}>
+          <div className="d-flex form-group mb-0">
+            {renderPageActionSelector()}
+            {renderGroupSelector()}
           </div>
-        </ModalBody>
-        <ModalFooter>
-          <form className="d-flex justify-content-between w-100" onSubmit={this.handleSubmit}>
-            <div className="d-flex form-group mb-0">
-              {this.renderPageActionSelector()}
-              {this.renderGroupSelector()}
-            </div>
-            <button type="submit" value="" className="btn btn-sm btn-danger text-nowrap" disabled={!this.validateForm()}>
-              <i className="icon icon-fire"></i> {t('Delete')}
-            </button>
-          </form>
-        </ModalFooter>
-      </Modal>
-    );
-  }
-
-}
+          <button type="submit" value="" className="btn btn-sm btn-danger text-nowrap" disabled={!validateForm()}>
+            <i className="icon icon-fire"></i> {t('Delete')}
+          </button>
+        </form>
+      </ModalFooter>
+    </Modal>
+  );
+};
 
-export default withTranslation()(UserGroupDeleteModal);
+export default UserGroupDeleteModal;

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

@@ -1,4 +1,6 @@
-import React, { Fragment } from 'react';
+import React, {
+  FC, Fragment, useState, useCallback, useEffect,
+} from 'react';
 
 import UserGroupTable from './UserGroupTable';
 import UserGroupCreateForm from './UserGroupCreateForm';
@@ -7,155 +9,115 @@ import UserGroupDeleteModal from './UserGroupDeleteModal';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { IUserGroup, IUserGroupRelation } from '~/interfaces/user';
+import { IUserGroupHasObjectId, IUserGroupRelation } from '~/interfaces/user';
 import Xss from '~/services/xss';
 import { CustomWindow } from '~/interfaces/global';
+import { apiv3Get, apiv3Delete } from '~/client/util/apiv3-client';
 
 type Props = {
   appContainer: AppContainer,
 };
-type State = {
-  userGroups: IUserGroup[],
-  userGroupRelations: IUserGroupRelation[],
-  selectedUserGroup: IUserGroup | undefined,
-  isDeleteModalShown: boolean,
-};
-
-class UserGroupPage extends React.Component<Props, State> {
-
-  xss: Xss;
-
-  state: State;
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      userGroups: [],
-      userGroupRelations: [],
-      selectedUserGroup: undefined, // not null but undefined (to use defaultProps in UserGroupDeleteModal)
-      isDeleteModalShown: false,
-    };
-
-    this.xss = (window as CustomWindow).xss;
 
-    this.showDeleteModal = this.showDeleteModal.bind(this);
-    this.hideDeleteModal = this.hideDeleteModal.bind(this);
-    this.addUserGroup = this.addUserGroup.bind(this);
-    this.deleteUserGroupById = this.deleteUserGroupById.bind(this);
-  }
+const UserGroupPage: FC<Props> = (props: Props) => {
+  const xss: Xss = (window as CustomWindow).xss;
+  const { isAclEnabled } = props.appContainer.config;
+
+  /*
+   * State
+   */
+  const [userGroups, setUserGroups] = useState<IUserGroupHasObjectId[]>([]);
+  const [userGroupRelations, setUserGroupRelations] = useState<IUserGroupRelation[]>([]);
+  const [selectedUserGroup, setSelectedUserGroup] = useState<IUserGroupHasObjectId | undefined>(undefined); // not null but undefined (to use defaultProps in UserGroupDeleteModal)
+  const [isDeleteModalShown, setDeleteModalShown] = useState<boolean>(false);
+
+  /*
+   * Functions
+   */
+  const syncUserGroupAndRelations = useCallback(async() => {
+    try {
+      const userGroupsRes = await apiv3Get('/user-groups', { pagination: false });
+      const userGroupRelationsRes = await apiv3Get('/user-group-relations');
 
-  async componentDidMount() {
-    await this.syncUserGroupAndRelations();
-  }
+      setUserGroups(userGroupsRes.data.userGroups);
+      setUserGroupRelations(userGroupRelationsRes.data.userGroupRelations);
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, []);
 
-  async showDeleteModal(group: IUserGroup) {
+  const showDeleteModal = useCallback(async(group: IUserGroupHasObjectId) => {
     try {
-      await this.syncUserGroupAndRelations();
+      await syncUserGroupAndRelations();
 
-      this.setState({
-        selectedUserGroup: group,
-        isDeleteModalShown: true,
-      });
+      setSelectedUserGroup(group);
+      setDeleteModalShown(true);
     }
     catch (err) {
       toastError(err);
     }
-  }
-
-  hideDeleteModal() {
-    this.setState({
-      selectedUserGroup: undefined,
-      isDeleteModalShown: false,
-    });
-  }
-
-  addUserGroup(userGroup, users) {
-    this.setState((prevState) => {
-      const userGroupRelations = Object.assign(prevState.userGroupRelations, {
-        [userGroup._id]: users,
-      });
+  }, []);
 
-      return {
-        userGroups: [...prevState.userGroups, userGroup],
-        userGroupRelations,
-      };
-    });
-  }
+  const hideDeleteModal = useCallback(() => {
+    setSelectedUserGroup(undefined);
+    setDeleteModalShown(false);
+  }, []);
 
-  async deleteUserGroupById(deleteGroupId: string, actionName: string, transferToUserGroupId: string) {
+  const addUserGroup = useCallback((userGroup: IUserGroupHasObjectId, userGroupRelations: IUserGroupRelation[]) => {
+    setUserGroups(prev => [...prev, userGroup]);
+    setUserGroupRelations(prev => ([...prev, ...userGroupRelations]));
+  }, []);
+
+  const deleteUserGroupById = useCallback(async(deleteGroupId: string, actionName: string, transferToUserGroupId: string) => {
     try {
-      const res = await this.props.appContainer.apiv3.delete(`/user-groups/${deleteGroupId}`, {
+      const res = await apiv3Delete(`/user-groups/${deleteGroupId}`, {
         actionName,
         transferToUserGroupId,
       });
 
-      this.setState((prevState) => {
-        const userGroups = prevState.userGroups.filter((userGroup) => {
-          return userGroup._id !== deleteGroupId;
-        });
-
-        delete prevState.userGroupRelations[deleteGroupId];
-
-        return {
-          userGroups,
-          userGroupRelations: prevState.userGroupRelations,
-          selectedUserGroup: undefined,
-          isDeleteModalShown: false,
-        };
-      });
+      setUserGroups(prev => prev.filter(userGroup => userGroup._id !== deleteGroupId));
+      setUserGroupRelations(prev => prev.filter(relation => relation.relatedGroup !== deleteGroupId));
+      setSelectedUserGroup(undefined);
+      setDeleteModalShown(false);
 
-      toastSuccess(`Deleted a group "${this.xss.process(res.data.userGroup.name)}"`);
+      toastSuccess(`Deleted a group "${xss.process(res.data.userGroup.name)}"`);
     }
     catch (err) {
       toastError(new Error('Unable to delete the group'));
     }
-  }
-
-  async syncUserGroupAndRelations() {
-    try {
-      const userGroupsRes = await this.props.appContainer.apiv3.get('/user-groups', { pagination: false });
-      const userGroupRelationsRes = await this.props.appContainer.apiv3.get('/user-group-relations');
-
-      this.setState({
-        userGroups: userGroupsRes.data.userGroups,
-        userGroupRelations: userGroupRelationsRes.data.userGroupRelations,
-      });
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  render() {
-    const { isAclEnabled } = this.props.appContainer.config;
-
-    return (
-      <Fragment>
-        <UserGroupCreateForm
-          isAclEnabled={isAclEnabled}
-          onCreate={this.addUserGroup}
-        />
-        <UserGroupTable
-          userGroups={this.state.userGroups}
-          isAclEnabled={isAclEnabled}
-          onDelete={this.showDeleteModal}
-          userGroupRelations={this.state.userGroupRelations}
-        />
-        <UserGroupDeleteModal
-          appContainer={this.props.appContainer}
-          userGroups={this.state.userGroups}
-          deleteUserGroup={this.state.selectedUserGroup}
-          onDelete={this.deleteUserGroupById}
-          isShow={this.state.isDeleteModalShown}
-          onShow={this.showDeleteModal}
-          onHide={this.hideDeleteModal}
-        />
-      </Fragment>
-    );
-  }
-
-}
+  }, []);
+
+  /*
+   * componentDidMount
+   */
+  useEffect(() => {
+    syncUserGroupAndRelations();
+  }, []);
+
+  return (
+    <Fragment>
+      <UserGroupCreateForm
+        isAclEnabled={isAclEnabled}
+        onCreate={addUserGroup}
+      />
+      <UserGroupTable
+        userGroups={userGroups}
+        isAclEnabled={isAclEnabled}
+        onDelete={showDeleteModal}
+        userGroupRelations={userGroupRelations}
+      />
+      <UserGroupDeleteModal
+        appContainer={props.appContainer}
+        userGroups={userGroups}
+        deleteUserGroup={selectedUserGroup}
+        onDelete={deleteUserGroupById}
+        isShow={isDeleteModalShown}
+        onShow={showDeleteModal}
+        onHide={hideDeleteModal}
+      />
+    </Fragment>
+  );
+};
 
 /**
  * Wrapper component for using unstated

+ 24 - 29
packages/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx

@@ -1,5 +1,5 @@
-import React from 'react';
-import { WithTranslation, withTranslation } from 'react-i18next';
+import React, { FC } from 'react';
+import { useTranslation } from 'react-i18next';
 
 import UserGroupEditForm from './UserGroupEditForm';
 import UserGroupUserTable from './UserGroupUserTable';
@@ -8,33 +8,28 @@ import UserGroupPageList from './UserGroupPageList';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 
-type Props = WithTranslation;
-
-class UserGroupDetailPage extends React.Component<Props> {
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <div>
-        <a href="/admin/user-groups" className="btn btn-outline-secondary">
-          <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
-          {t('admin:user_group_management.back_to_list')}
-        </a>
-        {/* TODO 85062: Link to the ancestors group */}
-        <div className="mt-4 form-box">
-          <UserGroupEditForm />
-        </div>
-        <h2 className="admin-setting-header mt-4">{t('admin:user_group_management.user_list')}</h2>
-        <UserGroupUserTable />
-        <UserGroupUserModal />
-        <h2 className="admin-setting-header mt-4">{t('Page')}</h2>
-        <div className="page-list">
-          <UserGroupPageList />
-        </div>
+const UserGroupDetailPage: FC = () => {
+  const { t } = useTranslation();
+
+  return (
+    <div>
+      <a href="/admin/user-groups" className="btn btn-outline-secondary">
+        <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
+        {t('admin:user_group_management.back_to_list')}
+      </a>
+      {/* TODO 85062: Link to the ancestors group */}
+      <div className="mt-4 form-box">
+        <UserGroupEditForm />
+      </div>
+      <h2 className="admin-setting-header mt-4">{t('admin:user_group_management.user_list')}</h2>
+      <UserGroupUserTable />
+      <UserGroupUserModal />
+      <h2 className="admin-setting-header mt-4">{t('Page')}</h2>
+      <div className="page-list">
+        <UserGroupPageList />
       </div>
-    );
-  }
+    </div>
+  );
 
 }
 
@@ -42,6 +37,6 @@ class UserGroupDetailPage extends React.Component<Props> {
 /**
  * Wrapper component for using unstated
  */
-const UserGroupDetailPageWrapper = withUnstatedContainers(withTranslation()(UserGroupDetailPage), [AppContainer]);
+const UserGroupDetailPageWrapper = withUnstatedContainers(UserGroupDetailPage, [AppContainer]);
 
 export default UserGroupDetailPageWrapper;

+ 5 - 3
packages/app/src/interfaces/user.ts

@@ -1,4 +1,5 @@
 import { Ref } from './common';
+import { HasObjectId } from './has-object-id';
 
 export type IUser = {
   name: string;
@@ -8,15 +9,16 @@ export type IUser = {
 }
 
 export type IUserGroupRelation = {
-  relatedGroup: IUserGroup,
-  relatedUser: IUser,
+  relatedGroup: Ref<IUserGroup>,
+  relatedUser: Ref<IUser>,
   createdAt: Date,
 }
 
 export type IUserGroup = {
-  _id: string;
   name: string;
   createdAt: Date;
   description: string;
   parent: Ref<IUserGroup>;
 }
+
+export type IUserGroupHasObjectId = IUserGroup & HasObjectId;