Просмотр исходного кода

Merge pull request #6153 from weseek/feat/98983-fix-grant-alert

create FixPageGrantAlert
yuken 3 лет назад
Родитель
Сommit
32f91cfcbc

+ 283 - 0
packages/app/src/components/PageAlert/FixPageGrantAlert.tsx

@@ -0,0 +1,283 @@
+import React, { useEffect, useState, useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+
+import { toastError, toastSuccess } from '~/client/util/apiNotification';
+import { apiv3Put } from '~/client/util/apiv3-client';
+import { PageGrant, IPageGrantData } from '~/interfaces/page';
+import { IRecordApplicableGrant, IResIsGrantNormalizedGrantData } from '~/interfaces/page-grant';
+import { useCurrentUser } from '~/stores/context';
+import { useSWRxApplicableGrant, useSWRxIsGrantNormalized, useSWRxCurrentPage } from '~/stores/page';
+
+type ModalProps = {
+  isOpen: boolean
+  pageId: string
+  dataApplicableGrant: IRecordApplicableGrant
+  currentAndParentPageGrantData: IResIsGrantNormalizedGrantData
+  close(): void
+}
+
+const FixPageGrantModal = (props: ModalProps): JSX.Element => {
+  const { t } = useTranslation();
+
+  const {
+    isOpen, pageId, dataApplicableGrant, currentAndParentPageGrantData, close,
+  } = props;
+
+  const [selectedGrant, setSelectedGrant] = useState<PageGrant>(PageGrant.GRANT_RESTRICTED);
+  const [selectedGroup, setSelectedGroup] = useState<{_id: string, name: string} | undefined>(undefined); // TODO: Typescriptize model
+
+  // Alert message state
+  const [shouldShowModalAlert, setShowModalAlert] = useState<boolean>(false);
+
+  const applicableGroups = dataApplicableGrant[PageGrant.GRANT_USER_GROUP]?.applicableGroups;
+
+  // Reset state when opened
+  useEffect(() => {
+    if (isOpen) {
+      setSelectedGrant(PageGrant.GRANT_RESTRICTED);
+      setSelectedGroup(undefined);
+      setShowModalAlert(false);
+    }
+  }, [isOpen]);
+
+  const submit = async() => {
+    // Validate input values
+    if (selectedGrant === PageGrant.GRANT_USER_GROUP && selectedGroup == null) {
+      setShowModalAlert(true);
+      return;
+    }
+
+    close();
+
+    try {
+      await apiv3Put(`/page/${pageId}/grant`, {
+        grant: selectedGrant,
+        grantedGroup: selectedGroup?._id,
+      });
+
+      toastSuccess(t('Successfully updated'));
+    }
+    catch (err) {
+      toastError(t('Failed to update'));
+    }
+  };
+
+  const getGrantLabel = useCallback((isForbidden: boolean, grantData?: IPageGrantData): string => {
+
+    if (isForbidden) {
+      return t('fix_page_grant.modal.grant_label.isForbidden');
+    }
+
+    if (grantData == null) {
+      return t('fix_page_grant.modal.grant_label.isForbidden');
+    }
+
+    if (grantData.grant === 4) {
+      return t('fix_page_grant.modal.radio_btn.only_me');
+    }
+
+    if (grantData.grant === 5) {
+      if (grantData.grantedGroup == null) {
+        return t('fix_page_grant.modal.grant_label.isForbidden');
+      }
+      return `${t('fix_page_grant.modal.radio_btn.grant_group')}: (${grantData.grantedGroup.name})`;
+    }
+
+    throw Error('cannnot get grant label'); // this error can't be throwed
+  }, [t]);
+
+  const renderGrantDataLabel = useCallback(() => {
+    const { isForbidden, currentPageGrant, parentPageGrant } = currentAndParentPageGrantData;
+
+    const currentGrantLabel = getGrantLabel(false, currentPageGrant);
+    const parentGrantLabel = getGrantLabel(isForbidden, parentPageGrant);
+
+    return (
+      <>
+        <p className="mt-3">{ t('fix_page_grant.modal.grant_label.parentPageGrantLabel') + parentGrantLabel }</p>
+        <p>{ t('fix_page_grant.modal.grant_label.currentPageGrantLabel') + currentGrantLabel }</p>
+        {/* eslint-disable-next-line react/no-danger */}
+        <p dangerouslySetInnerHTML={{ __html: t('fix_page_grant.modal.grant_label.docLink') }} />
+      </>
+    );
+  }, [t, currentAndParentPageGrantData, getGrantLabel]);
+
+  const renderModalBodyAndFooter = () => {
+    const isGrantAvailable = Object.keys(dataApplicableGrant || {}).length > 0;
+
+    if (!isGrantAvailable) {
+      return (
+        <p className="m-5">
+          { t('fix_page_grant.modal.no_grant_available') }
+        </p>
+      );
+    }
+
+    return (
+      <>
+        <ModalBody>
+          <div className="form-group">
+            {/* eslint-disable-next-line react/no-danger */}
+            <p className="mb-2" dangerouslySetInnerHTML={{ __html: t('fix_page_grant.modal.need_to_fix_grant') }} />
+
+            {/* grant data label */}
+            {renderGrantDataLabel()}
+
+            <div className="ml-2">
+              <div className="custom-control custom-radio mb-3">
+                <input
+                  className="custom-control-input"
+                  name="grantRestricted"
+                  id="grantRestricted"
+                  type="radio"
+                  disabled={!(PageGrant.GRANT_RESTRICTED in dataApplicableGrant)}
+                  checked={selectedGrant === PageGrant.GRANT_RESTRICTED}
+                  onChange={() => setSelectedGrant(PageGrant.GRANT_RESTRICTED)}
+                />
+                <label className="custom-control-label" htmlFor="grantRestricted">
+                  { t('fix_page_grant.modal.radio_btn.restrected') }
+                </label>
+              </div>
+              <div className="custom-control custom-radio mb-3">
+                <input
+                  className="custom-control-input"
+                  name="grantUser"
+                  id="grantUser"
+                  type="radio"
+                  disabled={!(PageGrant.GRANT_OWNER in dataApplicableGrant)}
+                  checked={selectedGrant === PageGrant.GRANT_OWNER}
+                  onChange={() => setSelectedGrant(PageGrant.GRANT_OWNER)}
+                />
+                <label className="custom-control-label" htmlFor="grantUser">
+                  { t('fix_page_grant.modal.radio_btn.only_me') }
+                </label>
+              </div>
+              <div className="custom-control custom-radio d-flex mb-3">
+                <input
+                  className="custom-control-input"
+                  name="grantUserGroup"
+                  id="grantUserGroup"
+                  type="radio"
+                  disabled={!(PageGrant.GRANT_USER_GROUP in dataApplicableGrant)}
+                  checked={selectedGrant === PageGrant.GRANT_USER_GROUP}
+                  onChange={() => setSelectedGrant(PageGrant.GRANT_USER_GROUP)}
+                />
+                <label className="custom-control-label" htmlFor="grantUserGroup">
+                  { t('fix_page_grant.modal.radio_btn.grant_group') }
+                </label>
+                <div className="dropdown ml-2">
+                  <button
+                    type="button"
+                    className="btn btn-secondary dropdown-toggle text-right w-100 border-0 shadow-none"
+                    data-toggle="dropdown"
+                    disabled={selectedGrant !== PageGrant.GRANT_USER_GROUP} // disable when its radio input is not selected
+                  >
+                    <span className="float-left ml-2">
+                      {
+                        selectedGroup == null
+                          ? t('fix_page_grant.modal.select_group_default_text')
+                          : selectedGroup.name
+                      }
+                    </span>
+                  </button>
+                  <div className="dropdown-menu">
+                    {
+                      applicableGroups != null && applicableGroups.map(g => (
+                        <button
+                          className="dropdown-item"
+                          type="button"
+                          onClick={() => setSelectedGroup(g)}
+                        >
+                          {g.name}
+                        </button>
+                      ))
+                    }
+                  </div>
+                </div>
+              </div>
+              {
+                shouldShowModalAlert && (
+                  <p className="alert alert-warning">
+                    {t('fix_page_grant.modal.alert_message')}
+                  </p>
+                )
+              }
+            </div>
+          </div>
+        </ModalBody>
+        <ModalFooter>
+          <button type="button" className="btn btn-primary" onClick={submit}>
+            { t('fix_page_grant.modal.btn_label') }
+          </button>
+        </ModalFooter>
+      </>
+    );
+  };
+
+  return (
+    <Modal size="lg" isOpen={isOpen} toggle={close} className="grw-create-page">
+      <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
+        { t('fix_page_grant.modal.title') }
+      </ModalHeader>
+      {renderModalBodyAndFooter()}
+    </Modal>
+  );
+};
+
+export const FixPageGrantAlert = (): JSX.Element => {
+  const { t } = useTranslation();
+
+  const { data: currentUser } = useCurrentUser();
+  const { data: pageData } = useSWRxCurrentPage();
+  const hasParent = pageData != null ? pageData.parent != null : false
+  const pageId  = pageData?._id;
+
+  const [isOpen, setOpen] = useState<boolean>(false);
+
+  const { data: dataIsGrantNormalized } = useSWRxIsGrantNormalized(currentUser != null ? pageId : null);
+  const { data: dataApplicableGrant } = useSWRxApplicableGrant(currentUser != null ? pageId : null);
+
+  // Dependencies
+  if (pageData == null) {
+    return <></>
+  }
+
+  if (!hasParent) {
+    return <></>;
+  }
+  if (dataIsGrantNormalized?.isGrantNormalized == null || dataIsGrantNormalized.isGrantNormalized) {
+    return <></>;
+  }
+
+  return (
+    <>
+      <div className="alert alert-warning py-3 pl-4 d-flex flex-column flex-lg-row">
+        <div className="flex-grow-1 d-flex align-items-center">
+          <i className="icon-fw icon-exclamation ml-1" aria-hidden="true" />
+          {t('fix_page_grant.alert.description')}
+        </div>
+        <div className="d-flex align-items-end align-items-lg-center">
+          <button type="button" className="btn btn-info btn-sm rounded-pill px-3" onClick={() => setOpen(true)}>
+            {t('fix_page_grant.alert.btn_label')}
+          </button>
+        </div>
+      </div>
+
+      {
+        pageId != null && dataApplicableGrant != null && (
+          <FixPageGrantModal
+            isOpen={isOpen}
+            pageId={pageId}
+            dataApplicableGrant={dataApplicableGrant}
+            currentAndParentPageGrantData={dataIsGrantNormalized.grantData}
+            close={() => setOpen(false)}
+          />
+        )
+      }
+    </>
+  );
+};

+ 2 - 0
packages/app/src/components/PageAlert/PageAlerts.tsx

@@ -1,3 +1,4 @@
+import {FixPageGrantAlert} from "./FixPageGrantAlert";
 
 
 export const PageAlerts = (): JSX.Element => {
@@ -7,6 +8,7 @@ export const PageAlerts = (): JSX.Element => {
     <div className="row d-edit-none">
       <div className="col-sm-12">
         {/* alerts */}
+        <FixPageGrantAlert/>
       </div>
     </div>
   );

+ 4 - 3
packages/app/src/pages/[[...path]].page.tsx

@@ -7,7 +7,7 @@ import {
 import Head from 'next/head';
 import { useRouter } from 'next/router';
 
-// import { PageAlerts } from '~/components/PageAlert/PageAlerts';
+import { PageAlerts } from '~/components/PageAlert/PageAlerts';
 // import { PageComments } from '~/components/PageComment/PageComments';
 // import { useTranslation } from '~/i18n';
 import { CrowiRequest } from '~/interfaces/crowi-request';
@@ -38,6 +38,7 @@ import {
   useAppTitle, useSiteUrl, useConfidential, useIsEnabledStaleNotification,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsMailerSetup,
   useAclEnabled, useHasSlackConfig, useDrawioUri, useHackmdUri, useMathJax, useNoCdn, useEditorConfig, useCsrfToken,
+  useCurrentPageId
 } from '../stores/context';
 
 import { CommonProps, getServerSideCommonProps, useCustomTitle } from './commons';
@@ -131,6 +132,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   if (props.pageWithMetaStr != null) {
     pageWithMeta = JSON.parse(props.pageWithMetaStr) as IPageWithMeta;
   }
+  useCurrentPageId(pageWithMeta?.data._id)
   useSWRxCurrentPage(undefined, pageWithMeta?.data); // store initial data
   useSWRxPageInfo(pageWithMeta?.data._id, undefined, pageWithMeta?.meta); // store initial data
 
@@ -188,8 +190,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
           <div className="row">
             <div className="col grw-page-content-container">
               <div id="content-main" className="content-main grw-container-convertible">
-                {/* <PageAlerts /> */}
-                PageAlerts<br />
+                <PageAlerts />
                 {/* <DisplaySwitcher /> */}
                 DisplaySwitcher<br />
                 <div id="page-editor-navbar-bottom-container" className="d-none d-edit-block"></div>