import React, {
type JSX,
memo,
useCallback,
useEffect,
useId,
useMemo,
useRef,
} from 'react';
import type {
IPageInfo,
IPageToDeleteWithMeta,
IPageToRenameWithMeta,
} from '@growi/core';
import {
isIPageInfoForEmpty,
isIPageInfoForEntity,
isIPageInfoForOperation,
} from '@growi/core';
import { useRect } from '@growi/ui/dist/utils';
import { useTranslation } from 'next-i18next';
import { DropdownItem } from 'reactstrap';
import { toggleLike, toggleSubscribe } from '~/client/services/page-operation';
import { toastError } from '~/client/util/toastr';
import OpenDefaultAiAssistantButton from '~/features/openai/client/components/AiAssistant/OpenDefaultAiAssistantButton';
import {
useIsGuestUser,
useIsReadOnlyUser,
useIsSearchPage,
} from '~/states/context';
import { useCurrentPagePath } from '~/states/page';
import { useDeviceLargerThanMd } from '~/states/ui/device';
import { EditorMode, useEditorMode } from '~/states/ui/editor';
import type { IPageForPageDuplicateModal } from '~/states/ui/modal/page-duplicate';
import { useTagEditModalActions } from '~/states/ui/modal/tag-edit';
import { useSetPageControlsX } from '~/states/ui/page';
import loggerFactory from '~/utils/logger';
import { useSWRxPageInfo, useSWRxTagsInfo } from '../../../stores/page';
import { useSWRxUsersList } from '../../../stores/user';
import type {
AdditionalMenuItemsRendererProps,
ForceHideMenuItems,
} from '../Common/Dropdown/PageItemControl';
import {
MenuItemType,
PageItemControl,
} from '../Common/Dropdown/PageItemControl';
import { BookmarkButtons } from './BookmarkButtons';
import LikeButtons from './LikeButtons';
import SearchButton from './SearchButton';
import SeenUserInfo from './SeenUserInfo';
import SubscribeButton from './SubscribeButton';
import styles from './PageControls.module.scss';
const logger = loggerFactory('growi:components/PageControls');
type TagsProps = {
onClickEditTagsButton: () => void;
};
const Tags = (props: TagsProps): JSX.Element => {
const { onClickEditTagsButton } = props;
const { t } = useTranslation();
return (
);
};
type WideViewMenuItemProps = AdditionalMenuItemsRendererProps & {
onClick: () => void;
expandContentWidth?: boolean;
};
const WideViewMenuItem = (props: WideViewMenuItemProps): JSX.Element => {
const { t } = useTranslation();
const { onClick, expandContentWidth } = props;
const wideViewId = useId();
return (
{}}
/>
);
};
type CommonProps = {
pageId?: string;
shareLinkId?: string | null;
revisionId?: string | null;
path?: string | null;
expandContentWidth?: boolean;
disableSeenUserInfoPopover?: boolean;
hideSubControls?: boolean;
showPageControlDropdown?: boolean;
forceHideMenuItems?: ForceHideMenuItems;
additionalMenuItemRenderer?: React.FunctionComponent;
onClickDuplicateMenuItem?: (
pageToDuplicate: IPageForPageDuplicateModal,
) => void;
onClickRenameMenuItem?: (pageToRename: IPageToRenameWithMeta) => void;
onClickDeleteMenuItem?: (pageToDelete: IPageToDeleteWithMeta) => void;
onClickSwitchContentWidth?: (pageId: string, value: boolean) => void;
};
type PageControlsSubstanceProps = CommonProps & {
pageInfo: IPageInfo | undefined;
onClickEditTagsButton: () => void;
};
const PageControlsSubstance = (
props: PageControlsSubstanceProps,
): JSX.Element => {
const {
pageInfo,
pageId,
revisionId,
path,
shareLinkId,
expandContentWidth,
disableSeenUserInfoPopover,
hideSubControls,
showPageControlDropdown,
forceHideMenuItems,
additionalMenuItemRenderer,
onClickEditTagsButton,
onClickDuplicateMenuItem,
onClickRenameMenuItem,
onClickDeleteMenuItem,
onClickSwitchContentWidth,
} = props;
const isGuestUser = useIsGuestUser();
const isReadOnlyUser = useIsReadOnlyUser();
const { editorMode } = useEditorMode();
const [isDeviceLargerThanMd] = useDeviceLargerThanMd();
const isSearchPage = useIsSearchPage();
const currentPagePath = useCurrentPagePath();
const { mutate: mutatePageInfo } = useSWRxPageInfo(pageId, shareLinkId);
const likerIds = isIPageInfoForEntity(pageInfo)
? (pageInfo.likerIds ?? []).slice(0, 15)
: [];
const seenUserIds = isIPageInfoForEntity(pageInfo)
? (pageInfo.seenUserIds ?? []).slice(0, 15)
: [];
const setPageControlsX = useSetPageControlsX();
const pageControlsRef = useRef(null);
const [pageControlsRect] = useRect(pageControlsRef);
useEffect(() => {
if (pageControlsRect?.x == null) {
return;
}
setPageControlsX(pageControlsRect.x);
}, [pageControlsRect?.x, setPageControlsX]);
// Put in a mixture of seenUserIds and likerIds data to make the cache work
const { data: usersList } = useSWRxUsersList([...likerIds, ...seenUserIds]);
const likers =
usersList != null
? usersList.filter(({ _id }) => likerIds.includes(_id)).slice(0, 15)
: [];
const seenUsers =
usersList != null
? usersList.filter(({ _id }) => seenUserIds.includes(_id)).slice(0, 15)
: [];
const subscribeClickhandler = useCallback(async () => {
if (isGuestUser) {
logger.warn('Guest users cannot subscribe to pages');
return;
}
if (!isIPageInfoForOperation(pageInfo) || pageId == null) {
logger.warn('PageInfo is not for operation or pageId is null');
return;
}
await toggleSubscribe(pageId, pageInfo.subscriptionStatus);
mutatePageInfo();
}, [isGuestUser, mutatePageInfo, pageId, pageInfo]);
const likeClickhandler = useCallback(async () => {
if (isGuestUser) {
logger.warn('Guest users cannot like pages');
return;
}
if (!isIPageInfoForOperation(pageInfo) || pageId == null) {
logger.warn('PageInfo is not for operation or pageId is null');
return;
}
await toggleLike(pageId, pageInfo.isLiked);
mutatePageInfo();
}, [isGuestUser, mutatePageInfo, pageId, pageInfo]);
const duplicateMenuItemClickHandler = useCallback(async (): Promise => {
if (onClickDuplicateMenuItem == null || pageId == null || path == null) {
logger.warn(
'Cannot duplicate the page because onClickDuplicateMenuItem, pageId or path is null',
);
return;
}
const page: IPageForPageDuplicateModal = { pageId, path };
onClickDuplicateMenuItem(page);
}, [onClickDuplicateMenuItem, pageId, path]);
const renameMenuItemClickHandler = useCallback(async (): Promise => {
if (onClickRenameMenuItem == null || pageId == null || path == null) {
logger.warn(
'Cannot rename the page because onClickRenameMenuItem, pageId or path is null',
);
return;
}
const page: IPageToRenameWithMeta = {
data: {
_id: pageId,
revision: revisionId ?? null,
path,
},
meta: pageInfo,
};
onClickRenameMenuItem(page);
}, [onClickRenameMenuItem, pageId, pageInfo, path, revisionId]);
const deleteMenuItemClickHandler = useCallback(async (): Promise => {
if (onClickDeleteMenuItem == null || pageId == null || path == null) {
logger.warn(
'Cannot delete the page because onClickDeleteMenuItem, pageId or path is null',
);
return;
}
const pageToDelete: IPageToDeleteWithMeta = {
data: {
_id: pageId,
revision: revisionId ?? null,
path,
},
meta: pageInfo,
};
onClickDeleteMenuItem(pageToDelete);
}, [onClickDeleteMenuItem, pageId, pageInfo, path, revisionId]);
const switchContentWidthClickHandler = useCallback(() => {
if (isGuestUser || isReadOnlyUser) {
logger.warn('Guest or read-only users cannot switch content width');
return;
}
if (onClickSwitchContentWidth == null || pageId == null) {
logger.warn(
'Cannot switch content width because onClickSwitchContentWidth or pageId is null',
);
return;
}
if (!isIPageInfoForEntity(pageInfo)) {
logger.warn('PageInfo is not for entity');
return;
}
try {
const newValue = !expandContentWidth;
onClickSwitchContentWidth(pageId, newValue);
} catch (err) {
toastError(err);
}
}, [
expandContentWidth,
isGuestUser,
isReadOnlyUser,
onClickSwitchContentWidth,
pageId,
pageInfo,
]);
const isEnableActions = useMemo(() => {
if (isGuestUser) {
return false;
}
if (currentPagePath == null) {
return false;
}
return true;
}, [currentPagePath, isGuestUser]);
const additionalMenuItemOnTopRenderer = useMemo(() => {
if (!isIPageInfoForEntity(pageInfo)) {
return undefined;
}
if (onClickSwitchContentWidth == null) {
return undefined;
}
const wideviewMenuItemRenderer = (props: WideViewMenuItemProps) => {
return (
);
};
return wideviewMenuItemRenderer;
}, [
pageInfo,
expandContentWidth,
onClickSwitchContentWidth,
switchContentWidthClickHandler,
]);
const forceHideMenuItemsWithAdditions = [
...(forceHideMenuItems ?? []),
MenuItemType.BOOKMARK,
MenuItemType.REVERT,
];
const isViewMode = editorMode === EditorMode.View;
return (
{isViewMode && isDeviceLargerThanMd && !isSearchPage && !isSearchPage && (
<>
>
)}
{revisionId != null && !isViewMode && (
)}
{!hideSubControls && (
{isIPageInfoForOperation(pageInfo) && (
)}
{isIPageInfoForOperation(pageInfo) && (
)}
{(isIPageInfoForOperation(pageInfo) ||
isIPageInfoForEmpty(pageInfo)) &&
pageId != null && (
)}
{isIPageInfoForEntity(pageInfo) && !isSearchPage && (
)}
)}
{showPageControlDropdown && (
)}
);
};
type PageControlsProps = CommonProps;
export const PageControls = memo((props: PageControlsProps): JSX.Element => {
const { pageId, revisionId, shareLinkId, ...rest } = props;
const { data: pageInfo, error } = useSWRxPageInfo(
pageId ?? null,
shareLinkId,
);
const { data: tagsInfoData } = useSWRxTagsInfo(pageId);
const { open: openTagEditModal } = useTagEditModalActions();
const onClickEditTagsButton = useCallback(() => {
if (tagsInfoData == null || pageId == null || revisionId == null) {
return;
}
openTagEditModal(tagsInfoData.tags, pageId, revisionId);
}, [pageId, revisionId, tagsInfoData, openTagEditModal]);
if (error != null) {
return <>>;
}
return (
);
});