SelectCollectionsModal.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. import React, { type JSX, useCallback, useEffect, useState } from 'react';
  2. import { useTranslation } from 'next-i18next';
  3. import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
  4. import { apiPost } from '~/client/util/apiv1-client';
  5. import { toastError, toastSuccess } from '~/client/util/toastr';
  6. const GROUPS_PAGE = [
  7. 'pages',
  8. 'revisions',
  9. 'tags',
  10. 'pagetagrelations',
  11. 'pageredirects',
  12. 'comments',
  13. 'sharelinks',
  14. ];
  15. const GROUPS_USER = [
  16. 'users',
  17. 'externalaccounts',
  18. 'usergroups',
  19. 'usergrouprelations',
  20. 'externalusergroups',
  21. 'externalusergrouprelations',
  22. 'useruisettings',
  23. 'editorsettings',
  24. 'bookmarks',
  25. 'bookmarkfolders',
  26. 'subscriptions',
  27. 'inappnotificationsettings',
  28. ];
  29. const GROUPS_CONFIG = [
  30. 'configs',
  31. 'migrations',
  32. 'updateposts',
  33. 'globalnotificationsettings',
  34. 'slackappintegrations',
  35. 'growiplugins',
  36. ];
  37. const ALL_GROUPED_COLLECTIONS =
  38. GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
  39. type Props = {
  40. isOpen: boolean;
  41. onExportingRequested: () => void;
  42. onClose: () => void;
  43. collections: string[];
  44. isAllChecked?: boolean;
  45. };
  46. const SelectCollectionsModal = (props: Props): JSX.Element => {
  47. const { t } = useTranslation();
  48. const { isOpen, onExportingRequested, onClose, collections, isAllChecked } =
  49. props;
  50. const [selectedCollections, setSelectedCollections] = useState<Set<string>>(
  51. new Set(),
  52. );
  53. const toggleCheckbox = useCallback((e) => {
  54. const { target } = e;
  55. const { name, checked } = target;
  56. setSelectedCollections((prevState) => {
  57. const selectedCollections = new Set(prevState);
  58. if (checked) {
  59. selectedCollections.add(name);
  60. } else {
  61. selectedCollections.delete(name);
  62. }
  63. return selectedCollections;
  64. });
  65. }, []);
  66. const checkAll = useCallback(() => {
  67. setSelectedCollections(new Set(collections));
  68. }, [collections]);
  69. const uncheckAll = useCallback(() => {
  70. setSelectedCollections(new Set());
  71. }, []);
  72. const doExport = useCallback(
  73. async (e) => {
  74. e.preventDefault();
  75. try {
  76. // TODO: use apiv3Post
  77. const result = await apiPost<any>('/v3/export', {
  78. collections: Array.from(selectedCollections),
  79. });
  80. if (!result.ok) {
  81. throw new Error('Error occured.');
  82. }
  83. toastSuccess('Export process has requested.');
  84. onExportingRequested();
  85. onClose();
  86. uncheckAll();
  87. } catch (err) {
  88. toastError(err);
  89. }
  90. },
  91. [onClose, onExportingRequested, selectedCollections, uncheckAll],
  92. );
  93. const validateForm = useCallback(() => {
  94. return selectedCollections.size > 0;
  95. }, [selectedCollections.size]);
  96. const renderWarnForUser = useCallback(() => {
  97. // whether selectedCollections includes one of GROUPS_USER
  98. const isUserRelatedDataSelected = GROUPS_USER.some((collectionName) => {
  99. return selectedCollections.has(collectionName);
  100. });
  101. if (!isUserRelatedDataSelected) {
  102. return <></>;
  103. }
  104. const html = t('admin:export_management.desc_password_seed');
  105. return (
  106. <div className="card">
  107. <div className="card-body">
  108. {/** biome-ignore lint/security/noDangerouslySetInnerHtml: ignore */}
  109. <p className="card-text" dangerouslySetInnerHTML={{ __html: html }} />
  110. </div>
  111. </div>
  112. );
  113. }, [selectedCollections, t]);
  114. const renderCheckboxes = useCallback(
  115. (collectionNames, color?) => {
  116. const checkboxColor = color ? `form-check-${color}` : 'form-check-info';
  117. return (
  118. <div className={`form-check ${checkboxColor}`}>
  119. <div className="row">
  120. {collectionNames.map((collectionName) => {
  121. return (
  122. <div className="col-sm-6 my-1" key={collectionName}>
  123. <input
  124. type="checkbox"
  125. className="form-check-input"
  126. id={collectionName}
  127. name={collectionName}
  128. value={collectionName}
  129. checked={selectedCollections.has(collectionName)}
  130. onChange={toggleCheckbox}
  131. />
  132. <label
  133. className="form-label text-capitalize form-check-label ms-3"
  134. htmlFor={collectionName}
  135. >
  136. {collectionName}
  137. </label>
  138. </div>
  139. );
  140. })}
  141. </div>
  142. </div>
  143. );
  144. },
  145. [selectedCollections, toggleCheckbox],
  146. );
  147. const renderGroups = useCallback(
  148. (groupList, color?) => {
  149. const collectionNames = groupList.filter((collectionName) => {
  150. return collections.includes(collectionName);
  151. });
  152. return renderCheckboxes(collectionNames, color);
  153. },
  154. [collections, renderCheckboxes],
  155. );
  156. const renderOthers = useCallback(() => {
  157. const collectionNames = collections.filter((collectionName) => {
  158. return !ALL_GROUPED_COLLECTIONS.includes(collectionName);
  159. });
  160. return renderCheckboxes(collectionNames);
  161. }, [collections, renderCheckboxes]);
  162. useEffect(() => {
  163. if (isAllChecked) checkAll();
  164. }, [isAllChecked, checkAll]);
  165. return (
  166. <Modal size="lg" isOpen={isOpen} toggle={onClose}>
  167. <ModalHeader tag="h4" toggle={onClose} className="text-info">
  168. {t('admin:export_management.export_collections')}
  169. </ModalHeader>
  170. <form onSubmit={doExport}>
  171. <ModalBody>
  172. <div className="row">
  173. <div className="col-sm-12">
  174. <button
  175. type="button"
  176. className="btn btn-sm btn-outline-secondary me-2"
  177. onClick={checkAll}
  178. >
  179. <span className="material-symbols-outlined">check_box</span>{' '}
  180. {t('admin:export_management.check_all')}
  181. </button>
  182. <button
  183. type="button"
  184. className="btn btn-sm btn-outline-secondary me-2"
  185. onClick={uncheckAll}
  186. >
  187. <span className="material-symbols-outlined">
  188. check_box_outline_blank
  189. </span>{' '}
  190. {t('admin:export_management.uncheck_all')}
  191. </button>
  192. </div>
  193. </div>
  194. <div className="row mt-4">
  195. <div className="col-sm-12">
  196. <h3 className="admin-setting-header">MongoDB Page Collections</h3>
  197. {renderGroups(GROUPS_PAGE)}
  198. </div>
  199. </div>
  200. <div className="row mt-4">
  201. <div className="col-sm-12">
  202. <h3 className="admin-setting-header">MongoDB User Collections</h3>
  203. {renderGroups(GROUPS_USER, 'danger')}
  204. {renderWarnForUser()}
  205. </div>
  206. </div>
  207. <div className="row mt-4">
  208. <div className="col-sm-12">
  209. <h3 className="admin-setting-header">
  210. MongoDB Config Collections
  211. </h3>
  212. {renderGroups(GROUPS_CONFIG)}
  213. </div>
  214. </div>
  215. <div className="row mt-4">
  216. <div className="col-sm-12">
  217. <h3 className="admin-setting-header">
  218. MongoDB Other Collections
  219. </h3>
  220. {renderOthers()}
  221. </div>
  222. </div>
  223. </ModalBody>
  224. <ModalFooter>
  225. <button
  226. type="button"
  227. className="btn btn-sm btn-outline-secondary"
  228. onClick={onClose}
  229. >
  230. {t('admin:export_management.cancel')}
  231. </button>
  232. <button
  233. type="submit"
  234. className="btn btn-sm btn-primary"
  235. disabled={!validateForm()}
  236. >
  237. {t('admin:export_management.export')}
  238. </button>
  239. </ModalFooter>
  240. </form>
  241. </Modal>
  242. );
  243. };
  244. export default SelectCollectionsModal;