|
@@ -4,19 +4,31 @@ import React, {
|
|
|
|
|
|
|
|
import nodePath from 'path';
|
|
import nodePath from 'path';
|
|
|
|
|
|
|
|
-import { pagePathUtils } from '@growi/core';
|
|
|
|
|
|
|
+
|
|
|
|
|
+import { pathUtils, pagePathUtils } from '@growi/core';
|
|
|
import { useTranslation } from 'next-i18next';
|
|
import { useTranslation } from 'next-i18next';
|
|
|
import { useDrag, useDrop } from 'react-dnd';
|
|
import { useDrag, useDrop } from 'react-dnd';
|
|
|
|
|
+import { DropdownToggle } from 'reactstrap';
|
|
|
|
|
|
|
|
|
|
+import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/page-operation';
|
|
|
import { apiv3Put } from '~/client/util/apiv3-client';
|
|
import { apiv3Put } from '~/client/util/apiv3-client';
|
|
|
-import { toastWarning, toastError } from '~/client/util/toastr';
|
|
|
|
|
-import { IPageHasId } from '~/interfaces/page';
|
|
|
|
|
|
|
+import { ValidationTarget } from '~/client/util/input-validator';
|
|
|
|
|
+import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
|
|
|
|
|
+import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
|
|
|
|
|
+import {
|
|
|
|
|
+ IPageHasId,
|
|
|
|
|
+ IPageInfoAll, IPageToDeleteWithMeta,
|
|
|
|
|
+} from '~/interfaces/page';
|
|
|
|
|
+import { useSWRMUTxCurrentUserBookmarks } from '~/stores/bookmark';
|
|
|
|
|
+import { useSWRMUTxPageInfo } from '~/stores/page';
|
|
|
import { mutatePageTree, useSWRxPageChildren } from '~/stores/page-listing';
|
|
import { mutatePageTree, useSWRxPageChildren } from '~/stores/page-listing';
|
|
|
import loggerFactory from '~/utils/logger';
|
|
import loggerFactory from '~/utils/logger';
|
|
|
|
|
|
|
|
|
|
+import ClosableTextInput from '../../Common/ClosableTextInput';
|
|
|
|
|
+import { PageItemControl } from '../../Common/Dropdown/PageItemControl';
|
|
|
|
|
|
|
|
import { ItemNode } from './ItemNode';
|
|
import { ItemNode } from './ItemNode';
|
|
|
-import SimpleItem, { SimpleItemProps } from './SimpleItem';
|
|
|
|
|
|
|
+import SimpleItem, { SimpleItemProps, NotDraggableForClosableTextInput, ToolOfSimpleItem } from './SimpleItem';
|
|
|
|
|
|
|
|
const logger = loggerFactory('growi:cli:Item');
|
|
const logger = loggerFactory('growi:cli:Item');
|
|
|
|
|
|
|
@@ -24,6 +36,148 @@ type Optional = 'itemRef' | 'itemClass' | 'mainClassName';
|
|
|
|
|
|
|
|
type PageTreeItemProps = Omit<SimpleItemProps, Optional> & {key};
|
|
type PageTreeItemProps = Omit<SimpleItemProps, Optional> & {key};
|
|
|
|
|
|
|
|
|
|
+const Ellipsis = (props) => {
|
|
|
|
|
+ const [isRenameInputShown, setRenameInputShown] = useState(false);
|
|
|
|
|
+ const { t } = useTranslation();
|
|
|
|
|
+
|
|
|
|
|
+ const {
|
|
|
|
|
+ page, onRenamed, onClickDuplicateMenuItem,
|
|
|
|
|
+ onClickDeleteMenuItem, isEnableActions, isReadOnlyUser,
|
|
|
|
|
+ } = props;
|
|
|
|
|
+
|
|
|
|
|
+ const { trigger: mutateCurrentUserBookmarks } = useSWRMUTxCurrentUserBookmarks();
|
|
|
|
|
+ const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(page._id ?? null);
|
|
|
|
|
+
|
|
|
|
|
+ const bookmarkMenuItemClickHandler = async(_pageId: string, _newValue: boolean): Promise<void> => {
|
|
|
|
|
+ const bookmarkOperation = _newValue ? bookmark : unbookmark;
|
|
|
|
|
+ await bookmarkOperation(_pageId);
|
|
|
|
|
+ mutateCurrentUserBookmarks();
|
|
|
|
|
+ mutatePageInfo();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const duplicateMenuItemClickHandler = useCallback((): void => {
|
|
|
|
|
+ if (onClickDuplicateMenuItem == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const { _id: pageId, path } = page;
|
|
|
|
|
+
|
|
|
|
|
+ if (pageId == null || path == null) {
|
|
|
|
|
+ throw Error('Any of _id and path must not be null.');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const pageToDuplicate = { pageId, path };
|
|
|
|
|
+
|
|
|
|
|
+ onClickDuplicateMenuItem(pageToDuplicate);
|
|
|
|
|
+ }, [onClickDuplicateMenuItem, page]);
|
|
|
|
|
+
|
|
|
|
|
+ const renameMenuItemClickHandler = useCallback(() => {
|
|
|
|
|
+ setRenameInputShown(true);
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
|
|
+ const onPressEnterForRenameHandler = async(inputText: string) => {
|
|
|
|
|
+ const parentPath = pathUtils.addTrailingSlash(nodePath.dirname(page.path ?? ''));
|
|
|
|
|
+ const newPagePath = nodePath.resolve(parentPath, inputText);
|
|
|
|
|
+
|
|
|
|
|
+ if (newPagePath === page.path) {
|
|
|
|
|
+ setRenameInputShown(false);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ setRenameInputShown(false);
|
|
|
|
|
+ await apiv3Put('/pages/rename', {
|
|
|
|
|
+ pageId: page._id,
|
|
|
|
|
+ revisionId: page.revision,
|
|
|
|
|
+ newPagePath,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (onRenamed != null) {
|
|
|
|
|
+ onRenamed(page.path, newPagePath);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ toastSuccess(t('renamed_pages', { path: page.path }));
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (err) {
|
|
|
|
|
+ setRenameInputShown(true);
|
|
|
|
|
+ toastError(err);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoAll | undefined): Promise<void> => {
|
|
|
|
|
+ if (onClickDeleteMenuItem == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (page._id == null || page.path == null) {
|
|
|
|
|
+ throw Error('_id and path must not be null.');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const pageToDelete: IPageToDeleteWithMeta = {
|
|
|
|
|
+ data: {
|
|
|
|
|
+ _id: page._id,
|
|
|
|
|
+ revision: page.revision as string,
|
|
|
|
|
+ path: page.path,
|
|
|
|
|
+ },
|
|
|
|
|
+ meta: pageInfo,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ onClickDeleteMenuItem(pageToDelete);
|
|
|
|
|
+ }, [onClickDeleteMenuItem, page]);
|
|
|
|
|
+
|
|
|
|
|
+ const pathRecoveryMenuItemClickHandler = async(pageId: string): Promise<void> => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await resumeRenameOperation(pageId);
|
|
|
|
|
+ toastSuccess(t('page_operation.paths_recovered'));
|
|
|
|
|
+ }
|
|
|
|
|
+ catch {
|
|
|
|
|
+ toastError(t('page_operation.path_recovery_failed'));
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {isRenameInputShown ? (
|
|
|
|
|
+ <div className="flex-fill">
|
|
|
|
|
+ <NotDraggableForClosableTextInput>
|
|
|
|
|
+ <ClosableTextInput
|
|
|
|
|
+ value={nodePath.basename(page.path ?? '')}
|
|
|
|
|
+ placeholder={t('Input page name')}
|
|
|
|
|
+ onClickOutside={() => { setRenameInputShown(false) }}
|
|
|
|
|
+ onPressEnter={onPressEnterForRenameHandler}
|
|
|
|
|
+ validationTarget={ValidationTarget.PAGE}
|
|
|
|
|
+ />
|
|
|
|
|
+ </NotDraggableForClosableTextInput>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <ToolOfSimpleItem page={page}/>
|
|
|
|
|
+ )}
|
|
|
|
|
+ <NotAvailableForGuest>
|
|
|
|
|
+ <div className="grw-pagetree-control d-flex">
|
|
|
|
|
+ <PageItemControl
|
|
|
|
|
+ pageId={page._id}
|
|
|
|
|
+ isEnableActions={isEnableActions}
|
|
|
|
|
+ isReadOnlyUser={isReadOnlyUser}
|
|
|
|
|
+ onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
|
|
|
|
|
+ onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
|
|
|
|
|
+ onClickRenameMenuItem={renameMenuItemClickHandler}
|
|
|
|
|
+ onClickDeleteMenuItem={deleteMenuItemClickHandler}
|
|
|
|
|
+ onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
|
|
|
|
|
+ isInstantRename
|
|
|
|
|
+ // Todo: It is wanted to find a better way to pass operationProcessData to PageItemControl
|
|
|
|
|
+ operationProcessData={page.processData}
|
|
|
|
|
+ >
|
|
|
|
|
+ {/* pass the color property to reactstrap dropdownToggle props. https://6-4-0--reactstrap.netlify.app/components/dropdowns/ */}
|
|
|
|
|
+ <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
|
|
|
|
|
+ <i id='option-button-in-page-tree' className="icon-options fa fa-rotate-90 p-1"></i>
|
|
|
|
|
+ </DropdownToggle>
|
|
|
|
|
+ </PageItemControl>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </NotAvailableForGuest>
|
|
|
|
|
+ </>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
export const PageTreeItem: FC<PageTreeItemProps> = (props: PageTreeItemProps) => {
|
|
export const PageTreeItem: FC<PageTreeItemProps> = (props: PageTreeItemProps) => {
|
|
|
const getNewPathAfterMoved = (droppedPagePath: string, newParentPagePath: string): string => {
|
|
const getNewPathAfterMoved = (droppedPagePath: string, newParentPagePath: string): string => {
|
|
|
const pageTitle = nodePath.basename(droppedPagePath);
|
|
const pageTitle = nodePath.basename(droppedPagePath);
|
|
@@ -43,12 +197,16 @@ export const PageTreeItem: FC<PageTreeItemProps> = (props: PageTreeItemProps) =>
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const { t } = useTranslation();
|
|
const { t } = useTranslation();
|
|
|
|
|
+
|
|
|
const {
|
|
const {
|
|
|
- itemNode, isOpen: _isOpen = false, onRenamed,
|
|
|
|
|
|
|
+ itemNode, isOpen: _isOpen = false, onRenamed, onClickDuplicateMenuItem,
|
|
|
|
|
+ onClickDeleteMenuItem, isEnableActions, isReadOnlyUser,
|
|
|
} = props;
|
|
} = props;
|
|
|
|
|
+
|
|
|
const { page } = itemNode;
|
|
const { page } = itemNode;
|
|
|
const [isOpen, setIsOpen] = useState(_isOpen);
|
|
const [isOpen, setIsOpen] = useState(_isOpen);
|
|
|
const [shouldHide, setShouldHide] = useState(false);
|
|
const [shouldHide, setShouldHide] = useState(false);
|
|
|
|
|
+
|
|
|
const { mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
|
|
const { mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
|
|
|
|
|
|
|
|
const displayDroppedItemByPageId = useCallback((pageId) => {
|
|
const displayDroppedItemByPageId = useCallback((pageId) => {
|
|
@@ -164,6 +322,7 @@ export const PageTreeItem: FC<PageTreeItemProps> = (props: PageTreeItemProps) =>
|
|
|
itemRef={itemRef}
|
|
itemRef={itemRef}
|
|
|
itemClass={PageTreeItem}
|
|
itemClass={PageTreeItem}
|
|
|
mainClassName={mainClassName}
|
|
mainClassName={mainClassName}
|
|
|
|
|
+ customComponent={Ellipsis}
|
|
|
/>
|
|
/>
|
|
|
);
|
|
);
|
|
|
};
|
|
};
|