Kaynağa Gözat

fix conflict

jam411 3 yıl önce
ebeveyn
işleme
52dae401eb

+ 17 - 25
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -180,7 +180,6 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: shareLinkId } = useShareLinkId();
   const { data: shareLinkId } = useShareLinkId();
-  const { data: isNotFound } = useIsNotFound();
 
 
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
   const { data: isAbleToShowTagLabel } = useIsAbleToShowTagLabel();
   const { data: isAbleToShowTagLabel } = useIsAbleToShowTagLabel();
@@ -296,14 +295,6 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
 
 
 
   const ControlComponents = useCallback(() => {
   const ControlComponents = useCallback(() => {
-    if (currentPage == null || pageId == null) {
-      return <></>;
-    }
-
-    function onPageEditorModeButtonClicked(viewType) {
-      mutateEditorMode(viewType);
-    }
-
     const additionalMenuItemsRenderer = () => {
     const additionalMenuItemsRenderer = () => {
       if (revisionId == null || pageId == null) {
       if (revisionId == null || pageId == null) {
         return <></>;
         return <></>;
@@ -323,24 +314,26 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
 
           { isViewMode && (
           { isViewMode && (
             <div className="h-50 w-100">
             <div className="h-50 w-100">
-              <SubNavButtons
-                isCompactMode={isCompactMode}
-                pageId={pageId}
-                revisionId={revisionId}
-                shareLinkId={shareLinkId}
-                path={path}
-                disableSeenUserInfoPopover={isSharedUser}
-                showPageControlDropdown={isAbleToShowPageManagement}
-                additionalMenuItemRenderer={additionalMenuItemsRenderer}
-                onClickDuplicateMenuItem={duplicateItemClickedHandler}
-                onClickRenameMenuItem={renameItemClickedHandler}
-                onClickDeleteMenuItem={deleteItemClickedHandler}
-              />
+              { pageId != null && (
+                <SubNavButtons
+                  isCompactMode={isCompactMode}
+                  pageId={pageId}
+                  revisionId={revisionId}
+                  shareLinkId={shareLinkId}
+                  path={path}
+                  disableSeenUserInfoPopover={isSharedUser}
+                  showPageControlDropdown={isAbleToShowPageManagement}
+                  additionalMenuItemRenderer={additionalMenuItemsRenderer}
+                  onClickDuplicateMenuItem={duplicateItemClickedHandler}
+                  onClickRenameMenuItem={renameItemClickedHandler}
+                  onClickDeleteMenuItem={deleteItemClickedHandler}
+                />
+              ) }
             </div>
             </div>
           ) }
           ) }
           {isAbleToShowPageEditorModeManager && (
           {isAbleToShowPageEditorModeManager && (
             <PageEditorModeManager
             <PageEditorModeManager
-              onPageEditorModeButtonClicked={onPageEditorModeButtonClicked}
+              onPageEditorModeButtonClicked={viewType => mutateEditorMode(viewType)}
               isBtnDisabled={isGuestUser}
               isBtnDisabled={isGuestUser}
               editorMode={editorMode}
               editorMode={editorMode}
             />
             />
@@ -356,7 +349,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       </>
       </>
     );
     );
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
-  }, [currentPage, currentUser, pageId, revisionId, shareLinkId, path, editorMode, isCompactMode, isViewMode, isSharedUser, isAbleToShowPageManagement, isAbleToShowPageEditorModeManager, isLinkSharingDisabled, isGuestUser, isPageTemplateModalShown, duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler, mutateEditorMode, templateMenuItemClickHandler]);
+  }, [currentUser, pageId, revisionId, shareLinkId, path, editorMode, isCompactMode, isViewMode, isSharedUser, isAbleToShowPageManagement, isAbleToShowPageEditorModeManager, isLinkSharingDisabled, isGuestUser, isPageTemplateModalShown, duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler, mutateEditorMode, templateMenuItemClickHandler]);
 
 
   if (currentPathname == null) {
   if (currentPathname == null) {
     return <></>;
     return <></>;
@@ -375,7 +368,6 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       isGuestUser={isGuestUser}
       isGuestUser={isGuestUser}
       isDrawerMode={isDrawerMode}
       isDrawerMode={isDrawerMode}
       isCompactMode={isCompactMode}
       isCompactMode={isCompactMode}
-      isNotFound={isNotFound}
       tags={isViewMode ? tagsInfoData?.tags : tagsForEditors}
       tags={isViewMode ? tagsInfoData?.tags : tagsForEditors}
       tagsUpdatedHandler={isViewMode ? tagsUpdatedHandlerForViewMode : tagsUpdatedHandlerForEditMode}
       tagsUpdatedHandler={isViewMode ? tagsUpdatedHandlerForViewMode : tagsUpdatedHandlerForEditMode}
       controls={ControlComponents}
       controls={ControlComponents}

+ 15 - 18
packages/app/src/components/Navbar/GrowiSubNavigation.tsx

@@ -35,7 +35,6 @@ export type GrowiSubNavigationProps = {
   isGuestUser?: boolean,
   isGuestUser?: boolean,
   isDrawerMode?: boolean,
   isDrawerMode?: boolean,
   isCompactMode?: boolean,
   isCompactMode?: boolean,
-  isNotFound?: boolean,
   tags?: string[],
   tags?: string[],
   tagsUpdatedHandler?: (newTags: string[]) => Promise<void> | void,
   tagsUpdatedHandler?: (newTags: string[]) => Promise<void> | void,
   controls: React.FunctionComponent,
   controls: React.FunctionComponent,
@@ -49,7 +48,7 @@ export const GrowiSubNavigation = (props: GrowiSubNavigationProps): JSX.Element
   const {
   const {
     page,
     page,
     showDrawerToggler, showTagLabel, showPageAuthors,
     showDrawerToggler, showTagLabel, showPageAuthors,
-    isGuestUser, isDrawerMode, isCompactMode, isNotFound,
+    isGuestUser, isDrawerMode, isCompactMode,
     tags, tagsUpdatedHandler,
     tags, tagsUpdatedHandler,
     controls: Controls,
     controls: Controls,
     additionalClasses = [],
     additionalClasses = [],
@@ -88,22 +87,20 @@ export const GrowiSubNavigation = (props: GrowiSubNavigationProps): JSX.Element
         </div>
         </div>
       </div>
       </div>
       {/* Right side. isNotFound for avoid flicker when called ForbiddenPage.tsx */}
       {/* Right side. isNotFound for avoid flicker when called ForbiddenPage.tsx */}
-      { !isNotFound && (
-        <div className="d-flex">
-          <Controls />
-          {/* Page Authors */}
-          { (showPageAuthors && !isCompactMode) && (
-            <ul className={`${AuthorInfoStyles['grw-author-info']} text-nowrap border-left d-none d-lg-block d-edit-none py-2 pl-4 mb-0 ml-3`}>
-              <li className="pb-1">
-                <AuthorInfo user={creator as IUser} date={createdAt} locate="subnav" />
-              </li>
-              <li className="mt-1 pt-1 border-top">
-                <AuthorInfo user={lastUpdateUser as IUser} date={updatedAt} mode="update" locate="subnav" />
-              </li>
-            </ul>
-          ) }
-        </div>
-      ) }
+      <div className="d-flex">
+        <Controls />
+        {/* Page Authors */}
+        { (showPageAuthors && !isCompactMode) && (
+          <ul className={`${AuthorInfoStyles['grw-author-info']} text-nowrap border-left d-none d-lg-block d-edit-none py-2 pl-4 mb-0 ml-3`}>
+            <li className="pb-1">
+              <AuthorInfo user={creator as IUser} date={createdAt} locate="subnav" />
+            </li>
+            <li className="mt-1 pt-1 border-top">
+              <AuthorInfo user={lastUpdateUser as IUser} date={updatedAt} mode="update" locate="subnav" />
+            </li>
+          </ul>
+        ) }
+      </div>
     </div>
     </div>
   );
   );
 };
 };

+ 62 - 65
packages/app/src/components/PageAttachment.tsx

@@ -1,65 +1,49 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, {
+  useCallback, useMemo, useState,
+} from 'react';
 
 
-import { useTranslation } from 'next-i18next';
+import { HasObjectId, IAttachment } from '@growi/core';
 
 
 import { useSWRxAttachments } from '~/stores/attachment';
 import { useSWRxAttachments } from '~/stores/attachment';
 import { useEditingMarkdown, useCurrentPageId, useIsGuestUser } from '~/stores/context';
 import { useEditingMarkdown, useCurrentPageId, useIsGuestUser } from '~/stores/context';
 
 
-import DeleteAttachmentModal from './PageAttachment/DeleteAttachmentModal';
-import PageAttachmentList from './PageAttachment/PageAttachmentList';
+import { DeleteAttachmentModal } from './PageAttachment/DeleteAttachmentModal';
+import { PageAttachmentList } from './PageAttachment/PageAttachmentList';
 import PaginationWrapper from './PaginationWrapper';
 import PaginationWrapper from './PaginationWrapper';
 
 
 // Utility
 // Utility
-const checkIfFileInUse = (markdown: string, attachment) => {
-  return markdown.match(attachment._id);
-};
-
-// Custom hook that handles processes related to inUseAttachments
-const useInUseAttachments = (attachments) => {
-  const { data: markdown } = useEditingMarkdown();
-  const [inUse, setInUse] = useState<any>({});
-
-  // Update inUse when either of attachments or markdown is updated
-  useEffect(() => {
-    if (markdown == null) {
-      return;
-    }
-
-    const newInUse = {};
-
-    for (const attachment of attachments) {
-      newInUse[attachment._id] = checkIfFileInUse(markdown, attachment);
-    }
-
-    setInUse(newInUse);
-  }, [attachments, markdown]);
-
-  return inUse;
+const checkIfFileInUse = (markdown: string, attachment): boolean => {
+  return markdown.indexOf(attachment._id) >= 0;
 };
 };
 
 
 const PageAttachment = (): JSX.Element => {
 const PageAttachment = (): JSX.Element => {
-  const { t } = useTranslation();
-
   // Static SWRs
   // Static SWRs
   const { data: pageId } = useCurrentPageId();
   const { data: pageId } = useCurrentPageId();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: markdown } = useEditingMarkdown();
 
 
   // States
   // States
   const [pageNumber, setPageNumber] = useState(1);
   const [pageNumber, setPageNumber] = useState(1);
-  const [attachmentToDelete, setAttachmentToDelete] = useState<any>(undefined);
+  const [attachmentToDelete, setAttachmentToDelete] = useState<(IAttachment & HasObjectId) | null>(null);
   const [deleting, setDeleting] = useState(false);
   const [deleting, setDeleting] = useState(false);
   const [deleteError, setDeleteError] = useState('');
   const [deleteError, setDeleteError] = useState('');
 
 
   // SWRs
   // SWRs
   const { data: dataAttachments, remove } = useSWRxAttachments(pageId, pageNumber);
   const { data: dataAttachments, remove } = useSWRxAttachments(pageId, pageNumber);
-  const {
-    attachments = [],
-    totalAttachments = 0,
-    limit,
-  } = dataAttachments ?? {};
 
 
   // Custom hooks
   // Custom hooks
-  const inUseAttachments = useInUseAttachments(attachments);
+  const inUseAttachmentsMap: { [id: string]: boolean } | undefined = useMemo(() => {
+    if (markdown == null || dataAttachments == null) {
+      return undefined;
+    }
+
+    const attachmentEntries = dataAttachments.attachments
+      .map((attachment) => {
+        return [attachment._id, checkIfFileInUse(markdown, attachment)];
+      });
+
+    return Object.fromEntries(attachmentEntries);
+  }, [dataAttachments, markdown]);
 
 
   // Methods
   // Methods
   const onChangePageHandler = useCallback((newPageNumber: number) => {
   const onChangePageHandler = useCallback((newPageNumber: number) => {
@@ -70,7 +54,7 @@ const PageAttachment = (): JSX.Element => {
     setAttachmentToDelete(attachment);
     setAttachmentToDelete(attachment);
   }, []);
   }, []);
 
 
-  const onAttachmentDeleteClickedConfirmHandler = useCallback(async(attachment) => {
+  const onAttachmentDeleteClickedConfirmHandler = useCallback(async(attachment: IAttachment & HasObjectId) => {
     setDeleting(true);
     setDeleting(true);
 
 
     try {
     try {
@@ -91,22 +75,32 @@ const PageAttachment = (): JSX.Element => {
   }, []);
   }, []);
 
 
   // Renderers
   // Renderers
-  const renderDeleteAttachmentModal = useCallback(() => {
-    if (isGuestUser) {
-      return <></>;
-    }
-
-    if (attachments.length === 0) {
+  const renderPageAttachmentList = useCallback(() => {
+    if (dataAttachments == null || inUseAttachmentsMap == null) {
       return (
       return (
-        <div data-testid="page-attachment">
-          {t('No_attachments_yet')}
+        <div className="text-muted text-center">
+          <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
         </div>
         </div>
       );
       );
     }
     }
 
 
-    let deleteInUse = null;
-    if (attachmentToDelete != null) {
-      deleteInUse = inUseAttachments[attachmentToDelete._id] || false;
+    return (
+      <PageAttachmentList
+        attachments={dataAttachments.attachments}
+        inUse={inUseAttachmentsMap}
+        onAttachmentDeleteClicked={onAttachmentDeleteClicked}
+        isUserLoggedIn={!isGuestUser}
+      />
+    );
+  }, [dataAttachments, inUseAttachmentsMap, isGuestUser, onAttachmentDeleteClicked]);
+
+  const renderDeleteAttachmentModal = useCallback(() => {
+    if (isGuestUser) {
+      return <></>;
+    }
+
+    if (dataAttachments == null || dataAttachments.attachments.length === 0 || attachmentToDelete == null) {
+      return <></>;
     }
     }
 
 
     const isOpen = attachmentToDelete != null;
     const isOpen = attachmentToDelete != null;
@@ -114,36 +108,39 @@ const PageAttachment = (): JSX.Element => {
     return (
     return (
       <DeleteAttachmentModal
       <DeleteAttachmentModal
         isOpen={isOpen}
         isOpen={isOpen}
-        animation="false"
         toggle={onToggleHandler}
         toggle={onToggleHandler}
         attachmentToDelete={attachmentToDelete}
         attachmentToDelete={attachmentToDelete}
-        inUse={deleteInUse}
         deleting={deleting}
         deleting={deleting}
         deleteError={deleteError}
         deleteError={deleteError}
         onAttachmentDeleteClickedConfirm={onAttachmentDeleteClickedConfirmHandler}
         onAttachmentDeleteClickedConfirm={onAttachmentDeleteClickedConfirmHandler}
       />
       />
     );
     );
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
-  }, [attachmentToDelete, attachments.length, deleteError, deleting, inUseAttachments, isGuestUser, onAttachmentDeleteClickedConfirmHandler, onToggleHandler, t]);
+  }, [attachmentToDelete, dataAttachments, deleteError, deleting, isGuestUser, onAttachmentDeleteClickedConfirmHandler, onToggleHandler]);
 
 
-  return (
-    <div data-testid="page-attachment">
-      <PageAttachmentList
-        attachments={attachments}
-        inUse={inUseAttachments}
-        onAttachmentDeleteClicked={onAttachmentDeleteClicked}
-        isUserLoggedIn={!isGuestUser}
-      />
-
-      {renderDeleteAttachmentModal()}
+  const renderPaginationWrapper = useCallback(() => {
+    if (dataAttachments == null || dataAttachments.attachments.length === 0) {
+      return <></>;
+    }
 
 
+    return (
       <PaginationWrapper
       <PaginationWrapper
         activePage={pageNumber}
         activePage={pageNumber}
         changePage={onChangePageHandler}
         changePage={onChangePageHandler}
-        totalItemsCount={totalAttachments}
-        pagingLimit={limit}
+        totalItemsCount={dataAttachments.totalAttachments}
+        pagingLimit={dataAttachments.limit}
         align="center"
         align="center"
       />
       />
+    );
+  }, [dataAttachments, onChangePageHandler, pageNumber]);
+
+  return (
+    <div data-testid="page-attachment">
+      {renderPageAttachmentList()}
+
+      {renderDeleteAttachmentModal()}
+
+      {renderPaginationWrapper()}
     </div>
     </div>
   );
   );
 };
 };

+ 0 - 97
packages/app/src/components/PageAttachment/DeleteAttachmentModal.jsx

@@ -1,97 +0,0 @@
-/* eslint-disable react/prop-types */
-import React from 'react';
-
-import {
-  Button,
-  Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
-
-import { UserPicture } from '@growi/ui';
-import Username from '../User/Username';
-
-export default class DeleteAttachmentModal extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this._onDeleteConfirm = this._onDeleteConfirm.bind(this);
-  }
-
-  _onDeleteConfirm() {
-    this.props.onAttachmentDeleteClickedConfirm(this.props.attachmentToDelete);
-  }
-
-  iconNameByFormat(format) {
-    if (format.match(/image\/.+/i)) {
-      return 'icon-picture';
-    }
-
-    return 'icon-doc';
-  }
-
-  renderByFileFormat(attachment) {
-    const content = (attachment.fileFormat.match(/image\/.+/i))
-      ? <img src={attachment.filePathProxied} alt="deleting image" />
-      : '';
-
-
-    return (
-      <div className="attachment-delete-image">
-        <p>
-          <i className={this.iconNameByFormat(attachment.fileFormat)}></i> {attachment.originalName}
-        </p>
-        <p>
-          uploaded by <UserPicture user={attachment.creator} size="sm"></UserPicture> <Username user={attachment.creator}></Username>
-        </p>
-        {content}
-      </div>
-    );
-  }
-
-  render() {
-    const attachment = this.props.attachmentToDelete;
-    if (attachment === null) {
-      return null;
-    }
-
-    const props = Object.assign({}, this.props);
-    delete props.onAttachmentDeleteClickedConfirm;
-    delete props.attachmentToDelete;
-    delete props.inUse;
-    delete props.deleting;
-    delete props.deleteError;
-
-    let deletingIndicator = '';
-    if (this.props.deleting) {
-      deletingIndicator = <div className="speeding-wheel-sm"></div>;
-    }
-    if (this.props.deleteError) {
-      deletingIndicator = <span>{this.props.deleteError}</span>;
-    }
-
-    const renderAttachment = this.renderByFileFormat(attachment);
-
-    return (
-      <Modal {...props} className="attachment-delete-modal" bssize="large" aria-labelledby="contained-modal-title-lg">
-        <ModalHeader tag="h4" toggle={this.props.toggle} className="bg-danger text-light">
-          <span id="contained-modal-title-lg">Delete attachment?</span>
-        </ModalHeader>
-        <ModalBody>
-          {renderAttachment}
-        </ModalBody>
-        <ModalFooter>
-          <div className="mr-3 d-inline-block">
-            {deletingIndicator}
-          </div>
-          <Button
-            color="danger"
-            onClick={this._onDeleteConfirm}
-            disabled={this.props.deleting}
-          >Delete!
-          </Button>
-        </ModalFooter>
-      </Modal>
-    );
-  }
-
-}

+ 98 - 0
packages/app/src/components/PageAttachment/DeleteAttachmentModal.tsx

@@ -0,0 +1,98 @@
+/* eslint-disable react/prop-types */
+import React, { useCallback } from 'react';
+
+import { HasObjectId, IAttachment } from '@growi/core';
+import { UserPicture } from '@growi/ui';
+import {
+  Button,
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+
+import Username from '../User/Username';
+
+
+function iconNameByFormat(format: string): string {
+  if (format.match(/image\/.+/i)) {
+    return 'icon-picture';
+  }
+
+  return 'icon-doc';
+}
+
+
+type Props = {
+  isOpen: boolean,
+  toggle: () => void,
+  attachmentToDelete: IAttachment & HasObjectId | null,
+  deleting: boolean,
+  deleteError: string,
+  onAttachmentDeleteClickedConfirm?: (attachment: IAttachment & HasObjectId) => Promise<void>,
+}
+
+export const DeleteAttachmentModal = (props: Props): JSX.Element => {
+
+  const {
+    isOpen, toggle,
+    attachmentToDelete, deleting, deleteError,
+    onAttachmentDeleteClickedConfirm,
+  } = props;
+
+  const onDeleteConfirm = useCallback(() => {
+    if (attachmentToDelete == null || onAttachmentDeleteClickedConfirm == null) {
+      return;
+    }
+    onAttachmentDeleteClickedConfirm(attachmentToDelete);
+  }, [attachmentToDelete, onAttachmentDeleteClickedConfirm]);
+
+  const renderByFileFormat = useCallback((attachment) => {
+    const content = (attachment.fileFormat.match(/image\/.+/i))
+      // eslint-disable-next-line @next/next/no-img-element
+      ? <img src={attachment.filePathProxied} alt="deleting image" />
+      : '';
+
+
+    return (
+      <div className="attachment-delete-image">
+        <p>
+          <i className={iconNameByFormat(attachment.fileFormat)}></i> {attachment.originalName}
+        </p>
+        <p>
+          uploaded by <UserPicture user={attachment.creator} size="sm"></UserPicture> <Username user={attachment.creator}></Username>
+        </p>
+        {content}
+      </div>
+    );
+  }, []);
+
+  let deletingIndicator = <></>;
+  if (deleting) {
+    deletingIndicator = <div className="speeding-wheel-sm"></div>;
+  }
+  if (deleteError) {
+    deletingIndicator = <span>{deleteError}</span>;
+  }
+
+
+  return (
+    <Modal isOpen={isOpen} className="attachment-delete-modal" size="lg" aria-labelledby="contained-modal-title-lg" fade={false}>
+      <ModalHeader tag="h4" toggle={toggle} className="bg-danger text-light">
+        <span id="contained-modal-title-lg">Delete attachment?</span>
+      </ModalHeader>
+      <ModalBody>
+        {renderByFileFormat(attachmentToDelete)}
+      </ModalBody>
+      <ModalFooter>
+        <div className="mr-3 d-inline-block">
+          {deletingIndicator}
+        </div>
+        <Button
+          color="danger"
+          onClick={onDeleteConfirm}
+          disabled={deleting}
+        >Delete!
+        </Button>
+      </ModalFooter>
+    </Modal>
+  );
+
+};

+ 0 - 45
packages/app/src/components/PageAttachment/PageAttachmentList.jsx

@@ -1,45 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { Attachment } from '@growi/ui';
-
-export default class PageAttachmentList extends React.Component {
-
-  render() {
-    if (this.props.attachments <= 0) {
-      return null;
-    }
-
-    const attachmentList = this.props.attachments.map((attachment, idx) => {
-      return (
-        <Attachment
-          key={`page:attachment:${attachment._id}`}
-          attachment={attachment}
-          inUse={this.props.inUse[attachment._id] || false}
-          onAttachmentDeleteClicked={this.props.onAttachmentDeleteClicked}
-          isUserLoggedIn={this.props.isUserLoggedIn}
-        />
-      );
-    });
-
-    return (
-      <div>
-        <ul className="pl-2">
-          {attachmentList}
-        </ul>
-      </div>
-    );
-  }
-
-}
-
-PageAttachmentList.propTypes = {
-  attachments: PropTypes.arrayOf(PropTypes.object),
-  inUse: PropTypes.objectOf(PropTypes.bool),
-  onAttachmentDeleteClicked: PropTypes.func,
-  isUserLoggedIn: PropTypes.bool,
-};
-PageAttachmentList.defaultProps = {
-  attachments: [],
-  inUse: {},
-};

+ 47 - 0
packages/app/src/components/PageAttachment/PageAttachmentList.tsx

@@ -0,0 +1,47 @@
+import React from 'react';
+
+
+import { HasObjectId, IAttachment } from '@growi/core';
+import { Attachment } from '@growi/ui';
+import { useTranslation } from 'next-i18next';
+
+
+type Props = {
+  attachments: (IAttachment & HasObjectId)[],
+  inUse: { [id: string]: boolean },
+  onAttachmentDeleteClicked?: (attachment: IAttachment & HasObjectId) => void,
+  isUserLoggedIn?: boolean,
+}
+
+export const PageAttachmentList = (props: Props): JSX.Element => {
+  const { t } = useTranslation();
+
+  const {
+    attachments, inUse, onAttachmentDeleteClicked, isUserLoggedIn,
+  } = props;
+
+  if (attachments.length === 0) {
+    return <>{t('No_attachments_yet')}</>;
+  }
+
+  const attachmentList = attachments.map((attachment) => {
+    return (
+      <Attachment
+        key={`page:attachment:${attachment._id}`}
+        attachment={attachment}
+        inUse={inUse[attachment._id] || false}
+        onAttachmentDeleteClicked={onAttachmentDeleteClicked}
+        isUserLoggedIn={isUserLoggedIn}
+      />
+    );
+  });
+
+  return (
+    <div>
+      <ul className="pl-2">
+        {attachmentList}
+      </ul>
+    </div>
+  );
+
+};

+ 2 - 5
packages/app/src/components/SavePageControls.tsx

@@ -59,7 +59,7 @@ export const SavePageControls = (props: Props): JSX.Element | null => {
   }, [mutateIsEnabledUnsavedWarning]);
   }, [mutateIsEnabledUnsavedWarning]);
 
 
 
 
-  if (isEditable == null || isAclEnabled == null) {
+  if (isEditable == null || isAclEnabled == null || grantData == null) {
     return null;
     return null;
   }
   }
 
 
@@ -67,10 +67,7 @@ export const SavePageControls = (props: Props): JSX.Element | null => {
     return null;
     return null;
   }
   }
 
 
-  const grant = grantData?.grant || PageGrant.GRANT_PUBLIC;
-  const grantedGroup = grantData?.grantedGroup;
-
-  // const {  pageContainer } = props;
+  const { grant, grantedGroup } = grantData;
 
 
   const isRootPage = isTopPage(currentPagePath ?? '');
   const isRootPage = isTopPage(currentPagePath ?? '');
   const labelSubmitButton = pageId == null ? t('Create') : t('Update');
   const labelSubmitButton = pageId == null ? t('Create') : t('Update');

+ 2 - 4
packages/app/src/interfaces/attachment.ts

@@ -1,10 +1,8 @@
-import type { IAttachment } from '@growi/core';
+import type { HasObjectId, IAttachment } from '@growi/core';
 
 
 import type { PaginateResult } from './mongoose-utils';
 import type { PaginateResult } from './mongoose-utils';
 
 
 
 
 export type IResAttachmentList = {
 export type IResAttachmentList = {
-  data: {
-    paginateResult: PaginateResult<IAttachment>
-  }
+  paginateResult: PaginateResult<IAttachment & HasObjectId>
 };
 };

+ 1 - 1
packages/app/src/server/service/installer.ts

@@ -144,7 +144,7 @@ export class InstallerService {
     const rootRevision = await Revision.findOne({ path: '/' });
     const rootRevision = await Revision.findOne({ path: '/' });
     rootPage.creator = adminUser._id;
     rootPage.creator = adminUser._id;
     rootPage.lastUpdateUser = adminUser._id;
     rootPage.lastUpdateUser = adminUser._id;
-    rootRevision.creator = adminUser._id;
+    rootRevision.author = adminUser._id;
     await Promise.all([rootPage.save(), rootRevision.save()]);
     await Promise.all([rootPage.save(), rootRevision.save()]);
 
 
     // create initial pages
     // create initial pages

+ 13 - 9
packages/app/src/stores/attachment.tsx

@@ -1,11 +1,13 @@
 import { useCallback } from 'react';
 import { useCallback } from 'react';
 
 
 import {
 import {
+  HasObjectId,
   IAttachment, Nullable, SWRResponseWithUtils, withUtils,
   IAttachment, Nullable, SWRResponseWithUtils, withUtils,
 } from '@growi/core';
 } from '@growi/core';
 import useSWR from 'swr';
 import useSWR from 'swr';
 
 
-import { apiGet, apiPost } from '~/client/util/apiv1-client';
+import { apiPost } from '~/client/util/apiv1-client';
+import { apiv3Get } from '~/client/util/apiv3-client';
 import { IResAttachmentList } from '~/interfaces/attachment';
 import { IResAttachmentList } from '~/interfaces/attachment';
 
 
 type Util = {
 type Util = {
@@ -13,7 +15,7 @@ type Util = {
 };
 };
 
 
 type IDataAttachmentList = {
 type IDataAttachmentList = {
-  attachments: IAttachment[]
+  attachments: (IAttachment & HasObjectId)[]
   totalAttachments: number
   totalAttachments: number
   limit: number
   limit: number
 };
 };
@@ -21,17 +23,19 @@ type IDataAttachmentList = {
 export const useSWRxAttachments = (pageId?: Nullable<string>, pageNumber?: number): SWRResponseWithUtils<Util, IDataAttachmentList, Error> => {
 export const useSWRxAttachments = (pageId?: Nullable<string>, pageNumber?: number): SWRResponseWithUtils<Util, IDataAttachmentList, Error> => {
   const shouldFetch = pageId != null && pageNumber != null;
   const shouldFetch = pageId != null && pageNumber != null;
 
 
-  const fetcher = useCallback(async(endpoint) => {
-    const res = await apiGet<IResAttachmentList>(endpoint, { pageId, pageNumber });
+  const fetcher = useCallback(async(endpoint, pageId, pageNumber) => {
+    const res = await apiv3Get<IResAttachmentList>(endpoint, { pageId, pageNumber });
+    const resAttachmentList = res.data;
+    const { paginateResult } = resAttachmentList;
     return {
     return {
-      attachments: res.data.paginateResult.docs,
-      totalAttachments: res.data.paginateResult.totalDocs,
-      limit: res.data.paginateResult.limit,
+      attachments: paginateResult.docs,
+      totalAttachments: paginateResult.totalDocs,
+      limit: paginateResult.limit,
     };
     };
-  }, [pageId, pageNumber]);
+  }, []);
 
 
   const swrResponse = useSWR(
   const swrResponse = useSWR(
-    shouldFetch ? ['/attachments/list', pageId, pageNumber] : null,
+    shouldFetch ? ['/attachment/list', pageId, pageNumber] : null,
     fetcher,
     fetcher,
   );
   );
 
 

+ 6 - 7
packages/app/src/stores/ui.tsx

@@ -1,7 +1,7 @@
 import { RefObject } from 'react';
 import { RefObject } from 'react';
 
 
 import {
 import {
-  isClient, isServer, pagePathUtils, Nullable,
+  isClient, isServer, pagePathUtils, Nullable, PageGrant,
 } from '@growi/core';
 } from '@growi/core';
 import { withUtils, SWRResponseWithUtils } from '@growi/core/src/utils/with-utils';
 import { withUtils, SWRResponseWithUtils } from '@growi/core/src/utils/with-utils';
 import { Breakpoint, addBreakpointListener } from '@growi/ui';
 import { Breakpoint, addBreakpointListener } from '@growi/ui';
@@ -27,7 +27,7 @@ import {
 import { localStorageMiddleware } from './middlewares/sync-to-storage';
 import { localStorageMiddleware } from './middlewares/sync-to-storage';
 import { useStaticSWR } from './use-static-swr';
 import { useStaticSWR } from './use-static-swr';
 
 
-const { isTrashTopPage } = pagePathUtils;
+const { isTrashTopPage, isUsersTopPage } = pagePathUtils;
 
 
 const logger = loggerFactory('growi:stores:ui');
 const logger = loggerFactory('growi:stores:ui');
 
 
@@ -367,7 +367,7 @@ export const useSidebarResizeDisabled = (isDisabled?: boolean): SWRResponse<bool
 };
 };
 
 
 export const useSelectedGrant = (initialData?: Nullable<IPageGrantData>): SWRResponse<Nullable<IPageGrantData>, Error> => {
 export const useSelectedGrant = (initialData?: Nullable<IPageGrantData>): SWRResponse<Nullable<IPageGrantData>, Error> => {
-  return useStaticSWR<Nullable<IPageGrantData>, Error>('selectedGrant', initialData);
+  return useStaticSWR<Nullable<IPageGrantData>, Error>('selectedGrant', initialData, { fallbackData: { grant: PageGrant.GRANT_PUBLIC } });
 };
 };
 
 
 export const useGlobalSearchFormRef = (initialData?: RefObject<IFocusable>): SWRResponse<RefObject<IFocusable>, Error> => {
 export const useGlobalSearchFormRef = (initialData?: RefObject<IFocusable>): SWRResponse<RefObject<IFocusable>, Error> => {
@@ -424,22 +424,21 @@ export const useIsAbleToShowPageManagement = (): SWRResponse<boolean, Error> =>
 export const useIsAbleToShowTagLabel = (): SWRResponse<boolean, Error> => {
 export const useIsAbleToShowTagLabel = (): SWRResponse<boolean, Error> => {
   const key = 'isAbleToShowTagLabel';
   const key = 'isAbleToShowTagLabel';
   const { data: pageId } = useCurrentPageId();
   const { data: pageId } = useCurrentPageId();
-  const { data: isUserPage } = useIsUserPage();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: isIdenticalPath } = useIsIdenticalPath();
   const { data: isIdenticalPath } = useIsIdenticalPath();
   const { data: isNotFound } = useIsNotFound();
   const { data: isNotFound } = useIsNotFound();
   const { data: editorMode } = useEditorMode();
   const { data: editorMode } = useEditorMode();
   const { data: shareLinkId } = useShareLinkId();
   const { data: shareLinkId } = useShareLinkId();
 
 
-  const includesUndefined = [isUserPage, currentPagePath, isIdenticalPath, isNotFound, editorMode].some(v => v === undefined);
+  const includesUndefined = [currentPagePath, isIdenticalPath, isNotFound, editorMode].some(v => v === undefined);
 
 
   const isViewMode = editorMode === EditorMode.View;
   const isViewMode = editorMode === EditorMode.View;
 
 
   return useSWRImmutable(
   return useSWRImmutable(
-    includesUndefined ? null : [key, editorMode, pageId],
+    includesUndefined ? null : [key, pageId, currentPagePath, isIdenticalPath, isNotFound, editorMode, shareLinkId],
     // "/trash" page does not exist on page collection and unable to add tags
     // "/trash" page does not exist on page collection and unable to add tags
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    () => !isUserPage && !isTrashTopPage(currentPagePath!) && shareLinkId == null && !isIdenticalPath && !(isViewMode && isNotFound),
+    () => !isUsersTopPage(currentPagePath!) && !isTrashTopPage(currentPagePath!) && shareLinkId == null && !isIdenticalPath && !(isViewMode && isNotFound),
   );
   );
 };
 };
 
 

+ 3 - 1
packages/ui/src/components/Attachment/Attachment.jsx

@@ -60,7 +60,9 @@ export class Attachment extends React.Component {
         <span className="mr-1 attachment-userpicture">
         <span className="mr-1 attachment-userpicture">
           <UserPicture user={attachment.creator} size="sm"></UserPicture>
           <UserPicture user={attachment.creator} size="sm"></UserPicture>
         </span>
         </span>
-        <a className="mr-2" href={attachment.filePathProxied}><i className={formatIcon}></i> {attachment.originalName}</a>
+        <a className="mr-2" href={attachment.filePathProxied} target="_blank" rel="noopener noreferrer">
+          <i className={formatIcon}></i> {attachment.originalName}
+        </a>
         <span className="mr-2">{fileType}</span>
         <span className="mr-2">{fileType}</span>
         <span className="mr-2">{fileInUse}</span>
         <span className="mr-2">{fileInUse}</span>
         <span className="mr-2">{btnDownload}</span>
         <span className="mr-2">{btnDownload}</span>