Ellipsis.tsx 6.4 KB

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