Ellipsis.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import type { FC } from 'react';
  2. import React, {
  3. useCallback, useState,
  4. } from 'react';
  5. import nodePath from 'path';
  6. import type { IPageInfoAll, IPageToDeleteWithMeta } from '@growi/core';
  7. import { pathUtils } from '@growi/core/dist/utils';
  8. import { useTranslation } from 'next-i18next';
  9. import { DropdownToggle } from 'reactstrap';
  10. import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/page-operation';
  11. import { apiv3Put } from '~/client/util/apiv3-client';
  12. import { ValidationTarget } from '~/client/util/input-validator';
  13. import { toastError, toastSuccess } from '~/client/util/toastr';
  14. import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
  15. import { useSWRMUTxCurrentUserBookmarks } from '~/stores/bookmark';
  16. import { useSWRMUTxPageInfo } from '~/stores/page';
  17. import ClosableTextInput from '../../Common/ClosableTextInput';
  18. import { PageItemControl } from '../../Common/Dropdown/PageItemControl';
  19. import {
  20. type TreeItemToolProps, NotDraggableForClosableTextInput, SimpleItemTool,
  21. } from '../../TreeItem';
  22. export const Ellipsis: FC<TreeItemToolProps> = (props) => {
  23. const [isRenameInputShown, setRenameInputShown] = useState(false);
  24. const { t } = useTranslation();
  25. const {
  26. itemNode, onRenamed, onClickDuplicateMenuItem,
  27. onClickDeleteMenuItem, isEnableActions, isReadOnlyUser,
  28. } = props;
  29. const { page } = itemNode;
  30. const { trigger: mutateCurrentUserBookmarks } = useSWRMUTxCurrentUserBookmarks();
  31. const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(page._id ?? null);
  32. const bookmarkMenuItemClickHandler = async(_pageId: string, _newValue: boolean): Promise<void> => {
  33. const bookmarkOperation = _newValue ? bookmark : unbookmark;
  34. await bookmarkOperation(_pageId);
  35. mutateCurrentUserBookmarks();
  36. mutatePageInfo();
  37. };
  38. const duplicateMenuItemClickHandler = useCallback((): void => {
  39. if (onClickDuplicateMenuItem == null) {
  40. return;
  41. }
  42. const { _id: pageId, path } = page;
  43. if (pageId == null || path == null) {
  44. throw Error('Any of _id and path must not be null.');
  45. }
  46. const pageToDuplicate = { pageId, path };
  47. onClickDuplicateMenuItem(pageToDuplicate);
  48. }, [onClickDuplicateMenuItem, page]);
  49. const renameMenuItemClickHandler = useCallback(() => {
  50. setRenameInputShown(true);
  51. }, []);
  52. const onPressEnterForRenameHandler = async(inputText: string) => {
  53. const parentPath = pathUtils.addTrailingSlash(nodePath.dirname(page.path ?? ''));
  54. const newPagePath = nodePath.resolve(parentPath, inputText);
  55. if (newPagePath === page.path) {
  56. setRenameInputShown(false);
  57. return;
  58. }
  59. try {
  60. setRenameInputShown(false);
  61. await apiv3Put('/pages/rename', {
  62. pageId: page._id,
  63. revisionId: page.revision,
  64. newPagePath,
  65. });
  66. if (onRenamed != null) {
  67. onRenamed(page.path, newPagePath);
  68. }
  69. toastSuccess(t('renamed_pages', { path: page.path }));
  70. }
  71. catch (err) {
  72. setRenameInputShown(true);
  73. toastError(err);
  74. }
  75. };
  76. const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoAll | undefined): Promise<void> => {
  77. if (onClickDeleteMenuItem == null) {
  78. return;
  79. }
  80. if (page._id == null || page.path == null) {
  81. throw Error('_id and path must not be null.');
  82. }
  83. const pageToDelete: IPageToDeleteWithMeta = {
  84. data: {
  85. _id: page._id,
  86. revision: page.revision as string,
  87. path: page.path,
  88. },
  89. meta: pageInfo,
  90. };
  91. onClickDeleteMenuItem(pageToDelete);
  92. }, [onClickDeleteMenuItem, page]);
  93. const pathRecoveryMenuItemClickHandler = async(pageId: string): Promise<void> => {
  94. try {
  95. await resumeRenameOperation(pageId);
  96. toastSuccess(t('page_operation.paths_recovered'));
  97. }
  98. catch {
  99. toastError(t('page_operation.path_recovery_failed'));
  100. }
  101. };
  102. const hasChildren = page.descendantCount ? page.descendantCount > 0 : false;
  103. return (
  104. <>
  105. {isRenameInputShown ? (
  106. <div className={`position-absolute ${hasChildren ? 'ms-5' : 'ms-4'}`}>
  107. <NotDraggableForClosableTextInput>
  108. <ClosableTextInput
  109. value={nodePath.basename(page.path ?? '')}
  110. placeholder={t('Input page name')}
  111. onClickOutside={() => { setRenameInputShown(false) }}
  112. onPressEnter={onPressEnterForRenameHandler}
  113. validationTarget={ValidationTarget.PAGE}
  114. />
  115. </NotDraggableForClosableTextInput>
  116. </div>
  117. ) : (
  118. <SimpleItemTool itemNode={itemNode} isEnableActions={false} isReadOnlyUser={false} />
  119. )}
  120. <NotAvailableForGuest>
  121. <div className="grw-pagetree-control d-flex">
  122. <PageItemControl
  123. pageId={page._id}
  124. isEnableActions={isEnableActions}
  125. isReadOnlyUser={isReadOnlyUser}
  126. onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
  127. onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
  128. onClickRenameMenuItem={renameMenuItemClickHandler}
  129. onClickDeleteMenuItem={deleteMenuItemClickHandler}
  130. onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
  131. isInstantRename
  132. // Todo: It is wanted to find a better way to pass operationProcessData to PageItemControl
  133. operationProcessData={page.processData}
  134. >
  135. {/* pass the color property to reactstrap dropdownToggle props. https://6-4-0--reactstrap.netlify.app/components/dropdowns/ */}
  136. <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
  137. <span id="option-button-in-page-tree" className="material-symbols-outlined p-1">more_vert</span>
  138. </DropdownToggle>
  139. </PageItemControl>
  140. </div>
  141. </NotAvailableForGuest>
  142. </>
  143. );
  144. };