|
|
@@ -9,28 +9,22 @@ import {
|
|
|
} from '@growi/core';
|
|
|
import { useTranslation } from 'next-i18next';
|
|
|
import { useRouter } from 'next/router';
|
|
|
-import { UncontrolledTooltip, DropdownToggle } from 'reactstrap';
|
|
|
+import { UncontrolledTooltip } from 'reactstrap';
|
|
|
|
|
|
-import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/page-operation';
|
|
|
-import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
|
|
|
+import { apiv3Post } from '~/client/util/apiv3-client';
|
|
|
import { ValidationTarget } from '~/client/util/input-validator';
|
|
|
import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
|
|
|
import { TriangleIcon } from '~/components/Icons/TriangleIcon';
|
|
|
import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
|
|
|
import { NotAvailableForReadOnlyUser } from '~/components/NotAvailableForReadOnlyUser';
|
|
|
-import {
|
|
|
- IPageInfoAll, IPageToDeleteWithMeta,
|
|
|
-} from '~/interfaces/page';
|
|
|
-import { useSWRMUTxCurrentUserBookmarks } from '~/stores/bookmark';
|
|
|
+import { IPageToDeleteWithMeta, IPageForItem } from '~/interfaces/page';
|
|
|
import { IPageForPageDuplicateModal } from '~/stores/modal';
|
|
|
-import { useSWRMUTxPageInfo } from '~/stores/page';
|
|
|
import { useSWRxPageChildren } from '~/stores/page-listing';
|
|
|
import { usePageTreeDescCountMap } from '~/stores/ui';
|
|
|
import { shouldRecoverPagePaths } from '~/utils/page-operation';
|
|
|
|
|
|
import ClosableTextInput from '../../Common/ClosableTextInput';
|
|
|
import CountBadge from '../../Common/CountBadge';
|
|
|
-import { PageItemControl } from '../../Common/Dropdown/PageItemControl';
|
|
|
|
|
|
import { ItemNode } from './ItemNode';
|
|
|
|
|
|
@@ -47,6 +41,7 @@ export type SimpleItemProps = {
|
|
|
itemRef?
|
|
|
itemClass?: React.FunctionComponent<SimpleItemProps>
|
|
|
mainClassName?: string
|
|
|
+ customComponent?: React.FunctionComponent<SimpleItemToolProps>
|
|
|
};
|
|
|
|
|
|
// Utility to mark target
|
|
|
@@ -86,14 +81,64 @@ const markTarget = (children: ItemNode[], targetPathOrId?: Nullable<string>): vo
|
|
|
type NotDraggableProps = {
|
|
|
children: ReactNode,
|
|
|
};
|
|
|
-const NotDraggableForClosableTextInput = (props: NotDraggableProps): JSX.Element => {
|
|
|
+export const NotDraggableForClosableTextInput = (props: NotDraggableProps): JSX.Element => {
|
|
|
return <div draggable onDragStart={e => e.preventDefault()}>{props.children}</div>;
|
|
|
};
|
|
|
|
|
|
+type SimpleItemToolPropsOptional = 'itemNode' | 'targetPathOrId' | 'isOpen' | 'itemRef' | 'itemClass' | 'mainClassName';
|
|
|
+type SimpleItemToolProps = Omit<SimpleItemProps, SimpleItemToolPropsOptional> & {page: IPageForItem};
|
|
|
|
|
|
-const SimpleItem: FC<SimpleItemProps> = (props: SimpleItemProps) => {
|
|
|
+export const SimpleItemTool: FC<SimpleItemToolProps> = (props) => {
|
|
|
const { t } = useTranslation();
|
|
|
const router = useRouter();
|
|
|
+ const { getDescCount } = usePageTreeDescCountMap();
|
|
|
+
|
|
|
+ const page = props.page;
|
|
|
+
|
|
|
+ const pageName = nodePath.basename(page.path ?? '') || '/';
|
|
|
+
|
|
|
+ const shouldShowAttentionIcon = page.processData != null ? shouldRecoverPagePaths(page.processData) : false;
|
|
|
+
|
|
|
+ const descendantCount = getDescCount(page._id) || page.descendantCount || 0;
|
|
|
+
|
|
|
+ const pageTreeItemClickHandler = (e) => {
|
|
|
+ e.preventDefault();
|
|
|
+
|
|
|
+ if (page.path == null || page._id == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const link = pathUtils.returnPathForURL(page.path, page._id);
|
|
|
+
|
|
|
+ router.push(link);
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ {shouldShowAttentionIcon && (
|
|
|
+ <>
|
|
|
+ <i id="path-recovery" className="fa fa-warning mr-2 text-warning"></i>
|
|
|
+ <UncontrolledTooltip placement="top" target="path-recovery" fade={false}>
|
|
|
+ {t('tooltip.operation.attention.rename')}
|
|
|
+ </UncontrolledTooltip>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ {page != null && page.path != null && page._id != null && (
|
|
|
+ <div className="grw-pagetree-title-anchor flex-grow-1">
|
|
|
+ <p onClick={pageTreeItemClickHandler} className={`text-truncate m-auto ${page.isEmpty && 'grw-sidebar-text-muted'}`}>{pageName}</p>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {descendantCount > 0 && (
|
|
|
+ <div className="grw-pagetree-count-wrapper">
|
|
|
+ <CountBadge count={descendantCount} />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const SimpleItem: FC<SimpleItemProps> = (props) => {
|
|
|
+ const { t } = useTranslation();
|
|
|
|
|
|
const {
|
|
|
itemNode, targetPathOrId, isOpen: _isOpen = false,
|
|
|
@@ -106,12 +151,9 @@ const SimpleItem: FC<SimpleItemProps> = (props: SimpleItemProps) => {
|
|
|
const [currentChildren, setCurrentChildren] = useState(children);
|
|
|
const [isOpen, setIsOpen] = useState(_isOpen);
|
|
|
const [isNewPageInputShown, setNewPageInputShown] = useState(false);
|
|
|
- const [isRenameInputShown, setRenameInputShown] = useState(false);
|
|
|
const [isCreating, setCreating] = useState(false);
|
|
|
|
|
|
const { data, mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
|
|
|
- const { trigger: mutateCurrentUserBookmarks } = useSWRMUTxCurrentUserBookmarks();
|
|
|
- const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(page._id ?? null);
|
|
|
|
|
|
// descendantCount
|
|
|
const { getDescCount } = usePageTreeDescCountMap();
|
|
|
@@ -138,83 +180,6 @@ const SimpleItem: FC<SimpleItemProps> = (props: SimpleItemProps) => {
|
|
|
}
|
|
|
}, [hasDescendants]);
|
|
|
|
|
|
- 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 onPressEnterForCreateHandler = async(inputText: string) => {
|
|
|
setNewPageInputShown(false);
|
|
|
const parentPath = pathUtils.addTrailingSlash(page.path as string);
|
|
|
@@ -252,33 +217,6 @@ const SimpleItem: FC<SimpleItemProps> = (props: SimpleItemProps) => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-
|
|
|
- /**
|
|
|
- * Users do not need to know if all pages have been renamed.
|
|
|
- * Make resuming rename operation appears to be working fine to allow users for a seamless operation.
|
|
|
- */
|
|
|
- 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'));
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- const pageTreeItemClickHandler = (e) => {
|
|
|
- e.preventDefault();
|
|
|
-
|
|
|
- if (page.path == null || page._id == null) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const link = pathUtils.returnPathForURL(page.path, page._id);
|
|
|
-
|
|
|
- router.push(link);
|
|
|
- };
|
|
|
-
|
|
|
// didMount
|
|
|
useEffect(() => {
|
|
|
if (hasChildren()) setIsOpen(true);
|
|
|
@@ -305,11 +243,6 @@ const SimpleItem: FC<SimpleItemProps> = (props: SimpleItemProps) => {
|
|
|
}
|
|
|
}, [data, isOpen, targetPathOrId]);
|
|
|
|
|
|
- // Rename process
|
|
|
- // Icon that draw attention from users for some actions
|
|
|
- const shouldShowAttentionIcon = page.processData != null ? shouldRecoverPagePaths(page.processData) : false;
|
|
|
- const pageName = nodePath.basename(page.path ?? '') || '/';
|
|
|
-
|
|
|
const ItemClassFixed = itemClass ?? SimpleItem;
|
|
|
|
|
|
const commonProps = {
|
|
|
@@ -322,6 +255,19 @@ const SimpleItem: FC<SimpleItemProps> = (props: SimpleItemProps) => {
|
|
|
onClickDeleteMenuItem,
|
|
|
};
|
|
|
|
|
|
+ const CustomComponent = props.customComponent;
|
|
|
+
|
|
|
+ const SimpleItemContent = CustomComponent ?? SimpleItemTool;
|
|
|
+
|
|
|
+ const SimpleItemContentProps = {
|
|
|
+ page,
|
|
|
+ onRenamed,
|
|
|
+ onClickDuplicateMenuItem,
|
|
|
+ onClickDeleteMenuItem,
|
|
|
+ isEnableActions,
|
|
|
+ isReadOnlyUser,
|
|
|
+ };
|
|
|
+
|
|
|
return (
|
|
|
<div
|
|
|
id={`pagetree-item-${page._id}`}
|
|
|
@@ -347,64 +293,8 @@ const SimpleItem: FC<SimpleItemProps> = (props: SimpleItemProps) => {
|
|
|
</button>
|
|
|
)}
|
|
|
</div>
|
|
|
- {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>
|
|
|
- )
|
|
|
- : (
|
|
|
- <>
|
|
|
- {shouldShowAttentionIcon && (
|
|
|
- <>
|
|
|
- <i id="path-recovery" className="fa fa-warning mr-2 text-warning"></i>
|
|
|
- <UncontrolledTooltip placement="top" target="path-recovery" fade={false}>
|
|
|
- {t('tooltip.operation.attention.rename')}
|
|
|
- </UncontrolledTooltip>
|
|
|
- </>
|
|
|
- )}
|
|
|
- {page != null && page.path != null && page._id != null && (
|
|
|
- <div className="grw-pagetree-title-anchor flex-grow-1">
|
|
|
- <p onClick={pageTreeItemClickHandler} className={`text-truncate m-auto ${page.isEmpty && 'grw-sidebar-text-muted'}`}>{pageName}</p>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </>
|
|
|
- )}
|
|
|
- {descendantCount > 0 && !isRenameInputShown && (
|
|
|
- <div className="grw-pagetree-count-wrapper">
|
|
|
- <CountBadge count={descendantCount} />
|
|
|
- </div>
|
|
|
- )}
|
|
|
- <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>
|
|
|
+
|
|
|
+ <SimpleItemContent {...SimpleItemContentProps}/>
|
|
|
|
|
|
{!pagePathUtils.isUsersTopPage(page.path ?? '') && (
|
|
|
<NotAvailableForGuest>
|