FixPageGrantAlert.tsx 10.0 KB

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