PageEditorModeManager.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. import React, {
  2. type ReactNode, useCallback, useMemo, type JSX,
  3. } from 'react';
  4. import { Origin } from '@growi/core';
  5. import { getParentPath } from '@growi/core/dist/utils/path-utils';
  6. import { useTranslation } from 'next-i18next';
  7. import { useCreatePage } from '~/client/services/create-page';
  8. import { toastError } from '~/client/util/toastr';
  9. import { EditorMode, useEditorMode } from '~/stores-universal/ui';
  10. import { useIsNotFound } from '~/stores/page';
  11. import { useIsDeviceLargerThanMd } from '~/stores/ui';
  12. import { useCurrentPageYjsData } from '~/stores/yjs';
  13. import { shouldCreateWipPage } from '../../../utils/should-create-wip-page';
  14. import styles from './PageEditorModeManager.module.scss';
  15. type PageEditorModeButtonProps = {
  16. currentEditorMode: EditorMode,
  17. editorMode: EditorMode,
  18. children?: ReactNode,
  19. isBtnDisabled?: boolean,
  20. onClick?: () => void,
  21. }
  22. const PageEditorModeButton = React.memo((props: PageEditorModeButtonProps) => {
  23. const {
  24. currentEditorMode, isBtnDisabled, editorMode, children, onClick,
  25. } = props;
  26. const classNames = ['btn py-1 px-2 d-flex align-items-center justify-content-center'];
  27. if (currentEditorMode === editorMode) {
  28. classNames.push('active');
  29. }
  30. if (isBtnDisabled) {
  31. classNames.push('disabled');
  32. }
  33. return (
  34. <button
  35. type="button"
  36. className={classNames.join(' ')}
  37. onClick={onClick}
  38. data-testid={`${editorMode}-button`}
  39. >
  40. {children}
  41. </button>
  42. );
  43. });
  44. type Props = {
  45. editorMode: EditorMode | undefined,
  46. isBtnDisabled: boolean,
  47. path?: string,
  48. }
  49. export const PageEditorModeManager = (props: Props): JSX.Element => {
  50. const {
  51. editorMode = EditorMode.View,
  52. isBtnDisabled,
  53. path,
  54. } = props;
  55. const { t } = useTranslation('commons');
  56. const { data: isNotFound } = useIsNotFound();
  57. const { mutate: mutateEditorMode } = useEditorMode();
  58. const { data: isDeviceLargerThanMd } = useIsDeviceLargerThanMd();
  59. const { data: currentPageYjsData } = useCurrentPageYjsData();
  60. const { isCreating, create } = useCreatePage();
  61. const editButtonClickedHandler = useCallback(async() => {
  62. if (isNotFound == null || isNotFound === false) {
  63. mutateEditorMode(EditorMode.Editor);
  64. return;
  65. }
  66. try {
  67. const parentPath = path != null ? getParentPath(path) : undefined; // does not have to exist
  68. await create(
  69. {
  70. path, parentPath, wip: shouldCreateWipPage(path), origin: Origin.View,
  71. },
  72. );
  73. }
  74. catch (err) {
  75. toastError(t('toaster.create_failed', { target: path }));
  76. }
  77. }, [create, isNotFound, mutateEditorMode, path, t]);
  78. const _isBtnDisabled = isCreating || isBtnDisabled;
  79. const circleColor = useMemo(() => {
  80. if ((currentPageYjsData?.awarenessStateSize ?? 0) > 0) {
  81. return 'bg-primary';
  82. }
  83. if (currentPageYjsData?.hasYdocsNewerThanLatestRevision ?? false) {
  84. return 'bg-secondary';
  85. }
  86. }, [currentPageYjsData]);
  87. return (
  88. <>
  89. <div
  90. className={`btn-group grw-page-editor-mode-manager ${styles['grw-page-editor-mode-manager']}`}
  91. role="group"
  92. aria-label="page-editor-mode-manager"
  93. id="grw-page-editor-mode-manager"
  94. data-testid="grw-page-editor-mode-manager"
  95. >
  96. {(isDeviceLargerThanMd || editorMode !== EditorMode.View) && (
  97. <PageEditorModeButton
  98. currentEditorMode={editorMode}
  99. editorMode={EditorMode.View}
  100. isBtnDisabled={_isBtnDisabled}
  101. onClick={() => mutateEditorMode(EditorMode.View)}
  102. >
  103. <span className="material-symbols-outlined fs-4">play_arrow</span>{t('View')}
  104. </PageEditorModeButton>
  105. )}
  106. {(isDeviceLargerThanMd || editorMode === EditorMode.View) && (
  107. <PageEditorModeButton
  108. currentEditorMode={editorMode}
  109. editorMode={EditorMode.Editor}
  110. isBtnDisabled={_isBtnDisabled}
  111. onClick={editButtonClickedHandler}
  112. >
  113. <span className="material-symbols-outlined me-1 fs-5">edit_square</span>{t('Edit')}
  114. { circleColor != null && <span className={`position-absolute top-0 start-100 translate-middle p-1 rounded-circle ${circleColor}`} />}
  115. </PageEditorModeButton>
  116. )}
  117. </div>
  118. </>
  119. );
  120. };