|
|
@@ -1,8 +1,21 @@
|
|
|
-import React, { useState, type FC, useCallback } from 'react';
|
|
|
+import type { ChangeEvent } from 'react';
|
|
|
+import React, {
|
|
|
+ useState, type FC, useCallback, useRef,
|
|
|
+} from 'react';
|
|
|
+
|
|
|
+import nodePath from 'path';
|
|
|
|
|
|
import { Origin } from '@growi/core';
|
|
|
+import { pathUtils, pagePathUtils } from '@growi/core/dist/utils';
|
|
|
+import { useRect } from '@growi/ui/dist/utils';
|
|
|
+import { useTranslation } from 'next-i18next';
|
|
|
+import { debounce } from 'throttle-debounce';
|
|
|
|
|
|
import { createPage } from '~/client/services/create-page';
|
|
|
+import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
|
|
|
+import type { InputValidationResult } from '~/client/util/use-input-validator';
|
|
|
+import { ValidationTarget, useInputValidator } from '~/client/util/use-input-validator';
|
|
|
+import { AutosizeSubmittableInput, getAdjustedMaxWidthForAutosizeInput } from '~/components/Common/SubmittableInput';
|
|
|
import { mutatePageTree } from '~/stores/page-listing';
|
|
|
import { usePageTreeDescCountMap } from '~/stores/ui';
|
|
|
|
|
|
@@ -10,7 +23,9 @@ import { shouldCreateWipPage } from '../../../utils/should-create-wip-page';
|
|
|
import type { TreeItemToolProps } from '../interfaces';
|
|
|
|
|
|
import { NewPageCreateButton } from './NewPageCreateButton';
|
|
|
-import { NewPageInput } from './NewPageInput';
|
|
|
+
|
|
|
+
|
|
|
+import newPageInputStyles from './NewPageInput.module.scss';
|
|
|
|
|
|
|
|
|
type UseNewPageInput = {
|
|
|
@@ -24,26 +39,15 @@ export const useNewPageInput = (): UseNewPageInput => {
|
|
|
const [showInput, setShowInput] = useState(false);
|
|
|
const [isProcessingSubmission, setProcessingSubmission] = useState(false);
|
|
|
|
|
|
- const { getDescCount } = usePageTreeDescCountMap();
|
|
|
-
|
|
|
const CreateButton: FC<TreeItemToolProps> = (props) => {
|
|
|
|
|
|
const { itemNode, stateHandlers } = props;
|
|
|
- const { page, children } = itemNode;
|
|
|
-
|
|
|
- // descendantCount
|
|
|
- const descendantCount = getDescCount(page._id) || page.descendantCount || 0;
|
|
|
-
|
|
|
- const isChildrenLoaded = children?.length > 0;
|
|
|
- const hasDescendants = descendantCount > 0 || isChildrenLoaded;
|
|
|
+ const { page } = itemNode;
|
|
|
|
|
|
const onClick = useCallback(() => {
|
|
|
setShowInput(true);
|
|
|
-
|
|
|
- if (hasDescendants) {
|
|
|
- stateHandlers?.setIsOpen(true);
|
|
|
- }
|
|
|
- }, [hasDescendants, stateHandlers]);
|
|
|
+ stateHandlers?.setIsOpen(true);
|
|
|
+ }, [stateHandlers]);
|
|
|
|
|
|
return (
|
|
|
<NewPageCreateButton
|
|
|
@@ -55,7 +59,9 @@ export const useNewPageInput = (): UseNewPageInput => {
|
|
|
|
|
|
const Input: FC<TreeItemToolProps> = (props) => {
|
|
|
|
|
|
- const { itemNode, stateHandlers } = props;
|
|
|
+ const { t } = useTranslation();
|
|
|
+
|
|
|
+ const { itemNode, stateHandlers, isEnableActions } = props;
|
|
|
const { page, children } = itemNode;
|
|
|
|
|
|
const { getDescCount } = usePageTreeDescCountMap();
|
|
|
@@ -64,41 +70,95 @@ export const useNewPageInput = (): UseNewPageInput => {
|
|
|
const isChildrenLoaded = children?.length > 0;
|
|
|
const hasDescendants = descendantCount > 0 || isChildrenLoaded;
|
|
|
|
|
|
- const submitHandler = useCallback(async(newPagePath: string) => {
|
|
|
+ const parentRef = useRef<HTMLDivElement>(null);
|
|
|
+ const [parentRect] = useRect(parentRef);
|
|
|
+
|
|
|
+ const [validationResult, setValidationResult] = useState<InputValidationResult>();
|
|
|
+
|
|
|
+ const inputValidator = useInputValidator(ValidationTarget.PAGE);
|
|
|
+
|
|
|
+ const changeHandler = useCallback(async(e: ChangeEvent<HTMLInputElement>) => {
|
|
|
+ const validationResult = inputValidator(e.target.value);
|
|
|
+ setValidationResult(validationResult ?? undefined);
|
|
|
+ }, [inputValidator]);
|
|
|
+ const changeHandlerDebounced = debounce(300, changeHandler);
|
|
|
+
|
|
|
+ const cancel = useCallback(() => {
|
|
|
+ setValidationResult(undefined);
|
|
|
+ setShowInput(false);
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const create = useCallback(async(inputText) => {
|
|
|
+ if (inputText.trim() === '') {
|
|
|
+ return cancel();
|
|
|
+ }
|
|
|
+
|
|
|
+ const parentPath = pathUtils.addTrailingSlash(page.path as string);
|
|
|
+ const newPagePath = nodePath.resolve(parentPath, inputText);
|
|
|
+ const isCreatable = pagePathUtils.isCreatablePage(newPagePath);
|
|
|
+
|
|
|
+ if (!isCreatable) {
|
|
|
+ toastWarning(t('you_can_not_create_page_with_this_name'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
setProcessingSubmission(true);
|
|
|
|
|
|
setShowInput(false);
|
|
|
|
|
|
- await createPage({
|
|
|
- path: newPagePath,
|
|
|
- body: undefined,
|
|
|
- // keep grant info undefined to inherit from parent
|
|
|
- grant: undefined,
|
|
|
- grantUserGroupIds: undefined,
|
|
|
- origin: Origin.View,
|
|
|
- wip: shouldCreateWipPage(newPagePath),
|
|
|
- });
|
|
|
+ try {
|
|
|
+ await createPage({
|
|
|
+ path: newPagePath,
|
|
|
+ body: undefined,
|
|
|
+ // keep grant info undefined to inherit from parent
|
|
|
+ grant: undefined,
|
|
|
+ grantUserGroupIds: undefined,
|
|
|
+ origin: Origin.View,
|
|
|
+ wip: shouldCreateWipPage(newPagePath),
|
|
|
+ });
|
|
|
|
|
|
- mutatePageTree();
|
|
|
+ mutatePageTree();
|
|
|
|
|
|
- if (!hasDescendants) {
|
|
|
- stateHandlers?.setIsOpen(true);
|
|
|
+ if (!hasDescendants) {
|
|
|
+ stateHandlers?.setIsOpen(true);
|
|
|
+ }
|
|
|
+
|
|
|
+ toastSuccess(t('successfully_saved_the_page'));
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ toastError(err);
|
|
|
}
|
|
|
- }, [hasDescendants, stateHandlers]);
|
|
|
+ finally {
|
|
|
+ setProcessingSubmission(false);
|
|
|
+ }
|
|
|
+ }, [cancel, hasDescendants, page.path, stateHandlers, t]);
|
|
|
|
|
|
- const submittionFailedHandler = useCallback(() => {
|
|
|
- setProcessingSubmission(false);
|
|
|
- }, []);
|
|
|
+ const inputContainerClass = newPageInputStyles['new-page-input-container'] ?? '';
|
|
|
+ const isInvalid = validationResult != null;
|
|
|
+
|
|
|
+ const maxWidth = parentRect != null
|
|
|
+ ? getAdjustedMaxWidthForAutosizeInput(parentRect.width, 'sm', validationResult != null ? false : undefined)
|
|
|
+ : undefined;
|
|
|
|
|
|
- return showInput
|
|
|
+ return isEnableActions && showInput
|
|
|
? (
|
|
|
- <NewPageInput
|
|
|
- page={page}
|
|
|
- isEnableActions={props.isEnableActions}
|
|
|
- onSubmit={submitHandler}
|
|
|
- onSubmittionFailed={submittionFailedHandler}
|
|
|
- onCanceled={() => setShowInput(false)}
|
|
|
- />
|
|
|
+ <div ref={parentRef} className={inputContainerClass}>
|
|
|
+ <AutosizeSubmittableInput
|
|
|
+ inputClassName={`form-control ${isInvalid ? 'is-invalid' : ''}`}
|
|
|
+ inputStyle={{ maxWidth }}
|
|
|
+ placeholder={t('Input page name')}
|
|
|
+ aria-describedby={isInvalid ? 'new-page-input-feedback' : undefined}
|
|
|
+ onChange={changeHandlerDebounced}
|
|
|
+ onSubmit={create}
|
|
|
+ onCancel={cancel}
|
|
|
+ autoFocus
|
|
|
+ />
|
|
|
+ { isInvalid && (
|
|
|
+ <div id="new-page-input" className="invalid-feedback d-block my-1">
|
|
|
+ {validationResult.message}
|
|
|
+ </div>
|
|
|
+ ) }
|
|
|
+ </div>
|
|
|
)
|
|
|
: <></>;
|
|
|
};
|