Ver código fonte

Merge pull request #4794 from weseek/feat/77525-enable-to-delete-pages

Feat/77525 enable to delete pages
Yohei Shiina 4 anos atrás
pai
commit
8f3600ce89

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

@@ -579,6 +579,7 @@
     "delete_completely": "Delete completely",
     "include_certain_path" : "Include {{pathToInclude}} path ",
     "delete_all_selected_page" : "Delete All",
+    "currently_not_implemented":"This is not currently implemented",
     "search_again" : "Search again",
     "number_of_list_to_display" : "Display",
     "page_number_unit" : "pages"

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

@@ -579,6 +579,7 @@
     "delete_completely": "完全に削除する",
     "include_certain_path": "{{pathToInclude}}下を含む ",
     "delete_all_selected_page" : "一括削除",
+    "currently_not_implemented":"現在未実装の機能です",
     "search_again" : "再検索",
     "number_of_list_to_display" : "表示件数",
     "page_number_unit" : "件"

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

@@ -852,6 +852,7 @@
 		"delete_completely": "完全删除",
     "include_certain_path": "包含 {{pathToInclude}} 路径 ",
     "delete_all_selected_page": "删除所有",
+    "currently_not_implemented": "这是当前未实现的功能",
     "search_again" : "再次搜索",
     "number_of_list_to_display" : "显示器的数量",
     "page_number_unit" : "例"

+ 6 - 3
packages/app/src/components/Page/PageManagement.jsx

@@ -153,10 +153,15 @@ const LegacyPageManagemenet = (props) => {
     );
   }
 
+  function generatePageObjectToDelete() {
+    return { pageId, revisionId, path };
+  }
+
   function renderModals() {
     if (currentUser == null) {
       return null;
     }
+    const pageToDelete = generatePageObjectToDelete();
 
     return (
       <>
@@ -181,9 +186,7 @@ const LegacyPageManagemenet = (props) => {
         <PageDeleteModal
           isOpen={isPageDeleteModalShown}
           onClose={closePageDeleteModalHandler}
-          pageId={pageId}
-          revisionId={revisionId}
-          path={path}
+          pages={[pageToDelete]}
           isAbleToDeleteCompletely={isAbleToDeleteCompletely}
         />
         <PagePresentationModal

+ 58 - 52
packages/app/src/components/PageDeleteModal.jsx → packages/app/src/components/PageDeleteModal.tsx

@@ -1,16 +1,20 @@
-import React, { useState } from 'react';
-import PropTypes from 'prop-types';
-
+import React, { useState, FC } from 'react';
+import toastr from 'toastr';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
+import { useTranslation } from 'react-i18next';
 
-import { withTranslation } from 'react-i18next';
-
-import { apiPost } from '~/client/util/apiv1-client';
+// import { apiPost } from '~/client/util/apiv1-client';
 
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
+export type IPageForPageDeleteModal = {
+  pageId: string,
+  revisionId: string,
+  path: string
+}
+
 const deleteIconAndKey = {
   completely: {
     color: 'danger',
@@ -24,9 +28,18 @@ const deleteIconAndKey = {
   },
 };
 
-const PageDeleteModal = (props) => {
+type Props = {
+  isOpen: boolean,
+  pages: IPageForPageDeleteModal[],
+  isDeleteCompletelyModal: boolean,
+  isAbleToDeleteCompletely: boolean,
+  onClose?: () => void,
+}
+
+const PageDeleteModal: FC<Props> = (props: Props) => {
+  const { t } = useTranslation('');
   const {
-    t, isOpen, onClose, isDeleteCompletelyModal, pageId, revisionId, path, isAbleToDeleteCompletely,
+    isOpen, onClose, isDeleteCompletelyModal, pages, isAbleToDeleteCompletely,
   } = props;
   const [isDeleteRecursively, setIsDeleteRecursively] = useState(true);
   const [isDeleteCompletely, setIsDeleteCompletely] = useState(isDeleteCompletelyModal && isAbleToDeleteCompletely);
@@ -46,27 +59,29 @@ const PageDeleteModal = (props) => {
   }
 
   async function deletePage() {
-    setErrs(null);
-
-    try {
-      // control flag
-      // If is it not true, Request value must be `null`.
-      const recursively = isDeleteRecursively ? true : null;
-      const completely = isDeleteCompletely ? true : null;
-
-      const response = await apiPost('/pages.remove', {
-        page_id: pageId,
-        revision_id: revisionId,
-        recursively,
-        completely,
-      });
-
-      const trashPagePath = response.page.path;
-      window.location.href = encodeURI(trashPagePath);
-    }
-    catch (err) {
-      setErrs(err);
-    }
+    toastr.warning(t('search_result.currently_not_implemented'));
+    // Todo implement page delete function at https://redmine.weseek.co.jp/issues/82222
+    // setErrs(null);
+
+    // try {
+    //   // control flag
+    //   // If is it not true, Request value must be `null`.
+    //   const recursively = isDeleteRecursively ? true : null;
+    //   const completely = isDeleteCompletely ? true : null;
+
+    //   const response = await apiPost('/pages.remove', {
+    //     page_id: pageId,
+    //     revision_id: revisionId,
+    //     recursively,
+    //     completely,
+    //   });
+
+    //   const trashPagePath = response.page.path;
+    //   window.location.href = encodeURI(trashPagePath);
+    // }
+    // catch (err) {
+    //   setErrs(err);
+    // }
   }
 
   async function deleteButtonHandler() {
@@ -80,12 +95,13 @@ const PageDeleteModal = (props) => {
           className="custom-control-input"
           id="deleteRecursively"
           type="checkbox"
-          checked={isDeleteRecursively}
+          // checked={isDeleteRecursively}
+          checked={false}
           onChange={changeIsDeleteRecursivelyHandler}
+          disabled // Todo: enable this at https://redmine.weseek.co.jp/issues/82222
         />
         <label className="custom-control-label" htmlFor="deleteRecursively">
           { t('modal_delete.delete_recursively') }
-          <p className="form-text text-muted mt-0"><code>{path}</code> { t('modal_delete.recursively') }</p>
         </label>
       </div>
     );
@@ -105,11 +121,14 @@ const PageDeleteModal = (props) => {
           name="completely"
           id="deleteCompletely"
           type="checkbox"
-          disabled
+          // disabled={!isAbleToDeleteCompletely}
+          disabled // Todo: will be implemented at https://redmine.weseek.co.jp/issues/82222
           checked={isDeleteCompletely}
           onChange={changeIsDeleteCompletelyHandler}
         />
-        <label className="custom-control-label text-danger" htmlFor="deleteCompletely">
+        {/* ↓↓ undo this comment out at https://redmine.weseek.co.jp/issues/82222 ↓↓ */}
+        {/* <label className="custom-control-label text-danger" htmlFor="deleteCompletely"> */}
+        <label className="custom-control-label" htmlFor="deleteCompletely">
           { t('modal_delete.delete_completely')}
           <p className="form-text text-muted mt-0"> { t('modal_delete.completely') }</p>
         </label>
@@ -132,7 +151,11 @@ const PageDeleteModal = (props) => {
       <ModalBody>
         <div className="form-group">
           <label>{ t('modal_delete.deleting_page') }:</label><br />
-          <code>{ path }</code>
+          {/* Todo: change the way to show path on modal when too many pages are selected */}
+          {/* https://redmine.weseek.co.jp/issues/82787 */}
+          {pages.map((page) => {
+            return <div><code>{ page.path }</code></div>;
+          })}
         </div>
         {renderDeleteRecursivelyForm()}
         {!isDeleteCompletelyModal && renderDeleteCompletelyForm()}
@@ -149,21 +172,4 @@ const PageDeleteModal = (props) => {
   );
 };
 
-PageDeleteModal.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
-
-  isOpen: PropTypes.bool.isRequired,
-  onClose: PropTypes.func.isRequired,
-
-  pageId: PropTypes.string.isRequired,
-  revisionId: PropTypes.string.isRequired,
-  path: PropTypes.string.isRequired,
-  isDeleteCompletelyModal: PropTypes.bool,
-  isAbleToDeleteCompletely: PropTypes.bool,
-};
-
-PageDeleteModal.defaultProps = {
-  isDeleteCompletelyModal: false,
-};
-
-export default withTranslation()(PageDeleteModal);
+export default PageDeleteModal;

+ 79 - 9
packages/app/src/components/SearchPage.jsx

@@ -11,12 +11,14 @@ import SearchPageLayout from './SearchPage/SearchPageLayout';
 import SearchResultContent from './SearchPage/SearchResultContent';
 import SearchResultList from './SearchPage/SearchResultList';
 import SearchControl from './SearchPage/SearchControl';
+import PageDeleteModal from './PageDeleteModal';
+
+import { CheckboxType } from '../interfaces/search';
 
 export const specificPathNames = {
   user: '/user',
   trash: '/trash',
 };
-
 class SearchPage extends React.Component {
 
   constructor(props) {
@@ -30,12 +32,15 @@ class SearchPage extends React.Component {
       searchResults: [],
       searchResultMeta: {},
       focusedSearchResultData: null,
-      selectedPages: new Set(),
+      selectedPagesIdList: new Set(),
       searchResultCount: 0,
       activePage: 1,
       pagingLimit: this.props.appContainer.config.pageLimitationL,
       excludeUserPages: true,
       excludeTrashPages: true,
+      selectAllCheckboxType: CheckboxType.NONE_CHECKED,
+      isDeleteConfirmModalShown: false,
+      deleteTargetPageIds: new Set(),
     };
 
     this.changeURL = this.changeURL.bind(this);
@@ -47,6 +52,9 @@ class SearchPage extends React.Component {
     this.switchExcludeTrashPagesHandler = this.switchExcludeTrashPagesHandler.bind(this);
     this.onPagingNumberChanged = this.onPagingNumberChanged.bind(this);
     this.onPagingLimitChanged = this.onPagingLimitChanged.bind(this);
+    this.deleteSinglePageButtonHandler = this.deleteSinglePageButtonHandler.bind(this);
+    this.deleteAllPagesButtonHandler = this.deleteAllPagesButtonHandler.bind(this);
+    this.closeDeleteConfirmModalHandler = this.closeDeleteConfirmModalHandler.bind(this);
   }
 
   componentDidMount() {
@@ -187,13 +195,65 @@ class SearchPage extends React.Component {
     });
   }
 
-  toggleCheckBox = (page) => {
-    if (this.state.selectedPages.has(page)) {
-      this.state.selectedPages.delete(page);
+  toggleCheckBox = (pageId) => {
+    const { selectedPagesIdList } = this.state;
+
+    if (selectedPagesIdList.has(pageId)) {
+      selectedPagesIdList.delete(pageId);
+    }
+    else {
+      selectedPagesIdList.add(pageId);
+    }
+    switch (selectedPagesIdList.size) {
+      case 0:
+        return this.setState({ selectAllCheckboxType: CheckboxType.NONE_CHECKED });
+      case this.state.searchResults.length:
+        return this.setState({ selectAllCheckboxType: CheckboxType.ALL_CHECKED });
+      default:
+        return this.setState({ selectAllCheckboxType: CheckboxType.INDETERMINATE });
+    }
+  }
+
+  toggleAllCheckBox = (nextSelectAllCheckboxType) => {
+    const { selectedPagesIdList, searchResults } = this.state;
+    if (nextSelectAllCheckboxType === CheckboxType.NONE_CHECKED) {
+      selectedPagesIdList.clear();
     }
     else {
-      this.state.selectedPages.add(page);
+      searchResults.forEach((page) => {
+        selectedPagesIdList.add(page.pageData._id);
+      });
     }
+    this.setState({
+      selectedPagesIdList,
+      selectAllCheckboxType: nextSelectAllCheckboxType,
+    });
+  };
+
+  getSelectedPagesToDelete() {
+    const filteredPages = this.state.searchResults.filter((page) => {
+      return Array.from(this.state.deleteTargetPageIds).find(id => id === page.pageData._id);
+    });
+    return filteredPages.map(page => ({
+      pageId: page.pageData._id,
+      revisionId: page.pageData.revision,
+      path: page.pageData.path,
+    }));
+  }
+
+  deleteSinglePageButtonHandler(pageId) {
+    this.setState({ deleteTargetPageIds: new Set([pageId]) });
+    this.setState({ isDeleteConfirmModalShown: true });
+  }
+
+  deleteAllPagesButtonHandler() {
+    if (this.state.selectedPagesIdList.size === 0) { return }
+    this.setState({ deleteTargetPageIds: this.state.selectedPagesIdList });
+    this.setState({ isDeleteConfirmModalShown: true });
+  }
+
+  closeDeleteConfirmModalHandler() {
+    this.setState({ isDeleteConfirmModalShown: false });
   }
 
   renderSearchResultContent = () => {
@@ -212,13 +272,14 @@ class SearchPage extends React.Component {
       <SearchResultList
         pages={this.state.searchResults || []}
         focusedSearchResultData={this.state.focusedSearchResultData}
-        selectedPages={this.state.selectedPages || []}
+        selectedPagesIdList={this.state.selectedPagesIdList || []}
         searchResultCount={this.state.searchResultCount}
         activePage={this.state.activePage}
         pagingLimit={this.state.pagingLimit}
-        onClickInvoked={this.selectPage}
-        onChangedInvoked={this.toggleCheckBox}
+        onClickSearchResultItem={this.selectPage}
+        onClickCheckbox={this.toggleCheckBox}
         onPagingNumberChanged={this.onPagingNumberChanged}
+        onClickDeleteButton={this.deleteSinglePageButtonHandler}
       />
     );
   }
@@ -227,8 +288,12 @@ class SearchPage extends React.Component {
     return (
       <SearchControl
         searchingKeyword={this.state.searchingKeyword}
+        searchResultCount={this.state.searchResultCount || 0}
         appContainer={this.props.appContainer}
         onSearchInvoked={this.onSearchInvoked}
+        onClickSelectAllCheckbox={this.toggleAllCheckBox}
+        selectAllCheckboxType={this.state.selectAllCheckboxType}
+        onClickDeleteAllButton={this.deleteAllPagesButtonHandler}
         onExcludeUserPagesSwitched={this.switchExcludeUserPagesHandler}
         onExcludeTrashPagesSwitched={this.switchExcludeTrashPagesHandler}
         excludeUserPages={this.state.excludeUserPages}
@@ -251,6 +316,11 @@ class SearchPage extends React.Component {
           initialPagingLimit={this.props.appContainer.config.pageLimitationL || 50}
         >
         </SearchPageLayout>
+        <PageDeleteModal
+          isOpen={this.state.isDeleteConfirmModalShown}
+          onClose={this.closeDeleteConfirmModalHandler}
+          pages={this.getSelectedPagesToDelete()}
+        />
       </div>
     );
   }

+ 0 - 91
packages/app/src/components/SearchPage/DeletePageListModal.jsx

@@ -1,91 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { withTranslation } from 'react-i18next';
-
-import {
-  Button,
-  Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
-
-class DeletePageListModal extends React.Component {
-
-  /*
-   * the threshold for omitting body
-   */
-  static get OMIT_BODY_THRES() { return 400 }
-
-  componentWillMount() {
-  }
-
-  render() {
-    const { t } = this.props;
-    if (this.props.pages == null || this.props.pages.length === 0) {
-      return <div></div>;
-    }
-
-    const listView = this.props.pages.map((page) => {
-      return (
-        <li key={page._id}>{page.path}</li>
-      );
-    });
-
-    return (
-      <Modal isOpen={this.props.isShown} toggle={this.props.cancel} className="page-list-delete-modal">
-        <ModalHeader tag="h4" toggle={this.props.cancel} className="bg-danger text-light">
-          {t('search_result.deletion_modal_header')}
-        </ModalHeader>
-        <ModalBody>
-          <ul>
-            {listView}
-          </ul>
-        </ModalBody>
-        <ModalFooter>
-          <div className="d-flex justify-content-between">
-            <span className="text-danger">{this.props.errorMessage}</span>
-            <span className="d-flex align-items-center">
-              <div className="custom-control custom-checkbox custom-checkbox-danger mr-2">
-                <input
-                  type="checkbox"
-                  className="custom-control-input"
-                  id="customCheck-delete-completely"
-                  checked={this.props.isDeleteCompletely}
-                  onChange={this.props.toggleDeleteCompletely}
-                />
-                <label
-                  className="custom-control-label text-danger"
-                  htmlFor="customCheck-delete-completely"
-                >
-                  {t('search_result.delete_completely')}
-                </label>
-              </div>
-              <Button color={this.props.isDeleteCompletely ? 'danger' : 'light'} onClick={this.props.confirmedToDelete}>
-                <i className="icon-trash"></i>
-                {t('search_result.delete')}
-              </Button>
-            </span>
-          </div>
-        </ModalFooter>
-      </Modal>
-    );
-  }
-
-}
-
-DeletePageListModal.defaultProps = {
-  isDeleteCompletely: false, // for when undefined is passed
-};
-
-DeletePageListModal.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  isShown: PropTypes.bool.isRequired,
-  pages: PropTypes.array,
-  errorMessage: PropTypes.string,
-  cancel: PropTypes.func.isRequired, //                 for cancel evnet handling
-  isDeleteCompletely: PropTypes.bool,
-  confirmedToDelete: PropTypes.func.isRequired, //      for confirmed event handling
-  toggleDeleteCompletely: PropTypes.func.isRequired, // for delete completely check event handling
-};
-
-export default withTranslation()(DeletePageListModal);

+ 20 - 26
packages/app/src/components/SearchPage/DeleteSelectedPageGroup.tsx

@@ -1,53 +1,49 @@
 import React, { FC } from 'react';
 import { useTranslation } from 'react-i18next';
-import loggerFactory from '~/utils/logger';
 import { CheckboxType } from '../../interfaces/search';
 
-const logger = loggerFactory('growi:searchResultList');
-
 type Props = {
-  checkboxState: CheckboxType,
-  onClickInvoked?: () => void,
-  onCheckInvoked?: (string:CheckboxType) => void,
+  isSelectAllCheckboxDisabled: boolean,
+  selectAllCheckboxType: CheckboxType,
+  onClickDeleteAllButton?: () => void,
+  onClickSelectAllCheckbox?: (nextSelectAllCheckboxType: CheckboxType) => void,
 }
 
 const DeleteSelectedPageGroup:FC<Props> = (props:Props) => {
   const { t } = useTranslation();
   const {
-    checkboxState, onClickInvoked, onCheckInvoked,
+    onClickDeleteAllButton, onClickSelectAllCheckbox, selectAllCheckboxType,
   } = props;
 
-  const changeCheckboxStateHandler = () => {
-    console.log(`changeCheckboxStateHandler is called. current changebox state is ${checkboxState}`);
-    // Todo: determine next checkboxState from one of the following and tell the parent component
-    // to change the checkboxState by passing onCheckInvoked function the next checkboxState
-    // - NONE_CHECKED
-    // - INDETERMINATE
-    // - ALL_CHECKED
-    // https://estoc.weseek.co.jp/redmine/issues/77525
-    // use CheckboxType by importing from packages/app/src/interfaces/
-    if (onCheckInvoked == null) { logger.error('onCheckInvoked is null') }
-    else { onCheckInvoked(CheckboxType.ALL_CHECKED) } // change this to an appropriate value
+  const onClickCheckbox = () => {
+    if (onClickSelectAllCheckbox != null) {
+      const next = selectAllCheckboxType === CheckboxType.ALL_CHECKED ? CheckboxType.NONE_CHECKED : CheckboxType.ALL_CHECKED;
+      onClickSelectAllCheckbox(next);
+    }
   };
 
+  const onClickDeleteButton = () => {
+    if (onClickDeleteAllButton != null) { onClickDeleteAllButton() }
+  };
 
   return (
+
     <div className="d-flex align-items-center">
+      {/** todo: implement the design for CheckboxType = INDETERMINATE */}
       <input
         id="check-all-pages"
         type="checkbox"
         name="check-all-pages"
         className="custom-control custom-checkbox ml-1 align-self-center"
-        onChange={changeCheckboxStateHandler}
-        checked={checkboxState === CheckboxType.INDETERMINATE || checkboxState === CheckboxType.ALL_CHECKED}
+        disabled={props.isSelectAllCheckboxDisabled}
+        onClick={onClickCheckbox}
+        checked={selectAllCheckboxType !== CheckboxType.NONE_CHECKED}
       />
       <button
         type="button"
         className="btn text-danger font-weight-light p-0 ml-2"
-        onClick={() => {
-          if (onClickInvoked == null) { logger.error('onClickInvoked is null') }
-          else { onClickInvoked() }
-        }}
+        disabled={selectAllCheckboxType === CheckboxType.NONE_CHECKED}
+        onClick={onClickDeleteButton}
       >
         <i className="icon-trash"></i>
         {t('search_result.delete_all_selected_page')}
@@ -57,6 +53,4 @@ const DeleteSelectedPageGroup:FC<Props> = (props:Props) => {
 
 };
 
-DeleteSelectedPageGroup.propTypes = {
-};
 export default DeleteSelectedPageGroup;

+ 9 - 18
packages/app/src/components/SearchPage/SearchControl.tsx

@@ -9,6 +9,10 @@ import { CheckboxType } from '../../interfaces/search';
 type Props = {
   searchingKeyword: string,
   appContainer: AppContainer,
+  searchResultCount: number,
+  selectAllCheckboxType: CheckboxType,
+  onClickDeleteAllButton?: () => void
+  onClickSelectAllCheckbox?: (nextSelectAllCheckboxType: CheckboxType) => void,
   excludeUserPages: boolean,
   excludeTrashPages: boolean,
   onSearchInvoked: (data: {keyword: string}) => boolean,
@@ -23,6 +27,7 @@ const SearchControl: FC <Props> = (props: Props) => {
   // later needs to be fixed: SearchControl to typescript componet
   const SearchPageFormTypeAny : any = SearchPageForm;
   const { t } = useTranslation('');
+  const { searchResultCount } = props;
 
   const switchExcludeUserPagesHandler = () => {
     if (props.onExcludeUserPagesSwitched != null) {
@@ -36,21 +41,6 @@ const SearchControl: FC <Props> = (props: Props) => {
     }
   };
 
-  const onDeleteSelectedPageHandler = () => {
-    console.log('onDeleteSelectedPageHandler is called');
-    // TODO: implement this function to delete selected pages.
-    // https://estoc.weseek.co.jp/redmine/issues/77525
-  };
-
-  const onCheckAllPagesInvoked = (nextCheckboxState:CheckboxType) => {
-    console.log(`onCheckAllPagesInvoked is called with arg ${nextCheckboxState}`);
-    // Todo: set the checkboxState, isChecked, and indeterminate value of checkbox element according to the passed argument
-    // https://estoc.weseek.co.jp/redmine/issues/77525
-
-    // setting checkbox to indeterminate is required to use of useRef to access checkbox element.
-    // ref: https://getbootstrap.com/docs/4.5/components/forms/#checkboxes
-  };
-
   const openSearchOptionModalHandler = () => {
     setIsFileterOptionModalShown(true);
   };
@@ -99,9 +89,10 @@ const SearchControl: FC <Props> = (props: Props) => {
         <div className="d-flex mr-auto ml-3">
           {/* Todo: design will be fixed in #80324. Function will be implemented in #77525 */}
           <DeleteSelectedPageGroup
-            checkboxState={'' || CheckboxType.NONE_CHECKED} // Todo: change the left value to appropriate value
-            onClickInvoked={onDeleteSelectedPageHandler}
-            onCheckInvoked={onCheckAllPagesInvoked}
+            isSelectAllCheckboxDisabled={searchResultCount === 0}
+            selectAllCheckboxType={props.selectAllCheckboxType}
+            onClickDeleteAllButton={props.onClickDeleteAllButton}
+            onClickSelectAllCheckbox={props.onClickSelectAllCheckbox}
           />
         </div>
         {/** filter option */}

+ 15 - 6
packages/app/src/components/SearchPage/SearchResultList.tsx

@@ -6,27 +6,36 @@ import { IPageSearchResultData } from '../../interfaces/search';
 
 type Props = {
   pages: IPageSearchResultData[],
-  selectedPages: IPageSearchResultData[],
-  onClickInvoked?: (pageId: string) => void,
+  selectedPagesIdList: Set<string>
   searchResultCount?: number,
   activePage?: number,
   pagingLimit?: number,
-  onPagingNumberChanged?: (activePage: number) => void,
   focusedSearchResultData?: IPageSearchResultData,
+  onPagingNumberChanged?: (activePage: number) => void,
+  onClickSearchResultItem?: (pageId: string) => void,
+  onClickCheckbox?: (pageId: string) => void,
+  onClickInvoked?: (pageId: string) => void,
+  onClickDeleteButton?: (pageId: string) => void,
 }
 
 const SearchResultList: FC<Props> = (props:Props) => {
-  const { focusedSearchResultData } = props;
+  const { focusedSearchResultData, selectedPagesIdList } = props;
+
   const focusedPageId = (focusedSearchResultData != null && focusedSearchResultData.pageData != null) ? focusedSearchResultData.pageData._id : '';
   return (
     <>
-      {props.pages.map((page) => {
+      {Array.isArray(props.pages) && props.pages.map((page) => {
+        const isChecked = selectedPagesIdList.has(page.pageData._id);
+
         return (
           <SearchResultListItem
             key={page.pageData._id}
             page={page}
-            onClickInvoked={props.onClickInvoked}
+            onClickSearchResultItem={props.onClickSearchResultItem}
+            onClickCheckbox={props.onClickCheckbox}
+            isChecked={isChecked}
             isSelected={page.pageData._id === focusedPageId || false}
+            onClickDeleteButton={props.onClickDeleteButton}
           />
         );
       })}

+ 34 - 17
packages/app/src/components/SearchPage/SearchResultListItem.tsx

@@ -1,6 +1,7 @@
 import React, { FC } from 'react';
 
 import Clamp from 'react-multiline-clamp';
+import toastr from 'toastr';
 
 import { useTranslation } from 'react-i18next';
 import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
@@ -11,13 +12,19 @@ import { IPageHasId } from '~/interfaces/page';
 
 type PageItemControlProps = {
   page: IPageHasId,
+  onClickDeleteButton?: (pageId: string)=>void,
 }
 
-const PageItemControl: FC<PageItemControlProps> = (props: {page: IPageHasId}) => {
+const PageItemControl: FC<PageItemControlProps> = (props: PageItemControlProps) => {
 
-  const { page } = props;
+  const { page, onClickDeleteButton } = props;
   const { t } = useTranslation('');
 
+  const deleteButtonHandler = () => {
+    if (onClickDeleteButton != null) {
+      onClickDeleteButton(page._id);
+    }
+  };
   return (
     <>
       <button
@@ -46,16 +53,16 @@ const PageItemControl: FC<PageItemControlProps> = (props: {page: IPageHasId}) =>
           TODO: add function to the following buttons like using modal or others
           ref: https://estoc.weseek.co.jp/redmine/issues/79026
         */}
-        <button className="dropdown-item text-danger" type="button" onClick={() => console.log('delete modal show')}>
+        <button className="dropdown-item text-danger" type="button" onClick={deleteButtonHandler}>
           <i className="icon-fw icon-fire"></i>{t('Delete')}
         </button>
-        <button className="dropdown-item" type="button" onClick={() => console.log('duplicate modal show')}>
+        <button className="dropdown-item" type="button" onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
           <i className="icon-fw icon-star"></i>{t('Add to bookmark')}
         </button>
-        <button className="dropdown-item" type="button" onClick={() => console.log('duplicate modal show')}>
+        <button className="dropdown-item" type="button" onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
           <i className="icon-fw icon-docs"></i>{t('Duplicate')}
         </button>
-        <button className="dropdown-item" type="button" onClick={() => console.log('rename function will be added')}>
+        <button className="dropdown-item" type="button" onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
           <i className="icon-fw  icon-action-redo"></i>{t('Move/Rename')}
         </button>
       </div>
@@ -67,11 +74,17 @@ const PageItemControl: FC<PageItemControlProps> = (props: {page: IPageHasId}) =>
 type Props = {
   page: IPageSearchResultData,
   isSelected: boolean,
-  onClickInvoked?: (pageId: string) => void,
+  isChecked: boolean,
+  onClickCheckbox?: (pageId: string) => void,
+  onClickSearchResultItem?: (pageId: string) => void,
+  onClickDeleteButton?: (pageId: string) => void,
 }
 
 const SearchResultListItem: FC<Props> = (props:Props) => {
-  const { page: { pageData, pageMeta }, isSelected } = props;
+  const {
+    // todo: refactoring variable name to clear what changed
+    page: { pageData, pageMeta }, isSelected, onClickSearchResultItem, onClickCheckbox, isChecked,
+  } = props;
 
   // Add prefix 'id_' in pageId, because scrollspy of bootstrap doesn't work when the first letter of id attr of target component is numeral.
   const pageId = `#${pageData._id}`;
@@ -86,23 +99,27 @@ const SearchResultListItem: FC<Props> = (props:Props) => {
     />
   );
 
-  const onClickInvoked = (pageId) => {
-    if (props.onClickInvoked != null) {
-      props.onClickInvoked(pageId);
-    }
-  };
-
   return (
     <li key={pageData._id} className={`page-list-li search-page-item w-100 border-bottom px-4 list-group-item-action ${isSelected ? 'active' : ''}`}>
       <a
         className="d-block pt-3"
         href={pageId}
-        onClick={() => onClickInvoked(pageData._id)}
+        onClick={() => onClickSearchResultItem != null && onClickSearchResultItem(pageData._id)}
       >
         <div className="d-flex">
           {/* checkbox */}
           <div className="form-check my-auto mr-3">
-            <input className="form-check-input my-auto" type="checkbox" value="" id="flexCheckDefault" />
+            <input
+              className="form-check-input my-auto"
+              type="checkbox"
+              id="flexCheckDefault"
+              onChange={() => {
+                if (onClickCheckbox != null) {
+                  onClickCheckbox(pageData._id);
+                }
+              }}
+              checked={isChecked}
+            />
           </div>
           <div className="w-100">
             {/* page path */}
@@ -122,7 +139,7 @@ const SearchResultListItem: FC<Props> = (props:Props) => {
               </div>
               {/* doropdown icon includes page control buttons */}
               <div className="ml-auto">
-                <PageItemControl page={pageData} />
+                <PageItemControl page={pageData} onClickDeleteButton={props.onClickDeleteButton} />
               </div>
             </div>
             <div className="my-2">