use-new-page-input.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import type { ChangeEvent } from 'react';
  2. import React, {
  3. useState, type FC, useCallback, useRef,
  4. } from 'react';
  5. import nodePath from 'path';
  6. import { Origin } from '@growi/core';
  7. import { pathUtils, pagePathUtils } from '@growi/core/dist/utils';
  8. import { useRect } from '@growi/ui/dist/utils';
  9. import { useTranslation } from 'next-i18next';
  10. import { debounce } from 'throttle-debounce';
  11. import { AutosizeSubmittableInput, getAdjustedMaxWidthForAutosizeInput } from '~/client/components/Common/SubmittableInput';
  12. import { useCreatePage } from '~/client/services/create-page';
  13. import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
  14. import type { InputValidationResult } from '~/client/util/use-input-validator';
  15. import { ValidationTarget, useInputValidator } from '~/client/util/use-input-validator';
  16. import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
  17. import { usePageTreeDescCountMap } from '~/stores/ui';
  18. import { shouldCreateWipPage } from '../../../../utils/should-create-wip-page';
  19. import type { TreeItemToolProps } from '../interfaces';
  20. import { NewPageCreateButton } from './NewPageCreateButton';
  21. import newPageInputStyles from './NewPageInput.module.scss';
  22. type UseNewPageInput = {
  23. Input: FC<TreeItemToolProps>,
  24. CreateButton: FC<TreeItemToolProps>,
  25. isProcessingSubmission: boolean,
  26. }
  27. export const useNewPageInput = (): UseNewPageInput => {
  28. const [showInput, setShowInput] = useState(false);
  29. const [isProcessingSubmission, setProcessingSubmission] = useState(false);
  30. const CreateButton: FC<TreeItemToolProps> = (props) => {
  31. const { itemNode, stateHandlers } = props;
  32. const { page } = itemNode;
  33. const onClick = useCallback(() => {
  34. setShowInput(true);
  35. stateHandlers?.setIsOpen(true);
  36. }, [stateHandlers]);
  37. return (
  38. <NewPageCreateButton
  39. page={page}
  40. onClick={onClick}
  41. />
  42. );
  43. };
  44. const Input: FC<TreeItemToolProps> = (props) => {
  45. const { t } = useTranslation();
  46. const { create: createPage } = useCreatePage();
  47. const { itemNode, stateHandlers, isEnableActions } = props;
  48. const { page, children } = itemNode;
  49. const { getDescCount } = usePageTreeDescCountMap();
  50. const descendantCount = getDescCount(page._id) || page.descendantCount || 0;
  51. const isChildrenLoaded = children?.length > 0;
  52. const hasDescendants = descendantCount > 0 || isChildrenLoaded;
  53. const parentRef = useRef<HTMLDivElement>(null);
  54. const [parentRect] = useRect(parentRef);
  55. const [validationResult, setValidationResult] = useState<InputValidationResult>();
  56. const inputValidator = useInputValidator(ValidationTarget.PAGE);
  57. const changeHandler = useCallback(async(e: ChangeEvent<HTMLInputElement>) => {
  58. const validationResult = inputValidator(e.target.value);
  59. setValidationResult(validationResult ?? undefined);
  60. }, [inputValidator]);
  61. const changeHandlerDebounced = debounce(300, changeHandler);
  62. const cancel = useCallback(() => {
  63. setValidationResult(undefined);
  64. setShowInput(false);
  65. }, []);
  66. const create = useCallback(async(inputText) => {
  67. if (inputText.trim() === '') {
  68. return cancel();
  69. }
  70. const parentPath = pathUtils.addTrailingSlash(page.path as string);
  71. const newPagePath = nodePath.resolve(parentPath, inputText);
  72. const isCreatable = pagePathUtils.isCreatablePage(newPagePath);
  73. if (!isCreatable) {
  74. toastWarning(t('you_can_not_create_page_with_this_name'));
  75. return;
  76. }
  77. setProcessingSubmission(true);
  78. setShowInput(false);
  79. try {
  80. await createPage(
  81. {
  82. path: newPagePath,
  83. parentPath,
  84. body: undefined,
  85. // keep grant info undefined to inherit from parent
  86. grant: undefined,
  87. grantUserGroupIds: undefined,
  88. origin: Origin.View,
  89. wip: shouldCreateWipPage(newPagePath),
  90. },
  91. {
  92. skipTransition: true,
  93. onCreated: () => {
  94. mutatePageTree();
  95. mutateRecentlyUpdated();
  96. if (!hasDescendants) {
  97. stateHandlers?.setIsOpen(true);
  98. }
  99. toastSuccess(t('successfully_saved_the_page'));
  100. },
  101. },
  102. );
  103. }
  104. catch (err) {
  105. toastError(err);
  106. }
  107. finally {
  108. setProcessingSubmission(false);
  109. }
  110. }, [cancel, hasDescendants, page.path, stateHandlers, t, createPage]);
  111. const inputContainerClass = newPageInputStyles['new-page-input-container'] ?? '';
  112. const isInvalid = validationResult != null;
  113. const maxWidth = parentRect != null
  114. ? getAdjustedMaxWidthForAutosizeInput(parentRect.width, 'sm', validationResult != null ? false : undefined)
  115. : undefined;
  116. return isEnableActions && showInput
  117. ? (
  118. <div ref={parentRef} className={inputContainerClass}>
  119. <AutosizeSubmittableInput
  120. inputClassName={`form-control ${isInvalid ? 'is-invalid' : ''}`}
  121. inputStyle={{ maxWidth }}
  122. placeholder={t('Input page name')}
  123. aria-describedby={isInvalid ? 'new-page-input-feedback' : undefined}
  124. onChange={changeHandlerDebounced}
  125. onSubmit={create}
  126. onCancel={cancel}
  127. autoFocus
  128. />
  129. { isInvalid && (
  130. <div id="new-page-input" className="invalid-feedback d-block my-1">
  131. {validationResult.message}
  132. </div>
  133. ) }
  134. </div>
  135. )
  136. : <></>;
  137. };
  138. return {
  139. Input,
  140. CreateButton,
  141. isProcessingSubmission,
  142. };
  143. };