Procházet zdrojové kódy

Merge pull request #8206 from weseek/fix/able-to-see-attachment-in-shared-page

imprv: Update RichAttachments feat on shared pages
Yuki Takei před 2 roky
rodič
revize
4e9784092d

+ 1 - 4
apps/app/src/components/Layout/ShareLinkLayout.tsx

@@ -2,7 +2,6 @@ import React, { ReactNode } from 'react';
 
 import dynamic from 'next/dynamic';
 
-import { useEditorModeClassName } from '../../client/services/layout';
 import { GrowiNavbar } from '../Navbar/GrowiNavbar';
 
 import { RawLayout } from './RawLayout';
@@ -21,10 +20,8 @@ type Props = {
 }
 
 export const ShareLinkLayout = ({ children }: Props): JSX.Element => {
-  const className = useEditorModeClassName();
-
   return (
-    <RawLayout className={className}>
+    <RawLayout className="share-link">
       <GrowiNavbar isGlobalSearchHidden />
 
       <div className="page-wrapper d-flex d-print-block">

+ 10 - 5
apps/app/src/components/ReactMarkdownComponents/RichAttachment.tsx

@@ -9,11 +9,14 @@ import { useDeleteAttachmentModal } from '~/stores/modal';
 
 import styles from './RichAttachment.module.scss';
 
-export const RichAttachment: React.FC<{
+type RichAttachmentProps = {
   attachmentId: string,
   url: string,
-  attachmentName: string
-}> = React.memo(({ attachmentId, url, attachmentName }) => {
+  attachmentName: string,
+}
+
+export const RichAttachment = React.memo((props: RichAttachmentProps) => {
+  const { attachmentId, attachmentName } = props;
   const { t } = useTranslation();
   const { data: attachment, remove } = useSWRxAttachment(attachmentId);
   const { open: openDeleteAttachmentModal } = useDeleteAttachmentModal();
@@ -58,13 +61,15 @@ export const RichAttachment: React.FC<{
           </div>
           <div className="pl-0">
             <div className="d-inline-block">
-              <a target="_blank" rel="noopener noreferrer" href={filePathProxied}>
+              {/* Since we need to include the "referer" to view the attachment on the shared page */}
+              {/* eslint-disable-next-line react/jsx-no-target-blank */}
+              <a target="_blank" rel="noopener" href={filePathProxied}>
                 {attachmentName || originalName}
               </a>
               <a className="ml-2 attachment-download" href={downloadPathProxied}>
                 <i className="icon-cloud-download" />
               </a>
-              <a className="ml-2 text-danger attachment-delete" onClick={onClickTrashButtonHandler}>
+              <a className="ml-2 text-danger attachment-delete d-share-link-none" type="button" onClick={onClickTrashButtonHandler}>
                 <i className="icon-trash" />
               </a>
             </div>

+ 46 - 44
apps/app/src/server/routes/apiv3/attachment.js

@@ -3,12 +3,13 @@ import { ErrorV3 } from '@growi/core/dist/models';
 import loggerFactory from '~/utils/logger';
 
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
+import { certifySharedPageAttachmentMiddleware } from '../../middlewares/certify-shared-page-attachment';
 
 const logger = loggerFactory('growi:routes:apiv3:attachment'); // eslint-disable-line no-unused-vars
 const express = require('express');
 
 const router = express.Router();
-const { query } = require('express-validator');
+const { query, param } = require('express-validator');
 
 const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
 
@@ -26,8 +27,8 @@ module.exports = (crowi) => {
   const Attachment = crowi.model('Attachment');
 
   const validator = {
-    attachment: [
-      query('attachmentId').isMongoId().withMessage('attachmentId is required'),
+    retrieveAttachment: [
+      param('id').isMongoId().withMessage('attachment id is required'),
     ],
     retrieveAttachments: [
       query('pageId').isMongoId().withMessage('pageId is required'),
@@ -36,47 +37,6 @@ module.exports = (crowi) => {
     ],
   };
 
-  /**
-   * @swagger
-   *
-   *    /attachment:
-   *      get:
-   *        tags: [Attachment]
-   *        description: Get attachment
-   *        responses:
-   *          200:
-   *            description: Return attachment
-   *        parameters:
-   *          - name: attachemnt_id
-   *            in: query
-   *            required: true
-   *            description: attachment id
-   *            schema:
-   *              type: string
-   */
-  router.get('/', accessTokenParser, loginRequired, validator.attachment, apiV3FormValidator, async(req, res) => {
-    try {
-      const attachmentId = req.query.attachmentId;
-
-      const attachment = await Attachment.findById(attachmentId).populate('creator').exec();
-
-      if (attachment == null) {
-        const message = 'Attachment not found';
-        return res.apiv3Err(message, 404);
-      }
-
-      if (attachment.creator != null && attachment.creator instanceof User) {
-        attachment.creator = serializeUserSecurely(attachment.creator);
-      }
-
-      return res.apiv3({ attachment });
-    }
-    catch (err) {
-      logger.error('Attachment retrieval failed', err);
-      return res.apiv3Err(err, 500);
-    }
-  });
-
   /**
    * @swagger
    *
@@ -135,5 +95,47 @@ module.exports = (crowi) => {
     }
   });
 
+  /**
+   * @swagger
+   *
+   *    /attachment/{id}:
+   *      get:
+   *        tags: [Attachment]
+   *        description: Get attachment
+   *        responses:
+   *          200:
+   *            description: Return attachment
+   *        parameters:
+   *          - name: id
+   *            in: path
+   *            required: true
+   *            description: attachment id
+   *            schema:
+   *              type: string
+   */
+  router.get('/:id', accessTokenParser, certifySharedPageAttachmentMiddleware, loginRequired, validator.retrieveAttachment, apiV3FormValidator,
+    async(req, res) => {
+      try {
+        const attachmentId = req.params.id;
+
+        const attachment = await Attachment.findById(attachmentId).populate('creator').exec();
+
+        if (attachment == null) {
+          const message = 'Attachment not found';
+          return res.apiv3Err(message, 404);
+        }
+
+        if (attachment.creator != null && attachment.creator instanceof User) {
+          attachment.creator = serializeUserSecurely(attachment.creator);
+        }
+
+        return res.apiv3({ attachment });
+      }
+      catch (err) {
+        logger.error('Attachment retrieval failed', err);
+        return res.apiv3Err(err, 500);
+      }
+    });
+
   return router;
 };

+ 1 - 1
apps/app/src/server/routes/index.js

@@ -159,7 +159,7 @@ module.exports = function(crowi, app) {
   app.get('/attachment/:id([0-9a-z]{24})'         , certifySharedPageAttachmentMiddleware , loginRequired, attachment.api.get);
   app.get('/attachment/profile/:id([0-9a-z]{24})' , loginRequired, attachment.api.get);
   app.get('/attachment/:pageId/:fileName'       , loginRequired, attachment.api.obsoletedGetForMongoDB); // DEPRECATED: remains for backward compatibility for v3.3.x or below
-  app.get('/download/:id([0-9a-z]{24})'         , loginRequired, attachment.api.download);
+  app.get('/download/:id([0-9a-z]{24})'         , certifySharedPageAttachmentMiddleware, loginRequired, attachment.api.download);
 
   app.get('/_search'                            , loginRequired, next.delegateToNext);
 

+ 1 - 0
apps/app/src/services/renderer/remark-plugins/attachment.ts

@@ -24,6 +24,7 @@ const rewriteNode = (node: Node) => {
   };
 };
 
+
 export const remarkPlugin: Plugin = () => {
   return (tree) => {
     visit(tree, (node) => {

+ 4 - 5
apps/app/src/stores/attachment.tsx

@@ -25,10 +25,9 @@ type IDataAttachmentList = {
 
 export const useSWRxAttachment = (attachmentId: string): SWRResponseWithUtils<Util, IAttachmentHasId, Error> => {
   const swrResponse = useSWR(
-    ['/attachment', attachmentId],
-    useCallback(async([endpoint, attachmentId]) => {
-      const params = { attachmentId };
-      const res = await apiv3Get(endpoint, params);
+    [`/attachment/${attachmentId}`],
+    useCallback(async([endpoint]) => {
+      const res = await apiv3Get(endpoint);
       return res.data.attachment;
     }, []),
   );
@@ -75,7 +74,7 @@ export const useSWRxAttachments = (pageId?: Nullable<string>, pageNumber?: numbe
       await apiPost('/attachments.remove', body);
       mutate();
       // Mutation for rich attachment rendering
-      mutateUseSWRxAttachment(['/attachment', body.attachment_id], body.attachment_id);
+      mutateUseSWRxAttachment([`/attachment/${body.attachment_id}`], body.attachment_id);
     }
     catch (err) {
       throw err;

+ 2 - 0
apps/app/src/styles/_mixins.scss

@@ -1,5 +1,7 @@
 @use './bootstrap/init' as bs;
 
+@import './mixins/share-link';
+
 @mixin variable-font-size($basesize) {
   font-size: $basesize * 0.6;
 

+ 7 - 0
apps/app/src/styles/_share-link.scss

@@ -0,0 +1,7 @@
+@use './mixins';
+
+@include mixins.share-link() {
+  .d-share-link-none {
+    display: none !important;
+  }
+}

+ 0 - 12
apps/app/src/styles/_sharelink.scss

@@ -1,12 +0,0 @@
-.share-link-form {
-  /* Chrome/Safari */
-  input[type='number']::-webkit-outer-spin-button,
-  input[type='number']::-webkit-inner-spin-button {
-    -webkit-appearance: none;
-  }
-
-  /* Firefox */
-  input[type='number'] {
-    -moz-appearance: textfield;
-  }
-}

+ 20 - 0
apps/app/src/styles/mixins/_share-link.scss

@@ -0,0 +1,20 @@
+@mixin share-link() {
+  .layout-root.share-link {
+    @content;
+  }
+}
+
+@mixin share-link-for-module($isContentGlobal: false) {
+  :global {
+    .layout-root.share-link {
+      @if ($isContentGlobal) {
+        @content;
+      }
+      @else {
+        :local {
+          @content;
+        }
+      }
+    }
+  }
+}

+ 1 - 0
apps/app/src/styles/style-app.scss

@@ -21,6 +21,7 @@
 @import 'mirror_mode';
 @import 'modal';
 @import 'page-path';
+@import 'share-link';
 @import 'tag';
 @import 'installer';