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

132486 able to open modal using swr

soumaeda 2 лет назад
Родитель
Сommit
d8ed592545

+ 11 - 35
apps/app/src/components/PageControls/PageControls.tsx

@@ -16,8 +16,9 @@ import {
 } from '~/client/services/page-operation';
 import { apiPost } from '~/client/util/apiv1-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
+import { tags } from '~/services/xss/recommended-whitelist';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
-import type { IPageForPageDuplicateModal } from '~/stores/modal';
+import { useTagEditModal, type IPageForPageDuplicateModal } from '~/stores/modal';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 
 import { useSWRxPageInfo, useSWRxTagsInfo } from '../../stores/page';
@@ -26,7 +27,6 @@ import {
   AdditionalMenuItemsRendererProps, ForceHideMenuItems, MenuItemType,
   PageItemControl,
 } from '../Common/Dropdown/PageItemControl';
-import { PageTagsSkeleton } from '../PageTags';
 
 import { BookmarkButtons } from './BookmarkButtons';
 import LikeButtons from './LikeButtons';
@@ -36,57 +36,33 @@ import SubscribeButton from './SubscribeButton';
 
 import styles from './PageControls.module.scss';
 
-
-const PageTags = dynamic(() => import('../PageTags').then(mod => mod.PageTags), {
-  ssr: false,
-  loading: PageTagsSkeleton,
-});
-
 type TagsProps = {
   pageId: string,
   revisionId: string,
 }
 
 const Tags = (props: TagsProps): JSX.Element => {
-  const { pageId, revisionId } = props;
+  const { pageId } = props;
 
   const { data: tagsInfoData } = useSWRxTagsInfo(pageId);
 
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
+  const { open: openTagEditModal } = useTagEditModal();
 
-  const updateStateAfterSave = useUpdateStateAfterSave(pageId);
-
-  const tagsUpdatedHandler = useCallback(async(newTags: string[]) => {
-    try {
-      await apiPost('/tags.update', { pageId, revisionId, tags: newTags });
-
-      updateStateAfterSave?.();
-      toastSuccess('updated tags successfully');
-    }
-    catch (err) {
-      toastError(err);
-    }
-
-  }, [pageId, revisionId, updateStateAfterSave]);
 
   const isTagLabelsDisabled = !!isGuestUser || !!isReadOnlyUser;
 
-  const isDisappear = true;
 
   return (
     <div className="grw-taglabels-container d-flex align-items-center">
-      { tagsInfoData?.tags != null
-        ? (
-          <PageTags
-            tags={tagsInfoData.tags}
-            isTagLabelsDisabled={isTagLabelsDisabled ?? false}
-            isDisappear={isDisappear}
-            tagsUpdateInvoked={tagsUpdatedHandler}
-          />
-        )
-        : <PageTagsSkeleton />
-      }
+      <a
+        className="btn btn-link btn-edit-tags text-muted p-0 d-flex align-items-center"
+        onClick={() => openTagEditModal(tagsInfoData?.tags)}
+      >
+        <i className="icon-tag me-2" />
+        Tags
+      </a>
     </div>
   );
 };

+ 1 - 15
apps/app/src/components/PageSideContents/PageSideContents.tsx

@@ -48,20 +48,6 @@ const Tags = (props: TagsProps): JSX.Element => {
 
   const updateStateAfterSave = useUpdateStateAfterSave(pageId);
 
-  const tagsUpdatedHandler = useCallback(async(newTags: string[]) => {
-    try {
-      await apiPost('/tags.update', { pageId, revisionId, tags: newTags });
-
-      updateStateAfterSave?.();
-
-      toastSuccess('updated tags successfully');
-    }
-    catch (err) {
-      toastError(err);
-    }
-
-  }, [pageId, revisionId, updateStateAfterSave]);
-
   if (!showTagLabel) {
     return <></>;
   }
@@ -78,7 +64,7 @@ const Tags = (props: TagsProps): JSX.Element => {
             tags={tagsInfoData.tags}
             isTagLabelsDisabled={isTagLabelsDisabled ?? false}
             isDisappear={isDisappear}
-            tagsUpdateInvoked={tagsUpdatedHandler}
+            pageId={pageId}
           />
         )
         : <PageTagsSkeleton />

+ 4 - 19
apps/app/src/components/PageTags/PageTags.tsx

@@ -3,7 +3,7 @@ import React, { FC, useState } from 'react';
 import { Skeleton } from '../Skeleton';
 
 import RenderTagLabels from './RenderTagLabels';
-import TagEditModal from './TagEditModal';
+import { TagEditModal } from './TagEditModal';
 
 import styles from './TagLabels.module.scss';
 
@@ -12,6 +12,7 @@ type Props = {
   isTagLabelsDisabled: boolean,
   isDisappear: boolean,
   tagsUpdateInvoked?: (tags: string[]) => Promise<void> | void,
+  pageId: string,
 }
 
 export const PageTagsSkeleton = (): JSX.Element => {
@@ -20,19 +21,9 @@ export const PageTagsSkeleton = (): JSX.Element => {
 
 export const PageTags:FC<Props> = (props: Props) => {
   const {
-    tags, isTagLabelsDisabled, isDisappear, tagsUpdateInvoked,
+    tags, isTagLabelsDisabled, isDisappear, pageId,
   } = props;
 
-  const [isTagEditModalShown, setIsTagEditModalShown] = useState(false);
-
-  const openEditorModal = () => {
-    setIsTagEditModalShown(true);
-  };
-
-  const closeEditorModal = () => {
-    setIsTagEditModalShown(false);
-  };
-
   if (tags == null) {
     return <PageTagsSkeleton />;
   }
@@ -42,17 +33,11 @@ export const PageTags:FC<Props> = (props: Props) => {
       <div className={`${styles['grw-tag-labels']} grw-tag-labels d-flex align-items-center`} data-testid="grw-tag-labels">
         <RenderTagLabels
           tags={tags}
-          openEditorModal={openEditorModal}
           isTagLabelsDisabled={isTagLabelsDisabled}
           isDisappear={isDisappear}
+          pageId={pageId}
         />
       </div>
-      <TagEditModal
-        tags={tags}
-        isOpen={isTagEditModalShown}
-        onClose={closeEditorModal}
-        onTagsUpdated={tagsUpdateInvoked}
-      />
     </>
   );
 };

+ 9 - 21
apps/app/src/components/PageTags/RenderTagLabels.tsx

@@ -3,6 +3,8 @@ import React from 'react';
 import { useTranslation } from 'next-i18next';
 
 import { useKeywordManager } from '~/client/services/search-operation';
+import { useTagEditModal } from '~/stores/modal';
+import { useSWRxTagsInfo } from '~/stores/page';
 
 import { NotAvailableForGuest } from '../NotAvailableForGuest';
 import { NotAvailableForReadOnlyUser } from '../NotAvailableForReadOnlyUser';
@@ -11,23 +13,18 @@ type RenderTagLabelsProps = {
   tags: string[],
   isTagLabelsDisabled: boolean,
   isDisappear: boolean,
-  openEditorModal?: () => void,
+  pageId: string,
 }
 
 const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
   const {
-    tags, isTagLabelsDisabled, isDisappear, openEditorModal,
+    tags, isTagLabelsDisabled, isDisappear, pageId,
   } = props;
   const { t } = useTranslation();
 
   const { pushState } = useKeywordManager();
-
-  function openEditorHandler() {
-    if (openEditorModal == null) {
-      return;
-    }
-    openEditorModal();
-  }
+  const { open: openTagEditModal } = useTagEditModal();
+  const { data: tagsInfoData } = useSWRxTagsInfo(pageId);
 
   const isTagsEmpty = tags.length === 0;
 
@@ -53,19 +50,10 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
                 ${isTagLabelsDisabled && 'disabled'}
                 ${isDisappear && 'border border-secondary p-1'}`
               }
-              onClick={openEditorHandler}
+              onClick={() => openTagEditModal(tagsInfoData?.tags)}
             >
-              {isDisappear ? (
-                <>
-                  <i className={`icon-tag me-2 ${isTagsEmpty && 'ms-1'}`} />
-                  Tags
-                </>
-              ) : (
-                <>
-                  {isTagsEmpty && <> {t('Add tags for this page')}</>}
-                  <i className={`icon-plus ${isTagsEmpty && 'ms-1'}`} />
-                </>
-              )}
+              {isTagsEmpty && <> {t('Add tags for this page')}</>}
+              <i className={`icon-plus ${isTagsEmpty && 'ms-1'}`} />
             </a>
           </div>
         </NotAvailableForReadOnlyUser>

+ 29 - 26
apps/app/src/components/PageTags/TagEditModal.tsx

@@ -5,41 +5,46 @@ import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
-import { TagsInput } from './TagsInput';
+import { useUpdateStateAfterSave } from '~/client/services/page-operation';
+import { apiPost } from '~/client/util/apiv1-client';
+import { toastError, toastSuccess } from '~/client/util/toastr';
+import { useTagEditModal } from '~/stores/modal';
+import { useSWRxTagsInfo } from '~/stores/page';
 
-type Props = {
-  tags: string[],
-  isOpen: boolean,
-  onClose?: () => void,
-  onTagsUpdated?: (tags: string[]) => Promise<void> | void,
-};
+import { TagsInput } from './TagsInput';
 
-function TagEditModal(props: Props): JSX.Element {
-  const { onClose, onTagsUpdated } = props;
+export const TagEditModal: React.FC = () => {
 
   const [tags, setTags] = useState<string[]>([]);
   const { t } = useTranslation();
+  const { data: tagEditModalData, close: closeTagEditModal } = useTagEditModal();
+  const isOpen = tagEditModalData?.isOpen;
+  const pageId = tagEditModalData?.pageId;
+  const revisionId = tagEditModalData?.revisionId;
+  const updateStateAfterSave = useUpdateStateAfterSave(pageId);
+  const { data: tagsInfoData } = useSWRxTagsInfo(pageId);
 
   useEffect(() => {
-    setTags(props.tags);
-  }, [props.tags]);
+    setTags(tags);
+  }, [tags]);
 
-  const closeModalHandler = useCallback(() => {
-    onClose?.();
-  }, [onClose]);
+  const handleSubmit = useCallback(async() => {
 
-  const handleSubmit = useCallback(() => {
-    if (onTagsUpdated == null) {
-      return;
-    }
+    try {
+      await apiPost('/tags.update', { pageId, revisionId, tags });
+      updateStateAfterSave?.();
 
-    onTagsUpdated(tags);
-    closeModalHandler();
-  }, [closeModalHandler, onTagsUpdated, tags]);
+      toastSuccess('updated tags successfully');
+    }
+    catch (err) {
+      toastError(err);
+    }
+    closeTagEditModal();
+  }, [tags, pageId, revisionId, closeTagEditModal]);
 
   return (
-    <Modal isOpen={props.isOpen} toggle={closeModalHandler} id="edit-tag-modal" autoFocus={false}>
-      <ModalHeader tag="h4" toggle={closeModalHandler} className="bg-primary text-light">
+    <Modal isOpen={isOpen} toggle={closeTagEditModal} id="edit-tag-modal" autoFocus={false}>
+      <ModalHeader tag="h4" toggle={closeTagEditModal} className="bg-primary text-light">
         {t('tag_edit_modal.edit_tags')}
       </ModalHeader>
       <ModalBody>
@@ -53,6 +58,4 @@ function TagEditModal(props: Props): JSX.Element {
     </Modal>
   );
 
-}
-
-export default TagEditModal;
+};

+ 2 - 0
apps/app/src/pages/[[...path]].page.tsx

@@ -76,6 +76,7 @@ const TemplateModal = dynamic(() => import('../components/TemplateModal').then(m
 const LinkEditModal = dynamic(() => import('../components/PageEditor/LinkEditModal').then(mod => mod.LinkEditModal), { ssr: false });
 const PageStatusAlert = dynamic(() => import('../components/PageStatusAlert').then(mod => mod.PageStatusAlert), { ssr: false });
 const QuestionnaireModalManager = dynamic(() => import('~/features/questionnaire/client/components/QuestionnaireModalManager'), { ssr: false });
+const TagEditModal = dynamic(() => import('../components/PageTags/TagEditModal').then(mod => mod.TagEditModal), { ssr: false });
 
 const logger = loggerFactory('growi:pages:all');
 
@@ -376,6 +377,7 @@ Page.getLayout = function getLayout(page: React.ReactElement<Props>) {
       <QuestionnaireModalManager />
       <TemplateModal />
       <LinkEditModal />
+      <TagEditModal />
     </>
   );
 };

+ 46 - 1
apps/app/src/stores/modal.tsx

@@ -3,14 +3,16 @@ import { useCallback, useMemo } from 'react';
 import type {
   IAttachmentHasId, IPageToDeleteWithMeta, IPageToRenameWithMeta, IUserGroupHasId,
 } from '@growi/core';
-import { SWRResponse } from 'swr';
+import useSWR, { SWRResponse, mutate } from 'swr';
 
 import Linker from '~/client/models/Linker';
 import MarkdownTable from '~/client/models/MarkdownTable';
 import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
+import { IPageTagsInfo } from '~/interfaces/tag';
 import type {
   OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction, OnPutBackedFunction, onDeletedBookmarkFolderFunction, OnSelectedFunction,
 } from '~/interfaces/ui';
+import { tags } from '~/services/xss/recommended-whitelist';
 import loggerFactory from '~/utils/logger';
 
 import { useStaticSWR } from './use-static-swr';
@@ -773,3 +775,46 @@ export const usePageSelectModal = (
     close: () => swrResponse.mutate({ isOpened: false }),
   };
 };
+
+
+type TagEditModalStatus = {
+  tags: [],
+  isOpen: boolean,
+  pageId: string,
+  revisionId: string,
+}
+
+type TagEditModalUtils = {
+  open(tags): Promise<void>
+  close(): Promise<void>,
+}
+
+
+export const useTagEditModal = (): SWRResponse<TagEditModalStatus, Error> & TagEditModalUtils => {
+  const initialStatus: TagEditModalStatus = {
+    isOpen: false,
+    tags: [],
+    pageId: '',
+    revisionId: '',
+  };
+
+  const swrResponse = useStaticSWR<TagEditModalStatus, Error>('TagEditModal', undefined, { fallbackData: initialStatus });
+  const { mutate } = swrResponse;
+
+  const open = async(tags) => {
+    mutate({
+      isOpen: true,
+      tags,
+    });
+  };
+
+  const close = async() => {
+    mutate(initialStatus);
+  };
+
+  return {
+    ...swrResponse,
+    open,
+    close,
+  };
+};