FixPageGrantAlert.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. import React, { useEffect, useState, useCallback } from 'react';
  2. import { PageGrant, GroupType } 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 type { IPageGrantData } from '~/interfaces/page';
  10. import type { 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 [selectedGroups, setSelectedGroups] = useState<PopulatedGrantedGroup[]>([]);
  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. setSelectedGroups([]);
  36. setShowModalAlert(false);
  37. }
  38. }, [isOpen]);
  39. const groupListItemClickHandler = (group: PopulatedGrantedGroup) => {
  40. if (selectedGroups.find(g => g.item._id === group.item._id) != null) {
  41. setSelectedGroups(selectedGroups.filter(g => g.item._id !== group.item._id));
  42. }
  43. else {
  44. setSelectedGroups([...selectedGroups, group]);
  45. }
  46. };
  47. const submit = async() => {
  48. // Validate input values
  49. if (selectedGrant === PageGrant.GRANT_USER_GROUP && selectedGroups.length === 0) {
  50. setShowModalAlert(true);
  51. return;
  52. }
  53. close();
  54. try {
  55. await apiv3Put(`/page/${pageId}/grant`, {
  56. grant: selectedGrant,
  57. userRelatedGrantedGroups: selectedGroups.length !== 0 ? selectedGroups.map((g) => {
  58. return { item: g.item._id, type: g.type };
  59. }) : null,
  60. });
  61. toastSuccess(t('Successfully updated'));
  62. }
  63. catch (err) {
  64. toastError(t('Failed to update'));
  65. }
  66. };
  67. const getGrantLabel = useCallback((isForbidden: boolean, grantData?: IPageGrantData): string => {
  68. if (isForbidden) {
  69. return t('fix_page_grant.modal.grant_label.isForbidden');
  70. }
  71. if (grantData == null) {
  72. return t('fix_page_grant.modal.grant_label.isForbidden');
  73. }
  74. if (grantData.grant === 1) {
  75. return t('fix_page_grant.modal.grant_label.public');
  76. }
  77. if (grantData.grant === 4) {
  78. return t('fix_page_grant.modal.radio_btn.only_me');
  79. }
  80. if (grantData.grant === 5) {
  81. if (grantData.userRelatedGrantedGroups == null || grantData.userRelatedGrantedGroups.length === 0) {
  82. return t('fix_page_grant.modal.grant_label.isForbidden');
  83. }
  84. return `${t('fix_page_grant.modal.radio_btn.grant_group')} (${grantData.userRelatedGrantedGroups.map(g => g.name).join(', ')})`;
  85. }
  86. throw Error('cannot get grant label'); // this error can't be throwed
  87. }, [t]);
  88. const renderGrantDataLabel = useCallback(() => {
  89. const { isForbidden, currentPageGrant, parentPageGrant } = currentAndParentPageGrantData;
  90. const currentGrantLabel = getGrantLabel(false, currentPageGrant);
  91. const parentGrantLabel = getGrantLabel(isForbidden, parentPageGrant);
  92. return (
  93. <>
  94. <p className="mt-3">{ t('fix_page_grant.modal.grant_label.parentPageGrantLabel') + parentGrantLabel }</p>
  95. <p>{ t('fix_page_grant.modal.grant_label.currentPageGrantLabel') + currentGrantLabel }</p>
  96. {/* eslint-disable-next-line react/no-danger */}
  97. <p dangerouslySetInnerHTML={{ __html: t('fix_page_grant.modal.grant_label.docLink') }} />
  98. </>
  99. );
  100. }, [t, currentAndParentPageGrantData, getGrantLabel]);
  101. const renderModalBodyAndFooter = () => {
  102. const isGrantAvailable = Object.keys(dataApplicableGrant || {}).length > 0;
  103. if (!isGrantAvailable) {
  104. return (
  105. <p className="m-5">
  106. { t('fix_page_grant.modal.no_grant_available') }
  107. </p>
  108. );
  109. }
  110. return (
  111. <>
  112. <ModalBody>
  113. <div>
  114. {/* eslint-disable-next-line react/no-danger */}
  115. <p className="mb-2" dangerouslySetInnerHTML={{ __html: t('fix_page_grant.modal.need_to_fix_grant') }} />
  116. {/* grant data label */}
  117. {renderGrantDataLabel()}
  118. <div className="ms-2">
  119. <div className="form-check mb-3">
  120. <input
  121. className="form-check-input"
  122. name="grantRestricted"
  123. id="grantRestricted"
  124. type="radio"
  125. disabled={!(PageGrant.GRANT_RESTRICTED in dataApplicableGrant)}
  126. checked={selectedGrant === PageGrant.GRANT_RESTRICTED}
  127. onChange={() => setSelectedGrant(PageGrant.GRANT_RESTRICTED)}
  128. />
  129. <label className="form-label form-check-label" htmlFor="grantRestricted">
  130. { t('fix_page_grant.modal.radio_btn.restrected') }
  131. </label>
  132. </div>
  133. <div className="form-check mb-3">
  134. <input
  135. className="form-check-input"
  136. name="grantUser"
  137. id="grantUser"
  138. type="radio"
  139. disabled={!(PageGrant.GRANT_OWNER in dataApplicableGrant)}
  140. checked={selectedGrant === PageGrant.GRANT_OWNER}
  141. onChange={() => setSelectedGrant(PageGrant.GRANT_OWNER)}
  142. />
  143. <label className="form-label form-check-label" htmlFor="grantUser">
  144. { t('fix_page_grant.modal.radio_btn.only_me') }
  145. </label>
  146. </div>
  147. <div className="form-check d-flex mb-3">
  148. <input
  149. className="form-check-input"
  150. name="grantUserGroup"
  151. id="grantUserGroup"
  152. type="radio"
  153. disabled={!(PageGrant.GRANT_USER_GROUP in dataApplicableGrant)}
  154. checked={selectedGrant === PageGrant.GRANT_USER_GROUP}
  155. onChange={() => setSelectedGrant(PageGrant.GRANT_USER_GROUP)}
  156. />
  157. <label className="form-label form-check-label" htmlFor="grantUserGroup">
  158. { t('fix_page_grant.modal.radio_btn.grant_group') }
  159. </label>
  160. <div className="dropdown ms-2">
  161. <button
  162. type="button"
  163. className="btn btn-secondary dropdown-toggle text-right w-100 border-0 shadow-none"
  164. disabled={selectedGrant !== PageGrant.GRANT_USER_GROUP} // disable when its radio input is not selected
  165. onClick={() => setIsGroupSelectModalShown(true)}
  166. >
  167. <span className="float-start ms-2">
  168. {
  169. selectedGroups.length === 0
  170. ? t('fix_page_grant.modal.select_group_default_text')
  171. : selectedGroups.map(g => g.item.name).join(', ')
  172. }
  173. </span>
  174. </button>
  175. </div>
  176. </div>
  177. {
  178. shouldShowModalAlert && (
  179. <p className="alert alert-warning">
  180. {t('fix_page_grant.modal.alert_message')}
  181. </p>
  182. )
  183. }
  184. </div>
  185. </div>
  186. </ModalBody>
  187. <ModalFooter>
  188. <button type="button" className="btn btn-primary" onClick={submit}>
  189. { t('fix_page_grant.modal.btn_label') }
  190. </button>
  191. </ModalFooter>
  192. </>
  193. );
  194. };
  195. return (
  196. <>
  197. <Modal size="lg" isOpen={isOpen} toggle={close}>
  198. <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
  199. { t('fix_page_grant.modal.title') }
  200. </ModalHeader>
  201. {renderModalBodyAndFooter()}
  202. </Modal>
  203. {applicableGroups != null && (
  204. <Modal
  205. isOpen={isGroupSelectModalShown}
  206. toggle={() => setIsGroupSelectModalShown(false)}
  207. >
  208. <ModalHeader tag="h4" toggle={() => setIsGroupSelectModalShown(false)} className="bg-purple text-light">
  209. {t('user_group.select_group')}
  210. </ModalHeader>
  211. <ModalBody>
  212. <>
  213. { applicableGroups.map((group) => {
  214. const groupIsGranted = selectedGroups?.find(g => g.item._id === group.item._id) != null;
  215. const activeClass = groupIsGranted ? 'active' : '';
  216. return (
  217. <button
  218. className={`btn btn-outline-primary w-100 d-flex justify-content-start mb-3 align-items-center p-3 ${activeClass}`}
  219. type="button"
  220. key={group.item._id}
  221. onClick={() => groupListItemClickHandler(group)}
  222. >
  223. <span className="align-middle"><input type="checkbox" checked={groupIsGranted} /></span>
  224. <h5 className="d-inline-block ml-3">{group.item.name}</h5>
  225. {group.type === GroupType.externalUserGroup && <span className="ml-2 badge badge-pill badge-info">{group.item.provider}</span>}
  226. {/* TODO: Replace <div className="small">(TBD) List group members</div> */}
  227. </button>
  228. );
  229. }) }
  230. <button type="button" className="btn btn-primary mt-2 float-right" onClick={() => setIsGroupSelectModalShown(false)}>{t('Done')}</button>
  231. </>
  232. </ModalBody>
  233. </Modal>
  234. )}
  235. </>
  236. );
  237. };
  238. export const FixPageGrantAlert = (): JSX.Element => {
  239. const { t } = useTranslation();
  240. const { data: currentUser } = useCurrentUser();
  241. const { data: pageData } = useSWRxCurrentPage();
  242. const hasParent = pageData != null ? pageData.parent != null : false;
  243. const pageId = pageData?._id;
  244. const [isOpen, setOpen] = useState<boolean>(false);
  245. const { data: dataIsGrantNormalized } = useSWRxIsGrantNormalized(currentUser != null ? pageId : null);
  246. const { data: dataApplicableGrant } = useSWRxApplicableGrant(currentUser != null ? pageId : null);
  247. // Dependencies
  248. if (pageData == null) {
  249. return <></>;
  250. }
  251. if (!hasParent) {
  252. return <></>;
  253. }
  254. if (dataIsGrantNormalized?.isGrantNormalized == null || dataIsGrantNormalized.isGrantNormalized) {
  255. return <></>;
  256. }
  257. return (
  258. <>
  259. <div className="alert alert-warning py-3 ps-4 d-flex flex-column flex-lg-row">
  260. <div className="flex-grow-1 d-flex align-items-center">
  261. <span className="material-symbols-outlined mx-1" aria-hidden="true">error</span>
  262. {t('fix_page_grant.alert.description')}
  263. </div>
  264. <div className="d-flex align-items-end align-items-lg-center">
  265. <button type="button" className="btn btn-info btn-sm rounded-pill px-3" onClick={() => setOpen(true)}>
  266. {t('fix_page_grant.alert.btn_label')}
  267. </button>
  268. </div>
  269. </div>
  270. {
  271. pageId != null && dataApplicableGrant != null && (
  272. <FixPageGrantModal
  273. isOpen={isOpen}
  274. pageId={pageId}
  275. dataApplicableGrant={dataApplicableGrant}
  276. currentAndParentPageGrantData={dataIsGrantNormalized.grantData}
  277. close={() => setOpen(false)}
  278. />
  279. )
  280. }
  281. </>
  282. );
  283. };