|
|
@@ -1,24 +1,14 @@
|
|
|
-import type { ChangeEvent, FC } from 'react';
|
|
|
-import React, {
|
|
|
- useCallback, useRef, useState,
|
|
|
-} from 'react';
|
|
|
-
|
|
|
-import nodePath from 'path';
|
|
|
+import type { FC } from 'react';
|
|
|
+import React, { useCallback } from 'react';
|
|
|
|
|
|
import type { IPageInfoExt, IPageToDeleteWithMeta } from '@growi/core';
|
|
|
import { getIdStringForRef } from '@growi/core';
|
|
|
-import { pathUtils } from '@growi/core/dist/utils';
|
|
|
-import { useRect } from '@growi/ui/dist/utils';
|
|
|
import { useTranslation } from 'next-i18next';
|
|
|
import { DropdownToggle } from 'reactstrap';
|
|
|
-import { debounce } from 'throttle-debounce';
|
|
|
|
|
|
-import { AutosizeSubmittableInput, getAdjustedMaxWidthForAutosizeInput } from '~/client/components/Common/SubmittableInput';
|
|
|
import { NotAvailableForGuest } from '~/client/components/NotAvailableForGuest';
|
|
|
import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/page-operation';
|
|
|
-import { apiv3Put } from '~/client/util/apiv3-client';
|
|
|
import { toastError, toastSuccess } from '~/client/util/toastr';
|
|
|
-import { ValidationTarget, useInputValidator, type InputValidationResult } from '~/client/util/use-input-validator';
|
|
|
import { useSWRMUTxCurrentUserBookmarks } from '~/stores/bookmark';
|
|
|
import { useSWRMUTxPageInfo } from '~/stores/page';
|
|
|
|
|
|
@@ -28,15 +18,11 @@ import type { TreeItemToolProps } from '../../TreeItem';
|
|
|
|
|
|
type UsePageItemControl = {
|
|
|
Control: FC<TreeItemToolProps>,
|
|
|
- RenameInput: FC<TreeItemToolProps>,
|
|
|
- showRenameInput: boolean,
|
|
|
}
|
|
|
|
|
|
export const usePageItemControl = (): UsePageItemControl => {
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
- const [showRenameInput, setShowRenameInput] = useState(false);
|
|
|
-
|
|
|
|
|
|
const Control: FC<TreeItemToolProps> = (props) => {
|
|
|
const {
|
|
|
@@ -74,8 +60,9 @@ export const usePageItemControl = (): UsePageItemControl => {
|
|
|
}, [onClickDuplicateMenuItem, page]);
|
|
|
|
|
|
const renameMenuItemClickHandler = useCallback(() => {
|
|
|
- setShowRenameInput(true);
|
|
|
- }, []);
|
|
|
+ // Use headless-tree's renamingFeature
|
|
|
+ item.startRenaming();
|
|
|
+ }, [item]);
|
|
|
|
|
|
const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoExt | undefined): Promise<void> => {
|
|
|
if (onClickDeleteMenuItem == null) {
|
|
|
@@ -135,102 +122,8 @@ export const usePageItemControl = (): UsePageItemControl => {
|
|
|
};
|
|
|
|
|
|
|
|
|
- const RenameInput: FC<TreeItemToolProps> = (props) => {
|
|
|
- const { item, onRenamed } = props;
|
|
|
- const page = item.getItemData();
|
|
|
-
|
|
|
- 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);
|
|
|
- setShowRenameInput(false);
|
|
|
- }, []);
|
|
|
-
|
|
|
- const rename = useCallback(async(inputText) => {
|
|
|
- if (inputText.trim() === '') {
|
|
|
- return cancel();
|
|
|
- }
|
|
|
-
|
|
|
- const parentPath = pathUtils.addTrailingSlash(nodePath.dirname(page.path ?? ''));
|
|
|
- const newPagePath = nodePath.resolve(parentPath, inputText);
|
|
|
-
|
|
|
- if (newPagePath === page.path) {
|
|
|
- setValidationResult(undefined);
|
|
|
- setShowRenameInput(false);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- await apiv3Put('/pages/rename', {
|
|
|
- pageId: page._id,
|
|
|
- revisionId: page.revision,
|
|
|
- newPagePath,
|
|
|
- });
|
|
|
-
|
|
|
- onRenamed?.(page.path, newPagePath);
|
|
|
- setShowRenameInput(false);
|
|
|
-
|
|
|
- toastSuccess(t('renamed_pages', { path: page.path }));
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- toastError(err);
|
|
|
- }
|
|
|
- finally {
|
|
|
- setValidationResult(undefined);
|
|
|
- }
|
|
|
-
|
|
|
- }, [cancel, onRenamed, page._id, page.path, page.revision]);
|
|
|
-
|
|
|
-
|
|
|
- if (!showRenameInput) {
|
|
|
- return <></>;
|
|
|
- }
|
|
|
-
|
|
|
- const isInvalid = validationResult != null;
|
|
|
-
|
|
|
- const maxWidth = parentRect != null
|
|
|
- ? getAdjustedMaxWidthForAutosizeInput(parentRect.width, 'sm', validationResult != null ? false : undefined)
|
|
|
- : undefined;
|
|
|
-
|
|
|
- return (
|
|
|
- <div ref={parentRef} className="flex-fill">
|
|
|
- <AutosizeSubmittableInput
|
|
|
- value={nodePath.basename(page.path ?? '')}
|
|
|
- inputClassName={`form-control ${isInvalid ? 'is-invalid' : ''}`}
|
|
|
- inputStyle={{ maxWidth }}
|
|
|
- placeholder={t('Input page name')}
|
|
|
- aria-describedby={isInvalid ? 'rename-feedback' : undefined}
|
|
|
- onChange={changeHandlerDebounced}
|
|
|
- onSubmit={rename}
|
|
|
- onCancel={cancel}
|
|
|
- autoFocus
|
|
|
- />
|
|
|
- { isInvalid && (
|
|
|
- <div id="rename-feedback" className="invalid-feedback d-block my-1">
|
|
|
- {validationResult.message}
|
|
|
- </div>
|
|
|
- ) }
|
|
|
- </div>
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
-
|
|
|
return {
|
|
|
Control,
|
|
|
- RenameInput,
|
|
|
- showRenameInput,
|
|
|
};
|
|
|
|
|
|
};
|