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

Merge branch 'master' into imprv/90470-show-toast-err-when-page-item-is-not-droppable

kaori 4 лет назад
Родитель
Сommit
291d86b6f0

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

@@ -196,6 +196,9 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
         </div>
         <div className="text-danger mt-5">
           {t('admin:user_group_management.delete_modal.desc')}
+
+          {/* TODO 85462: Add a note: "All child groups will disappear */}
+
         </div>
       </ModalBody>
       <ModalFooter>

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

@@ -87,7 +87,7 @@ const UserGroupForm: FC<Props> = (props: Props) => {
           </div>
         </div>
 
-        {/* TODO 85062: select parent dropdown */}
+        {/* TODO 88238: select parent dropdown */}
 
         <div className="form-group row">
           <div className="offset-md-2 col-md-10">

+ 38 - 13
packages/app/src/components/Admin/UserGroup/UserGroupCreateModal.tsx → packages/app/src/components/Admin/UserGroup/UserGroupModal.tsx

@@ -1,34 +1,40 @@
-import React, { FC, useState, useCallback } from 'react';
+import React, {
+  FC, useState, useEffect, useCallback,
+} from 'react';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 import { useTranslation } from 'react-i18next';
+import { TFunctionResult } from 'i18next';
 
+import { Ref } from '~/interfaces/common';
 import { IUserGroup, IUserGroupHasId } from '~/interfaces/user';
 import { CustomWindow } from '~/interfaces/global';
 import Xss from '~/services/xss';
 
 type Props = {
   userGroup?: IUserGroupHasId,
-  onClickCreateButton?: (userGroupData: Partial<IUserGroup>) => Promise<IUserGroupHasId | void>
+  buttonLabel?: TFunctionResult,
+  onClickButton?: (userGroupData: Partial<IUserGroupHasId>) => Promise<IUserGroupHasId | void>
   isShow?: boolean
   onHide?: () => Promise<void> | void
 };
 
-const UserGroupCreateModal: FC<Props> = (props: Props) => {
+const UserGroupModal: FC<Props> = (props: Props) => {
   const xss: Xss = (window as CustomWindow).xss;
 
   const { t } = useTranslation();
 
   const {
-    userGroup, onClickCreateButton, isShow, onHide,
+    userGroup, buttonLabel, onClickButton, isShow, onHide,
   } = props;
 
   /*
    * State
    */
-  const [currentName, setName] = useState(userGroup != null ? userGroup.name : '');
-  const [currentDescription, setDescription] = useState(userGroup != null ? userGroup.description : '');
+  const [currentName, setName] = useState('');
+  const [currentDescription, setDescription] = useState('');
+  const [currentParent, setParent] = useState<Ref<IUserGroup> | null>(null);
 
   /*
    * Function
@@ -41,15 +47,29 @@ const UserGroupCreateModal: FC<Props> = (props: Props) => {
     setDescription(e.target.value);
   }, []);
 
-  const onClickCreateButtonHandler = useCallback(async(e) => {
+  const onClickButtonHandler = useCallback(async(e) => {
     e.preventDefault(); // no reload
 
-    if (onClickCreateButton == null) {
+    if (onClickButton == null) {
       return;
     }
 
-    await onClickCreateButton({ name: currentName, description: currentDescription });
-  }, [currentName, currentDescription, onClickCreateButton]);
+    await onClickButton({
+      _id: userGroup?._id,
+      name: currentName,
+      description: currentDescription,
+      parent: currentParent,
+    });
+  }, [userGroup, currentName, currentDescription, currentParent, onClickButton]);
+
+  // componentDidMount
+  useEffect(() => {
+    if (userGroup != null) {
+      setName(userGroup.name);
+      setDescription(userGroup.description);
+      setParent(userGroup.parent);
+    }
+  }, [userGroup]);
 
   return (
     <Modal className="modal-md" isOpen={isShow} toggle={onHide}>
@@ -79,12 +99,17 @@ const UserGroupCreateModal: FC<Props> = (props: Props) => {
           </label>
           <textarea className="form-control" name="description" value={currentDescription} onChange={onChangeDescriptionHandler} />
         </div>
+
+        {/* TODO 90732: Add a drop-down to show selectable parents */}
+
+        {/* TODO 85462: Add a note that "if you change the parent, the offspring will also be moved together */}
+
       </ModalBody>
 
       <ModalFooter>
         <div className="form-group">
-          <button type="button" className="btn btn-primary" onClick={onClickCreateButtonHandler}>
-            {t('Create')}
+          <button type="button" className="btn btn-primary" onClick={onClickButtonHandler}>
+            {buttonLabel}
           </button>
         </div>
       </ModalFooter>
@@ -92,4 +117,4 @@ const UserGroupCreateModal: FC<Props> = (props: Props) => {
   );
 };
 
-export default UserGroupCreateModal;
+export default UserGroupModal;

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

@@ -2,16 +2,17 @@ import React, { FC, useState, useCallback } from 'react';
 import { useTranslation } from 'react-i18next';
 
 import UserGroupTable from './UserGroupTable';
-import UserGroupCreateModal from './UserGroupCreateModal';
+import UserGroupModal from './UserGroupModal';
 import UserGroupDeleteModal from './UserGroupDeleteModal';
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { IUserGroup, IUserGroupHasId } from '~/interfaces/user';
 import Xss from '~/services/xss';
 import { CustomWindow } from '~/interfaces/global';
-import { apiv3Delete, apiv3Post } from '~/client/util/apiv3-client';
+import { apiv3Delete, apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
 import { useSWRxUserGroupList, useSWRxChildUserGroupList, useSWRxUserGroupRelationList } from '~/stores/user-group';
 import { useIsAclEnabled } from '~/stores/context';
+import userGroup from '~/server/models/user-group';
 
 const UserGroupPage: FC = () => {
   const xss: Xss = (window as CustomWindow).xss;
@@ -37,6 +38,7 @@ const UserGroupPage: FC = () => {
    */
   const [selectedUserGroup, setSelectedUserGroup] = useState<IUserGroupHasId | undefined>(undefined); // not null but undefined (to use defaultProps in UserGroupDeleteModal)
   const [isCreateModalShown, setCreateModalShown] = useState<boolean>(false);
+  const [isUpdateModalShown, setUpdateModalShown] = useState<boolean>(false);
   const [isDeleteModalShown, setDeleteModalShown] = useState<boolean>(false);
 
   /*
@@ -50,6 +52,16 @@ const UserGroupPage: FC = () => {
     setCreateModalShown(false);
   }, [setCreateModalShown]);
 
+  const showUpdateModal = useCallback((group: IUserGroupHasId) => {
+    setUpdateModalShown(true);
+    setSelectedUserGroup(group);
+  }, [setUpdateModalShown]);
+
+  const hideUpdateModal = useCallback(() => {
+    setUpdateModalShown(false);
+    setSelectedUserGroup(undefined);
+  }, [setUpdateModalShown]);
+
   const syncUserGroupAndRelations = useCallback(async() => {
     try {
       await mutateUserGroups();
@@ -90,6 +102,20 @@ const UserGroupPage: FC = () => {
     }
   }, [t, mutateUserGroups]);
 
+  const updateUserGroup = useCallback(async(userGroupData: IUserGroupHasId) => {
+    try {
+      await apiv3Put(`/user-groups/${userGroupData._id}`, {
+        name: userGroupData.name,
+        description: userGroupData.description,
+      });
+      toastSuccess(t('toaster.update_successed', { target: t('UserGroup') }));
+      await mutateUserGroups();
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [t, mutateUserGroups]);
+
   const deleteUserGroupById = useCallback(async(deleteGroupId: string, actionName: string, transferToUserGroupId: string) => {
     try {
       const res = await apiv3Delete(`/user-groups/${deleteGroupId}`, {
@@ -114,7 +140,7 @@ const UserGroupPage: FC = () => {
     <div data-testid="admin-user-groups">
       {
         isAclEnabled ? (
-          <div className="mb-2">
+          <div className="mb-3">
             <button type="button" className="btn btn-outline-secondary" onClick={showCreateModal}>
               {t('admin:user_group_management.create_group')}
             </button>
@@ -123,19 +149,32 @@ const UserGroupPage: FC = () => {
           t('admin:user_group_management.deny_create_group')
         )
       }
-      <UserGroupCreateModal
-        onClickCreateButton={createUserGroup}
+
+      <UserGroupModal
+        buttonLabel={t('Create')}
+        onClickButton={createUserGroup}
         isShow={isCreateModalShown}
         onHide={hideCreateModal}
       />
+
+      <UserGroupModal
+        userGroup={selectedUserGroup}
+        buttonLabel={t('Update')}
+        onClickButton={updateUserGroup}
+        isShow={isUpdateModalShown}
+        onHide={hideUpdateModal}
+      />
+
       <UserGroupTable
         headerLabel={t('admin:user_group_management.group_list')}
         userGroups={userGroups}
         childUserGroups={childUserGroups}
         isAclEnabled={isAclEnabled ?? false}
+        onEdit={showUpdateModal}
         onDelete={showDeleteModal}
         userGroupRelations={userGroupRelations}
       />
+
       <UserGroupDeleteModal
         userGroups={userGroups}
         deleteUserGroup={selectedUserGroup}

+ 28 - 11
packages/app/src/components/Admin/UserGroup/UserGroupTable.tsx

@@ -15,6 +15,7 @@ type Props = {
   userGroupRelations: IUserGroupRelation[],
   childUserGroups: IUserGroupHasId[],
   isAclEnabled: boolean,
+  onEdit?: (userGroup: IUserGroupHasId) => void | Promise<void>,
   onDelete?: (userGroup: IUserGroupHasId) => void | Promise<void>,
 };
 
@@ -65,22 +66,38 @@ const UserGroupTable: FC<Props> = (props: Props) => {
   /*
    * Function
    */
-  const onClickDelete = useCallback((e) => { // no preventDefault
-    if (props.onDelete == null) {
-      return;
-    }
-
+  const findUserGroup = (e: React.ChangeEvent<HTMLInputElement>): IUserGroupHasId | undefined => {
     const groupId = e.target.getAttribute('data-user-group-id');
-    const group = props.userGroups.find((group) => {
+    return props.userGroups.find((group) => {
       return group._id === groupId;
     });
+  };
+
+  const onClickEdit = (e) => {
+    if (props.onEdit == null) {
+      return;
+    }
+
+    const userGroup = findUserGroup(e);
+    if (userGroup == null) {
+      return;
+    }
+
+    props.onEdit(userGroup);
+  };
+
+  const onClickDelete = (e) => { // no preventDefault
+    if (props.onDelete == null) {
+      return;
+    }
 
-    if (group == null) {
+    const userGroup = findUserGroup(e);
+    if (userGroup == null) {
       return;
     }
 
-    props.onDelete(group);
-  }, [props]);
+    props.onDelete(userGroup);
+  };
 
   /*
    * useEffect
@@ -159,9 +176,9 @@ const UserGroupTable: FC<Props> = (props: Props) => {
                           <i className="icon-settings"></i>
                         </button>
                         <div className="dropdown-menu" role="menu" aria-labelledby={`admin-group-menu-button-${group._id}`}>
-                          <a className="dropdown-item" href={`/admin/user-group-detail/${group._id}`}>
+                          <button className="dropdown-item" type="button" role="button" onClick={onClickEdit} data-user-group-id={group._id}>
                             <i className="icon-fw icon-note"></i> {t('Edit')}
-                          </a>
+                          </button>
                           <button className="dropdown-item" type="button" role="button" onClick={onClickDelete} data-user-group-id={group._id}>
                             <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
                           </button>

+ 41 - 3
packages/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx

@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
 
 import UserGroupForm from '../UserGroup/UserGroupForm';
 import UserGroupTable from '../UserGroup/UserGroupTable';
-import UserGroupCreateModal from '../UserGroup/UserGroupCreateModal';
+import UserGroupModal from '../UserGroup/UserGroupModal';
 import UserGroupDeleteModal from '../UserGroup/UserGroupDeleteModal';
 import UserGroupDropdown from '../UserGroup/UserGroupDropdown';
 import UserGroupUserTable from './UserGroupUserTable';
@@ -39,6 +39,7 @@ const UserGroupDetailPage: FC = () => {
   const [isAlsoNameSearched, setAlsoNameSearched] = useState<boolean>(false);
   const [selectedUserGroup, setSelectedUserGroup] = useState<IUserGroupHasId | undefined>(undefined); // not null but undefined (to use defaultProps in UserGroupDeleteModal)
   const [isCreateModalShown, setCreateModalShown] = useState<boolean>(false);
+  const [isUpdateModalShown, setUpdateModalShown] = useState<boolean>(false);
   const [isDeleteModalShown, setDeleteModalShown] = useState<boolean>(false);
 
   /*
@@ -113,6 +114,31 @@ const UserGroupDetailPage: FC = () => {
     mutateUserGroupRelations();
   }, [userGroup, mutateUserGroupRelations]);
 
+  const showUpdateModal = useCallback((group: IUserGroupHasId) => {
+    setUpdateModalShown(true);
+    setSelectedUserGroup(group);
+  }, [setUpdateModalShown]);
+
+  const hideUpdateModal = useCallback(() => {
+    setUpdateModalShown(false);
+    setSelectedUserGroup(undefined);
+  }, [setUpdateModalShown]);
+
+  const updateChildUserGroup = useCallback(async(userGroupData: IUserGroupHasId) => {
+    try {
+      await apiv3Put(`/user-groups/${userGroupData._id}`, {
+        name: userGroupData.name,
+        description: userGroupData.description,
+        parentId: userGroupData.parent,
+      });
+      mutateChildUserGroups();
+      toastSuccess(t('toaster.update_successed', { target: t('UserGroup') }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [t, mutateChildUserGroups]);
+
   const onClickAddChildButtonHandler = async(selectedUserGroup: IUserGroupHasId) => {
     try {
       await apiv3Put(`/user-groups/${selectedUserGroup._id}`, {
@@ -228,8 +254,18 @@ const UserGroupDetailPage: FC = () => {
         onClickAddExistingUserGroupButtonHandler={onClickAddChildButtonHandler}
         onClickCreateUserGroupButtonHandler={showCreateModal}
       />
-      <UserGroupCreateModal
-        onClickCreateButton={createChildUserGroup}
+
+      <UserGroupModal
+        userGroup={selectedUserGroup}
+        buttonLabel={t('Update')}
+        onClickButton={updateChildUserGroup}
+        isShow={isUpdateModalShown}
+        onHide={hideUpdateModal}
+      />
+
+      <UserGroupModal
+        buttonLabel={t('Create')}
+        onClickButton={createChildUserGroup}
         isShow={isCreateModalShown}
         onHide={hideCreateModal}
       />
@@ -238,9 +274,11 @@ const UserGroupDetailPage: FC = () => {
         userGroups={childUserGroups}
         childUserGroups={grandChildUserGroups}
         isAclEnabled={isAclEnabled ?? false}
+        onEdit={showUpdateModal}
         onDelete={showDeleteModal}
         userGroupRelations={childUserGroupRelations}
       />
+
       <UserGroupDeleteModal
         userGroups={childUserGroups}
         deleteUserGroup={selectedUserGroup}

+ 22 - 3
packages/app/src/components/PageCreateModal.jsx

@@ -1,8 +1,11 @@
 
-import React, { useEffect, useState } from 'react';
+import React, {
+  useEffect, useState, useMemo,
+} from 'react';
 import PropTypes from 'prop-types';
 
 import { Modal, ModalHeader, ModalBody } from 'reactstrap';
+import { debounce } from 'throttle-debounce';
 
 import { withTranslation } from 'react-i18next';
 import { format } from 'date-fns';
@@ -19,7 +22,7 @@ import { usePageCreateModal } from '~/stores/modal';
 import PagePathAutoComplete from './PagePathAutoComplete';
 
 const {
-  userPageRoot, isCreatablePage, generateEditorPath,
+  userPageRoot, isCreatablePage, generateEditorPath, isUsersHomePage,
 } = pagePathUtils;
 
 const PageCreateModal = (props) => {
@@ -39,12 +42,25 @@ const PageCreateModal = (props) => {
   const [todayInput2, setTodayInput2] = useState('');
   const [pageNameInput, setPageNameInput] = useState(pageNameInputInitialValue);
   const [template, setTemplate] = useState(null);
+  const [isMatchedWithUserHomePagePath, setIsMatchedWithUserHomePagePath] = useState(false);
 
   // ensure pageNameInput is synced with selectedPagePath || currentPagePath
   useEffect(() => {
     setPageNameInput(isCreatablePage(pathname) ? pathUtils.addTrailingSlash(pathname) : '/');
   }, [pathname]);
 
+  const checkIsUsersHomePageDebounce = useMemo(() => {
+    const checkIsUsersHomePage = () => {
+      setIsMatchedWithUserHomePagePath(isUsersHomePage(pageNameInput));
+    };
+
+    return debounce(1000, checkIsUsersHomePage);
+  }, [pageNameInput]);
+
+  useEffect(() => {
+    checkIsUsersHomePageDebounce(pageNameInput);
+  }, [checkIsUsersHomePageDebounce, pageNameInput]);
+
   function transitBySubmitEvent(e, transitHandler) {
     // prevent page transition by submit
     e.preventDefault();
@@ -189,7 +205,6 @@ const PageCreateModal = (props) => {
           <h3 className="grw-modal-head pb-2">{t('Create under')}</h3>
 
           <div className="d-sm-flex align-items-center justify-items-between">
-
             <div className="flex-fill">
               {isReachable
                 ? (
@@ -221,12 +236,16 @@ const PageCreateModal = (props) => {
                 data-testid="btn-create-page-under-below"
                 className="grw-btn-create-page btn btn-outline-primary rounded-pill text-nowrap ml-3"
                 onClick={createInputPage}
+                disabled={isMatchedWithUserHomePagePath}
               >
                 <i className="icon-fw icon-doc"></i>{t('Create')}
               </button>
             </div>
 
           </div>
+          { isMatchedWithUserHomePagePath && (
+            <p className="text-danger mt-2">Error: Cannot create page under /user page directory.</p>
+          ) }
 
         </fieldset>
       </div>

+ 3 - 2
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -356,8 +356,6 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
         createFromPageTree: true,
       });
 
-      setCreating(false);
-
       mutateChildren();
 
       if (!hasDescendants) {
@@ -369,6 +367,9 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
     catch (err) {
       toastError(err);
     }
+    finally {
+      setCreating(false);
+    }
   };
 
   const inputValidator = (title: string | null): AlertInfo | null => {

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

@@ -1262,7 +1262,7 @@ module.exports = function(crowi, app) {
     }
     catch (err) {
       logger.error('Error occured while get setting', err);
-      return res.json(ApiResponse.error('Failed to revert deleted page.'));
+      return res.json(ApiResponse.error(err));
     }
 
     const result = {};