FixPageGrantAlert.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import React, { useEffect, useState, useCallback } from 'react';
  2. import { PageGrant } from '@growi/core';
  3. import { useTranslation } from 'react-i18next';
  4. import {
  5. Modal, ModalHeader, ModalBody, ModalFooter,
  6. } from 'reactstrap';
  7. import { apiv3Put } from '~/client/util/apiv3-client';
  8. import { toastError, toastSuccess } from '~/client/util/toastr';
  9. import { IPageGrantData } from '~/interfaces/page';
  10. import { PopulatedGrantedGroup, IRecordApplicableGrant, IResIsGrantNormalizedGrantData } from '~/interfaces/page-grant';
  11. import { useCurrentUser } from '~/stores/context';
  12. import { useSWRxApplicableGrant, useSWRxIsGrantNormalized, useSWRxCurrentPage } from '~/stores/page';
  13. type ModalProps = {
  14. isOpen: boolean
  15. pageId: string
  16. dataApplicableGrant: IRecordApplicableGrant
  17. currentAndParentPageGrantData: IResIsGrantNormalizedGrantData
  18. close(): void
  19. }
  20. const FixPageGrantModal = (props: ModalProps): JSX.Element => {
  21. const { t } = useTranslation();
  22. const {
  23. isOpen, pageId, dataApplicableGrant, currentAndParentPageGrantData, close,
  24. } = props;
  25. const [selectedGrant, setSelectedGrant] = useState<PageGrant>(PageGrant.GRANT_RESTRICTED);
  26. const [isGroupSelectModalShown, setIsGroupSelectModalShown] = useState(false);
  27. const [selectedGroup, setSelectedGroup] = useState<PopulatedGrantedGroup | undefined>(undefined);
  28. // Alert message state
  29. const [shouldShowModalAlert, setShowModalAlert] = useState<boolean>(false);
  30. const applicableGroups = dataApplicableGrant[PageGrant.GRANT_USER_GROUP]?.applicableGroups;
  31. // Reset state when opened
  32. useEffect(() => {
  33. if (isOpen) {
  34. setSelectedGrant(PageGrant.GRANT_RESTRICTED);
  35. setSelectedGroup(undefined);
  36. setShowModalAlert(false);
  37. }
  38. }, [isOpen]);
  39. const submit = async() => {
  40. // Validate input values
  41. if (selectedGrant === PageGrant.GRANT_USER_GROUP && selectedGroup == null) {
  42. setShowModalAlert(true);
  43. return;
  44. }
  45. close();
  46. try {
  47. await apiv3Put(`/page/${pageId}/grant`, {
  48. grant: selectedGrant,
  49. grantedGroups: selectedGroup?.item._id != null ? [{ item: selectedGroup?.item._id, type: selectedGroup.type }] : null,
  50. });
  51. toastSuccess(t('Successfully updated'));
  52. }
  53. catch (err) {
  54. toastError(t('Failed to update'));
  55. }
  56. };
  57. const getGrantLabel = useCallback((isForbidden: boolean, grantData?: IPageGrantData): string => {
  58. if (isForbidden) {
  59. return t('fix_page_grant.modal.grant_label.isForbidden');
  60. }
  61. if (grantData == null) {
  62. return t('fix_page_grant.modal.grant_label.isForbidden');
  63. }
  64. if (grantData.grant === 1) {
  65. return t('fix_page_grant.modal.grant_label.public');
  66. }
  67. if (grantData.grant === 4) {
  68. return t('fix_page_grant.modal.radio_btn.only_me');
  69. }
  70. if (grantData.grant === 5) {
  71. if (grantData.grantedGroups == null || grantData.grantedGroups.length === 0) {
  72. return t('fix_page_grant.modal.grant_label.isForbidden');
  73. }
  74. return `${t('fix_page_grant.modal.radio_btn.grant_group')}: (${grantData.grantedGroups[0].name})`;
  75. }
  76. throw Error('cannot get grant label'); // this error can't be throwed
  77. }, [t]);
  78. const renderGrantDataLabel = useCallback(() => {
  79. const { isForbidden, currentPageGrant, parentPageGrant } = currentAndParentPageGrantData;
  80. const currentGrantLabel = getGrantLabel(false, currentPageGrant);
  81. const parentGrantLabel = getGrantLabel(isForbidden, parentPageGrant);
  82. return (
  83. <>
  84. <p className="mt-3">{ t('fix_page_grant.modal.grant_label.parentPageGrantLabel') + parentGrantLabel }</p>
  85. <p>{ t('fix_page_grant.modal.grant_label.currentPageGrantLabel') + currentGrantLabel }</p>
  86. {/* eslint-disable-next-line react/no-danger */}
  87. <p dangerouslySetInnerHTML={{ __html: t('fix_page_grant.modal.grant_label.docLink') }} />
  88. </>
  89. );
  90. }, [t, currentAndParentPageGrantData, getGrantLabel]);
  91. const renderModalBodyAndFooter = () => {
  92. const isGrantAvailable = Object.keys(dataApplicableGrant || {}).length > 0;
  93. if (!isGrantAvailable) {
  94. return (
  95. <p className="m-5">
  96. { t('fix_page_grant.modal.no_grant_available') }
  97. </p>
  98. );
  99. }
  100. return (
  101. <>
  102. <ModalBody>
  103. <div>
  104. {/* eslint-disable-next-line react/no-danger */}
  105. <p className="mb-2" dangerouslySetInnerHTML={{ __html: t('fix_page_grant.modal.need_to_fix_grant') }} />
  106. {/* grant data label */}
  107. {renderGrantDataLabel()}
  108. <div className="ms-2">
  109. <div className="form-check mb-3">
  110. <input
  111. className="form-check-input"
  112. name="grantRestricted"
  113. id="grantRestricted"
  114. type="radio"
  115. disabled={!(PageGrant.GRANT_RESTRICTED in dataApplicableGrant)}
  116. checked={selectedGrant === PageGrant.GRANT_RESTRICTED}
  117. onChange={() => setSelectedGrant(PageGrant.GRANT_RESTRICTED)}
  118. />
  119. <label className="form-label form-check-label" htmlFor="grantRestricted">
  120. { t('fix_page_grant.modal.radio_btn.restrected') }
  121. </label>
  122. </div>
  123. <div className="form-check mb-3">
  124. <input
  125. className="form-check-input"
  126. name="grantUser"
  127. id="grantUser"
  128. type="radio"
  129. disabled={!(PageGrant.GRANT_OWNER in dataApplicableGrant)}
  130. checked={selectedGrant === PageGrant.GRANT_OWNER}
  131. onChange={() => setSelectedGrant(PageGrant.GRANT_OWNER)}
  132. />
  133. <label className="form-label form-check-label" htmlFor="grantUser">
  134. { t('fix_page_grant.modal.radio_btn.only_me') }
  135. </label>
  136. </div>
  137. <div className="form-check d-flex mb-3">
  138. <input
  139. className="form-check-input"
  140. name="grantUserGroup"
  141. id="grantUserGroup"
  142. type="radio"
  143. disabled={!(PageGrant.GRANT_USER_GROUP in dataApplicableGrant)}
  144. checked={selectedGrant === PageGrant.GRANT_USER_GROUP}
  145. onChange={() => setSelectedGrant(PageGrant.GRANT_USER_GROUP)}
  146. />
  147. <label className="form-label form-check-label" htmlFor="grantUserGroup">
  148. { t('fix_page_grant.modal.radio_btn.grant_group') }
  149. </label>
  150. <div className="dropdown ms-2">
  151. <button
  152. type="button"
  153. className="btn btn-secondary dropdown-toggle text-right w-100 border-0 shadow-none"
  154. data-toggle="dropdown"
  155. disabled={selectedGrant !== PageGrant.GRANT_USER_GROUP} // disable when its radio input is not selected
  156. >
  157. <span className="float-start ms-2">
  158. {
  159. selectedGroup == null
  160. ? t('fix_page_grant.modal.select_group_default_text')
  161. : selectedGroup.item.name
  162. }
  163. </span>
  164. </button>
  165. <div className="dropdown-menu">
  166. {
  167. applicableGroups != null && applicableGroups.map(g => (
  168. <button
  169. key={g.item._id}
  170. className="dropdown-item"
  171. type="button"
  172. onClick={() => setSelectedGroup(g)}
  173. >
  174. {g.item.name}
  175. </button>
  176. ))
  177. }
  178. </div>
  179. </div>
  180. </div>
  181. {
  182. shouldShowModalAlert && (
  183. <p className="alert alert-warning">
  184. {t('fix_page_grant.modal.alert_message')}
  185. </p>
  186. )
  187. }
  188. </div>
  189. </div>
  190. </ModalBody>
  191. <ModalFooter>
  192. <button type="button" className="btn btn-primary" onClick={submit}>
  193. { t('fix_page_grant.modal.btn_label') }
  194. </button>
  195. </ModalFooter>
  196. </>
  197. );
  198. };
  199. return (
  200. <Modal size="lg" isOpen={isOpen} toggle={close}>
  201. <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
  202. { t('fix_page_grant.modal.title') }
  203. </ModalHeader>
  204. {renderModalBodyAndFooter()}
  205. </Modal>
  206. );
  207. };
  208. export const FixPageGrantAlert = (): JSX.Element => {
  209. const { t } = useTranslation();
  210. const { data: currentUser } = useCurrentUser();
  211. const { data: pageData } = useSWRxCurrentPage();
  212. const hasParent = pageData != null ? pageData.parent != null : false;
  213. const pageId = pageData?._id;
  214. const [isOpen, setOpen] = useState<boolean>(false);
  215. const { data: dataIsGrantNormalized } = useSWRxIsGrantNormalized(currentUser != null ? pageId : null);
  216. const { data: dataApplicableGrant } = useSWRxApplicableGrant(currentUser != null ? pageId : null);
  217. // Dependencies
  218. if (pageData == null) {
  219. return <></>;
  220. }
  221. if (!hasParent) {
  222. return <></>;
  223. }
  224. if (dataIsGrantNormalized?.isGrantNormalized == null || dataIsGrantNormalized.isGrantNormalized) {
  225. return <></>;
  226. }
  227. return (
  228. <>
  229. <div className="alert alert-warning py-3 ps-4 d-flex flex-column flex-lg-row">
  230. <div className="flex-grow-1 d-flex align-items-center">
  231. <i className="icon-fw icon-exclamation ms-1" aria-hidden="true" />
  232. {t('fix_page_grant.alert.description')}
  233. </div>
  234. <div className="d-flex align-items-end align-items-lg-center">
  235. <button type="button" className="btn btn-info btn-sm rounded-pill px-3" onClick={() => setOpen(true)}>
  236. {t('fix_page_grant.alert.btn_label')}
  237. </button>
  238. </div>
  239. </div>
  240. {
  241. pageId != null && dataApplicableGrant != null && (
  242. <FixPageGrantModal
  243. isOpen={isOpen}
  244. pageId={pageId}
  245. dataApplicableGrant={dataApplicableGrant}
  246. currentAndParentPageGrantData={dataIsGrantNormalized.grantData}
  247. close={() => setOpen(false)}
  248. />
  249. )
  250. }
  251. </>
  252. );
  253. };