|
|
@@ -1,4 +1,4 @@
|
|
|
-import React, { FC, useState, useCallback } from 'react';
|
|
|
+import React, { useState, useCallback } from 'react';
|
|
|
import {
|
|
|
Dropdown, DropdownMenu, DropdownToggle, DropdownItem,
|
|
|
} from 'reactstrap';
|
|
|
@@ -6,132 +6,203 @@ import {
|
|
|
import toastr from 'toastr';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
|
|
-import { IPageHasId } from '~/interfaces/page';
|
|
|
-import { apiv3Put } from '~/client/util/apiv3-client';
|
|
|
-import { toastError } from '~/client/util/apiNotification';
|
|
|
-import { useSWRBookmarkInfo } from '~/stores/bookmark';
|
|
|
-
|
|
|
-type PageItemControlProps = {
|
|
|
- page: Partial<IPageHasId>
|
|
|
- isEnableActions?: boolean
|
|
|
- isDeletable: boolean
|
|
|
- onClickDeleteButtonHandler?: (pageId: string) => void
|
|
|
- onClickRenameButtonHandler?: (pageId: string) => void
|
|
|
+import loggerFactory from '~/utils/logger';
|
|
|
+
|
|
|
+import {
|
|
|
+ IPageInfo, IPageInfoCommon, isExistPageInfo,
|
|
|
+} from '~/interfaces/page';
|
|
|
+import { useSWRxPageInfo } from '~/stores/page';
|
|
|
+
|
|
|
+const logger = loggerFactory('growi:cli:PageItemControl');
|
|
|
+
|
|
|
+
|
|
|
+export type AdditionalMenuItemsRendererProps = { pageInfo: IPageInfoCommon | IPageInfo };
|
|
|
+
|
|
|
+type CommonProps = {
|
|
|
+ pageInfo?: IPageInfoCommon | IPageInfo,
|
|
|
+ isEnableActions?: boolean,
|
|
|
+ hideBookmarkMenuItem?: boolean,
|
|
|
+ onClickBookmarkMenuItem?: (pageId: string, newValue?: boolean) => Promise<void>,
|
|
|
+ onClickRenameMenuItem?: (pageId: string) => void,
|
|
|
+ onClickDeleteMenuItem?: (pageId: string) => void,
|
|
|
+
|
|
|
+ additionalMenuItemRenderer?: React.FunctionComponent<AdditionalMenuItemsRendererProps>,
|
|
|
}
|
|
|
|
|
|
-const PageItemControl: FC<PageItemControlProps> = (props: PageItemControlProps) => {
|
|
|
+
|
|
|
+type DropdownMenuProps = CommonProps & {
|
|
|
+ pageId: string,
|
|
|
+}
|
|
|
+
|
|
|
+const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.Element => {
|
|
|
+ const { t } = useTranslation('');
|
|
|
|
|
|
const {
|
|
|
- page, isEnableActions, onClickDeleteButtonHandler, isDeletable, onClickRenameButtonHandler,
|
|
|
+ pageId, pageInfo, isEnableActions, hideBookmarkMenuItem,
|
|
|
+ onClickBookmarkMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem,
|
|
|
+ additionalMenuItemRenderer: AdditionalMenuItems,
|
|
|
} = props;
|
|
|
- const { t } = useTranslation('');
|
|
|
- const [isOpen, setIsOpen] = useState(false);
|
|
|
- const { data: bookmarkInfo, error: bookmarkInfoError, mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(page._id, isOpen);
|
|
|
|
|
|
- const deleteButtonClickedHandler = useCallback(() => {
|
|
|
- if (onClickDeleteButtonHandler != null && page._id != null) {
|
|
|
- onClickDeleteButtonHandler(page._id);
|
|
|
- }
|
|
|
- }, [onClickDeleteButtonHandler, page._id]);
|
|
|
|
|
|
- const renameButtonClickedHandler = useCallback(() => {
|
|
|
- if (onClickRenameButtonHandler != null && page._id != null) {
|
|
|
- onClickRenameButtonHandler(page._id);
|
|
|
+ // eslint-disable-next-line react-hooks/rules-of-hooks
|
|
|
+ const bookmarkItemClickedHandler = useCallback(async() => {
|
|
|
+ if (!isExistPageInfo(pageInfo) || onClickBookmarkMenuItem == null) {
|
|
|
+ return;
|
|
|
}
|
|
|
- }, [onClickRenameButtonHandler, page._id]);
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ await onClickRenameMenuItem(pageId);
|
|
|
+ }, [onClickRenameMenuItem, pageId]);
|
|
|
|
|
|
- const bookmarkToggleHandler = (async() => {
|
|
|
- try {
|
|
|
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
- await apiv3Put('/bookmarks', { pageId: page._id, bool: !bookmarkInfo!.isBookmarked });
|
|
|
- mutateBookmarkInfo();
|
|
|
+ // eslint-disable-next-line react-hooks/rules-of-hooks
|
|
|
+ const deleteItemClickedHandler = useCallback(async() => {
|
|
|
+ if (!isExistPageInfo(pageInfo) || onClickDeleteMenuItem == null) {
|
|
|
+ return;
|
|
|
}
|
|
|
- catch (err) {
|
|
|
- toastError(err);
|
|
|
+ if (!pageInfo.isDeletable) {
|
|
|
+ logger.warn('This page could not be deleted.');
|
|
|
+ return;
|
|
|
}
|
|
|
- });
|
|
|
+ await onClickDeleteMenuItem(pageId);
|
|
|
+ }, [onClickDeleteMenuItem, pageId, pageInfo]);
|
|
|
+
|
|
|
+ if (pageId == null || pageInfo == null) {
|
|
|
+ return <></>;
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <DropdownMenu positionFixed modifiers={{ preventOverflow: { boundariesElement: undefined } }}>
|
|
|
+
|
|
|
+ { !isEnableActions && (
|
|
|
+ <DropdownItem>
|
|
|
+ <p>
|
|
|
+ {t('search_result.currently_not_implemented')}
|
|
|
+ </p>
|
|
|
+ </DropdownItem>
|
|
|
+ ) }
|
|
|
+
|
|
|
+ {/* Bookmark */}
|
|
|
+ { !hideBookmarkMenuItem && isExistPageInfo(pageInfo) && isEnableActions && (
|
|
|
+ <DropdownItem onClick={bookmarkItemClickedHandler}>
|
|
|
+ <i className="fa fa-fw fa-bookmark-o"></i>
|
|
|
+ { pageInfo.isBookmarked ? t('remove_bookmark') : t('add_bookmark') }
|
|
|
+ </DropdownItem>
|
|
|
+ ) }
|
|
|
+
|
|
|
+ {/* Duplicate */}
|
|
|
+ { isExistPageInfo(pageInfo) && isEnableActions && (
|
|
|
+ <DropdownItem onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
|
|
|
+ <i className="icon-fw icon-docs"></i>
|
|
|
+ {t('Duplicate')}
|
|
|
+ </DropdownItem>
|
|
|
+ ) }
|
|
|
+
|
|
|
+ {/* Move/Rename */}
|
|
|
+ { isEnableActions && pageInfo.isMovable && (
|
|
|
+ <DropdownItem onClick={renameItemClickedHandler}>
|
|
|
+ <i className="icon-fw icon-action-redo"></i>
|
|
|
+ {t('Move/Rename')}
|
|
|
+ </DropdownItem>
|
|
|
+ ) }
|
|
|
+
|
|
|
+ { AdditionalMenuItems && <AdditionalMenuItems pageInfo={pageInfo} /> }
|
|
|
+
|
|
|
+ {/* divider */}
|
|
|
+ {/* Delete */}
|
|
|
+ { isExistPageInfo(pageInfo) && isEnableActions && pageInfo.isMovable && (
|
|
|
+ <>
|
|
|
+ <DropdownItem divider />
|
|
|
+ <DropdownItem
|
|
|
+ className={`pt-2 ${pageInfo.isDeletable ? 'text-danger' : ''}`}
|
|
|
+ disabled={!pageInfo.isDeletable}
|
|
|
+ onClick={deleteItemClickedHandler}
|
|
|
+ >
|
|
|
+ <i className="icon-fw icon-trash"></i>
|
|
|
+ {t('Delete')}
|
|
|
+ </DropdownItem>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </DropdownMenu>
|
|
|
+ );
|
|
|
+});
|
|
|
|
|
|
- const renderBookmarkText = () => {
|
|
|
- if (bookmarkInfoError != null || bookmarkInfo == null) {
|
|
|
- return '';
|
|
|
- }
|
|
|
- return bookmarkInfo.isBookmarked ? t('remove_bookmark') : t('add_bookmark');
|
|
|
- };
|
|
|
|
|
|
+type PageItemControlSubstanceProps = CommonProps & {
|
|
|
+ pageId: string,
|
|
|
+ fetchOnOpen?: boolean,
|
|
|
+}
|
|
|
|
|
|
- const dropdownToggle = () => {
|
|
|
- setIsOpen(!isOpen);
|
|
|
- };
|
|
|
+export const PageItemControlSubstance = (props: PageItemControlSubstanceProps): JSX.Element => {
|
|
|
|
|
|
+ const {
|
|
|
+ pageId, pageInfo: presetPageInfo, fetchOnOpen,
|
|
|
+ onClickBookmarkMenuItem,
|
|
|
+ } = props;
|
|
|
+
|
|
|
+ const [isOpen, setIsOpen] = useState(false);
|
|
|
+
|
|
|
+ const shouldFetch = presetPageInfo == null && (!fetchOnOpen || isOpen);
|
|
|
+ const { data: fetchedPageInfo, mutate: mutatePageInfo } = useSWRxPageInfo(shouldFetch ? pageId : null);
|
|
|
+
|
|
|
+ // 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]);
|
|
|
|
|
|
return (
|
|
|
- <Dropdown isOpen={isOpen} toggle={dropdownToggle}>
|
|
|
- <DropdownToggle color="transparent" className="btn-link border-0 rounded grw-btn-page-management p-0">
|
|
|
+ <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)}>
|
|
|
+ <DropdownToggle color="transparent" className="border-0 rounded grw-btn-page-management p-0">
|
|
|
<i className="icon-options fa fa-rotate-90 text-muted p-1"></i>
|
|
|
</DropdownToggle>
|
|
|
- <DropdownMenu positionFixed modifiers={{ preventOverflow: { boundariesElement: undefined } }}>
|
|
|
-
|
|
|
- {/* TODO: if there is the following button in XD add it here
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- className="btn btn-link p-0"
|
|
|
- value={page.path}
|
|
|
- onClick={(e) => {
|
|
|
- window.location.href = e.currentTarget.value;
|
|
|
- }}
|
|
|
- >
|
|
|
- <i className="icon-login" />
|
|
|
- </button>
|
|
|
- */}
|
|
|
-
|
|
|
- {/*
|
|
|
- TODO: add function to the following buttons like using modal or others
|
|
|
- ref: https://estoc.weseek.co.jp/redmine/issues/79026
|
|
|
- */}
|
|
|
-
|
|
|
- {/* TODO: show dropdown when permalink section is implemented */}
|
|
|
-
|
|
|
- {!isEnableActions && (
|
|
|
- <DropdownItem>
|
|
|
- <p>
|
|
|
- {t('search_result.currently_not_implemented')}
|
|
|
- </p>
|
|
|
- </DropdownItem>
|
|
|
- )}
|
|
|
- {isEnableActions && (
|
|
|
- <DropdownItem onClick={bookmarkToggleHandler}>
|
|
|
- <i className="fa fa-fw fa-bookmark-o"></i>
|
|
|
- {renderBookmarkText()}
|
|
|
- </DropdownItem>
|
|
|
- )}
|
|
|
- {isEnableActions && (
|
|
|
- <DropdownItem onClick={() => toastr.warning(t('search_result.currently_not_implemented'))}>
|
|
|
- <i className="icon-fw icon-docs"></i>
|
|
|
- {t('Duplicate')}
|
|
|
- </DropdownItem>
|
|
|
- )}
|
|
|
- {isEnableActions && (
|
|
|
- <DropdownItem onClick={renameButtonClickedHandler}>
|
|
|
- <i className="icon-fw icon-action-redo"></i>
|
|
|
- {t('Move/Rename')}
|
|
|
- </DropdownItem>
|
|
|
- )}
|
|
|
- {isDeletable && isEnableActions && (
|
|
|
- <>
|
|
|
- <DropdownItem divider />
|
|
|
- <DropdownItem className="text-danger pt-2" onClick={deleteButtonClickedHandler}>
|
|
|
- <i className="icon-fw icon-trash"></i>
|
|
|
- {t('Delete')}
|
|
|
- </DropdownItem>
|
|
|
- </>
|
|
|
- )}
|
|
|
- </DropdownMenu>
|
|
|
-
|
|
|
|
|
|
+ <PageItemControlDropdownMenu
|
|
|
+ {...props}
|
|
|
+ pageInfo={presetPageInfo ?? fetchedPageInfo}
|
|
|
+ onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
|
|
|
+ />
|
|
|
</Dropdown>
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
-export default PageItemControl;
|
|
|
+
|
|
|
+type PageItemControlProps = CommonProps & {
|
|
|
+ pageId?: string,
|
|
|
+}
|
|
|
+
|
|
|
+export const PageItemControl = (props: PageItemControlProps): JSX.Element => {
|
|
|
+ const { pageId } = props;
|
|
|
+
|
|
|
+ if (pageId == null) {
|
|
|
+ return <></>;
|
|
|
+ }
|
|
|
+
|
|
|
+ return <PageItemControlSubstance pageId={pageId} {...props} />;
|
|
|
+};
|
|
|
+
|
|
|
+
|
|
|
+type AsyncPageItemControlProps = CommonProps & {
|
|
|
+ pageId?: string,
|
|
|
+}
|
|
|
+
|
|
|
+export const AsyncPageItemControl = (props: AsyncPageItemControlProps): JSX.Element => {
|
|
|
+ const { pageId } = props;
|
|
|
+
|
|
|
+ if (pageId == null) {
|
|
|
+ return <></>;
|
|
|
+ }
|
|
|
+
|
|
|
+ return <PageItemControlSubstance pageId={pageId} fetchOnOpen {...props} />;
|
|
|
+};
|