PageRenameModal.jsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import React, {
  2. useState, useEffect, useCallback,
  3. } from 'react';
  4. import PropTypes from 'prop-types';
  5. import {
  6. Modal, ModalHeader, ModalBody, ModalFooter,
  7. } from 'reactstrap';
  8. import { withTranslation } from 'react-i18next';
  9. import { debounce } from 'throttle-debounce';
  10. import { usePageRenameModal } from '~/stores/modal';
  11. import { withUnstatedContainers } from './UnstatedUtils';
  12. import { toastError } from '~/client/util/apiNotification';
  13. import AppContainer from '~/client/services/AppContainer';
  14. import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
  15. import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
  16. import ComparePathsTable from './ComparePathsTable';
  17. import DuplicatedPathsTable from './DuplicatedPathsTable';
  18. const PageRenameModal = (props) => {
  19. const {
  20. t, appContainer,
  21. } = props;
  22. const { crowi } = appContainer.config;
  23. const { data: renameModalData, close: closeRenameModal } = usePageRenameModal();
  24. const { isOpened, page } = renameModalData;
  25. const { pageId, revisionId, path } = page;
  26. const [pageNameInput, setPageNameInput] = useState('');
  27. const [errs, setErrs] = useState(null);
  28. const [subordinatedPages, setSubordinatedPages] = useState([]);
  29. const [existingPaths, setExistingPaths] = useState([]);
  30. const [isRenameRecursively, SetIsRenameRecursively] = useState(true);
  31. const [isRenameRedirect, SetIsRenameRedirect] = useState(false);
  32. const [isRemainMetadata, SetIsRemainMetadata] = useState(false);
  33. const [subordinatedError] = useState(null);
  34. const [isRenameRecursivelyWithoutExistPath, setIsRenameRecursivelyWithoutExistPath] = useState(true);
  35. function changeIsRenameRecursivelyHandler() {
  36. SetIsRenameRecursively(!isRenameRecursively);
  37. }
  38. function changeIsRenameRecursivelyWithoutExistPathHandler() {
  39. setIsRenameRecursivelyWithoutExistPath(!isRenameRecursivelyWithoutExistPath);
  40. }
  41. function changeIsRenameRedirectHandler() {
  42. SetIsRenameRedirect(!isRenameRedirect);
  43. }
  44. function changeIsRemainMetadataHandler() {
  45. SetIsRemainMetadata(!isRemainMetadata);
  46. }
  47. const updateSubordinatedList = useCallback(async() => {
  48. try {
  49. const res = await apiv3Get('/pages/subordinated-list', { path });
  50. setSubordinatedPages(res.data.subordinatedPages);
  51. }
  52. catch (err) {
  53. setErrs(err);
  54. toastError(t('modal_rename.label.Failed to get subordinated pages'));
  55. }
  56. }, [path, t]);
  57. useEffect(() => {
  58. if (isOpened) {
  59. updateSubordinatedList();
  60. setPageNameInput(path);
  61. }
  62. }, [isOpened, path, updateSubordinatedList]);
  63. const checkExistPaths = useCallback(async(newParentPath) => {
  64. try {
  65. const res = await apiv3Get('/page/exist-paths', { fromPath: path, toPath: newParentPath });
  66. const { existPaths } = res.data;
  67. setExistingPaths(existPaths);
  68. }
  69. catch (err) {
  70. setErrs(err);
  71. toastError(t('modal_rename.label.Fail to get exist path'));
  72. }
  73. }, [path, t]);
  74. // eslint-disable-next-line react-hooks/exhaustive-deps
  75. const checkExistPathsDebounce = useCallback(() => {
  76. debounce(1000, checkExistPaths);
  77. }, [checkExistPaths]);
  78. useEffect(() => {
  79. if (pageId != null && path != null && pageNameInput !== path) {
  80. checkExistPathsDebounce(pageNameInput, subordinatedPages);
  81. }
  82. }, [pageNameInput, subordinatedPages, pageId, path, checkExistPathsDebounce]);
  83. /**
  84. * change pageNameInput
  85. * @param {string} value
  86. */
  87. function inputChangeHandler(value) {
  88. setErrs(null);
  89. setPageNameInput(value);
  90. }
  91. async function rename() {
  92. setErrs(null);
  93. try {
  94. const response = await apiv3Put('/pages/rename', {
  95. revisionId,
  96. pageId,
  97. isRecursively: isRenameRecursively,
  98. isRenameRedirect,
  99. isRemainMetadata,
  100. newPagePath: pageNameInput,
  101. path,
  102. });
  103. const { page } = response.data;
  104. const url = new URL(page.path, 'https://dummy');
  105. if (isRenameRedirect) {
  106. url.searchParams.append('withRedirect', true);
  107. }
  108. const onRenamed = renameModalData.opts?.onRenamed;
  109. if (onRenamed != null) {
  110. onRenamed(path);
  111. }
  112. closeRenameModal();
  113. }
  114. catch (err) {
  115. setErrs(err);
  116. }
  117. }
  118. return (
  119. <Modal size="lg" isOpen={isOpened} toggle={closeRenameModal} autoFocus={false}>
  120. <ModalHeader tag="h4" toggle={closeRenameModal} className="bg-primary text-light">
  121. { t('modal_rename.label.Move/Rename page') }
  122. </ModalHeader>
  123. <ModalBody>
  124. <div className="form-group">
  125. <label>{ t('modal_rename.label.Current page name') }</label><br />
  126. <code>{ path }</code>
  127. </div>
  128. <div className="form-group">
  129. <label htmlFor="newPageName">{ t('modal_rename.label.New page name') }</label><br />
  130. <div className="input-group">
  131. <div className="input-group-prepend">
  132. <span className="input-group-text">{crowi.url}</span>
  133. </div>
  134. <form className="flex-fill" onSubmit={(e) => { e.preventDefault(); rename() }}>
  135. <input
  136. type="text"
  137. value={pageNameInput}
  138. className="form-control"
  139. onChange={e => inputChangeHandler(e.target.value)}
  140. required
  141. autoFocus
  142. />
  143. </form>
  144. </div>
  145. </div>
  146. <div className="custom-control custom-checkbox custom-checkbox-warning">
  147. <input
  148. className="custom-control-input"
  149. name="recursively"
  150. id="cbRenameRecursively"
  151. type="checkbox"
  152. checked={isRenameRecursively}
  153. onChange={changeIsRenameRecursivelyHandler}
  154. />
  155. <label className="custom-control-label" htmlFor="cbRenameRecursively">
  156. { t('modal_rename.label.Recursively') }
  157. <p className="form-text text-muted mt-0">{ t('modal_rename.help.recursive') }</p>
  158. </label>
  159. {existingPaths.length !== 0 && (
  160. <div
  161. className="custom-control custom-checkbox custom-checkbox-warning"
  162. style={{ display: isRenameRecursively ? '' : 'none' }}
  163. >
  164. <input
  165. className="custom-control-input"
  166. name="withoutExistRecursively"
  167. id="cbRenamewithoutExistRecursively"
  168. type="checkbox"
  169. checked={isRenameRecursivelyWithoutExistPath}
  170. onChange={changeIsRenameRecursivelyWithoutExistPathHandler}
  171. />
  172. <label className="custom-control-label" htmlFor="cbRenamewithoutExistRecursively">
  173. { t('modal_rename.label.Rename without exist path') }
  174. </label>
  175. </div>
  176. )}
  177. {isRenameRecursively && path != null && <ComparePathsTable path={path} subordinatedPages={subordinatedPages} newPagePath={pageNameInput} />}
  178. {isRenameRecursively && existingPaths.length !== 0 && <DuplicatedPathsTable existingPaths={existingPaths} oldPagePath={pageNameInput} />}
  179. </div>
  180. <div className="custom-control custom-checkbox custom-checkbox-success">
  181. <input
  182. className="custom-control-input"
  183. name="create_redirect"
  184. id="cbRenameRedirect"
  185. type="checkbox"
  186. checked={isRenameRedirect}
  187. onChange={changeIsRenameRedirectHandler}
  188. />
  189. <label className="custom-control-label" htmlFor="cbRenameRedirect">
  190. { t('modal_rename.label.Redirect') }
  191. <p className="form-text text-muted mt-0">{ t('modal_rename.help.redirect') }</p>
  192. </label>
  193. </div>
  194. <div className="custom-control custom-checkbox custom-checkbox-primary">
  195. <input
  196. className="custom-control-input"
  197. name="remain_metadata"
  198. id="cbRemainMetadata"
  199. type="checkbox"
  200. checked={isRemainMetadata}
  201. onChange={changeIsRemainMetadataHandler}
  202. />
  203. <label className="custom-control-label" htmlFor="cbRemainMetadata">
  204. { t('modal_rename.label.Do not update metadata') }
  205. <p className="form-text text-muted mt-0">{ t('modal_rename.help.metadata') }</p>
  206. </label>
  207. </div>
  208. <div> {subordinatedError} </div>
  209. </ModalBody>
  210. <ModalFooter>
  211. <ApiErrorMessageList errs={errs} targetPath={pageNameInput} />
  212. <button
  213. type="button"
  214. className="btn btn-primary"
  215. onClick={rename}
  216. disabled={(isRenameRecursively && !isRenameRecursivelyWithoutExistPath && existingPaths.length !== 0)}
  217. >Rename
  218. </button>
  219. </ModalFooter>
  220. </Modal>
  221. );
  222. };
  223. /**
  224. * Wrapper component for using unstated
  225. */
  226. const PageRenameModalWrapper = withUnstatedContainers(PageRenameModal, [AppContainer]);
  227. PageRenameModal.propTypes = {
  228. t: PropTypes.func.isRequired, // i18next
  229. appContainer: PropTypes.instanceOf(AppContainer).isRequired,
  230. };
  231. export default withTranslation()(PageRenameModalWrapper);