mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-02 03:02:19 +08:00
feat(Vditor): support s3 pro (#6441)
* feat(Vditor): support s3 pro * fix(Edit): refactor upload handling to improve custom request support * fix(Edit): enhance file upload handling with custom request support and add localization for upload messages * fix: input issue * feat: enhance upload functionality by integrating collection manager in markdown editor * feat: add upload error handling and improve localization messages for storage configuration * fix: streamline upload loading message handling and improve baseUrl retrieval in storage actions * fix: remove unused imports and refactor storage afterSave and afterDestroy hooks for better async handling * refactor: enhance file upload functionality and improve loading messages across multiple locales * refactor: streamline upload method selection process in PluginFileManagerClient * fix: implement file upload support check and streamline storage retrieval process * fix: add ACL permission check for logged-in users in Vditor plugin * fix: change ACL permission from 'loggedIn' to 'public' for Vditor plugin * fix: update file storage description in Markdown editor to include default attachments * test: add unit tests for vditor storage check functionality * test: add unit tests for PluginFileManagerClient uploadFile method * fix: handle optional chaining for collection options in public forms plugin * fix: add getStorageByCollectionName action and update permissions * fix: update storage response structure in vditor plugin * fix: update ACL permissions for vditor and storages to require logged-in users * fix: simplify uploadFile method by removing redundant storageId retrieval * fix: update storage assertions in vditor tests to use inline snapshots * fix: enhance file upload functionality with improved storage handling and error messages * fix: remove getStorageByCollectionName method and update ACL permissions for storages * fix: remove commonUpload method from storage types and update imports * fix: update parseCollectionData method to include dataSourceKey for improved data handling * fix: update useStorageUploadProps to enhance storage upload properties handling * fix: update action handling for storages to include createPresignedUrl * fix: ensure focus in input box before file upload
This commit is contained in:
parent
eff252f3f5
commit
dcbaa767d2
@ -7,10 +7,12 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { useAPIClient, useApp, withDynamicSchemaProps } from '@nocobase/client';
|
||||
import { useAPIClient, useCompile, usePlugin, withDynamicSchemaProps } from '@nocobase/client';
|
||||
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Vditor from 'vditor';
|
||||
import { defaultToolbar } from '../interfaces/markdown-vditor';
|
||||
import { NAMESPACE } from '../locale';
|
||||
import { useCDN } from './const';
|
||||
import useStyle from './style';
|
||||
|
||||
@ -24,11 +26,15 @@ export const Edit = withDynamicSchemaProps((props) => {
|
||||
const vdFullscreen = useRef(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const containerParentRef = useRef<HTMLDivElement>(null);
|
||||
const app = useApp();
|
||||
const apiClient = useAPIClient();
|
||||
const cdn = useCDN();
|
||||
const { wrapSSR, hashId, componentCls: containerClassName } = useStyle();
|
||||
const locale = apiClient.auth.locale || 'en-US';
|
||||
const fileManagerPlugin: any = usePlugin('@nocobase/plugin-file-manager');
|
||||
const compile = useCompile();
|
||||
const compileRef = useRef(compile);
|
||||
compileRef.current = compile;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const lang: any = useMemo(() => {
|
||||
const currentLang = locale.replace(/-/g, '_');
|
||||
@ -41,7 +47,6 @@ export const Edit = withDynamicSchemaProps((props) => {
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const uploadFileCollection = fileCollection || 'attachments';
|
||||
const toolbarConfig = toolbar ?? defaultToolbar;
|
||||
|
||||
const vditor = new Vditor(containerRef.current, {
|
||||
@ -68,24 +73,63 @@ export const Edit = withDynamicSchemaProps((props) => {
|
||||
onChange(value);
|
||||
},
|
||||
upload: {
|
||||
url: app.getApiUrl(`${uploadFileCollection}:create`),
|
||||
headers: apiClient.getHeaders(),
|
||||
multiple: false,
|
||||
fieldName: 'file',
|
||||
max: 1024 * 1024 * 1024, // 1G
|
||||
format(files, responseText) {
|
||||
const response = JSON.parse(responseText);
|
||||
const formatResponse = {
|
||||
msg: '',
|
||||
code: 0,
|
||||
data: {
|
||||
errFiles: [],
|
||||
succMap: {
|
||||
[response.data.filename]: response.data.url,
|
||||
},
|
||||
},
|
||||
};
|
||||
return JSON.stringify(formatResponse);
|
||||
async handler(files: File[]) {
|
||||
const file = files[0];
|
||||
|
||||
// Need to ensure focus is in the current input box before uploading
|
||||
vditor.focus();
|
||||
|
||||
const { data: checkData } = await apiClient.resource('vditor').check({
|
||||
fileCollectionName: fileCollection,
|
||||
});
|
||||
|
||||
if (!checkData?.data?.isSupportToUploadFiles) {
|
||||
vditor.tip(
|
||||
t('vditor.uploadError.message', { ns: NAMESPACE, storageTitle: checkData.data.storage?.title }),
|
||||
0,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
vditor.tip(t('uploading'), 0);
|
||||
const { data, errorMessage } = await fileManagerPlugin.uploadFile({
|
||||
file,
|
||||
fileCollectionName: fileCollection,
|
||||
storageId: checkData?.data?.storage?.id,
|
||||
storageType: checkData?.data?.storage?.type,
|
||||
storageRules: checkData?.data?.storage?.rules,
|
||||
});
|
||||
|
||||
if (errorMessage) {
|
||||
vditor.tip(compileRef.current(errorMessage), 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
vditor.tip(t('Response data is empty', { ns: NAMESPACE }), 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = data.filename;
|
||||
const fileUrl = data.url;
|
||||
|
||||
// Check if the uploaded file is an image
|
||||
const isImage = file.type.startsWith('image/');
|
||||
|
||||
if (isImage) {
|
||||
// Insert as an image - will be displayed in the editor
|
||||
vditor.insertValue(``);
|
||||
} else {
|
||||
// For non-image files, insert as a download link
|
||||
vditor.insertValue(`[${fileName}](${fileUrl})`);
|
||||
}
|
||||
|
||||
// hide the tip
|
||||
vditor.tip(t(''), 10);
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -94,7 +138,8 @@ export const Edit = withDynamicSchemaProps((props) => {
|
||||
vdRef.current?.destroy();
|
||||
vdRef.current = undefined;
|
||||
};
|
||||
}, [fileCollection, toolbar?.join(',')]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [toolbar?.join(',')]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorReady && vdRef.current) {
|
||||
@ -155,7 +200,7 @@ export const Edit = withDynamicSchemaProps((props) => {
|
||||
|
||||
return wrapSSR(
|
||||
<div ref={containerParentRef} className={`${hashId} ${containerClassName}`}>
|
||||
<div id={hashId} ref={containerRef}></div>
|
||||
<div ref={containerRef}></div>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
|
@ -7,9 +7,9 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { ISchema } from '@formily/react';
|
||||
import { CollectionFieldInterface, interfacesProperties } from '@nocobase/client';
|
||||
import { generateNTemplate } from '../locale';
|
||||
import { ISchema } from '@formily/react';
|
||||
|
||||
const { defaultProps, operators } = interfacesProperties;
|
||||
|
||||
@ -58,7 +58,9 @@ export class MarkdownVditorFieldInterface extends CollectionFieldInterface {
|
||||
'x-reactions': {
|
||||
fulfill: {
|
||||
schema: {
|
||||
description: generateNTemplate('Used to store files uploaded in the Markdown editor'),
|
||||
description: generateNTemplate(
|
||||
'Used to store files uploaded in the Markdown editor (default: attachments)',
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
import { tval } from '@nocobase/utils/client';
|
||||
|
||||
const NAMESPACE = 'field-markdown-vditor';
|
||||
export const NAMESPACE = 'field-markdown-vditor';
|
||||
|
||||
export function generateNTemplate(key: string) {
|
||||
return tval(key, { ns: NAMESPACE })
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"Vditor": "Markdown(Vditor)",
|
||||
"File collection": "Dateisammlung",
|
||||
"Used to store files uploaded in the Markdown editor": "Wird verwendet, um Dateien zu speichern, die im Markdown-Editor hochgeladen wurden",
|
||||
"Used to store files uploaded in the Markdown editor (default: attachments)": "Dateisammlung, die in den Markdown-Editor hochgeladen wurde (Standard: attachments)",
|
||||
"Toolbar": "Editor-Symbolleistenkonfiguration",
|
||||
"Emoji": "Emoji",
|
||||
"Headings": "Überschriften",
|
||||
@ -29,5 +29,10 @@
|
||||
"Both": "Editor & Vorschau",
|
||||
"Preview": "Vorschau",
|
||||
"Fullscreen": "Vollbild umschalten",
|
||||
"Outline": "Gliederung"
|
||||
"Outline": "Gliederung",
|
||||
"uploading": "Hochladen...",
|
||||
"upload failed": "Hochladen fehlgeschlagen",
|
||||
"vditor.uploadError.message": "Dateien können nicht in den aktuellen Speicher hochgeladen werden. Sie versuchen, Dateien in den Markdown-Editor hochzuladen, aber die aktuelle Speicher-Konfiguration unterstützt diese Operation nicht. Um die Upload-Funktionalität zu aktivieren, schließen Sie bitte die folgenden Einstellungen ab: 1. Gehen Sie zu \"Dateimanager\". 2. Wählen Sie den aktuell verwendeten Speicher ({{storageTitle}}) aus. 3. Legen Sie die \"Basis-URL\" fest und aktivieren Sie die Option \"Öffentlicher Zugriff\".",
|
||||
"Storage configuration not found. Please configure a storage provider first.": "Speicherkonfiguration nicht gefunden. Bitte konfigurieren Sie zuerst einen Speicheranbieter.",
|
||||
"Response data is empty": "Antwortdaten sind leer"
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"Vditor": "Markdown(Vditor)",
|
||||
"File collection": "File collection",
|
||||
"Used to store files uploaded in the Markdown editor": "Used to store files uploaded in the Markdown editor",
|
||||
"Used to store files uploaded in the Markdown editor (default: attachments)": "Used to store files uploaded in the Markdown editor (default: attachments)",
|
||||
"Toolbar": "Editor toolbar configuration",
|
||||
"Emoji": "Emoji",
|
||||
"Headings": "Headings",
|
||||
@ -29,5 +29,10 @@
|
||||
"Both": "Editor & Preview",
|
||||
"Preview": "Preview",
|
||||
"Fullscreen": "Toggle Fullscreen",
|
||||
"Outline": "Outline"
|
||||
}
|
||||
"Outline": "Outline",
|
||||
"uploading": "Uploading...",
|
||||
"upload failed": "upload failed",
|
||||
"vditor.uploadError.message": "Unable to upload files to the current storage. You are trying to upload files to the Markdown editor, but the current storage configuration does not support this operation. To enable upload functionality, please complete the following settings: 1. Go to \"File Manager\". 2. Select the storage currently in use ({{storageTitle}}). 3. Set \"Base URL\" and enable the \"Public access\" option.",
|
||||
"Storage configuration not found. Please configure a storage provider first.": "Storage configuration not found. Please configure a storage provider first.",
|
||||
"Response data is empty": "Response data is empty"
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
{
|
||||
"Vditor": "Markdown(Vditor)",
|
||||
"File collection": "Raccolta file",
|
||||
"Used to store files uploaded in the Markdown editor": "Utilizzato per archiviare i file caricati nell'editor Markdown",
|
||||
"Used to store files uploaded in the Markdown editor (default: attachments)": "Utilizzato per archiviare i file caricati nell'editor Markdown (default: attachments)",
|
||||
"Toolbar": "Configurazione barra degli strumenti dell'editor",
|
||||
"Emoji": "Emoji",
|
||||
"Headings": "Intestazioni",
|
||||
@ -29,5 +29,10 @@
|
||||
"Both": "Editor e Anteprima",
|
||||
"Preview": "Anteprima",
|
||||
"Fullscreen": "Attiva/disattiva schermo intero",
|
||||
"Outline": "Struttura"
|
||||
"Outline": "Struttura",
|
||||
"uploading": "Uploading...",
|
||||
"upload failed": "upload failed",
|
||||
"vditor.uploadError.message": "Impossibile caricare file nello storage corrente. Stai tentando di caricare file nell'editor Markdown, ma la configurazione di archiviazione corrente non supporta questa operazione. Per abilitare la funzionalità di caricamento, completa le seguenti impostazioni: 1. Vai a \"Gestione file\". 2. Seleziona lo storage attualmente in uso ({{storageTitle}}). 3. Imposta \"URL di base\" e abilita l'opzione \"Accesso pubblico\".",
|
||||
"Storage configuration not found. Please configure a storage provider first.": "Configurazione dello storage non trovata. Configura prima un provider di storage.",
|
||||
"Response data is empty": "I dati di risposta sono vuoti"
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"Vditor": "Markdown(Vditor)",
|
||||
"File collection": "ファイルコレクション",
|
||||
"Used to store files uploaded in the Markdown editor": "Markdownエディタでアップロードされたファイルを保存するために使用",
|
||||
"Used to store files uploaded in the Markdown editor (default: attachments)": "Markdownエディタでアップロードされたファイルを保存するために使用 (デフォルト: attachments)",
|
||||
"Toolbar": "ツールバー",
|
||||
"Emoji": "絵文字",
|
||||
"Headings": "見出し",
|
||||
@ -29,5 +29,10 @@
|
||||
"Both": "編集とプレビュー",
|
||||
"Preview": "プレビュー",
|
||||
"Fullscreen": "全画面表示の切り替え",
|
||||
"Outline": "アウトライン"
|
||||
}
|
||||
"Outline": "アウトライン",
|
||||
"uploading": "Uploading...",
|
||||
"upload failed": "upload failed",
|
||||
"vditor.uploadError.message": "現在のストレージにファイルをアップロードできません。Markdownエディタにファイルをアップロードしようとしていますが、現在のストレージ設定ではこの操作はサポートされていません。アップロード機能を有効にするには、次の設定を完了してください:1.「ファイルマネージャー」に移動します。2.現在使用しているストレージ({{storageTitle}})を選択します。3.「ベースURL」を設定し、「パブリックアクセス」オプションを有効にします。",
|
||||
"Storage configuration not found. Please configure a storage provider first.": "ストレージ構成が見つかりません。最初にストレージプロバイダーを構成してください。",
|
||||
"Response data is empty": "応答データが空です"
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"Vditor": "Markdown(Vditor)",
|
||||
"File collection": "파일 데이터 테이블",
|
||||
"Used to store files uploaded in the Markdown editor": "Markdown 편집기에 업로드된 파일을 저장하는 데 사용됩니다",
|
||||
"Used to store files uploaded in the Markdown editor (default: attachments)": "Markdown 편집기에 업로드된 파일을 저장하는 데 사용됩니다 (default: attachments)",
|
||||
"Toolbar": "편집기 도구 모음 구성",
|
||||
"Emoji": "이모지",
|
||||
"Headings": "제목크기",
|
||||
@ -29,5 +29,10 @@
|
||||
"Both": "에디터 & 미리보기",
|
||||
"Preview": "미리보기",
|
||||
"Fullscreen": "전체화면",
|
||||
"Outline": "개요"
|
||||
}
|
||||
"Outline": "개요",
|
||||
"uploading": "Uploading...",
|
||||
"upload failed": "upload failed",
|
||||
"vditor.uploadError.message": "현재 스토리지에 파일을 업로드할 수 없습니다. Markdown 편집기에 파일을 업로드하려고 하지만 현재 스토리지 구성에서는 이 작업을 지원하지 않습니다. 업로드 기능을 활성화하려면 다음 설정을 완료하십시오: 1. \"파일 관리\"로 이동합니다. 2. 현재 사용 중인 스토리지({{storageTitle}})를 선택합니다. 3. \"기본 URL\"을 설정하고 \"공개 액세스\" 옵션을 활성화합니다.",
|
||||
"Storage configuration not found. Please configure a storage provider first.": "저장소 구성을 찾을 수 없습니다. 먼저 저장소 공급자를 구성하십시오.",
|
||||
"Response data is empty": "응답 데이터가 비어 있습니다"
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"Vditor": "Markdown(Vditor)",
|
||||
"File collection": "文件数据表",
|
||||
"Used to store files uploaded in the Markdown editor": "用于存储在Markdown编辑器中上传的文件",
|
||||
"Used to store files uploaded in the Markdown editor (default: attachments)": "用于存储在Markdown编辑器中上传的文件 (默认: attachments)",
|
||||
"Toolbar": "编辑器工具栏配置",
|
||||
"Emoji": "表情",
|
||||
"Headings": "标题",
|
||||
@ -29,5 +29,10 @@
|
||||
"Both": "编辑 & 预览",
|
||||
"Preview": "预览",
|
||||
"Fullscreen": "全屏切换",
|
||||
"Outline": "大纲"
|
||||
}
|
||||
"Outline": "大纲",
|
||||
"uploading": "上传中...",
|
||||
"upload failed": "上传失败",
|
||||
"vditor.uploadError.message": "无法上传文件到当前存储。您正在尝试上传文件到 Markdown 编辑器,但当前的存储配置不支持此操作。要启用上传功能,请完成以下设置:1. 前往「文件管理器」。2. 选择当前正在使用的存储({{storageTitle}})。3. 设置「基础 URL」并启用「公开访问」选项。",
|
||||
"Storage configuration not found. Please configure a storage provider first.": "存储配置未找到。请先配置存储提供程序。",
|
||||
"Response data is empty": "接口返回的数据为空"
|
||||
}
|
||||
|
@ -0,0 +1,282 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { mockServer } from '@nocobase/test';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
describe('vditor:check', () => {
|
||||
let app;
|
||||
let db;
|
||||
let agent;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer({
|
||||
cors: {
|
||||
origin: '*',
|
||||
},
|
||||
plugins: ['field-sort', 'users', 'auth', 'file-manager', 'field-markdown-vditor'],
|
||||
});
|
||||
|
||||
await app.load();
|
||||
|
||||
db = app.db;
|
||||
agent = app.agent();
|
||||
|
||||
// 确保集合已同步
|
||||
await db.sync();
|
||||
|
||||
// 清除数据并准备测试数据
|
||||
await db.clean({ drop: true });
|
||||
|
||||
// 确保集合重新同步
|
||||
await db.sync();
|
||||
|
||||
// 创建测试所需的存储和文件集合
|
||||
await db.getRepository('storages').create({
|
||||
values: {
|
||||
name: 'default-storage',
|
||||
type: 'local',
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
await db.getRepository('storages').create({
|
||||
values: {
|
||||
name: 's3-storage',
|
||||
type: 's3-compatible',
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
await db.getRepository('storages').create({
|
||||
values: {
|
||||
name: 's3-storage-with-baseurl',
|
||||
type: 's3-compatible',
|
||||
default: false,
|
||||
options: {
|
||||
baseUrl: 'https://example.com',
|
||||
public: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 创建测试文件集合
|
||||
await db.collection({
|
||||
name: 'file-collection-default',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await db.collection({
|
||||
name: 'file-collection-s3',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
},
|
||||
],
|
||||
storage: 's3-storage',
|
||||
});
|
||||
|
||||
await db.collection({
|
||||
name: 'file-collection-s3-with-baseurl',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
},
|
||||
],
|
||||
storage: 's3-storage-with-baseurl',
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should return default storage when no fileCollectionName provided', async () => {
|
||||
const response = await agent.resource('vditor').check();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.data.isSupportToUploadFiles).toBe(true);
|
||||
expect(response.body.data.storage).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": 1,
|
||||
"name": "default-storage",
|
||||
"rules": {},
|
||||
"title": null,
|
||||
"type": "local",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should return default storage when fileCollectionName without storage is provided', async () => {
|
||||
const response = await agent.resource('vditor').check({
|
||||
fileCollectionName: 'file-collection-default',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.data.isSupportToUploadFiles).toBe(true);
|
||||
expect(response.body.data.storage).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": 1,
|
||||
"name": "default-storage",
|
||||
"rules": {},
|
||||
"title": null,
|
||||
"type": "local",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should return s3 storage and not support upload when s3 collection without baseUrl is provided', async () => {
|
||||
const response = await agent.resource('vditor').check({
|
||||
fileCollectionName: 'file-collection-s3',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.data.isSupportToUploadFiles).toBe(false);
|
||||
expect(response.body.data.storage).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": 2,
|
||||
"name": "s3-storage",
|
||||
"rules": {},
|
||||
"title": null,
|
||||
"type": "s3-compatible",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should return s3 storage and support upload when s3 collection with baseUrl is provided', async () => {
|
||||
const response = await agent.resource('vditor').check({
|
||||
fileCollectionName: 'file-collection-s3-with-baseurl',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.data.isSupportToUploadFiles).toBe(true);
|
||||
expect(response.body.data.storage).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": 3,
|
||||
"name": "s3-storage-with-baseurl",
|
||||
"rules": {},
|
||||
"title": null,
|
||||
"type": "s3-compatible",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle non-existent fileCollectionName gracefully', async () => {
|
||||
const response = await agent.resource('vditor').check({
|
||||
fileCollectionName: 'non-existent-collection',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.data.isSupportToUploadFiles).toBe(true);
|
||||
expect(response.body.data.storage).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": 1,
|
||||
"name": "default-storage",
|
||||
"rules": {},
|
||||
"title": null,
|
||||
"type": "local",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle s3 storage with baseUrl but not public attribute', async () => {
|
||||
// 创建测试所需的非公开 s3 存储
|
||||
await db.getRepository('storages').create({
|
||||
values: {
|
||||
name: 's3-storage-with-baseurl-not-public',
|
||||
type: 's3-compatible',
|
||||
default: false,
|
||||
options: {
|
||||
baseUrl: 'https://example.com',
|
||||
public: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 创建关联的文件集合
|
||||
await db.collection({
|
||||
name: 'file-collection-s3-baseurl-not-public',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
},
|
||||
],
|
||||
storage: 's3-storage-with-baseurl-not-public',
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
const response = await agent.resource('vditor').check({
|
||||
fileCollectionName: 'file-collection-s3-baseurl-not-public',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.data.isSupportToUploadFiles).toBe(false);
|
||||
expect(response.body.data.storage).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": 4,
|
||||
"name": "s3-storage-with-baseurl-not-public",
|
||||
"rules": {},
|
||||
"title": null,
|
||||
"type": "s3-compatible",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle non-s3 storage types correctly', async () => {
|
||||
// 创建一个非 s3 类型的存储
|
||||
await db.getRepository('storages').create({
|
||||
values: {
|
||||
name: 'other-storage-type',
|
||||
type: 'oss',
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// 创建关联的文件集合
|
||||
await db.collection({
|
||||
name: 'file-collection-oss',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
},
|
||||
],
|
||||
storage: 'other-storage-type',
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
const response = await agent.resource('vditor').check({
|
||||
fileCollectionName: 'file-collection-oss',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.data.isSupportToUploadFiles).toBe(true);
|
||||
expect(response.body.data.storage).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": 4,
|
||||
"name": "other-storage-type",
|
||||
"rules": {},
|
||||
"title": null,
|
||||
"type": "oss",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
@ -11,6 +11,11 @@ import { Plugin } from '@nocobase/server';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
// @ts-ignore
|
||||
import pkg from '../../package.json';
|
||||
|
||||
const namespace = pkg.name;
|
||||
|
||||
export class PluginFieldMarkdownVditorServer extends Plugin {
|
||||
async afterAdd() {}
|
||||
|
||||
@ -18,6 +23,63 @@ export class PluginFieldMarkdownVditorServer extends Plugin {
|
||||
|
||||
async load() {
|
||||
await this.copyVditorDist();
|
||||
this.setResource();
|
||||
this.app.acl.allow('vditor', 'check', 'loggedIn');
|
||||
}
|
||||
|
||||
setResource() {
|
||||
this.app.resourceManager.define({
|
||||
name: 'vditor',
|
||||
actions: {
|
||||
check: async (context, next) => {
|
||||
const { fileCollectionName } = context.action.params;
|
||||
let storage;
|
||||
|
||||
const fileCollection = this.db.getCollection(fileCollectionName || 'attachments');
|
||||
const storageName = fileCollection?.options?.storage;
|
||||
if (storageName) {
|
||||
storage = await this.db.getRepository('storages').findOne({
|
||||
where: {
|
||||
name: storageName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
storage = await this.db.getRepository('storages').findOne({
|
||||
where: {
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!storage) {
|
||||
context.throw(
|
||||
400,
|
||||
context.t('Storage configuration not found. Please configure a storage provider first.', {
|
||||
ns: namespace,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const isSupportToUploadFiles =
|
||||
storage.type !== 's3-compatible' || (storage.options?.baseUrl && storage.options?.public);
|
||||
|
||||
const storageInfo = {
|
||||
id: storage.id,
|
||||
title: storage.title,
|
||||
name: storage.name,
|
||||
type: storage.type,
|
||||
rules: storage.rules,
|
||||
};
|
||||
|
||||
context.body = {
|
||||
isSupportToUploadFiles: !!isSupportToUploadFiles,
|
||||
storage: storageInfo,
|
||||
};
|
||||
|
||||
await next();
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async copyVditorDist() {
|
||||
|
@ -7,19 +7,8 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import {
|
||||
Input,
|
||||
Upload,
|
||||
useCollection,
|
||||
useCollectionField,
|
||||
useCollectionManager,
|
||||
useCollectionRecordData,
|
||||
usePlugin,
|
||||
useRequest,
|
||||
withDynamicSchemaProps,
|
||||
} from '@nocobase/client';
|
||||
import React, { useEffect } from 'react';
|
||||
import { connect, mapProps, mapReadPretty, useField } from '@formily/react';
|
||||
import { useCollection, useCollectionField, useCollectionManager, usePlugin, useRequest } from '@nocobase/client';
|
||||
import { useEffect } from 'react';
|
||||
import FileManagerPlugin from '../';
|
||||
|
||||
export function useStorage(storage) {
|
||||
|
@ -8,15 +8,16 @@
|
||||
*/
|
||||
|
||||
import { Plugin, useCollection } from '@nocobase/client';
|
||||
import { STORAGE_TYPE_ALI_OSS, STORAGE_TYPE_LOCAL, STORAGE_TYPE_S3, STORAGE_TYPE_TX_COS } from '../constants';
|
||||
import { FileManagerProvider } from './FileManagerProvider';
|
||||
import { FileSizeField } from './FileSizeField';
|
||||
import { FileStoragePane } from './FileStorage';
|
||||
import { useAttachmentFieldProps, useFileCollectionStorageRules } from './hooks';
|
||||
import { useStorageCfg } from './hooks/useStorageUploadProps';
|
||||
import { AttachmentFieldInterface } from './interfaces/attachment';
|
||||
import { NAMESPACE } from './locale';
|
||||
import { storageTypes } from './schemas/storageTypes';
|
||||
import { AttachmentFieldInterface } from './interfaces/attachment';
|
||||
import { FileCollectionTemplate } from './templates';
|
||||
import { useAttachmentFieldProps, useFileCollectionStorageRules } from './hooks';
|
||||
import { FileSizeField } from './FileSizeField';
|
||||
import { STORAGE_TYPE_ALI_OSS, STORAGE_TYPE_LOCAL, STORAGE_TYPE_S3, STORAGE_TYPE_TX_COS } from '../constants';
|
||||
|
||||
export class PluginFileManagerClient extends Plugin {
|
||||
// refer by plugin-field-attachment-url
|
||||
@ -58,6 +59,7 @@ export class PluginFileManagerClient extends Plugin {
|
||||
this.app.addScopes({
|
||||
useAttachmentFieldProps,
|
||||
useFileCollectionStorageRules,
|
||||
useStorageCfg,
|
||||
});
|
||||
|
||||
this.app.addComponents({
|
||||
@ -72,6 +74,49 @@ export class PluginFileManagerClient extends Plugin {
|
||||
getStorageType(name: string) {
|
||||
return this.storageTypes.get(name);
|
||||
}
|
||||
|
||||
async uploadFile(options?: {
|
||||
file: File;
|
||||
fileCollectionName?: string;
|
||||
storageType?: string;
|
||||
/** 后面可能会废弃这个参数 */
|
||||
storageId?: number;
|
||||
storageRules?: {
|
||||
size: number;
|
||||
};
|
||||
}): Promise<{ errorMessage?: string; data?: any }> {
|
||||
const storageTypeObj = this.getStorageType(options?.storageType);
|
||||
if (storageTypeObj?.upload) {
|
||||
// 1. If storageType is provided, call the upload method directly
|
||||
return await storageTypeObj.upload({
|
||||
file: options.file,
|
||||
apiClient: this.app.apiClient,
|
||||
storageType: options.storageType,
|
||||
storageId: options.storageId,
|
||||
storageRules: options.storageRules,
|
||||
fileCollectionName: options.fileCollectionName,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. If storageType is not provided, use the default upload method
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', options.file);
|
||||
const res = await this.app.apiClient.request({
|
||||
url: `${options.fileCollectionName || 'attachments'}:create`,
|
||||
method: 'post',
|
||||
data: formData,
|
||||
});
|
||||
|
||||
return {
|
||||
data: res.data?.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
errorMessage: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default PluginFileManagerClient;
|
||||
|
@ -53,7 +53,7 @@ const collection = {
|
||||
name: 'baseUrl',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: `{{t("Access base URL", { ns: "${NAMESPACE}" })}}`,
|
||||
title: `{{t("Base URL", { ns: "${NAMESPACE}" })}}`,
|
||||
type: 'string',
|
||||
'x-component': 'TextAreaWithGlobalScope',
|
||||
required: true,
|
||||
|
@ -7,11 +7,8 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { NAMESPACE } from '../../locale';
|
||||
import common from './common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default {
|
||||
title: `{{t("Aliyun OSS", { ns: "${NAMESPACE}" })}}`,
|
||||
|
@ -7,10 +7,8 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NAMESPACE } from '../../locale';
|
||||
import common from './common';
|
||||
import React from 'react';
|
||||
|
||||
export default {
|
||||
title: `{{t("Tencent COS", { ns: "${NAMESPACE}" })}}`,
|
||||
|
@ -20,7 +20,7 @@
|
||||
"Storage type": "Speichertyp",
|
||||
"Default storage": "Standardspeicher",
|
||||
"Storage base URL": "Basis-URL des Speichers",
|
||||
"Access base URL": "Basis-URL für den Zugriff",
|
||||
"Base URL": "Basis-URL",
|
||||
"Base URL for file access, could be your CDN base URL. For example: \"https://cdn.nocobase.com\".": "Basis-URL für den Dateizugriff, könnte Ihre CDN-Basis-URL sein. Zum Beispiel: \"https://cdn.nocobase.com\".",
|
||||
"Destination": "Ziel",
|
||||
"Use the built-in static file server": "Den integrierten statischen Dateiserver verwenden",
|
||||
|
@ -30,7 +30,7 @@
|
||||
"Allow uploading multiple files": "Consenti caricamento di più file",
|
||||
"Storage": "Spazio di archiviazione",
|
||||
"Storages": "Spazi di archiviazione",
|
||||
"Access base URL": "URL base accesso",
|
||||
"Base URL": "URL base",
|
||||
"Base URL for file access, could be your CDN base URL. For example: \"https://cdn.nocobase.com\".": "URL base per l'accesso ai file, potrebbe essere l'URL base del tuo CDN. Ad esempio: \"https://cdn.nocobase.com\".",
|
||||
"Relative path the file will be saved to. Left blank as root path. The leading and trailing slashes \"/\" will be ignored. For example: \"user/avatar\".": "Percorso relativo in cui verrà salvato il file. Lasciare vuoto per il percorso radice. Le barre iniziali e finali \"/\" verranno ignorate. Ad esempio: \"user/avatar\".",
|
||||
"Default storage will be used when not selected": "Se non selezionato verrà utilizzato lo spazio di archiviazione predefinito",
|
||||
|
@ -32,7 +32,7 @@
|
||||
"Allow uploading multiple files": "複数ファイルのアップロードを許可する",
|
||||
"Storage": "ストレージ",
|
||||
"Storages": "ストレージ",
|
||||
"Access base URL": "アクセスURLのベース",
|
||||
"Base URL": "ベースURL",
|
||||
"Base URL for file access, could be your CDN base URL. For example: \"https://cdn.nocobase.com\".": "术语调整,“基礎URL”改为“ベースURL”",
|
||||
"Relative path the file will be saved to. Left blank as root path. The leading and trailing slashes \"/\" will be ignored. For example: \"user/avatar\".": "更清晰的表达方式,符合技术描述",
|
||||
"Default storage will be used when not selected": "空欄の場合はデフォルトのストレージが使用されます。",
|
||||
|
@ -18,7 +18,7 @@
|
||||
"Storage name": "存储空间标识",
|
||||
"Storage type": "存储类型",
|
||||
"Default storage": "默认存储空间",
|
||||
"Access base URL": "访问 URL 基础",
|
||||
"Base URL": "基础 URL",
|
||||
"Base URL for file access, could be your CDN base URL. For example: \"https://cdn.nocobase.com\".": "文件访问的基础 URL,可以是你的 CDN 基础 URL。例如:\"https://cdn.nocobase.com\"。",
|
||||
"Destination": "上传目标文件夹",
|
||||
"Use the built-in static file server": "使用内置静态文件服务",
|
||||
|
@ -30,6 +30,8 @@ export async function getBasicInfo(context, next) {
|
||||
name: result.name,
|
||||
type: result.type,
|
||||
rules: result.rules,
|
||||
baseUrl: result.options?.baseUrl,
|
||||
public: result.options?.public,
|
||||
};
|
||||
|
||||
next();
|
||||
|
@ -7,12 +7,12 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { basename } from 'path';
|
||||
import fs from 'fs';
|
||||
import { basename } from 'path';
|
||||
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { isURL, Registry } from '@nocobase/utils';
|
||||
import { Collection, Model, Transactionable } from '@nocobase/database';
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { Registry } from '@nocobase/utils';
|
||||
import { STORAGE_TYPE_ALI_OSS, STORAGE_TYPE_LOCAL, STORAGE_TYPE_S3, STORAGE_TYPE_TX_COS } from '../constants';
|
||||
import initActions from './actions';
|
||||
import { AttachmentInterface } from './interfaces/attachment-interface';
|
||||
|
@ -14,8 +14,9 @@ import { parseAssociationNames } from './hook';
|
||||
class PasswordError extends Error {}
|
||||
|
||||
export class PluginPublicFormsServer extends Plugin {
|
||||
async parseCollectionData(formCollection, appends) {
|
||||
const collection = this.db.getCollection(formCollection);
|
||||
async parseCollectionData(dataSourceKey, formCollection, appends) {
|
||||
const dataSource = this.app.dataSourceManager.dataSources.get(dataSourceKey);
|
||||
const collection = dataSource.collectionManager.getCollection(formCollection);
|
||||
const collections = [
|
||||
{
|
||||
name: collection.name,
|
||||
@ -77,7 +78,7 @@ export class PluginPublicFormsServer extends Plugin {
|
||||
const schema = await uiSchema.getJsonSchema(filterByTk);
|
||||
const { getAssociationAppends } = parseAssociationNames(dataSourceKey, collectionName, this.app, schema);
|
||||
const { appends } = getAssociationAppends();
|
||||
const collections = await this.parseCollectionData(collectionName, appends);
|
||||
const collections = await this.parseCollectionData(dataSourceKey, collectionName, appends);
|
||||
return {
|
||||
dataSource: {
|
||||
key: dataSourceKey,
|
||||
@ -172,8 +173,9 @@ export class PluginPublicFormsServer extends Plugin {
|
||||
};
|
||||
} else if (
|
||||
(actionName === 'list' && ctx.PublicForm['targetCollections'].includes(resourceName)) ||
|
||||
(collection.options.template === 'file' && actionName === 'create') ||
|
||||
(resourceName === 'storages' && actionName === 'getBasicInfo') ||
|
||||
(collection?.options.template === 'file' && actionName === 'create') ||
|
||||
(resourceName === 'storages' && ['getBasicInfo', 'createPresignedUrl'].includes(actionName)) ||
|
||||
(resourceName === 'vditor' && ['check'].includes(actionName)) ||
|
||||
(resourceName === 'map-configuration' && actionName === 'get')
|
||||
) {
|
||||
ctx.permission = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user