Browse Source

Commonized forms

Taichi Masuyama 4 years ago
parent
commit
81760127ca

+ 6 - 1
packages/app/resource/locales/en_US/translation.json

@@ -21,6 +21,7 @@
   "Done": "Done",
   "Cancel": "Cancel",
   "Create": "Create",
+  "Description": "Description",
   "Admin": "Admin",
   "administrator": "Admin",
   "Tag": "Tag",
@@ -119,6 +120,7 @@
   "Legacy_Slack_Integration": "Legacy Slack Integration",
   "User_Management": "User Management",
   "external_account_management": "External Account Management",
+  "UserGroup": "UserGroup",
   "UserGroup Management": "UserGroup Management",
   "Full Text Search Management": "Full Text Search Management",
   "Import Data": "Import Data",
@@ -243,7 +245,7 @@
     "expire": "Expiration",
     "Days": "Days",
     "Custom": "Custom",
-    "description": "description",
+    "description": "Description",
     "enter_desc": "Enter description",
     "Unlimited": "unlimited",
     "Issue": "Issue",
@@ -501,7 +503,10 @@
     "page_not_found_in_preview": "\"{{path}}\" is not a GROWI page."
   },
   "toaster": {
+    "create_succeeded": "Succeeded to create {{target}}",
+    "create_failed": "Failed to create {{target}}",
     "update_successed": "Succeeded to update {{target}}",
+    "update_failed": "Failed to update {{target}}",
     "initialize_successed": "Succeeded to initialize {{target}}",
     "give_user_admin": "Succeeded to give {{username}} admin",
     "remove_user_admin": "Succeeded to remove {{username}} admin",

+ 5 - 0
packages/app/resource/locales/ja_JP/translation.json

@@ -21,6 +21,7 @@
   "Done": "完了",
   "Cancel": "キャンセル",
   "Create": "作成",
+  "Description": "説明",
   "Admin": "管理",
   "administrator": "管理者",
   "Tag": "タグ",
@@ -119,6 +120,7 @@
   "Legacy_Slack_Integration": "Slack連携 (レガシー)",
   "User_Management": "ユーザー管理",
   "external_account_management": "外部アカウント管理",
+  "UserGroup": "グループ",
   "UserGroup Management": "グループ管理",
   "Full Text Search Management": "全文検索管理",
   "Import Data": "データインポート",
@@ -501,7 +503,10 @@
     "page_not_found_in_preview": "\"{{path}}\" というページはありません。"
   },
   "toaster": {
+    "create_succeeded": "新しい{{target}}が作成されました",
+    "create_failed": "{{target}}の作成に失敗しました",
     "update_successed": "{{target}}を更新しました",
+    "update_failed": "{{target}}の更新に失敗しました",
     "initialize_successed": "{{target}}を初期化しました",
     "give_user_admin": "{{username}}を管理者に設定しました",
     "remove_user_admin": "{{username}}を管理者から外しました",

+ 5 - 0
packages/app/resource/locales/zh_CN/translation.json

@@ -22,6 +22,7 @@
   "Done": "Done",
   "Cancel": "取消",
 	"Create": "创建",
+  "Description": "描述",
 	"Admin": "管理",
 	"administrator": "管理员",
 	"Tag": "标签",
@@ -127,6 +128,7 @@
   "Legacy_Slack_Integration": "旧版Slack一体化",
 	"User_Management": "用户管理",
 	"external_account_management": "外部账户管理",
+  "UserGroup": "用户组",
 	"UserGroup Management": "用户组管理",
 	"Full Text Search Management": "全文搜索管理",
 	"Import Data": "导入数据",
@@ -479,7 +481,10 @@
     "page_not_found_in_preview": "\"{{path}}\" is not a GROWI page."
   },
 	"toaster": {
+    "create_succeeded": "Succeeded to create {{target}}",
+    "create_failed": "Failed to create {{target}}",
 		"update_successed": "Succeeded to update {{target}}",
+    "update_failed": "Failed to update {{target}}",
     "initialize_successed": "Succeeded to initialize {{target}}",
 		"give_user_admin": "Succeeded to give {{username}} admin",
     "remove_user_admin": "Succeeded to remove {{username}} admin ",

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

@@ -1,7 +1,7 @@
 import React, {
   FC, useCallback, useState, useMemo,
 } from 'react';
-import { TFunction } from 'i18next';
+import { TFunctionResult } from 'i18next';
 import { useTranslation } from 'react-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
@@ -35,7 +35,7 @@ type AvailableOption = {
   actionForPages: string,
   iconClass: string,
   styleClass: string,
-  label: ReturnType<TFunction>,
+  label: TFunctionResult,
 };
 
 // actionName master constants

+ 48 - 24
packages/app/src/components/Admin/UserGroup/UserGroupForm.tsx

@@ -1,6 +1,7 @@
 import React, { FC, useCallback, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import dateFnsFormat from 'date-fns/format';
+import { TFunctionResult } from 'i18next';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
@@ -10,19 +11,24 @@ import { CustomWindow } from '~/interfaces/global';
 import Xss from '~/services/xss';
 
 type Props = {
-  userGroup: IUserGroupHasId,
-  onSubmit?: (userGroupData: Partial<IUserGroup>) => Promise<IUserGroupHasId>
+  userGroup?: IUserGroupHasId,
+  successedMessage: TFunctionResult;
+  failedMessage: TFunctionResult;
+  submitButtonLabel: TFunctionResult;
+  onSubmit?: (userGroupData: Partial<IUserGroup>) => Promise<IUserGroupHasId | void>
 };
 
 const UserGroupForm: FC<Props> = (props: Props) => {
   const xss: Xss = (window as CustomWindow).xss;
+
   const { t } = useTranslation();
 
   /*
    * State
    */
-  const [currentName, setName] = useState(props.userGroup.name);
-  const [nameCache, setNameCache] = useState(props.userGroup.name); // to validate the same name
+  const [currentName, setName] = useState(props.userGroup != null ? props.userGroup.name : '');
+  const [currentDescription, setDescription] = useState(props.userGroup != null ? props.userGroup.description : '');
+  const [currentParent, setParent] = useState(props.userGroup != null ? props.userGroup.parent : '');
 
   /*
    * Function
@@ -31,6 +37,10 @@ const UserGroupForm: FC<Props> = (props: Props) => {
     setName(e.target.value);
   }, []);
 
+  const onChangeDescriptionHandler = useCallback((e) => {
+    setDescription(e.target.value);
+  }, []);
+
   const onSubmitHandler = useCallback(async(e) => {
     e.preventDefault(); // no reload
 
@@ -39,46 +49,60 @@ const UserGroupForm: FC<Props> = (props: Props) => {
     }
 
     try {
-      const newUserGroup = await props.onSubmit({ name: currentName });
+      await props.onSubmit({ name: currentName, description: currentDescription, parent: currentParent });
 
-      toastSuccess(`Updated the group name to "${xss.process(newUserGroup.name)}"`);
-      setNameCache(currentName);
+      toastSuccess(props.successedMessage);
     }
     catch (err) {
-      toastError(new Error('Unable to update the group name'));
+      toastError(props.failedMessage);
     }
-  }, [currentName, props.onSubmit]);
-
-  const validateForm = useCallback(() => { return currentName !== nameCache && currentName !== '' }, [currentName, nameCache]);
-
+  }, [currentName, currentDescription, currentParent, props.onSubmit, props.successedMessage, props.failedMessage]);
 
   return (
     <form onSubmit={onSubmitHandler}>
+      {/* TODO 85062: improve style */}
+      {
+        props.userGroup != null && (
+          <div className="row mb-2">
+            <p className="col-md-4">{t('Created')}</p>
+            <p className="col">{dateFnsFormat(new Date(props.userGroup.createdAt), 'yyyy-MM-dd')}</p>
+          </div>
+        )
+      }
+
       <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')}
+            {t('admin:user_group_management.group_name')}
           </label>
           <div className="col-md-4">
-            <input className="form-control" type="text" name="name" value={currentName} onChange={onChangeNameHandler} />
+            <input
+              className="form-control"
+              type="text"
+              name="name"
+              placeholder={t('admin:user_group_management.group_example')}
+              value={currentName}
+              onChange={onChangeNameHandler}
+              required
+            />
           </div>
         </div>
         <div className="form-group row">
-          <label className="col-md-2 col-form-label">{t('Created')}</label>
+          <label htmlFor="description" className="col-md-2 col-form-label">
+            {t('Description')}
+          </label>
           <div className="col-md-4">
-            <input
-              type="text"
-              className="form-control"
-              value={dateFnsFormat(new Date(props.userGroup.createdAt), 'yyyy-MM-dd')}
-              disabled
-            />
+            <textarea className="form-control" name="description" value={currentDescription} onChange={onChangeDescriptionHandler} required />
           </div>
         </div>
+
+        {/* TODO 85062: select parent dropdown */}
+
         <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 type="submit" className="btn btn-primary">
+              {props.submitButtonLabel}
             </button>
           </div>
         </div>
@@ -90,6 +114,6 @@ const UserGroupForm: FC<Props> = (props: Props) => {
 /**
  * Wrapper component for using unstated
  */
-const UserGroupFormWrapper = withUnstatedContainers(UserGroupForm, [AppContainer]);
+const UserGroupFormWrapper = withUnstatedContainers<unknown, Props>(UserGroupForm, [AppContainer]);
 
 export default UserGroupFormWrapper;

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

@@ -1,18 +1,19 @@
 import React, {
   FC, Fragment, useState, useCallback, useEffect,
 } from 'react';
+import { useTranslation } from 'react-i18next';
 
 import UserGroupTable from './UserGroupTable';
-import UserGroupCreateForm from './UserGroupCreateForm';
+import UserGroupForm from './UserGroupForm';
 import UserGroupDeleteModal from './UserGroupDeleteModal';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { IUserGroupHasId, IUserGroupRelation } from '~/interfaces/user';
+import { IUserGroup, IUserGroupHasId, IUserGroupRelation } from '~/interfaces/user';
 import Xss from '~/services/xss';
 import { CustomWindow } from '~/interfaces/global';
-import { apiv3Get, apiv3Delete } from '~/client/util/apiv3-client';
+import { apiv3Get, apiv3Delete, apiv3Post } from '~/client/util/apiv3-client';
 
 type Props = {
   appContainer: AppContainer,
@@ -20,6 +21,7 @@ type Props = {
 
 const UserGroupPage: FC<Props> = (props: Props) => {
   const xss: Xss = (window as CustomWindow).xss;
+  const { t } = useTranslation();
   const { isAclEnabled } = props.appContainer.config;
 
   /*
@@ -63,9 +65,20 @@ const UserGroupPage: FC<Props> = (props: Props) => {
     setDeleteModalShown(false);
   }, []);
 
-  const addUserGroup = useCallback((userGroup: IUserGroupHasId, userGroupRelations: IUserGroupRelation[]) => {
-    setUserGroups(prev => [...prev, userGroup]);
-    setUserGroupRelations(prev => ([...prev, ...userGroupRelations]));
+  const addUserGroup = useCallback(async(userGroupData: IUserGroup) => {
+    try {
+      const res = await apiv3Post('/user-groups', {
+        name: userGroupData.name,
+        description: userGroupData.description,
+        parent: userGroupData.parent,
+      });
+
+      const newUserGroup = res.data.userGroup;
+      setUserGroups(prev => [...prev, newUserGroup]);
+    }
+    catch (err) {
+      toastError(err);
+    }
   }, []);
 
   const deleteUserGroupById = useCallback(async(deleteGroupId: string, actionName: string, transferToUserGroupId: string) => {
@@ -96,10 +109,25 @@ const UserGroupPage: FC<Props> = (props: Props) => {
 
   return (
     <Fragment>
-      <UserGroupCreateForm
-        isAclEnabled={isAclEnabled}
-        onCreate={addUserGroup}
-      />
+      {
+        isAclEnabled ? (
+          <div className="mb-2">
+            <button type="button" className="btn btn-outline-secondary" data-toggle="collapse" data-target="#createGroupForm">
+              {t('admin:user_group_management.create_group')}
+            </button>
+            <div id="createGroupForm" className="collapse">
+              <UserGroupForm
+                successedMessage={t('toaster.create_succeeded', { target: t('UserGroup') })}
+                failedMessage={t('toaster.create_failed', { target: t('UserGroup') })}
+                submitButtonLabel={t('Create')}
+                onSubmit={addUserGroup}
+              />
+            </div>
+          </div>
+        ) : (
+          t('admin:user_group_management.deny_create_group')
+        )
+      }
       <UserGroupTable
         userGroups={userGroups}
         isAclEnabled={isAclEnabled}

+ 8 - 2
packages/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx

@@ -3,7 +3,7 @@ import React, {
 } from 'react';
 import { useTranslation } from 'react-i18next';
 
-import UserGroupEditForm from './UserGroupEditForm';
+import UserGroupForm from '../UserGroup/UserGroupForm';
 import UserGroupUserTable from './UserGroupUserTable';
 import UserGroupUserModal from './UserGroupUserModal';
 import UserGroupPageList from './UserGroupPageList';
@@ -141,7 +141,13 @@ const UserGroupDetailPage: FC = () => {
       </a>
       {/* TODO 85062: Link to the ancestors group */}
       <div className="mt-4 form-box">
-        <UserGroupEditForm />
+        <UserGroupForm
+          userGroup={userGroup}
+          successedMessage={t('toaster.update_successed', { target: t('UserGroup') })}
+          failedMessage={t('toaster.update_failed', { target: t('UserGroup') })}
+          submitButtonLabel={t('Update')}
+          onSubmit={updateUserGroup}
+        />
       </div>
       <h2 className="admin-setting-header mt-4">{t('admin:user_group_management.user_list')}</h2>
       <UserGroupUserTable />

+ 0 - 111
packages/app/src/components/Admin/UserGroupDetail/UserGroupEditForm.jsx

@@ -1,111 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } 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';
-
-class UserGroupEditForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    const { adminUserGroupDetailContainer } = props;
-    const { userGroup } = adminUserGroupDetailContainer.state;
-
-    this.state = {
-      name: userGroup.name,
-      nameCache: userGroup.name, // cache for name. update every submit
-    };
-
-    this.xss = window.xss;
-
-    this.changeUserGroupName = this.changeUserGroupName.bind(this);
-    this.handleSubmit = this.handleSubmit.bind(this);
-    this.validateForm = this.validateForm.bind(this);
-  }
-
-  changeUserGroupName(event) {
-    this.setState({
-      name: event.target.value,
-    });
-  }
-
-  async handleSubmit(e) {
-    e.preventDefault();
-
-    try {
-      const res = await this.props.adminUserGroupDetailContainer.updateUserGroup({
-        name: this.state.name,
-      });
-
-      toastSuccess(`Updated the group name to "${this.xss.process(res.data.userGroup.name)}"`);
-      this.setState({ nameCache: this.state.name });
-    }
-    catch (err) {
-      toastError(new Error('Unable to update the group name'));
-    }
-  }
-
-  validateForm() {
-    return (
-      this.state.name !== this.state.nameCache
-      && this.state.name !== ''
-    );
-  }
-
-  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>
-          </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">
-            <div className="offset-md-2 col-md-10">
-              <button type="submit" className="btn btn-primary" disabled={!this.validateForm()}>
-                {t('Update')}
-              </button>
-            </div>
-          </div>
-        </fieldset>
-      </form>
-    );
-  }
-
-}
-
-UserGroupEditForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const UserGroupEditFormWrapper = withUnstatedContainers(UserGroupEditForm, [AppContainer, AdminUserGroupDetailContainer]);
-
-export default withTranslation()(UserGroupEditFormWrapper);