FixPageGrantAlert.tsx 10 KB

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