|
@@ -1,14 +1,16 @@
|
|
|
-import React, {
|
|
|
|
|
- useState, useCallback, useEffect, type JSX,
|
|
|
|
|
-} from 'react';
|
|
|
|
|
-
|
|
|
|
|
|
|
+import React, { type JSX, useCallback, useEffect, useState } from 'react';
|
|
|
import {
|
|
import {
|
|
|
- type IPageInfoExt, isIPageInfoForOperation, isIPageInfoForEmpty,
|
|
|
|
|
|
|
+ type IPageInfoExt,
|
|
|
|
|
+ isIPageInfoForEmpty,
|
|
|
|
|
+ isIPageInfoForOperation,
|
|
|
} from '@growi/core/dist/interfaces';
|
|
} from '@growi/core/dist/interfaces';
|
|
|
import { LoadingSpinner } from '@growi/ui/dist/components';
|
|
import { LoadingSpinner } from '@growi/ui/dist/components';
|
|
|
import { useTranslation } from 'next-i18next';
|
|
import { useTranslation } from 'next-i18next';
|
|
|
import {
|
|
import {
|
|
|
- Dropdown, DropdownMenu, DropdownToggle, DropdownItem,
|
|
|
|
|
|
|
+ Dropdown,
|
|
|
|
|
+ DropdownItem,
|
|
|
|
|
+ DropdownMenu,
|
|
|
|
|
+ DropdownToggle,
|
|
|
} from 'reactstrap';
|
|
} from 'reactstrap';
|
|
|
|
|
|
|
|
import { NotAvailableForGuest } from '~/client/components/NotAvailableForGuest';
|
|
import { NotAvailableForGuest } from '~/client/components/NotAvailableForGuest';
|
|
@@ -19,7 +21,6 @@ import { shouldRecoverPagePaths } from '~/utils/page-operation';
|
|
|
|
|
|
|
|
const logger = loggerFactory('growi:cli:PageItemControl');
|
|
const logger = loggerFactory('growi:cli:PageItemControl');
|
|
|
|
|
|
|
|
-
|
|
|
|
|
export const MenuItemType = {
|
|
export const MenuItemType = {
|
|
|
BOOKMARK: 'bookmark',
|
|
BOOKMARK: 'bookmark',
|
|
|
RENAME: 'rename',
|
|
RENAME: 'rename',
|
|
@@ -29,276 +30,355 @@ export const MenuItemType = {
|
|
|
PATH_RECOVERY: 'pathRecovery',
|
|
PATH_RECOVERY: 'pathRecovery',
|
|
|
SWITCH_CONTENT_WIDTH: 'switch_content_width',
|
|
SWITCH_CONTENT_WIDTH: 'switch_content_width',
|
|
|
} as const;
|
|
} as const;
|
|
|
-export type MenuItemType = typeof MenuItemType[keyof typeof MenuItemType];
|
|
|
|
|
|
|
+export type MenuItemType = (typeof MenuItemType)[keyof typeof MenuItemType];
|
|
|
|
|
|
|
|
export type ForceHideMenuItems = MenuItemType[];
|
|
export type ForceHideMenuItems = MenuItemType[];
|
|
|
|
|
|
|
|
export type AdditionalMenuItemsRendererProps = { pageInfo: IPageInfoExt };
|
|
export type AdditionalMenuItemsRendererProps = { pageInfo: IPageInfoExt };
|
|
|
|
|
|
|
|
type CommonProps = {
|
|
type CommonProps = {
|
|
|
- pageInfo?: IPageInfoExt,
|
|
|
|
|
- isEnableActions?: boolean,
|
|
|
|
|
- isReadOnlyUser?: boolean,
|
|
|
|
|
- forceHideMenuItems?: ForceHideMenuItems,
|
|
|
|
|
-
|
|
|
|
|
- onClickBookmarkMenuItem?: (pageId: string, newValue?: boolean) => Promise<void>,
|
|
|
|
|
- onClickRenameMenuItem?: (pageId: string, pageInfo: IPageInfoExt | undefined) => Promise<void> | void,
|
|
|
|
|
- onClickDuplicateMenuItem?: (pageId: string) => Promise<void> | void,
|
|
|
|
|
- onClickDeleteMenuItem?: (pageId: string, pageInfo: IPageInfoExt | undefined) => Promise<void> | void,
|
|
|
|
|
- onClickRevertMenuItem?: (pageId: string) => Promise<void> | void,
|
|
|
|
|
- onClickPathRecoveryMenuItem?: (pageId: string) => Promise<void> | void,
|
|
|
|
|
-
|
|
|
|
|
- additionalMenuItemOnTopRenderer?: React.FunctionComponent<AdditionalMenuItemsRendererProps>,
|
|
|
|
|
- additionalMenuItemRenderer?: React.FunctionComponent<AdditionalMenuItemsRendererProps>,
|
|
|
|
|
- isInstantRename?: boolean,
|
|
|
|
|
- alignEnd?: boolean,
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
|
|
+ pageInfo?: IPageInfoExt;
|
|
|
|
|
+ isEnableActions?: boolean;
|
|
|
|
|
+ isReadOnlyUser?: boolean;
|
|
|
|
|
+ forceHideMenuItems?: ForceHideMenuItems;
|
|
|
|
|
+
|
|
|
|
|
+ onClickBookmarkMenuItem?: (
|
|
|
|
|
+ pageId: string,
|
|
|
|
|
+ newValue?: boolean,
|
|
|
|
|
+ ) => Promise<void>;
|
|
|
|
|
+ onClickRenameMenuItem?: (
|
|
|
|
|
+ pageId: string,
|
|
|
|
|
+ pageInfo: IPageInfoExt | undefined,
|
|
|
|
|
+ ) => Promise<void> | void;
|
|
|
|
|
+ onClickDuplicateMenuItem?: (pageId: string) => Promise<void> | void;
|
|
|
|
|
+ onClickDeleteMenuItem?: (
|
|
|
|
|
+ pageId: string,
|
|
|
|
|
+ pageInfo: IPageInfoExt | undefined,
|
|
|
|
|
+ ) => Promise<void> | void;
|
|
|
|
|
+ onClickRevertMenuItem?: (pageId: string) => Promise<void> | void;
|
|
|
|
|
+ onClickPathRecoveryMenuItem?: (pageId: string) => Promise<void> | void;
|
|
|
|
|
+
|
|
|
|
|
+ additionalMenuItemOnTopRenderer?: React.FunctionComponent<AdditionalMenuItemsRendererProps>;
|
|
|
|
|
+ additionalMenuItemRenderer?: React.FunctionComponent<AdditionalMenuItemsRendererProps>;
|
|
|
|
|
+ isInstantRename?: boolean;
|
|
|
|
|
+ alignEnd?: boolean;
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
type DropdownMenuProps = CommonProps & {
|
|
type DropdownMenuProps = CommonProps & {
|
|
|
- pageId: string,
|
|
|
|
|
- isLoading?: boolean,
|
|
|
|
|
- isDataUnavailable?: boolean,
|
|
|
|
|
- operationProcessData?: IPageOperationProcessData,
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.Element => {
|
|
|
|
|
- const { t } = useTranslation('');
|
|
|
|
|
-
|
|
|
|
|
- const {
|
|
|
|
|
- pageId, isLoading, isDataUnavailable, pageInfo, isEnableActions, isReadOnlyUser, forceHideMenuItems, operationProcessData,
|
|
|
|
|
- onClickBookmarkMenuItem, onClickRenameMenuItem, onClickDuplicateMenuItem, onClickDeleteMenuItem,
|
|
|
|
|
- onClickRevertMenuItem, onClickPathRecoveryMenuItem,
|
|
|
|
|
- additionalMenuItemOnTopRenderer: AdditionalMenuItemsOnTop,
|
|
|
|
|
- additionalMenuItemRenderer: AdditionalMenuItems,
|
|
|
|
|
- isInstantRename, alignEnd,
|
|
|
|
|
- } = props;
|
|
|
|
|
-
|
|
|
|
|
- // eslint-disable-next-line react-hooks/rules-of-hooks
|
|
|
|
|
- const bookmarkItemClickedHandler = useCallback(async() => {
|
|
|
|
|
- if (onClickBookmarkMenuItem == null) return;
|
|
|
|
|
-
|
|
|
|
|
- if (!isIPageInfoForEmpty(pageInfo) && !isIPageInfoForOperation(pageInfo)) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- await onClickBookmarkMenuItem(pageId, !pageInfo.isBookmarked);
|
|
|
|
|
- }, [onClickBookmarkMenuItem, pageId, pageInfo]);
|
|
|
|
|
-
|
|
|
|
|
- // eslint-disable-next-line react-hooks/rules-of-hooks
|
|
|
|
|
- const renameItemClickedHandler = useCallback(async() => {
|
|
|
|
|
- if (onClickRenameMenuItem == null) return;
|
|
|
|
|
-
|
|
|
|
|
- if (!(isIPageInfoForEmpty(pageInfo) || isIPageInfoForOperation(pageInfo)) || !pageInfo?.isMovable) {
|
|
|
|
|
- logger.warn('This page could not be renamed.');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- await onClickRenameMenuItem(pageId, pageInfo);
|
|
|
|
|
- }, [onClickRenameMenuItem, pageId, pageInfo]);
|
|
|
|
|
-
|
|
|
|
|
- // eslint-disable-next-line react-hooks/rules-of-hooks
|
|
|
|
|
- const duplicateItemClickedHandler = useCallback(async() => {
|
|
|
|
|
- if (onClickDuplicateMenuItem == null) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- await onClickDuplicateMenuItem(pageId);
|
|
|
|
|
- }, [onClickDuplicateMenuItem, pageId]);
|
|
|
|
|
-
|
|
|
|
|
- const revertItemClickedHandler = useCallback(async() => {
|
|
|
|
|
- if (onClickRevertMenuItem == null) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- await onClickRevertMenuItem(pageId);
|
|
|
|
|
- }, [onClickRevertMenuItem, pageId]);
|
|
|
|
|
-
|
|
|
|
|
- // eslint-disable-next-line react-hooks/rules-of-hooks
|
|
|
|
|
- const deleteItemClickedHandler = useCallback(async() => {
|
|
|
|
|
- if (onClickDeleteMenuItem == null) return;
|
|
|
|
|
-
|
|
|
|
|
- if (!(isIPageInfoForEmpty(pageInfo) || isIPageInfoForOperation(pageInfo)) || !pageInfo?.isDeletable) {
|
|
|
|
|
- logger.warn('This page could not be deleted.');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- await onClickDeleteMenuItem(pageId, pageInfo);
|
|
|
|
|
- }, [onClickDeleteMenuItem, pageId, pageInfo]);
|
|
|
|
|
|
|
+ pageId: string;
|
|
|
|
|
+ isLoading?: boolean;
|
|
|
|
|
+ isDataUnavailable?: boolean;
|
|
|
|
|
+ operationProcessData?: IPageOperationProcessData;
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
- // eslint-disable-next-line react-hooks/rules-of-hooks
|
|
|
|
|
- const pathRecoveryItemClickedHandler = useCallback(async() => {
|
|
|
|
|
- if (onClickPathRecoveryMenuItem == null) {
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+const PageItemControlDropdownMenu = React.memo(
|
|
|
|
|
+ (props: DropdownMenuProps): JSX.Element => {
|
|
|
|
|
+ const { t } = useTranslation('');
|
|
|
|
|
+
|
|
|
|
|
+ const {
|
|
|
|
|
+ pageId,
|
|
|
|
|
+ isLoading,
|
|
|
|
|
+ isDataUnavailable,
|
|
|
|
|
+ pageInfo,
|
|
|
|
|
+ isEnableActions,
|
|
|
|
|
+ isReadOnlyUser,
|
|
|
|
|
+ forceHideMenuItems,
|
|
|
|
|
+ operationProcessData,
|
|
|
|
|
+ onClickBookmarkMenuItem,
|
|
|
|
|
+ onClickRenameMenuItem,
|
|
|
|
|
+ onClickDuplicateMenuItem,
|
|
|
|
|
+ onClickDeleteMenuItem,
|
|
|
|
|
+ onClickRevertMenuItem,
|
|
|
|
|
+ onClickPathRecoveryMenuItem,
|
|
|
|
|
+ additionalMenuItemOnTopRenderer: AdditionalMenuItemsOnTop,
|
|
|
|
|
+ additionalMenuItemRenderer: AdditionalMenuItems,
|
|
|
|
|
+ isInstantRename,
|
|
|
|
|
+ alignEnd,
|
|
|
|
|
+ } = props;
|
|
|
|
|
+
|
|
|
|
|
+ // eslint-disable-next-line react-hooks/rules-of-hooks
|
|
|
|
|
+ const bookmarkItemClickedHandler = useCallback(async () => {
|
|
|
|
|
+ if (onClickBookmarkMenuItem == null) return;
|
|
|
|
|
+
|
|
|
|
|
+ if (
|
|
|
|
|
+ !isIPageInfoForEmpty(pageInfo) &&
|
|
|
|
|
+ !isIPageInfoForOperation(pageInfo)
|
|
|
|
|
+ ) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await onClickBookmarkMenuItem(pageId, !pageInfo.isBookmarked);
|
|
|
|
|
+ }, [onClickBookmarkMenuItem, pageId, pageInfo]);
|
|
|
|
|
+
|
|
|
|
|
+ // eslint-disable-next-line react-hooks/rules-of-hooks
|
|
|
|
|
+ const renameItemClickedHandler = useCallback(async () => {
|
|
|
|
|
+ if (onClickRenameMenuItem == null) return;
|
|
|
|
|
+
|
|
|
|
|
+ if (
|
|
|
|
|
+ !(isIPageInfoForEmpty(pageInfo) || isIPageInfoForOperation(pageInfo)) ||
|
|
|
|
|
+ !pageInfo?.isMovable
|
|
|
|
|
+ ) {
|
|
|
|
|
+ logger.warn('This page could not be renamed.');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await onClickRenameMenuItem(pageId, pageInfo);
|
|
|
|
|
+ }, [onClickRenameMenuItem, pageId, pageInfo]);
|
|
|
|
|
+
|
|
|
|
|
+ // eslint-disable-next-line react-hooks/rules-of-hooks
|
|
|
|
|
+ const duplicateItemClickedHandler = useCallback(async () => {
|
|
|
|
|
+ if (onClickDuplicateMenuItem == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ await onClickDuplicateMenuItem(pageId);
|
|
|
|
|
+ }, [onClickDuplicateMenuItem, pageId]);
|
|
|
|
|
+
|
|
|
|
|
+ const revertItemClickedHandler = useCallback(async () => {
|
|
|
|
|
+ if (onClickRevertMenuItem == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ await onClickRevertMenuItem(pageId);
|
|
|
|
|
+ }, [onClickRevertMenuItem, pageId]);
|
|
|
|
|
+
|
|
|
|
|
+ // eslint-disable-next-line react-hooks/rules-of-hooks
|
|
|
|
|
+ const deleteItemClickedHandler = useCallback(async () => {
|
|
|
|
|
+ if (onClickDeleteMenuItem == null) return;
|
|
|
|
|
+
|
|
|
|
|
+ if (
|
|
|
|
|
+ !(isIPageInfoForEmpty(pageInfo) || isIPageInfoForOperation(pageInfo)) ||
|
|
|
|
|
+ !pageInfo?.isDeletable
|
|
|
|
|
+ ) {
|
|
|
|
|
+ logger.warn('This page could not be deleted.');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ await onClickDeleteMenuItem(pageId, pageInfo);
|
|
|
|
|
+ }, [onClickDeleteMenuItem, pageId, pageInfo]);
|
|
|
|
|
+
|
|
|
|
|
+ // eslint-disable-next-line react-hooks/rules-of-hooks
|
|
|
|
|
+ const pathRecoveryItemClickedHandler = useCallback(async () => {
|
|
|
|
|
+ if (onClickPathRecoveryMenuItem == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ await onClickPathRecoveryMenuItem(pageId);
|
|
|
|
|
+ }, [onClickPathRecoveryMenuItem, pageId]);
|
|
|
|
|
+
|
|
|
|
|
+ let contents = <></>;
|
|
|
|
|
+
|
|
|
|
|
+ if (isDataUnavailable) {
|
|
|
|
|
+ // Show message when data is not available (e.g., fetch error)
|
|
|
|
|
+ contents = (
|
|
|
|
|
+ <div className="text-warning text-center px-3">
|
|
|
|
|
+ <span className="material-symbols-outlined">error_outline</span> No
|
|
|
|
|
+ data available
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ } else if (isLoading) {
|
|
|
|
|
+ contents = (
|
|
|
|
|
+ <div className="text-muted text-center my-2">
|
|
|
|
|
+ <LoadingSpinner />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ } else if (pageId != null && pageInfo != null) {
|
|
|
|
|
+ const showDeviderBeforeAdditionalMenuItems =
|
|
|
|
|
+ (forceHideMenuItems?.length ?? 0) < 3;
|
|
|
|
|
+ const showDeviderBeforeDelete =
|
|
|
|
|
+ AdditionalMenuItems != null || showDeviderBeforeAdditionalMenuItems;
|
|
|
|
|
+
|
|
|
|
|
+ // PathRecovery
|
|
|
|
|
+ // Todo: It is wanted to find a better way to pass operationProcessData to PageItemControl
|
|
|
|
|
+ const shouldShowPathRecoveryButton =
|
|
|
|
|
+ operationProcessData != null
|
|
|
|
|
+ ? shouldRecoverPagePaths(operationProcessData)
|
|
|
|
|
+ : false;
|
|
|
|
|
+
|
|
|
|
|
+ contents = (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {!isEnableActions && (
|
|
|
|
|
+ <DropdownItem>
|
|
|
|
|
+ <p>{t('search_result.currently_not_implemented')}</p>
|
|
|
|
|
+ </DropdownItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {AdditionalMenuItemsOnTop && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <AdditionalMenuItemsOnTop pageInfo={pageInfo} />
|
|
|
|
|
+ <DropdownItem divider />
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Bookmark */}
|
|
|
|
|
+ {!forceHideMenuItems?.includes(MenuItemType.BOOKMARK) &&
|
|
|
|
|
+ isEnableActions &&
|
|
|
|
|
+ (isIPageInfoForEmpty(pageInfo) ||
|
|
|
|
|
+ isIPageInfoForOperation(pageInfo)) && (
|
|
|
|
|
+ <DropdownItem
|
|
|
|
|
+ onClick={bookmarkItemClickedHandler}
|
|
|
|
|
+ className="grw-page-control-dropdown-item"
|
|
|
|
|
+ data-testid={
|
|
|
|
|
+ pageInfo.isBookmarked
|
|
|
|
|
+ ? 'remove-bookmark-btn'
|
|
|
|
|
+ : 'add-bookmark-btn'
|
|
|
|
|
+ }
|
|
|
|
|
+ >
|
|
|
|
|
+ <span className="material-symbols-outlined grw-page-control-dropdown-icon">
|
|
|
|
|
+ bookmark
|
|
|
|
|
+ </span>
|
|
|
|
|
+ {pageInfo.isBookmarked
|
|
|
|
|
+ ? t('remove_bookmark')
|
|
|
|
|
+ : t('add_bookmark')}
|
|
|
|
|
+ </DropdownItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Move/Rename */}
|
|
|
|
|
+ {!forceHideMenuItems?.includes(MenuItemType.RENAME) &&
|
|
|
|
|
+ isEnableActions &&
|
|
|
|
|
+ !isReadOnlyUser &&
|
|
|
|
|
+ (isIPageInfoForEmpty(pageInfo) ||
|
|
|
|
|
+ isIPageInfoForOperation(pageInfo)) &&
|
|
|
|
|
+ pageInfo.isMovable && (
|
|
|
|
|
+ <DropdownItem
|
|
|
|
|
+ onClick={renameItemClickedHandler}
|
|
|
|
|
+ data-testid="rename-page-btn"
|
|
|
|
|
+ className="grw-page-control-dropdown-item"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">
|
|
|
|
|
+ redo
|
|
|
|
|
+ </span>
|
|
|
|
|
+ {t(isInstantRename ? 'Rename' : 'Move/Rename')}
|
|
|
|
|
+ </DropdownItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Duplicate */}
|
|
|
|
|
+ {!forceHideMenuItems?.includes(MenuItemType.DUPLICATE) &&
|
|
|
|
|
+ isEnableActions &&
|
|
|
|
|
+ !isReadOnlyUser && (
|
|
|
|
|
+ <DropdownItem
|
|
|
|
|
+ onClick={duplicateItemClickedHandler}
|
|
|
|
|
+ data-testid="open-page-duplicate-modal-btn"
|
|
|
|
|
+ className="grw-page-control-dropdown-item"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">
|
|
|
|
|
+ file_copy
|
|
|
|
|
+ </span>
|
|
|
|
|
+ {t('Duplicate')}
|
|
|
|
|
+ </DropdownItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Revert */}
|
|
|
|
|
+ {!forceHideMenuItems?.includes(MenuItemType.REVERT) &&
|
|
|
|
|
+ isEnableActions &&
|
|
|
|
|
+ !isReadOnlyUser &&
|
|
|
|
|
+ (isIPageInfoForEmpty(pageInfo) ||
|
|
|
|
|
+ isIPageInfoForOperation(pageInfo)) &&
|
|
|
|
|
+ pageInfo.isRevertible && (
|
|
|
|
|
+ <DropdownItem
|
|
|
|
|
+ onClick={revertItemClickedHandler}
|
|
|
|
|
+ className="grw-page-control-dropdown-item"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">
|
|
|
|
|
+ undo
|
|
|
|
|
+ </span>
|
|
|
|
|
+ {t('modal_putback.label.Put Back Page')}
|
|
|
|
|
+ </DropdownItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {AdditionalMenuItems && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {showDeviderBeforeAdditionalMenuItems && <DropdownItem divider />}
|
|
|
|
|
+ <AdditionalMenuItems pageInfo={pageInfo} />
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* PathRecovery */}
|
|
|
|
|
+ {!forceHideMenuItems?.includes(MenuItemType.PATH_RECOVERY) &&
|
|
|
|
|
+ isEnableActions &&
|
|
|
|
|
+ !isReadOnlyUser &&
|
|
|
|
|
+ shouldShowPathRecoveryButton && (
|
|
|
|
|
+ <DropdownItem
|
|
|
|
|
+ onClick={pathRecoveryItemClickedHandler}
|
|
|
|
|
+ className="grw-page-control-dropdown-item"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">
|
|
|
|
|
+ build
|
|
|
|
|
+ </span>
|
|
|
|
|
+ {t('PathRecovery')}
|
|
|
|
|
+ </DropdownItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* divider */}
|
|
|
|
|
+ {/* Delete */}
|
|
|
|
|
+ {!forceHideMenuItems?.includes(MenuItemType.DELETE) &&
|
|
|
|
|
+ isEnableActions &&
|
|
|
|
|
+ !isReadOnlyUser &&
|
|
|
|
|
+ (isIPageInfoForEmpty(pageInfo) ||
|
|
|
|
|
+ isIPageInfoForOperation(pageInfo)) &&
|
|
|
|
|
+ pageInfo.isDeletable && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {showDeviderBeforeDelete && <DropdownItem divider />}
|
|
|
|
|
+ <DropdownItem
|
|
|
|
|
+ className={`pt-2 grw-page-control-dropdown-item ${pageInfo.isDeletable ? 'text-danger' : ''}`}
|
|
|
|
|
+ disabled={!pageInfo.isDeletable}
|
|
|
|
|
+ onClick={deleteItemClickedHandler}
|
|
|
|
|
+ data-testid="open-page-delete-modal-btn"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">
|
|
|
|
|
+ delete
|
|
|
|
|
+ </span>
|
|
|
|
|
+ {t('Delete')}
|
|
|
|
|
+ </DropdownItem>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </>
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
- await onClickPathRecoveryMenuItem(pageId);
|
|
|
|
|
- }, [onClickPathRecoveryMenuItem, pageId]);
|
|
|
|
|
|
|
|
|
|
- let contents = <></>;
|
|
|
|
|
-
|
|
|
|
|
- if (isDataUnavailable) {
|
|
|
|
|
- // Show message when data is not available (e.g., fetch error)
|
|
|
|
|
- contents = (
|
|
|
|
|
- <div className="text-warning text-center px-3">
|
|
|
|
|
- <span className="material-symbols-outlined">error_outline</span> No data available
|
|
|
|
|
- </div>
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- else if (isLoading) {
|
|
|
|
|
- contents = (
|
|
|
|
|
- <div className="text-muted text-center my-2">
|
|
|
|
|
- <LoadingSpinner />
|
|
|
|
|
- </div>
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- else if (pageId != null && pageInfo != null) {
|
|
|
|
|
-
|
|
|
|
|
- const showDeviderBeforeAdditionalMenuItems = (forceHideMenuItems?.length ?? 0) < 3;
|
|
|
|
|
- const showDeviderBeforeDelete = AdditionalMenuItems != null || showDeviderBeforeAdditionalMenuItems;
|
|
|
|
|
-
|
|
|
|
|
- // PathRecovery
|
|
|
|
|
- // Todo: It is wanted to find a better way to pass operationProcessData to PageItemControl
|
|
|
|
|
- const shouldShowPathRecoveryButton = operationProcessData != null ? shouldRecoverPagePaths(operationProcessData) : false;
|
|
|
|
|
-
|
|
|
|
|
- contents = (
|
|
|
|
|
- <>
|
|
|
|
|
- { !isEnableActions && (
|
|
|
|
|
- <DropdownItem>
|
|
|
|
|
- <p>
|
|
|
|
|
- {t('search_result.currently_not_implemented')}
|
|
|
|
|
- </p>
|
|
|
|
|
- </DropdownItem>
|
|
|
|
|
- ) }
|
|
|
|
|
-
|
|
|
|
|
- { AdditionalMenuItemsOnTop && (
|
|
|
|
|
- <>
|
|
|
|
|
- <AdditionalMenuItemsOnTop pageInfo={pageInfo} />
|
|
|
|
|
- <DropdownItem divider />
|
|
|
|
|
- </>
|
|
|
|
|
- ) }
|
|
|
|
|
-
|
|
|
|
|
- {/* Bookmark */}
|
|
|
|
|
- { !forceHideMenuItems?.includes(MenuItemType.BOOKMARK) && isEnableActions && (isIPageInfoForEmpty(pageInfo) || isIPageInfoForOperation(pageInfo)) && (
|
|
|
|
|
- <DropdownItem
|
|
|
|
|
- onClick={bookmarkItemClickedHandler}
|
|
|
|
|
- className="grw-page-control-dropdown-item"
|
|
|
|
|
- data-testid={pageInfo.isBookmarked ? 'remove-bookmark-btn' : 'add-bookmark-btn'}
|
|
|
|
|
- >
|
|
|
|
|
- <span className="material-symbols-outlined grw-page-control-dropdown-icon">bookmark</span>
|
|
|
|
|
- { pageInfo.isBookmarked ? t('remove_bookmark') : t('add_bookmark') }
|
|
|
|
|
- </DropdownItem>
|
|
|
|
|
- ) }
|
|
|
|
|
-
|
|
|
|
|
- {/* Move/Rename */}
|
|
|
|
|
- { !forceHideMenuItems?.includes(MenuItemType.RENAME) && isEnableActions && !isReadOnlyUser
|
|
|
|
|
- && (isIPageInfoForEmpty(pageInfo) || isIPageInfoForOperation(pageInfo))
|
|
|
|
|
- && pageInfo.isMovable && (
|
|
|
|
|
- <DropdownItem
|
|
|
|
|
- onClick={renameItemClickedHandler}
|
|
|
|
|
- data-testid="rename-page-btn"
|
|
|
|
|
- className="grw-page-control-dropdown-item"
|
|
|
|
|
- >
|
|
|
|
|
- <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">redo</span>
|
|
|
|
|
- {t(isInstantRename ? 'Rename' : 'Move/Rename')}
|
|
|
|
|
- </DropdownItem>
|
|
|
|
|
- ) }
|
|
|
|
|
-
|
|
|
|
|
- {/* Duplicate */}
|
|
|
|
|
- { !forceHideMenuItems?.includes(MenuItemType.DUPLICATE) && isEnableActions && !isReadOnlyUser && (
|
|
|
|
|
- <DropdownItem
|
|
|
|
|
- onClick={duplicateItemClickedHandler}
|
|
|
|
|
- data-testid="open-page-duplicate-modal-btn"
|
|
|
|
|
- className="grw-page-control-dropdown-item"
|
|
|
|
|
- >
|
|
|
|
|
- <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">file_copy</span>
|
|
|
|
|
- {t('Duplicate')}
|
|
|
|
|
- </DropdownItem>
|
|
|
|
|
- ) }
|
|
|
|
|
-
|
|
|
|
|
- {/* Revert */}
|
|
|
|
|
- { !forceHideMenuItems?.includes(MenuItemType.REVERT) && isEnableActions && !isReadOnlyUser
|
|
|
|
|
- && (isIPageInfoForEmpty(pageInfo) || isIPageInfoForOperation(pageInfo))
|
|
|
|
|
- && pageInfo.isRevertible && (
|
|
|
|
|
- <DropdownItem
|
|
|
|
|
- onClick={revertItemClickedHandler}
|
|
|
|
|
- className="grw-page-control-dropdown-item"
|
|
|
|
|
- >
|
|
|
|
|
- <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">undo</span>
|
|
|
|
|
- {t('modal_putback.label.Put Back Page')}
|
|
|
|
|
- </DropdownItem>
|
|
|
|
|
- ) }
|
|
|
|
|
-
|
|
|
|
|
- { AdditionalMenuItems && (
|
|
|
|
|
- <>
|
|
|
|
|
- { showDeviderBeforeAdditionalMenuItems && <DropdownItem divider /> }
|
|
|
|
|
- <AdditionalMenuItems pageInfo={pageInfo} />
|
|
|
|
|
- </>
|
|
|
|
|
- ) }
|
|
|
|
|
-
|
|
|
|
|
- {/* PathRecovery */}
|
|
|
|
|
- { !forceHideMenuItems?.includes(MenuItemType.PATH_RECOVERY) && isEnableActions && !isReadOnlyUser && shouldShowPathRecoveryButton && (
|
|
|
|
|
- <DropdownItem
|
|
|
|
|
- onClick={pathRecoveryItemClickedHandler}
|
|
|
|
|
- className="grw-page-control-dropdown-item"
|
|
|
|
|
- >
|
|
|
|
|
- <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">build</span>
|
|
|
|
|
- {t('PathRecovery')}
|
|
|
|
|
- </DropdownItem>
|
|
|
|
|
- ) }
|
|
|
|
|
-
|
|
|
|
|
- {/* divider */}
|
|
|
|
|
- {/* Delete */}
|
|
|
|
|
- { !forceHideMenuItems?.includes(MenuItemType.DELETE) && isEnableActions && !isReadOnlyUser
|
|
|
|
|
- && (isIPageInfoForEmpty(pageInfo) || isIPageInfoForOperation(pageInfo))
|
|
|
|
|
- && pageInfo.isDeletable && (
|
|
|
|
|
- <>
|
|
|
|
|
- { showDeviderBeforeDelete && <DropdownItem divider /> }
|
|
|
|
|
- <DropdownItem
|
|
|
|
|
- className={`pt-2 grw-page-control-dropdown-item ${pageInfo.isDeletable ? 'text-danger' : ''}`}
|
|
|
|
|
- disabled={!pageInfo.isDeletable}
|
|
|
|
|
- onClick={deleteItemClickedHandler}
|
|
|
|
|
- data-testid="open-page-delete-modal-btn"
|
|
|
|
|
- >
|
|
|
|
|
- <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">delete</span>
|
|
|
|
|
- {t('Delete')}
|
|
|
|
|
- </DropdownItem>
|
|
|
|
|
- </>
|
|
|
|
|
- )}
|
|
|
|
|
- </>
|
|
|
|
|
|
|
+ return (
|
|
|
|
|
+ <DropdownMenu
|
|
|
|
|
+ className="d-print-none"
|
|
|
|
|
+ data-testid="page-item-control-menu"
|
|
|
|
|
+ end={alignEnd}
|
|
|
|
|
+ container="body"
|
|
|
|
|
+ persist={!!alignEnd}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ zIndex: 1055,
|
|
|
|
|
+ }} /* make it larger than $zindex-modal of bootstrap */
|
|
|
|
|
+ >
|
|
|
|
|
+ {contents}
|
|
|
|
|
+ </DropdownMenu>
|
|
|
);
|
|
);
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return (
|
|
|
|
|
- <DropdownMenu
|
|
|
|
|
- className="d-print-none"
|
|
|
|
|
- data-testid="page-item-control-menu"
|
|
|
|
|
- end={alignEnd}
|
|
|
|
|
- container="body"
|
|
|
|
|
- persist={!!alignEnd}
|
|
|
|
|
- style={{ zIndex: 1055 }} /* make it larger than $zindex-modal of bootstrap */
|
|
|
|
|
- >
|
|
|
|
|
- {contents}
|
|
|
|
|
- </DropdownMenu>
|
|
|
|
|
- );
|
|
|
|
|
-});
|
|
|
|
|
|
|
+ },
|
|
|
|
|
+);
|
|
|
|
|
|
|
|
PageItemControlDropdownMenu.displayName = 'PageItemControl';
|
|
PageItemControlDropdownMenu.displayName = 'PageItemControl';
|
|
|
|
|
|
|
|
-
|
|
|
|
|
type PageItemControlSubstanceProps = CommonProps & {
|
|
type PageItemControlSubstanceProps = CommonProps & {
|
|
|
- pageId: string,
|
|
|
|
|
- children?: React.ReactNode,
|
|
|
|
|
- operationProcessData?: IPageOperationProcessData,
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-export const PageItemControlSubstance = (props: PageItemControlSubstanceProps): JSX.Element => {
|
|
|
|
|
|
|
+ pageId: string;
|
|
|
|
|
+ children?: React.ReactNode;
|
|
|
|
|
+ operationProcessData?: IPageOperationProcessData;
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
|
|
+export const PageItemControlSubstance = (
|
|
|
|
|
+ props: PageItemControlSubstanceProps,
|
|
|
|
|
+): JSX.Element => {
|
|
|
const {
|
|
const {
|
|
|
- pageId, pageInfo: presetPageInfo, children, onClickBookmarkMenuItem, onClickRenameMenuItem,
|
|
|
|
|
- onClickDuplicateMenuItem, onClickDeleteMenuItem, onClickPathRecoveryMenuItem,
|
|
|
|
|
|
|
+ pageId,
|
|
|
|
|
+ pageInfo: presetPageInfo,
|
|
|
|
|
+ children,
|
|
|
|
|
+ onClickBookmarkMenuItem,
|
|
|
|
|
+ onClickRenameMenuItem,
|
|
|
|
|
+ onClickDuplicateMenuItem,
|
|
|
|
|
+ onClickDeleteMenuItem,
|
|
|
|
|
+ onClickPathRecoveryMenuItem,
|
|
|
} = props;
|
|
} = props;
|
|
|
|
|
|
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
const [shouldFetch, setShouldFetch] = useState(false);
|
|
const [shouldFetch, setShouldFetch] = useState(false);
|
|
|
|
|
|
|
|
- const { data: fetchedPageInfo, error: fetchError, mutate: mutatePageInfo } = useSWRxPageInfo(shouldFetch ? pageId : null);
|
|
|
|
|
|
|
+ const {
|
|
|
|
|
+ data: fetchedPageInfo,
|
|
|
|
|
+ error: fetchError,
|
|
|
|
|
+ mutate: mutatePageInfo,
|
|
|
|
|
+ } = useSWRxPageInfo(shouldFetch ? pageId : null);
|
|
|
|
|
|
|
|
// update shouldFetch (and will never be false)
|
|
// update shouldFetch (and will never be false)
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
@@ -311,42 +391,47 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
|
|
|
}, [isOpen, presetPageInfo, shouldFetch]);
|
|
}, [isOpen, presetPageInfo, shouldFetch]);
|
|
|
|
|
|
|
|
// mutate after handle event
|
|
// mutate after handle event
|
|
|
- const bookmarkMenuItemClickHandler = useCallback(async(_pageId: string, _newValue: boolean) => {
|
|
|
|
|
- if (onClickBookmarkMenuItem != null) {
|
|
|
|
|
- await onClickBookmarkMenuItem(_pageId, _newValue);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (shouldFetch) {
|
|
|
|
|
- mutatePageInfo();
|
|
|
|
|
- }
|
|
|
|
|
- }, [mutatePageInfo, onClickBookmarkMenuItem, shouldFetch]);
|
|
|
|
|
|
|
+ const bookmarkMenuItemClickHandler = useCallback(
|
|
|
|
|
+ async (_pageId: string, _newValue: boolean) => {
|
|
|
|
|
+ if (onClickBookmarkMenuItem != null) {
|
|
|
|
|
+ await onClickBookmarkMenuItem(_pageId, _newValue);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (shouldFetch) {
|
|
|
|
|
+ mutatePageInfo();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [mutatePageInfo, onClickBookmarkMenuItem, shouldFetch],
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
// isLoading should be true only when fetching is in progress (data and error are both undefined)
|
|
// isLoading should be true only when fetching is in progress (data and error are both undefined)
|
|
|
- const isLoading = shouldFetch && fetchedPageInfo == null && fetchError == null;
|
|
|
|
|
- const isDataUnavailable = !isLoading && fetchedPageInfo == null && presetPageInfo == null;
|
|
|
|
|
|
|
+ const isLoading =
|
|
|
|
|
+ shouldFetch && fetchedPageInfo == null && fetchError == null;
|
|
|
|
|
+ const isDataUnavailable =
|
|
|
|
|
+ !isLoading && fetchedPageInfo == null && presetPageInfo == null;
|
|
|
|
|
|
|
|
- const renameMenuItemClickHandler = useCallback(async() => {
|
|
|
|
|
|
|
+ const renameMenuItemClickHandler = useCallback(async () => {
|
|
|
if (onClickRenameMenuItem == null) {
|
|
if (onClickRenameMenuItem == null) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
await onClickRenameMenuItem(pageId, fetchedPageInfo ?? presetPageInfo);
|
|
await onClickRenameMenuItem(pageId, fetchedPageInfo ?? presetPageInfo);
|
|
|
}, [onClickRenameMenuItem, pageId, fetchedPageInfo, presetPageInfo]);
|
|
}, [onClickRenameMenuItem, pageId, fetchedPageInfo, presetPageInfo]);
|
|
|
|
|
|
|
|
- const duplicateMenuItemClickHandler = useCallback(async() => {
|
|
|
|
|
|
|
+ const duplicateMenuItemClickHandler = useCallback(async () => {
|
|
|
if (onClickDuplicateMenuItem == null) {
|
|
if (onClickDuplicateMenuItem == null) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
await onClickDuplicateMenuItem(pageId);
|
|
await onClickDuplicateMenuItem(pageId);
|
|
|
}, [onClickDuplicateMenuItem, pageId]);
|
|
}, [onClickDuplicateMenuItem, pageId]);
|
|
|
|
|
|
|
|
- const deleteMenuItemClickHandler = useCallback(async() => {
|
|
|
|
|
|
|
+ const deleteMenuItemClickHandler = useCallback(async () => {
|
|
|
if (onClickDeleteMenuItem == null) {
|
|
if (onClickDeleteMenuItem == null) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
await onClickDeleteMenuItem(pageId, fetchedPageInfo ?? presetPageInfo);
|
|
await onClickDeleteMenuItem(pageId, fetchedPageInfo ?? presetPageInfo);
|
|
|
}, [onClickDeleteMenuItem, pageId, fetchedPageInfo, presetPageInfo]);
|
|
}, [onClickDeleteMenuItem, pageId, fetchedPageInfo, presetPageInfo]);
|
|
|
|
|
|
|
|
- const pathRecoveryMenuItemClickHandler = useCallback(async() => {
|
|
|
|
|
|
|
+ const pathRecoveryMenuItemClickHandler = useCallback(async () => {
|
|
|
if (onClickPathRecoveryMenuItem == null) {
|
|
if (onClickPathRecoveryMenuItem == null) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
@@ -355,14 +440,23 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<NotAvailableForGuest>
|
|
<NotAvailableForGuest>
|
|
|
- <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)} className="grw-page-item-control" data-testid="open-page-item-control-btn">
|
|
|
|
|
- { children ?? (
|
|
|
|
|
- <DropdownToggle role="button" color="transparent" className="border-0 rounded btn-page-item-control d-flex align-items-center justify-content-center">
|
|
|
|
|
|
|
+ <Dropdown
|
|
|
|
|
+ isOpen={isOpen}
|
|
|
|
|
+ toggle={() => setIsOpen(!isOpen)}
|
|
|
|
|
+ className="grw-page-item-control"
|
|
|
|
|
+ data-testid="open-page-item-control-btn"
|
|
|
|
|
+ >
|
|
|
|
|
+ {children ?? (
|
|
|
|
|
+ <DropdownToggle
|
|
|
|
|
+ role="button"
|
|
|
|
|
+ color="transparent"
|
|
|
|
|
+ className="border-0 rounded btn-page-item-control d-flex align-items-center justify-content-center"
|
|
|
|
|
+ >
|
|
|
<span className="material-symbols-outlined">more_vert</span>
|
|
<span className="material-symbols-outlined">more_vert</span>
|
|
|
</DropdownToggle>
|
|
</DropdownToggle>
|
|
|
- ) }
|
|
|
|
|
|
|
+ )}
|
|
|
|
|
|
|
|
- { isOpen && (
|
|
|
|
|
|
|
+ {isOpen && (
|
|
|
<PageItemControlDropdownMenu
|
|
<PageItemControlDropdownMenu
|
|
|
{...props}
|
|
{...props}
|
|
|
isLoading={isLoading}
|
|
isLoading={isLoading}
|
|
@@ -374,21 +468,17 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
|
|
|
onClickDeleteMenuItem={deleteMenuItemClickHandler}
|
|
onClickDeleteMenuItem={deleteMenuItemClickHandler}
|
|
|
onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
|
|
onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
|
|
|
/>
|
|
/>
|
|
|
- ) }
|
|
|
|
|
|
|
+ )}
|
|
|
</Dropdown>
|
|
</Dropdown>
|
|
|
-
|
|
|
|
|
</NotAvailableForGuest>
|
|
</NotAvailableForGuest>
|
|
|
-
|
|
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-
|
|
|
|
|
export type PageItemControlProps = CommonProps & {
|
|
export type PageItemControlProps = CommonProps & {
|
|
|
- pageId?: string,
|
|
|
|
|
- children?: React.ReactNode,
|
|
|
|
|
- operationProcessData?: IPageOperationProcessData,
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ pageId?: string;
|
|
|
|
|
+ children?: React.ReactNode;
|
|
|
|
|
+ operationProcessData?: IPageOperationProcessData;
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
export const PageItemControl = (props: PageItemControlProps): JSX.Element => {
|
|
export const PageItemControl = (props: PageItemControlProps): JSX.Element => {
|
|
|
const { pageId } = props;
|
|
const { pageId } = props;
|