ConflictDiffModal.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. import React, {
  2. useState, useEffect, useCallback, useMemo,
  3. } from 'react';
  4. import type { IRevisionOnConflict } from '@growi/core';
  5. import {
  6. MergeViewer, CodeMirrorEditorDiff, GlobalCodeMirrorEditorKey, useCodeMirrorEditorIsolated,
  7. } from '@growi/editor';
  8. import { UserPicture } from '@growi/ui/dist/components';
  9. import { format } from 'date-fns';
  10. import { useTranslation } from 'next-i18next';
  11. import {
  12. Modal, ModalHeader, ModalBody, ModalFooter,
  13. } from 'reactstrap';
  14. import { toastError, toastSuccess } from '~/client/util/toastr';
  15. import { useCurrentPathname, useCurrentUser } from '~/stores/context';
  16. import { useConflictDiffModal } from '~/stores/modal';
  17. import { useCurrentPagePath, useSWRxCurrentPage, useCurrentPageId } from '~/stores/page';
  18. import {
  19. useRemoteRevisionBody, useRemoteRevisionId, useRemoteRevisionLastUpdatedAt, useRemoteRevisionLastUpdateUser, useSetRemoteLatestPageData,
  20. } from '~/stores/remote-latest-page';
  21. import styles from './ConflictDiffModal.module.scss';
  22. type ConflictDiffModalCoreProps = {
  23. // optionsToSave: OptionsToSave | undefined;
  24. request: IRevisionOnConflictWithStringDate,
  25. latest: IRevisionOnConflictWithStringDate,
  26. onClose?: () => void,
  27. onResolved?: () => void,
  28. };
  29. type IRevisionOnConflictWithStringDate = Omit<IRevisionOnConflict, 'createdAt'> & {
  30. createdAt: string
  31. }
  32. const ConflictDiffModalCore = (props: ConflictDiffModalCoreProps): JSX.Element => {
  33. const {
  34. request, latest, onClose, onResolved,
  35. } = props;
  36. const [resolvedRevision, setResolvedRevision] = useState<string>('');
  37. const [isRevisionselected, setIsRevisionSelected] = useState<boolean>(false);
  38. const [revisionSelectedToggler, setRevisionSelectedToggler] = useState<boolean>(false);
  39. const [isModalExpanded, setIsModalExpanded] = useState<boolean>(false);
  40. const { t } = useTranslation();
  41. const { data: conflictDiffModalStatus, close: closeConflictDiffModal } = useConflictDiffModal();
  42. const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.DIFF);
  43. // const { data: remoteRevisionId } = useRemoteRevisionId();
  44. // const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
  45. // const { data: pageId } = useCurrentPageId();
  46. // const { data: currentPagePath } = useCurrentPagePath();
  47. // const { data: currentPathname } = useCurrentPathname();
  48. const selectRevisionHandler = useCallback((selectedRevision: string) => {
  49. setResolvedRevision(selectedRevision);
  50. setRevisionSelectedToggler(prev => !prev);
  51. if (!isRevisionselected) {
  52. setIsRevisionSelected(true);
  53. }
  54. }, [isRevisionselected]);
  55. const closeModalHandler = useCallback(() => {
  56. closeConflictDiffModal();
  57. onClose?.();
  58. }, [closeConflictDiffModal, onClose]);
  59. const resolveConflictHandler = useCallback(async() => {
  60. const newBody = codeMirrorEditor?.getDoc();
  61. // TODO: impl
  62. onResolved?.();
  63. }, [codeMirrorEditor, onResolved]);
  64. useEffect(() => {
  65. codeMirrorEditor?.initDoc(resolvedRevision);
  66. // Enable selecting the same revision after editing by including revisionSelectedToggler in the dependency array of useEffect
  67. }, [codeMirrorEditor, resolvedRevision, revisionSelectedToggler]);
  68. const headerButtons = useMemo(() => (
  69. <div className="d-flex align-items-center">
  70. <button type="button" className="btn" onClick={() => setIsModalExpanded(prev => !prev)}>
  71. <span className="material-symbols-outlined">{isModalExpanded ? 'close_fullscreen' : 'open_in_full'}</span>
  72. </button>
  73. <button type="button" className="btn" onClick={closeModalHandler} aria-label="Close">
  74. <span className="material-symbols-outlined">close</span>
  75. </button>
  76. </div>
  77. ), [closeModalHandler, isModalExpanded]);
  78. return (
  79. <Modal isOpen={conflictDiffModalStatus?.isOpened} className={`${styles['conflict-diff-modal']} ${isModalExpanded ? ' grw-modal-expanded' : ''}`} size="xl">
  80. <ModalHeader tag="h4" className="d-flex align-items-center" close={headerButtons}>
  81. <span className="material-symbols-outlined me-1">error</span>{t('modal_resolve_conflict.resolve_conflict')}
  82. </ModalHeader>
  83. <ModalBody className="mx-4 my-1">
  84. <div className="row">
  85. <div className="col-12 text-center mt-2 mb-4">
  86. <h3 className="fw-bold text-muted">{t('modal_resolve_conflict.resolve_conflict_message')}</h3>
  87. </div>
  88. <div className="col-6">
  89. <h4 className="fw-bold my-2 text-muted">{t('modal_resolve_conflict.requested_revision')}</h4>
  90. <div className="d-flex align-items-center my-3">
  91. <div>
  92. <UserPicture user={request.user} size="lg" noLink noTooltip />
  93. </div>
  94. <div className="ms-3 text-muted">
  95. <p className="my-0">updated by {request.user.username}</p>
  96. <p className="my-0">{request.createdAt}</p>
  97. </div>
  98. </div>
  99. </div>
  100. <div className="col-6">
  101. <h4 className="fw-bold my-2 text-muted">{t('modal_resolve_conflict.latest_revision')}</h4>
  102. <div className="d-flex align-items-center my-3">
  103. <div>
  104. <UserPicture user={latest.user} size="lg" noLink noTooltip />
  105. </div>
  106. <div className="ms-3 text-muted">
  107. <p className="my-0">updated by {latest.user.username}</p>
  108. <p className="my-0">{latest.createdAt}</p>
  109. </div>
  110. </div>
  111. </div>
  112. <MergeViewer
  113. leftBody={request.revisionBody}
  114. rightBody={latest.revisionBody}
  115. />
  116. <div className="col-6">
  117. <div className="text-center my-4">
  118. <button
  119. type="button"
  120. className="btn btn-outline-primary"
  121. onClick={() => { selectRevisionHandler(request.revisionBody) }}
  122. >
  123. <span className="material-symbols-outlined me-1">arrow_circle_down</span>
  124. {t('modal_resolve_conflict.select_revision', { revision: 'mine' })}
  125. </button>
  126. </div>
  127. </div>
  128. <div className="col-6">
  129. <div className="text-center my-4">
  130. <button
  131. type="button"
  132. className="btn btn-outline-primary"
  133. onClick={() => { selectRevisionHandler(latest.revisionBody) }}
  134. >
  135. <span className="material-symbols-outlined me-1">arrow_circle_down</span>
  136. {t('modal_resolve_conflict.select_revision', { revision: 'theirs' })}
  137. </button>
  138. </div>
  139. </div>
  140. <div className="col-12">
  141. <div className="border border-dark">
  142. <h4 className="fw-bold my-2 mx-2 text-muted">{t('modal_resolve_conflict.selected_editable_revision')}</h4>
  143. <CodeMirrorEditorDiff />
  144. </div>
  145. </div>
  146. </div>
  147. </ModalBody>
  148. <ModalFooter>
  149. <button
  150. type="button"
  151. className="btn btn-outline-secondary"
  152. onClick={closeModalHandler}
  153. >
  154. {t('Cancel')}
  155. </button>
  156. <button
  157. type="button"
  158. className="btn btn-primary ms-3"
  159. onClick={resolveConflictHandler}
  160. disabled={!isRevisionselected}
  161. >
  162. {t('modal_resolve_conflict.resolve_and_save')}
  163. </button>
  164. </ModalFooter>
  165. </Modal>
  166. );
  167. };
  168. const dummyTest1 = `# :tada: グローウィ へようこそ
  169. [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
  170. [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/weseek/growi/blob/master/LICENSE)
  171. グローウィ は個人・法人向けの Wiki | ナレッジベースツールです。
  172. 会社や大学の研究室、サークルでのナレッジ情報を簡単に共有でき、作られたページは誰でも編集が可能です。
  173. 知っている情報をカジュアルに書き出しみんなで編集することで、**チーム内での暗黙知を減らす**ことができます。
  174. 当たり前に共有される情報を日々増やしていきましょう。
  175. ### :beginner: 簡単なページの作り方
  176. - 右上の "**作成**"ボタンまたは右下の**鉛筆アイコン**のボタンからページを書き始めることができます
  177. - ページタイトルは後から変更できますので、適当に入力しても大丈夫です
  178. - タイトル入力欄では、半角の / (スラッシュ) でページ階層を作れます
  179. - (例)/カテゴリ1/カテゴリ2/作りたいページタイトル のように入力してみてください
  180. - \`\`- \` を行頭につけると、この文章のような箇条書きを書くことができます\`\`
  181. - 画像やPDF、Word/Excel/PowerPointなどの添付ファイルも、コピー&ペースト、ドラッグ&ドロップで貼ることができます
  182. - 書けたら "**更新**" ボタンを押してページを公開しましょう
  183. - \`Ctrl(⌘) + S\` でも保存できます
  184. さらに詳しくはこちら: [ページを作成する](https://docs.growi.org/ja/guide/features/create_page.html)
  185. <div class="mt-4 card border-primary">
  186. <div class="card-header bg-primary text-light">Tips</div>
  187. <div class="card-body"><ul>
  188. <li>Ctrl(⌘) + "/" でショートカットヘルプを表示します</li>
  189. <li>HTML/CSS の記述には、<a href="https://getbootstrap.com/docs/4.6/components/">Bootstrap 4</a> を利用できます</li>
  190. </ul></div>
  191. </div>
  192. `;
  193. const dummyTest2 = `# :tada: GROWI へようこそ
  194. [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
  195. [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/weseek/growi/blob/master/LICENSE)
  196. GROWI は個人・法人向けの Wiki | ナレッジベースツールです。
  197. 会社や大学の研究室、サークルでのナレッジ情報を簡単に共有でき、作られたページは誰でも編集が可能です。
  198. 知っている情報をカジュアルに書き出しみんなで編集することで、**チーム内での暗黙知を減らす**ことができます。
  199. 当たり前に共有される情報を日々増やしていきましょう。
  200. ### :beginner: 簡単なページの作り方
  201. - 右上の "**作成**"ボタンまたは右下の**鉛筆アイコン**のボタンからページを書き始めることができます
  202. - ページタイトルは後から変更できますので、適当に入力しても大丈夫です
  203. - タイトル入力欄では、半角の / (スラッシュ) でページ階層を作れます
  204. - (例)/カテゴリ1/カテゴリ2/作りたいページタイトル のように入力してみてください
  205. - \`\`- \` を行頭につけると、この文章のような箇条書きを書くことができます\`\`
  206. - 画像やPDF、Word/Excel/PowerPointなどの添付ファイルも、コピー&ペースト、ドラッグ&ドロップで貼ることができます
  207. - 書けたら "**更新**" ボタンを押してページを公開しましょう
  208. - \`Ctrl(⌘) + S\` でも保存できます
  209. さらに詳しくはこちら: [ページを作成する](https://docs.growi.org/ja/guide/features/create_page.html)
  210. <div class="mt-4 card border-primary">
  211. <div class="card-header bg-primary text-light">Tips</div>
  212. <div class="card-body"><ul>
  213. <li>Ctrl(⌘) + "/" でショートカットヘルプを表示します</li>
  214. <li>HTML/CSS の記述には、<a href="https://getbootstrap.com/docs/4.6/components/">Bootstrap 4</a> を利用できます</li>
  215. </ul></div>
  216. </div>
  217. `;
  218. type ConflictDiffModalProps = {
  219. onClose?: () => void,
  220. onResolved?: () => void,
  221. // optionsToSave: OptionsToSave | undefined;
  222. // afterResolvedHandler: () => void,
  223. };
  224. export const ConflictDiffModal = (props: ConflictDiffModalProps): JSX.Element => {
  225. const {
  226. onClose, onResolved,
  227. } = props;
  228. const { data: currentUser } = useCurrentUser();
  229. // state for current page
  230. const { data: currentPage } = useSWRxCurrentPage();
  231. // state for latest page
  232. const { data: remoteRevisionId } = useRemoteRevisionId();
  233. const { data: remoteRevisionBody } = useRemoteRevisionBody();
  234. const { data: remoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
  235. const { data: remoteRevisionLastUpdatedAt } = useRemoteRevisionLastUpdatedAt();
  236. const { data: conflictDiffModalStatus } = useConflictDiffModal();
  237. const currentTime: Date = new Date();
  238. const isRemotePageDataInappropriate = remoteRevisionId == null || remoteRevisionBody == null || remoteRevisionLastUpdateUser == null;
  239. if (!conflictDiffModalStatus?.isOpened || currentUser == null || currentPage == null) {
  240. return <></>;
  241. }
  242. const request: IRevisionOnConflictWithStringDate = {
  243. revisionId: '',
  244. revisionBody: dummyTest1,
  245. createdAt: format(currentTime, 'yyyy/MM/dd HH:mm:ss'),
  246. user: currentUser,
  247. };
  248. const latest: IRevisionOnConflictWithStringDate = {
  249. revisionId: '',
  250. revisionBody: dummyTest2,
  251. createdAt: format(currentTime, 'yyyy/MM/dd HH:mm:ss'),
  252. user: currentUser,
  253. };
  254. // const latest: IRevisionOnConflictWithStringDate = {
  255. // revisionId: remoteRevisionId,
  256. // revisionBody: remoteRevisionBody,
  257. // createdAt: format(new Date(remoteRevisionLastUpdatedAt || currentTime.toString()), 'yyyy/MM/dd HH:mm:ss'),
  258. // user: remoteRevisionLastUpdateUser,
  259. // };
  260. const propsForCore = {
  261. onResolved,
  262. onClose,
  263. request,
  264. latest,
  265. };
  266. return <ConflictDiffModalCore {...propsForCore} />;
  267. };