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

Merge pull request #10812 from growilabs/fix/179364-handle-pipeline-errors

fix: Handle format and lint errors
Yuki Takei 1 месяц назад
Родитель
Сommit
6d50d4c7e0

+ 96 - 55
apps/app/src/client/components/Admin/MarkdownSetting/ContentDispositionSettings.tsx

@@ -1,9 +1,12 @@
-import React, { useState, useCallback, useEffect } from 'react';
-
+import type React from 'react';
+import { useCallback, useEffect, useState } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useForm } from 'react-hook-form';
 
-import { useContentDisposition, type ContentDispositionSettings } from '../../../services/admin-content-disposition';
+import {
+  type ContentDispositionSettings as ContentDispositionSettingsType,
+  useContentDisposition,
+} from '../../../services/admin-content-disposition';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 interface MimeTypeListProps {
@@ -15,19 +18,32 @@ interface MimeTypeListProps {
   isUpdating: boolean;
 }
 
-const normalizeMimeType = (mimeType: string): string => mimeType.trim().toLowerCase();
+const normalizeMimeType = (mimeType: string): string =>
+  mimeType.trim().toLowerCase();
 
 const MimeTypeList = ({
-  title, items, emptyText, onRemove, removeLabel, isUpdating,
+  title,
+  items,
+  emptyText,
+  onRemove,
+  removeLabel,
+  isUpdating,
 }: MimeTypeListProps) => (
   <div className="col-md-6 col-sm-12 mb-4">
     <div className="card shadow-sm rounded-3">
       <div className="card-header bg-transparent fw-bold">{title}</div>
       <div className="card-body">
         <ul className="list-group list-group-flush">
-          {items.length === 0 && <li className="list-group-item text-muted small border-0">{emptyText}</li>}
+          {items.length === 0 && (
+            <li className="list-group-item text-muted small border-0">
+              {emptyText}
+            </li>
+          )}
           {items.map((m: string) => (
-            <li key={m} className="list-group-item d-flex justify-content-between align-items-center border-0 px-0">
+            <li
+              key={m}
+              className="list-group-item d-flex justify-content-between align-items-center border-0 px-0"
+            >
               <code>{m}</code>
               <button
                 type="button"
@@ -47,9 +63,8 @@ const MimeTypeList = ({
 
 const ContentDispositionSettings: React.FC = () => {
   const { t } = useTranslation('admin');
-  const {
-    currentSettings, isLoading, isUpdating, updateSettings,
-  } = useContentDisposition();
+  const { currentSettings, isLoading, isUpdating, updateSettings } =
+    useContentDisposition();
 
   const [currentInput, setCurrentInput] = useState<string>('');
   const [error, setError] = useState<string | null>(null);
@@ -60,7 +75,7 @@ const ContentDispositionSettings: React.FC = () => {
     watch,
     reset,
     formState: { isDirty },
-  } = useForm<ContentDispositionSettings>({
+  } = useForm<ContentDispositionSettingsType>({
     defaultValues: {
       inlineMimeTypes: [],
       attachmentMimeTypes: [],
@@ -76,45 +91,53 @@ const ContentDispositionSettings: React.FC = () => {
   const inlineMimeTypes = watch('inlineMimeTypes');
   const attachmentMimeTypes = watch('attachmentMimeTypes');
 
-  const handleSetMimeType = useCallback((disposition: 'inline' | 'attachment') => {
-    const mimeType = normalizeMimeType(currentInput);
-    if (!mimeType) return;
+  const handleSetMimeType = useCallback(
+    (disposition: 'inline' | 'attachment') => {
+      const mimeType = normalizeMimeType(currentInput);
+      if (!mimeType) return;
 
-    const otherDisposition = disposition === 'inline' ? 'attachment' : 'inline';
+      const otherDisposition =
+        disposition === 'inline' ? 'attachment' : 'inline';
 
-    const currentTargetList = watch(`${disposition}MimeTypes`);
-    const currentOtherList = watch(`${otherDisposition}MimeTypes`);
+      const currentTargetList = watch(`${disposition}MimeTypes`);
+      const currentOtherList = watch(`${otherDisposition}MimeTypes`);
 
-    if (!currentTargetList.includes(mimeType)) {
-      setValue(`${disposition}MimeTypes`, [...currentTargetList, mimeType], { shouldDirty: true });
-    }
+      if (!currentTargetList.includes(mimeType)) {
+        setValue(`${disposition}MimeTypes`, [...currentTargetList, mimeType], {
+          shouldDirty: true,
+        });
+      }
 
-    setValue(
-      `${otherDisposition}MimeTypes`,
-      currentOtherList.filter(m => m !== mimeType),
-      { shouldDirty: true },
-    );
-
-    setCurrentInput('');
-    setError(null);
-  }, [currentInput, setValue, watch]);
-
-  const handleRemove = useCallback((mimeType: string, disposition: 'inline' | 'attachment') => {
-    const currentList = watch(`${disposition}MimeTypes`);
-    setValue(
-      `${disposition}MimeTypes`,
-      currentList.filter(m => m !== mimeType),
-      { shouldDirty: true },
-    );
-  }, [setValue, watch]);
-
-  const onSubmit = async(data: ContentDispositionSettings) => {
+      setValue(
+        `${otherDisposition}MimeTypes`,
+        currentOtherList.filter((m) => m !== mimeType),
+        { shouldDirty: true },
+      );
+
+      setCurrentInput('');
+      setError(null);
+    },
+    [currentInput, setValue, watch],
+  );
+
+  const handleRemove = useCallback(
+    (mimeType: string, disposition: 'inline' | 'attachment') => {
+      const currentList = watch(`${disposition}MimeTypes`);
+      setValue(
+        `${disposition}MimeTypes`,
+        currentList.filter((m) => m !== mimeType),
+        { shouldDirty: true },
+      );
+    },
+    [setValue, watch],
+  );
+
+  const onSubmit = async (data: ContentDispositionSettingsType) => {
     try {
       setError(null);
       await updateSettings(data);
       reset(data);
-    }
-    catch (err) {
+    } catch (err) {
       setError((err as Error).message);
     }
   };
@@ -124,12 +147,14 @@ const ContentDispositionSettings: React.FC = () => {
   return (
     <div className="row">
       <div className="col-12">
-        <h2 className="mb-4 border-0">{t('markdown_settings.content-disposition_header')}</h2>
+        <h2 className="mb-4 border-0">
+          {t('markdown_settings.content-disposition_header')}
+        </h2>
 
         <div className="card shadow-sm mb-4 rounded-3 border-0">
           <div className="card-body">
             <div className="form-group">
-              <label className="form-label fw-bold">
+              <label htmlFor="mime-type-input" className="form-label fw-bold">
                 {t('markdown_settings.content-disposition_options.add_header')}
               </label>
               <div className="d-flex align-items-center gap-2 mb-3">
@@ -137,7 +162,7 @@ const ContentDispositionSettings: React.FC = () => {
                   type="text"
                   className="form-control rounded-3 w-50"
                   value={currentInput}
-                  onChange={e => setCurrentInput(e.target.value)}
+                  onChange={(e) => setCurrentInput(e.target.value)}
                   placeholder="e.g. image/png"
                 />
                 <button
@@ -146,7 +171,9 @@ const ContentDispositionSettings: React.FC = () => {
                   onClick={() => handleSetMimeType('inline')}
                   disabled={!currentInput.trim() || isUpdating}
                 >
-                  {t('markdown_settings.content-disposition_options.inline_button')}
+                  {t(
+                    'markdown_settings.content-disposition_options.inline_button',
+                  )}
                 </button>
                 <button
                   className="btn btn-primary text-white px-3 flex-shrink-0 rounded-3 fw-bold"
@@ -154,7 +181,9 @@ const ContentDispositionSettings: React.FC = () => {
                   onClick={() => handleSetMimeType('attachment')}
                   disabled={!currentInput.trim() || isUpdating}
                 >
-                  {t('markdown_settings.content-disposition_options.attachment_button')}
+                  {t(
+                    'markdown_settings.content-disposition_options.attachment_button',
+                  )}
                 </button>
               </div>
               <small className="form-text text-muted">
@@ -168,19 +197,31 @@ const ContentDispositionSettings: React.FC = () => {
 
         <div className="row">
           <MimeTypeList
-            title={t('markdown_settings.content-disposition_options.inline_header')}
+            title={t(
+              'markdown_settings.content-disposition_options.inline_header',
+            )}
             items={inlineMimeTypes}
-            emptyText={t('markdown_settings.content-disposition_options.no_inline')}
-            onRemove={m => handleRemove(m, 'inline')}
-            removeLabel={t('markdown_settings.content-disposition_options.remove_button')}
+            emptyText={t(
+              'markdown_settings.content-disposition_options.no_inline',
+            )}
+            onRemove={(m) => handleRemove(m, 'inline')}
+            removeLabel={t(
+              'markdown_settings.content-disposition_options.remove_button',
+            )}
             isUpdating={isUpdating}
           />
           <MimeTypeList
-            title={t('markdown_settings.content-disposition_options.attachment_header')}
+            title={t(
+              'markdown_settings.content-disposition_options.attachment_header',
+            )}
             items={attachmentMimeTypes}
-            emptyText={t('markdown_settings.content-disposition_options.no_attachment')}
-            onRemove={m => handleRemove(m, 'attachment')}
-            removeLabel={t('markdown_settings.content-disposition_options.remove_button')}
+            emptyText={t(
+              'markdown_settings.content-disposition_options.no_attachment',
+            )}
+            onRemove={(m) => handleRemove(m, 'attachment')}
+            removeLabel={t(
+              'markdown_settings.content-disposition_options.remove_button',
+            )}
             isUpdating={isUpdating}
           />
         </div>

+ 30 - 15
apps/app/src/client/services/admin-content-disposition.ts

@@ -1,5 +1,4 @@
 import { useCallback } from 'react';
-
 import useSWR from 'swr';
 import useSWRMutation from 'swr/mutation';
 
@@ -27,36 +26,52 @@ interface UseContentDisposition {
   currentSettings: ContentDispositionSettings | undefined;
   isLoading: boolean;
   isUpdating: boolean;
-  updateSettings: (newSettings: ContentDispositionSettings) => Promise<ContentDispositionSettings>;
+  updateSettings: (
+    newSettings: ContentDispositionSettings,
+  ) => Promise<ContentDispositionSettings>;
 }
 
 export const useContentDisposition = (): UseContentDisposition => {
   const { data, isLoading, mutate } = useSWR(
     '/content-disposition-settings/',
-    endpoint => apiv3Get<ContentDispositionGetResponse>(endpoint).then(res => res.data.currentDispositionSettings),
+    (endpoint) =>
+      apiv3Get<ContentDispositionGetResponse>(endpoint).then(
+        (res) => res.data.currentDispositionSettings,
+      ),
   );
 
   const { trigger, isMutating: isUpdating } = useSWRMutation(
     '/content-disposition-settings/',
-    async(endpoint: string, { arg }: { arg: ContentDispositionUpdateRequest }) => {
-      const response = await apiv3Put<ContentDispositionUpdateResponse>(endpoint, arg);
+    async (
+      endpoint: string,
+      { arg }: { arg: ContentDispositionUpdateRequest },
+    ) => {
+      const response = await apiv3Put<ContentDispositionUpdateResponse>(
+        endpoint,
+        arg,
+      );
       return response.data.currentDispositionSettings;
     },
   );
 
-  const updateSettings = useCallback(async(newSettings: ContentDispositionSettings): Promise<ContentDispositionSettings> => {
-    const request: ContentDispositionUpdateRequest = {
-      newInlineMimeTypes: newSettings.inlineMimeTypes,
-      newAttachmentMimeTypes: newSettings.attachmentMimeTypes,
-    };
+  const updateSettings = useCallback(
+    async (
+      newSettings: ContentDispositionSettings,
+    ): Promise<ContentDispositionSettings> => {
+      const request: ContentDispositionUpdateRequest = {
+        newInlineMimeTypes: newSettings.inlineMimeTypes,
+        newAttachmentMimeTypes: newSettings.attachmentMimeTypes,
+      };
 
-    const updatedData = await trigger(request);
+      const updatedData = await trigger(request);
 
-    // Update local cache and avoid an unnecessary extra GET request
-    await mutate(updatedData, { revalidate: false });
+      // Update local cache and avoid an unnecessary extra GET request
+      await mutate(updatedData, { revalidate: false });
 
-    return updatedData;
-  }, [trigger, mutate]);
+      return updatedData;
+    },
+    [trigger, mutate],
+  );
 
   return {
     currentSettings: data,

+ 121 - 93
apps/app/src/server/routes/apiv3/content-disposition-settings.ts

@@ -1,33 +1,45 @@
 import type { IUserHasId } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
 import type { Request } from 'express';
+import express from 'express';
 import { body } from 'express-validator';
 
 import { SupportedAction } from '~/interfaces/activity';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
+import adminRequiredFactory from '~/server/middlewares/admin-required';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
+import loginRequiredFactory from '~/server/middlewares/login-required';
 import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 
 import type { ApiV3Response } from './interfaces/apiv3-response';
 
 const logger = loggerFactory('growi:routes:apiv3:content-disposition-settings');
-const express = require('express');
 
 const router = express.Router();
 
 module.exports = (crowi) => {
-  const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
-  const adminRequired = require('~/server/middlewares/admin-required')(crowi);
+  const loginRequiredStrictly = loginRequiredFactory(crowi);
+  const adminRequired = adminRequiredFactory(crowi);
   const addActivity = generateAddActivityMiddleware();
-  const activityEvent = crowi.event('activity');
+  const activityEvent = crowi.events.activity;
 
   const validateUpdateMimeTypes = [
-    body('newInlineMimeTypes').exists().withMessage('Inline mime types field is required.').bail(),
-    body('newInlineMimeTypes').isArray().withMessage('Inline mime types must be an array.'),
-
-    body('newAttachmentMimeTypes').exists().withMessage('Attachment mime types field is required.').bail(),
-    body('newAttachmentMimeTypes').isArray().withMessage('Attachment mime types must be an array.'),
+    body('newInlineMimeTypes')
+      .exists()
+      .withMessage('Inline mime types field is required.')
+      .bail(),
+    body('newInlineMimeTypes')
+      .isArray()
+      .withMessage('Inline mime types must be an array.'),
+
+    body('newAttachmentMimeTypes')
+      .exists()
+      .withMessage('Attachment mime types field is required.')
+      .bail(),
+    body('newAttachmentMimeTypes')
+      .isArray()
+      .withMessage('Attachment mime types must be an array.'),
   ];
 
   interface AuthorizedRequest extends Request {
@@ -45,37 +57,37 @@ module.exports = (crowi) => {
   }
 
   /**
- * @swagger
- *
- * /content-disposition-settings/:
- *   put:
- *     tags: [Content-Disposition Settings]
- *     summary: Replace content disposition settings for configurable MIME types with recieved lists.
- *     security:
- *       - cookieAuth: []
- *     responses:
- *       200:
- *         description: Successfully set content disposition settings.
- *         content:
- *           application/json:
- *             schema:
- *               type: object
- *               properties:
- *                 currentDispositionSettings:
- *                   type: object
- *                   properties:
- *                     inlineMimeTypes:
- *                       type: array
- *                       description: The list of MIME types set to inline.
- *                       items:
- *                         type: string
- *                     attachmentMimeTypes:
- *                       type: array
- *                       description: The list of MIME types set to attachment.
- *                       items:
- *                         type: string
- *
- */
+   * @swagger
+   *
+   * /content-disposition-settings/:
+   *   put:
+   *     tags: [Content-Disposition Settings]
+   *     summary: Replace content disposition settings for configurable MIME types with recieved lists.
+   *     security:
+   *       - cookieAuth: []
+   *     responses:
+   *       200:
+   *         description: Successfully set content disposition settings.
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
+   *               properties:
+   *                 currentDispositionSettings:
+   *                   type: object
+   *                   properties:
+   *                     inlineMimeTypes:
+   *                       type: array
+   *                       description: The list of MIME types set to inline.
+   *                       items:
+   *                         type: string
+   *                     attachmentMimeTypes:
+   *                       type: array
+   *                       description: The list of MIME types set to attachment.
+   *                       items:
+   *                         type: string
+   *
+   */
   router.put(
     '/',
     loginRequiredStrictly,
@@ -84,14 +96,16 @@ module.exports = (crowi) => {
     apiV3FormValidator,
     addActivity,
 
-    async(req: UpdateMimeTypesRequest, res: ApiV3Response) => {
+    async (req: UpdateMimeTypesRequest, res: ApiV3Response) => {
       const newInlineMimeTypes: string[] = req.body.newInlineMimeTypes;
       const newAttachmentMimeTypes: string[] = req.body.newAttachmentMimeTypes;
 
       // Ensure no MIME type is in both lists.
       const inlineSet = new Set(newInlineMimeTypes);
       const attachmentSet = new Set(newAttachmentMimeTypes);
-      const intersection = [...inlineSet].filter(mimeType => attachmentSet.has(mimeType));
+      const intersection = [...inlineSet].filter((mimeType) =>
+        attachmentSet.has(mimeType),
+      );
 
       if (intersection.length > 0) {
         const msg = `MIME types cannot be in both inline and attachment lists: ${intersection.join(', ')}`;
@@ -123,9 +137,9 @@ module.exports = (crowi) => {
             attachmentMimeTypes: Array.from(attachmentSet),
           },
         });
-      }
-      catch (err) {
-        const msg = 'Error occurred in updating content disposition for MIME types';
+      } catch (err) {
+        const msg =
+          'Error occurred in updating content disposition for MIME types';
         logger.error(msg, err);
         return res.apiv3Err(
           new ErrorV3(msg, 'update-content-disposition-failed'),
@@ -135,54 +149,68 @@ module.exports = (crowi) => {
   );
 
   /**
- * @swagger
- *
- * /content-disposition-settings:
- *   get:
- *     tags: [Content-Disposition Settings]
- *     summary: Get content disposition settings for configurable MIME types
- *     security:
- *       - cookieAuth: []
- *     responses:
- *       200:
- *         description: Successfully retrieved content disposition settings.
- *         content:
- *           application/json:
- *             schema:
- *               type: object
- *               properties:
- *                 currentDispositionSettings:
- *                   type: object
- *                   properties:
- *                     inlineMimeTypes:
- *                       type: array
- *                       description: The list of MIME types set to inline.
- *                       items:
- *                         type: string
- *                     attachmentMimeTypes:
- *                       type: array
- *                       description: The list of MIME types set to attachment.
- *                       items:
- *                         type: string
- *
- */
-  router.get('/', loginRequiredStrictly, adminRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    try {
-      const inlineDispositionSettings = configManager.getConfig('attachments:contentDisposition:inlineMimeTypes');
-      const attachmentDispositionSettings = configManager.getConfig('attachments:contentDisposition:attachmentMimeTypes');
-
-      return res.apiv3({
-        currentDispositionSettings: {
-          inlineMimeTypes: inlineDispositionSettings.inlineMimeTypes,
-          attachmentMimeTypes: attachmentDispositionSettings.attachmentMimeTypes,
-        },
-      });
-    }
-    catch (err) {
-      logger.error('Error retrieving content disposition settings:', err);
-      return res.apiv3Err(new ErrorV3('Failed to retrieve content disposition settings', 'get-content-disposition-failed'));
-    }
-  });
+   * @swagger
+   *
+   * /content-disposition-settings:
+   *   get:
+   *     tags: [Content-Disposition Settings]
+   *     summary: Get content disposition settings for configurable MIME types
+   *     security:
+   *       - cookieAuth: []
+   *     responses:
+   *       200:
+   *         description: Successfully retrieved content disposition settings.
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
+   *               properties:
+   *                 currentDispositionSettings:
+   *                   type: object
+   *                   properties:
+   *                     inlineMimeTypes:
+   *                       type: array
+   *                       description: The list of MIME types set to inline.
+   *                       items:
+   *                         type: string
+   *                     attachmentMimeTypes:
+   *                       type: array
+   *                       description: The list of MIME types set to attachment.
+   *                       items:
+   *                         type: string
+   *
+   */
+  router.get(
+    '/',
+    loginRequiredStrictly,
+    adminRequired,
+    (req: AuthorizedRequest, res: ApiV3Response) => {
+      try {
+        const inlineDispositionSettings = configManager.getConfig(
+          'attachments:contentDisposition:inlineMimeTypes',
+        );
+        const attachmentDispositionSettings = configManager.getConfig(
+          'attachments:contentDisposition:attachmentMimeTypes',
+        );
+
+        return res.apiv3({
+          currentDispositionSettings: {
+            inlineMimeTypes: inlineDispositionSettings.inlineMimeTypes,
+            attachmentMimeTypes:
+              attachmentDispositionSettings.attachmentMimeTypes,
+          },
+        });
+      } catch (err) {
+        logger.error('Error retrieving content disposition settings:', err);
+        return res.apiv3Err(
+          new ErrorV3(
+            'Failed to retrieve content disposition settings',
+            'get-content-disposition-failed',
+          ),
+        );
+      }
+    },
+  );
 
   return router;
 };

+ 4 - 5
apps/app/src/server/service/file-uploader/aws/index.ts

@@ -290,11 +290,10 @@ class AwsFileUploader extends AbstractFileUploader {
     }
   }
 
-  override async respond(
-    res: Response,
-    attachment: IAttachmentDocument,
-    opts?: RespondOptions,
-  ): Promise<void> {
+  /**
+   * @inheritdoc
+   */
+  override respond(): void {
     throw new Error('AwsFileUploader does not support ResponseMode.DELEGATE.');
   }
 

+ 5 - 5
apps/app/src/server/service/file-uploader/utils/headers.spec.ts

@@ -1,9 +1,6 @@
-import {
-  vi, describe, it, expect, beforeEach,
-} from 'vitest';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
 
 import { configManager } from '../../config-manager';
-
 import { determineDisposition } from './headers';
 
 vi.mock('../../config-manager', () => ({
@@ -17,7 +14,10 @@ describe('determineDisposition', () => {
     vi.resetAllMocks();
   });
 
-  const setupMocks = (inlineMimeTypes: string[], attachmentMimeTypes: string[]) => {
+  const setupMocks = (
+    inlineMimeTypes: string[],
+    attachmentMimeTypes: string[],
+  ) => {
     vi.mocked(configManager.getConfig).mockImplementation(((key: string) => {
       if (key === 'attachments:contentDisposition:inlineMimeTypes') {
         return { inlineMimeTypes };

+ 10 - 5
apps/app/src/server/service/file-uploader/utils/security.ts

@@ -1,4 +1,7 @@
-export const defaultContentDispositionSettings: Record<string, 'inline' | 'attachment'> = {
+export const defaultContentDispositionSettings: Record<
+  string,
+  'inline' | 'attachment'
+> = {
   // Image Types
   'image/jpeg': 'inline',
   'image/png': 'inline',
@@ -31,11 +34,14 @@ export const defaultContentDispositionSettings: Record<string, 'inline' | 'attac
 
   // Other Common Document Formats
   'application/msword': 'attachment',
-  'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'attachment',
+  'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
+    'attachment',
   'application/vnd.ms-excel': 'attachment',
-  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'attachment',
+  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
+    'attachment',
   'application/vnd.ms-powerpoint': 'attachment',
-  'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'attachment',
+  'application/vnd.openxmlformats-officedocument.presentationml.presentation':
+    'attachment',
   'application/zip': 'attachment',
   'application/x-rar-compressed': 'attachment',
   'text/csv': 'attachment',
@@ -68,7 +74,6 @@ export const strictMimeTypeSettings: Record<string, 'inline' | 'attachment'> = {
   'font/otf': 'attachment',
 };
 
-
 export const laxMimeTypeSettings: Record<string, 'inline' | 'attachment'> = {
   // Documents
   'application/pdf': 'inline',