refactor(plugin-fm): change api and allow to select storage (#1250)
* refactor(plugin-fm): change api and allow to select storage * fix(plugin-fm): fix lint errors and demo actions * refactor(plugin-fm): refactor action codes * fix(plugin-fm): fix api in test * fix(plugin-fm): fix build * fix(plugin-fm): fix locale * refactor(plugin-fm): hide storage from api and use sourceField param * fix(plugin-fm): fix storage select load * fix: improve code * fix(plugin-fm): change to attachmentField * refactor(plugin-fm): change middleware name * fix(plugin-fm): fix params in test cases --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
b33c00be8f
commit
36d16bc015
@ -1,4 +1,3 @@
|
|||||||
export * from './attachment';
|
|
||||||
export * from './checkbox';
|
export * from './checkbox';
|
||||||
export * from './checkboxGroup';
|
export * from './checkboxGroup';
|
||||||
export * from './chinaRegion';
|
export * from './chinaRegion';
|
||||||
|
@ -209,7 +209,6 @@ export default {
|
|||||||
"Field source":"Field source",
|
"Field source":"Field source",
|
||||||
"Preview":"Preview",
|
"Preview":"Preview",
|
||||||
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.",
|
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.",
|
||||||
"Storage type": "Storage type",
|
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
"Edit collection": "Edit collection",
|
"Edit collection": "Edit collection",
|
||||||
"Configure fields": "Configure fields",
|
"Configure fields": "Configure fields",
|
||||||
@ -245,7 +244,6 @@ export default {
|
|||||||
"Radio group": "Radio group",
|
"Radio group": "Radio group",
|
||||||
"Checkbox group": "Checkbox group",
|
"Checkbox group": "Checkbox group",
|
||||||
"China region": "China region",
|
"China region": "China region",
|
||||||
"Attachment": "Attachment",
|
|
||||||
"Date & Time": "Date & Time",
|
"Date & Time": "Date & Time",
|
||||||
"Datetime": "Datetime",
|
"Datetime": "Datetime",
|
||||||
"Relation": "Relation",
|
"Relation": "Relation",
|
||||||
@ -523,7 +521,6 @@ export default {
|
|||||||
"Redirect to": "Redirect to",
|
"Redirect to": "Redirect to",
|
||||||
"Save action": "Save action",
|
"Save action": "Save action",
|
||||||
"Exists": "Exists",
|
"Exists": "Exists",
|
||||||
"Filename": "Filename",
|
|
||||||
"Add condition": "Add condition",
|
"Add condition": "Add condition",
|
||||||
"Add condition group": "Add condition group",
|
"Add condition group": "Add condition group",
|
||||||
"exists": "exists",
|
"exists": "exists",
|
||||||
@ -540,7 +537,6 @@ export default {
|
|||||||
"Expression": "Expression",
|
"Expression": "Expression",
|
||||||
"Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "Input +, -, *, /, ( ) to calculate, input @ to open field variables.",
|
"Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "Input +, -, *, /, ( ) to calculate, input @ to open field variables.",
|
||||||
"Formula error.": "Formula error.",
|
"Formula error.": "Formula error.",
|
||||||
"Accept": "Accept",
|
|
||||||
"Rich Text": "Rich Text",
|
"Rich Text": "Rich Text",
|
||||||
"Junction collection": "Junction collection",
|
"Junction collection": "Junction collection",
|
||||||
"Leave it blank, unless you need a custom intermediate table": "Leave it blank, unless you need a custom intermediate table",
|
"Leave it blank, unless you need a custom intermediate table": "Leave it blank, unless you need a custom intermediate table",
|
||||||
@ -593,22 +589,6 @@ export default {
|
|||||||
"Action permission": "Action permission",
|
"Action permission": "Action permission",
|
||||||
"Field permission": "Field permission",
|
"Field permission": "Field permission",
|
||||||
"Scope name": "Scope name",
|
"Scope name": "Scope name",
|
||||||
"File storages": "File storages",
|
|
||||||
"Storage display name": "Storage display name",
|
|
||||||
"Storage name": "Storage name",
|
|
||||||
"Default storage": "Default storage",
|
|
||||||
"Add storage": "Add storage",
|
|
||||||
"Edit storage": "Edit storage",
|
|
||||||
"Storage base URL": "Storage base URL",
|
|
||||||
"Destination": "Destination",
|
|
||||||
"Use the built-in static file server": "Use the built-in static file server",
|
|
||||||
"Local storage": "Local storage",
|
|
||||||
"Aliyun OSS": "Aliyun OSS",
|
|
||||||
"Amazon S3": "Amazon S3",
|
|
||||||
"Tencent COS": "Tencent COS",
|
|
||||||
"Region": "Region",
|
|
||||||
"Bucket": "Bucket",
|
|
||||||
"Path": "Path",
|
|
||||||
"Unsaved changes": "Unsaved changes",
|
"Unsaved changes": "Unsaved changes",
|
||||||
"Are you sure you don't want to save?": "Are you sure you don't want to save?",
|
"Are you sure you don't want to save?": "Are you sure you don't want to save?",
|
||||||
"Dragging": "Dragging",
|
"Dragging": "Dragging",
|
||||||
|
@ -189,7 +189,6 @@ export default {
|
|||||||
"Field source": "ソースフィールド",
|
"Field source": "ソースフィールド",
|
||||||
"Preview": "プレビュー",
|
"Preview": "プレビュー",
|
||||||
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "ランダムに生成され、変更可能です。 アルファベット、数字、アンダースコアをサポートし、アルファベットから始まる必要があります。",
|
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "ランダムに生成され、変更可能です。 アルファベット、数字、アンダースコアをサポートし、アルファベットから始まる必要があります。",
|
||||||
"Storage type": "ストレージタイプ",
|
|
||||||
"Edit": "編集",
|
"Edit": "編集",
|
||||||
"Edit collection": "コレクションの編集",
|
"Edit collection": "コレクションの編集",
|
||||||
"Configure fields": "フィールドの設定",
|
"Configure fields": "フィールドの設定",
|
||||||
@ -217,7 +216,6 @@ export default {
|
|||||||
"Radio group": "ラジオボタングループ",
|
"Radio group": "ラジオボタングループ",
|
||||||
"Checkbox group": "チェックボックスグループ",
|
"Checkbox group": "チェックボックスグループ",
|
||||||
"China region": "中国地域",
|
"China region": "中国地域",
|
||||||
"Attachment": "添付ファイル",
|
|
||||||
"Date & Time": "日付と時間",
|
"Date & Time": "日付と時間",
|
||||||
"Datetime": "日付",
|
"Datetime": "日付",
|
||||||
"Relation": "関連づけ",
|
"Relation": "関連づけ",
|
||||||
@ -330,7 +328,6 @@ export default {
|
|||||||
"Add option": "オプションを追加",
|
"Add option": "オプションを追加",
|
||||||
"Related collection": "関連付けコレクション",
|
"Related collection": "関連付けコレクション",
|
||||||
"Allow linking to multiple records": "複数のレコードの関連付けを許可する",
|
"Allow linking to multiple records": "複数のレコードの関連付けを許可する",
|
||||||
"Allow uploading multiple files": "複数ファイルのアップロードを許可する",
|
|
||||||
"Configure calendar": "カレンダーの設定",
|
"Configure calendar": "カレンダーの設定",
|
||||||
"Title field": "タイトルフィールド",
|
"Title field": "タイトルフィールド",
|
||||||
"Start date field": "開始日フィールド",
|
"Start date field": "開始日フィールド",
|
||||||
@ -429,7 +426,6 @@ export default {
|
|||||||
"Redirect to": "リダイレクトする",
|
"Redirect to": "リダイレクトする",
|
||||||
"Save action": "操作を保存",
|
"Save action": "操作を保存",
|
||||||
"Exists": "存在する",
|
"Exists": "存在する",
|
||||||
"Filename": "ファイル名",
|
|
||||||
"Add condition": "条件の追加",
|
"Add condition": "条件の追加",
|
||||||
"Add condition group": "条件グループの追加",
|
"Add condition group": "条件グループの追加",
|
||||||
"exists": "が存在する",
|
"exists": "が存在する",
|
||||||
@ -446,7 +442,6 @@ export default {
|
|||||||
"Expression": "表达式",
|
"Expression": "表达式",
|
||||||
"Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "+、-、*、/、( ) で算術演算、@でフィールド変数を開くことができます。",
|
"Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "+、-、*、/、( ) で算術演算、@でフィールド変数を開くことができます。",
|
||||||
"Formula error.": "式の検証エラーです。",
|
"Formula error.": "式の検証エラーです。",
|
||||||
"Accept": "ファイル形式",
|
|
||||||
"Rich Text": "リッチテキスト",
|
"Rich Text": "リッチテキスト",
|
||||||
"Junction collection": "中間コレクション",
|
"Junction collection": "中間コレクション",
|
||||||
"Leave it blank, unless you need a custom intermediate table": "カスタム中間テーブルが必要でない限り、デフォルトで空白のままにします",
|
"Leave it blank, unless you need a custom intermediate table": "カスタム中間テーブルが必要でない限り、デフォルトで空白のままにします",
|
||||||
@ -495,19 +490,6 @@ export default {
|
|||||||
"Action permission": "操作権限",
|
"Action permission": "操作権限",
|
||||||
"Field permission": "フィールド権限",
|
"Field permission": "フィールド権限",
|
||||||
"Scope name": "スコープ名",
|
"Scope name": "スコープ名",
|
||||||
"File storages": "ファイルストレージ",
|
|
||||||
"Storage display name": "ファイルストレージ名",
|
|
||||||
"Storage name": "ファイルストレージ識別子",
|
|
||||||
"Default storage": "デフォルトストレージ",
|
|
||||||
"Add storage": "ファイルストレージを追加",
|
|
||||||
"Edit storage": "ファイルストレージを編集",
|
|
||||||
"Storage base URL": "Storage base URL",
|
|
||||||
"Destination": "ファイルパス",
|
|
||||||
"Use the built-in static file server": "組み込みの静的ファイル サービスを使用する",
|
|
||||||
"Local storage": "ローカルストレージ",
|
|
||||||
"Aliyun OSS": "Aliyun OSS",
|
|
||||||
"Tencent COS": "Tencent COS",
|
|
||||||
"Amazon S3": "Amazon S3",
|
|
||||||
"Unsaved changes": "変更が保存されていません",
|
"Unsaved changes": "変更が保存されていません",
|
||||||
"Are you sure you don't want to save?": "変更を保存しなくてもよいですか?",
|
"Are you sure you don't want to save?": "変更を保存しなくてもよいですか?",
|
||||||
"Dragging": "ドラッグ",
|
"Dragging": "ドラッグ",
|
||||||
|
@ -133,7 +133,6 @@ export default {
|
|||||||
"Collection name": "Имя Коллекции",
|
"Collection name": "Имя Коллекции",
|
||||||
"Categories":"Категории таблиц данных",
|
"Categories":"Категории таблиц данных",
|
||||||
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Случайно сгенерированный и может быть изменен. Поддерживает буквы, цифры и подчеркивания, должно начинаться с буквы.",
|
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Случайно сгенерированный и может быть изменен. Поддерживает буквы, цифры и подчеркивания, должно начинаться с буквы.",
|
||||||
"Storage type": "Тип Хранилища",
|
|
||||||
"Edit": "Изменить",
|
"Edit": "Изменить",
|
||||||
"Edit collection": "Изменить Коллекцию",
|
"Edit collection": "Изменить Коллекцию",
|
||||||
"Configure fields": "Конфигурировать поля",
|
"Configure fields": "Конфигурировать поля",
|
||||||
@ -159,7 +158,6 @@ export default {
|
|||||||
"Radio group": "Радио группа",
|
"Radio group": "Радио группа",
|
||||||
"Checkbox group": "Чекбокс группа",
|
"Checkbox group": "Чекбокс группа",
|
||||||
"China region": "Китай регион",
|
"China region": "Китай регион",
|
||||||
"Attachment": "Вложение",
|
|
||||||
"Date & Time": "Дата & Время",
|
"Date & Time": "Дата & Время",
|
||||||
"Datetime": "Датавремя",
|
"Datetime": "Датавремя",
|
||||||
"Relation": "Связь",
|
"Relation": "Связь",
|
||||||
@ -272,7 +270,6 @@ export default {
|
|||||||
"Add option": "Добавить опцию",
|
"Add option": "Добавить опцию",
|
||||||
"Related collection": "Связанная коллекция",
|
"Related collection": "Связанная коллекция",
|
||||||
"Allow linking to multiple records": "Позволить прилинковать много записей",
|
"Allow linking to multiple records": "Позволить прилинковать много записей",
|
||||||
"Allow uploading multiple files": "Позволить загружать много файлов",
|
|
||||||
"Configure calendar": "Настроить календарь",
|
"Configure calendar": "Настроить календарь",
|
||||||
"Title field": "Поле заголовка",
|
"Title field": "Поле заголовка",
|
||||||
"Start date field": "Поле даты начала",
|
"Start date field": "Поле даты начала",
|
||||||
@ -371,7 +368,6 @@ export default {
|
|||||||
"Redirect to": "Перенаправить на",
|
"Redirect to": "Перенаправить на",
|
||||||
"Save action": "Сохранить действие",
|
"Save action": "Сохранить действие",
|
||||||
"Exists": "Существуют",
|
"Exists": "Существуют",
|
||||||
"Filename": "Имя файла",
|
|
||||||
"Add condition": "Добавить правило",
|
"Add condition": "Добавить правило",
|
||||||
"Add condition group": "Добавить группу правил",
|
"Add condition group": "Добавить группу правил",
|
||||||
"exists": "существуют",
|
"exists": "существуют",
|
||||||
@ -388,7 +384,6 @@ export default {
|
|||||||
"Expression": "Переменная",
|
"Expression": "Переменная",
|
||||||
"Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "Введите +, -, *, /, ( ) для вычисления, введите @ чтобы открыть переменные поля.",
|
"Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "Введите +, -, *, /, ( ) для вычисления, введите @ чтобы открыть переменные поля.",
|
||||||
"Formula error.": "Ошибка формулы.",
|
"Formula error.": "Ошибка формулы.",
|
||||||
"Accept": "Подтвердить",
|
|
||||||
"Rich Text": "Rich Text",
|
"Rich Text": "Rich Text",
|
||||||
"Junction collection": "Коллекция Узлов",
|
"Junction collection": "Коллекция Узлов",
|
||||||
"Leave it blank, unless you need a custom intermediate table": "Оставьте это поле пустым, если вам не нужна пользовательская промежуточная таблица.",
|
"Leave it blank, unless you need a custom intermediate table": "Оставьте это поле пустым, если вам не нужна пользовательская промежуточная таблица.",
|
||||||
@ -433,19 +428,6 @@ export default {
|
|||||||
"Action permission": "Разрешения на действия",
|
"Action permission": "Разрешения на действия",
|
||||||
"Field permission": "Разрешения на поля",
|
"Field permission": "Разрешения на поля",
|
||||||
"Scope name": "Имя области",
|
"Scope name": "Имя области",
|
||||||
"File storages": "Файловые хранилища",
|
|
||||||
"Storage display name": "Имя храшилища на экране",
|
|
||||||
"Storage name": "Имя хранилища",
|
|
||||||
"Default storage": "Хранилище по умолчанию",
|
|
||||||
"Add storage": "Добавить хранилище",
|
|
||||||
"Edit storage": "Изменить хранилище",
|
|
||||||
"Storage base URL": "Базовый URL хранилища",
|
|
||||||
"Destination": "Назначение",
|
|
||||||
"Use the built-in static file server": "Использовать встроенный статический файл-сервер",
|
|
||||||
"Local storage": "Локальное хранилище",
|
|
||||||
"Aliyun OSS": "Aliyun OSS",
|
|
||||||
"Tencent COS": "Tencent COS",
|
|
||||||
"Amazon S3": "Amazon S3",
|
|
||||||
"Unsaved changes": "Несохраненные изменения",
|
"Unsaved changes": "Несохраненные изменения",
|
||||||
"Are you sure you don't want to save?": "Вы уверены, что не хотите сохранить?",
|
"Are you sure you don't want to save?": "Вы уверены, что не хотите сохранить?",
|
||||||
"Dragging": "Перетаскивание",
|
"Dragging": "Перетаскивание",
|
||||||
|
@ -132,7 +132,6 @@ export default {
|
|||||||
"Collection display name": "Koleksiyon görünen adı",
|
"Collection display name": "Koleksiyon görünen adı",
|
||||||
"Collection name": "Koleksiyon adı",
|
"Collection name": "Koleksiyon adı",
|
||||||
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Rastgele oluşturulur ve değiştirilebilir. Desteklenen içerik; harfler, sayılar ve alt çizgiler. Bir harfle başlamalıdır.",
|
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Rastgele oluşturulur ve değiştirilebilir. Desteklenen içerik; harfler, sayılar ve alt çizgiler. Bir harfle başlamalıdır.",
|
||||||
"Storage type": "Depolama türü",
|
|
||||||
"Edit": "Düzenle",
|
"Edit": "Düzenle",
|
||||||
"Edit collection": "Koleksiyon düzenle",
|
"Edit collection": "Koleksiyon düzenle",
|
||||||
"Configure fields": "Alanları düzenle",
|
"Configure fields": "Alanları düzenle",
|
||||||
@ -158,7 +157,6 @@ export default {
|
|||||||
"Radio group": "Radio Seçim grup",
|
"Radio group": "Radio Seçim grup",
|
||||||
"Checkbox group": "Checkbox grup",
|
"Checkbox group": "Checkbox grup",
|
||||||
"China region": "Çin bölgesi",
|
"China region": "Çin bölgesi",
|
||||||
"Attachment": "Dosya eki",
|
|
||||||
"Date & Time": "Tarih & Saat",
|
"Date & Time": "Tarih & Saat",
|
||||||
"Datetime": "Datetime",
|
"Datetime": "Datetime",
|
||||||
"Relation": "Relation",
|
"Relation": "Relation",
|
||||||
@ -272,7 +270,6 @@ export default {
|
|||||||
"Add option": "Seçenek ekle",
|
"Add option": "Seçenek ekle",
|
||||||
"Related collection": "Bağlantılı koleksiyon",
|
"Related collection": "Bağlantılı koleksiyon",
|
||||||
"Allow linking to multiple records": "Birden çok kayda bağlanmaya izin ver",
|
"Allow linking to multiple records": "Birden çok kayda bağlanmaya izin ver",
|
||||||
"Allow uploading multiple files": "Birden çok dosya yüklemeye izin ver",
|
|
||||||
"Configure calendar": "Takvimi yapılandır",
|
"Configure calendar": "Takvimi yapılandır",
|
||||||
"Start date field": "Başlangıç tarihi alanı",
|
"Start date field": "Başlangıç tarihi alanı",
|
||||||
"End date field": "Bitiş tarihi alanı",
|
"End date field": "Bitiş tarihi alanı",
|
||||||
@ -370,7 +367,6 @@ export default {
|
|||||||
"Redirect to": "Yönlendirilecek yer",
|
"Redirect to": "Yönlendirilecek yer",
|
||||||
"Save action": "Kaydet işlemi",
|
"Save action": "Kaydet işlemi",
|
||||||
"Exists": "Var olanlar",
|
"Exists": "Var olanlar",
|
||||||
"Filename": "Dosya adı",
|
|
||||||
"Add condition": "Koşul ekle",
|
"Add condition": "Koşul ekle",
|
||||||
"Add condition group": "Koşul grubu ekle",
|
"Add condition group": "Koşul grubu ekle",
|
||||||
"exists": "var olanlar",
|
"exists": "var olanlar",
|
||||||
@ -387,7 +383,6 @@ export default {
|
|||||||
"Expression": "Expression",
|
"Expression": "Expression",
|
||||||
"Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "Input +, -, *, /, ( ) to calculate, input @ to open field variables.",
|
"Input +, -, *, /, ( ) to calculate, input @ to open field variables.": "Input +, -, *, /, ( ) to calculate, input @ to open field variables.",
|
||||||
"Formula error.": "Formül hatalı.",
|
"Formula error.": "Formül hatalı.",
|
||||||
"Accept": "Kabul et",
|
|
||||||
"Rich Text": "Zengin Metin",
|
"Rich Text": "Zengin Metin",
|
||||||
"Junction collection": "Bağlantı koleksiyonu",
|
"Junction collection": "Bağlantı koleksiyonu",
|
||||||
"Leave it blank, unless you need a custom intermediate table": "Özel bir ara tabloya ihtiyacınız yoksa boş bırakın",
|
"Leave it blank, unless you need a custom intermediate table": "Özel bir ara tabloya ihtiyacınız yoksa boş bırakın",
|
||||||
@ -432,18 +427,6 @@ export default {
|
|||||||
"Action permission": "İşlem yetkisi",
|
"Action permission": "İşlem yetkisi",
|
||||||
"Field permission": "Alan yetkisi",
|
"Field permission": "Alan yetkisi",
|
||||||
"Scope name": "Kapsam adı",
|
"Scope name": "Kapsam adı",
|
||||||
"File storages": "Dosya depoları",
|
|
||||||
"Storage display name": "Depo görünen adı",
|
|
||||||
"Storage name": "Depo adı",
|
|
||||||
"Default storage": "Varsayılan depo",
|
|
||||||
"Add storage": "Depo ekle",
|
|
||||||
"Edit storage": "Depo düzenle",
|
|
||||||
"Storage base URL": "Depolama temel URLsi",
|
|
||||||
"Destination": "Hedef",
|
|
||||||
"Use the built-in static file server": "Yerleşik statik dosya sunucusunu kullanın",
|
|
||||||
"Local storage": "Lokal depolama",
|
|
||||||
"Aliyun OSS": "Aliyun OSS",
|
|
||||||
"Amazon S3": "Amazon S3",
|
|
||||||
"Unsaved changes": "Değişiklikler kaydedilmedi",
|
"Unsaved changes": "Değişiklikler kaydedilmedi",
|
||||||
"Are you sure you don't want to save?": "kaydetmek istemediğinizden emin misiniz??",
|
"Are you sure you don't want to save?": "kaydetmek istemediğinizden emin misiniz??",
|
||||||
"Dragging": "Sürükleme",
|
"Dragging": "Sürükleme",
|
||||||
|
@ -223,8 +223,6 @@ export default {
|
|||||||
"Field source":"来源字段",
|
"Field source":"来源字段",
|
||||||
"Preview":"预览",
|
"Preview":"预览",
|
||||||
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "随机生成,可修改。支持英文、数字和下划线,必须以英文字母开头。",
|
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "随机生成,可修改。支持英文、数字和下划线,必须以英文字母开头。",
|
||||||
"Storage type": "存储类型",
|
|
||||||
"Types will be used in database": "数据库使用的类型",
|
|
||||||
"Edit": "编辑",
|
"Edit": "编辑",
|
||||||
"Edit collection": "编辑数据表",
|
"Edit collection": "编辑数据表",
|
||||||
"Configure fields": "配置字段",
|
"Configure fields": "配置字段",
|
||||||
@ -267,7 +265,6 @@ export default {
|
|||||||
"Radio group": "单选框",
|
"Radio group": "单选框",
|
||||||
"Checkbox group": "复选框",
|
"Checkbox group": "复选框",
|
||||||
"China region": "中国行政区",
|
"China region": "中国行政区",
|
||||||
"Attachment": "附件",
|
|
||||||
"Date & Time": "日期 & 时间",
|
"Date & Time": "日期 & 时间",
|
||||||
"Datetime": "日期",
|
"Datetime": "日期",
|
||||||
"Relation": "关系类型",
|
"Relation": "关系类型",
|
||||||
@ -437,7 +434,6 @@ export default {
|
|||||||
"Add option": "添加选项",
|
"Add option": "添加选项",
|
||||||
"Related collection": "关系表",
|
"Related collection": "关系表",
|
||||||
"Allow linking to multiple records": "允许关联多条记录",
|
"Allow linking to multiple records": "允许关联多条记录",
|
||||||
"Allow uploading multiple files": "允许上传多个文件",
|
|
||||||
|
|
||||||
"Daily": "每天",
|
"Daily": "每天",
|
||||||
"Weekly": "每周",
|
"Weekly": "每周",
|
||||||
@ -564,7 +560,6 @@ export default {
|
|||||||
'Save action': '保存操作',
|
'Save action': '保存操作',
|
||||||
|
|
||||||
'Exists': '存在',
|
'Exists': '存在',
|
||||||
'Filename': '文件名',
|
|
||||||
'Add condition': '添加条件',
|
'Add condition': '添加条件',
|
||||||
'Add condition group': '添加条件分组',
|
'Add condition group': '添加条件分组',
|
||||||
'exists': '存在',
|
'exists': '存在',
|
||||||
@ -578,7 +573,6 @@ export default {
|
|||||||
'Role UID': '角色标识',
|
'Role UID': '角色标识',
|
||||||
|
|
||||||
'Precision': '精确度',
|
'Precision': '精确度',
|
||||||
'Accept': '文件格式',
|
|
||||||
'Rich Text': '富文本',
|
'Rich Text': '富文本',
|
||||||
'Junction collection': '中间表',
|
'Junction collection': '中间表',
|
||||||
'Leave it blank, unless you need a custom intermediate table': '默认留空,除非你需要一个自定义的中间表',
|
'Leave it blank, unless you need a custom intermediate table': '默认留空,除非你需要一个自定义的中间表',
|
||||||
@ -637,23 +631,6 @@ export default {
|
|||||||
'Field permission': '字段权限',
|
'Field permission': '字段权限',
|
||||||
'Scope name': '数据范围名称',
|
'Scope name': '数据范围名称',
|
||||||
|
|
||||||
'File storages': '文件存储',
|
|
||||||
'Storage display name': '文件存储名称',
|
|
||||||
'Storage name': '文件存储标识',
|
|
||||||
'Default storage': '默认存储',
|
|
||||||
'Add storage': '添加文件存储',
|
|
||||||
'Edit storage': '编辑文件存储',
|
|
||||||
'Storage base URL': 'Base URL',
|
|
||||||
'Destination': '存储路径',
|
|
||||||
'Use the built-in static file server': '使用内置静态文件服务',
|
|
||||||
'Local storage': '本地存储',
|
|
||||||
'Aliyun OSS': '阿里云 OSS',
|
|
||||||
'Amazon S3': '亚马逊 S3',
|
|
||||||
'Tencent COS': '腾讯云 COS',
|
|
||||||
'Region': '区域',
|
|
||||||
'Bucket': '存储桶',
|
|
||||||
'Path': '路径(相对)',
|
|
||||||
|
|
||||||
'Unsaved changes': '未保存修改',
|
'Unsaved changes': '未保存修改',
|
||||||
'Are you sure you don\'t want to save?': '你确定不保存修改吗?',
|
'Are you sure you don\'t want to save?': '你确定不保存修改吗?',
|
||||||
'Dragging': '拖拽中',
|
'Dragging': '拖拽中',
|
||||||
@ -708,7 +685,6 @@ export default {
|
|||||||
"Print": "打印",
|
"Print": "打印",
|
||||||
"Done": "完成",
|
"Done": "完成",
|
||||||
'Sign up successfully, and automatically jump to the sign in page': '注册成功,即将跳转到登录页面',
|
'Sign up successfully, and automatically jump to the sign in page': '注册成功,即将跳转到登录页面',
|
||||||
'File manager': '文件管理器',
|
|
||||||
'ACL': '访问控制',
|
'ACL': '访问控制',
|
||||||
'Collection manager': '数据表管理',
|
'Collection manager': '数据表管理',
|
||||||
'Plugin manager': '插件管理器',
|
'Plugin manager': '插件管理器',
|
||||||
|
@ -270,7 +270,7 @@ export default {
|
|||||||
uiSchema: {
|
uiSchema: {
|
||||||
'x-uid': '5xf7izxjny5',
|
'x-uid': '5xf7izxjny5',
|
||||||
name: '9az9003ijcm',
|
name: '9az9003ijcm',
|
||||||
'x-component-props': { multiple: true, action: 'attachments:upload' },
|
'x-component-props': { multiple: true, action: 'attachments:create' },
|
||||||
type: 'array',
|
type: 'array',
|
||||||
'x-component': 'Upload.Attachment',
|
'x-component': 'Upload.Attachment',
|
||||||
title: '附件',
|
title: '附件',
|
||||||
@ -660,7 +660,7 @@ export default {
|
|||||||
uiSchema: {
|
uiSchema: {
|
||||||
'x-uid': 'wosew16td91',
|
'x-uid': 'wosew16td91',
|
||||||
name: 'p82ihvtkxtf',
|
name: 'p82ihvtkxtf',
|
||||||
'x-component-props': { multiple: true, action: 'attachments:upload' },
|
'x-component-props': { multiple: true, action: 'attachments:create' },
|
||||||
type: 'array',
|
type: 'array',
|
||||||
'x-component': 'Upload.Attachment',
|
'x-component': 'Upload.Attachment',
|
||||||
title: '附件',
|
title: '附件',
|
||||||
|
@ -270,7 +270,7 @@ export default {
|
|||||||
uiSchema: {
|
uiSchema: {
|
||||||
'x-uid': '5xf7izxjny5',
|
'x-uid': '5xf7izxjny5',
|
||||||
name: '9az9003ijcm',
|
name: '9az9003ijcm',
|
||||||
'x-component-props': { multiple: true, action: 'attachments:upload' },
|
'x-component-props': { multiple: true, action: 'attachments:create' },
|
||||||
type: 'array',
|
type: 'array',
|
||||||
'x-component': 'Upload.Attachment',
|
'x-component': 'Upload.Attachment',
|
||||||
title: '附件',
|
title: '附件',
|
||||||
@ -660,7 +660,7 @@ export default {
|
|||||||
uiSchema: {
|
uiSchema: {
|
||||||
'x-uid': 'wosew16td91',
|
'x-uid': 'wosew16td91',
|
||||||
name: 'p82ihvtkxtf',
|
name: 'p82ihvtkxtf',
|
||||||
'x-component-props': { multiple: true, action: 'attachments:upload' },
|
'x-component-props': { multiple: true, action: 'attachments:create' },
|
||||||
type: 'array',
|
type: 'array',
|
||||||
'x-component': 'Upload.Attachment',
|
'x-component': 'Upload.Attachment',
|
||||||
title: '附件',
|
title: '附件',
|
||||||
|
@ -270,7 +270,7 @@ export default {
|
|||||||
uiSchema: {
|
uiSchema: {
|
||||||
'x-uid': '5xf7izxjny5',
|
'x-uid': '5xf7izxjny5',
|
||||||
name: '9az9003ijcm',
|
name: '9az9003ijcm',
|
||||||
'x-component-props': { multiple: true, action: 'attachments:upload' },
|
'x-component-props': { multiple: true, action: 'attachments:create' },
|
||||||
type: 'array',
|
type: 'array',
|
||||||
'x-component': 'Upload.Attachment',
|
'x-component': 'Upload.Attachment',
|
||||||
title: '附件',
|
title: '附件',
|
||||||
@ -660,7 +660,7 @@ export default {
|
|||||||
uiSchema: {
|
uiSchema: {
|
||||||
'x-uid': 'wosew16td91',
|
'x-uid': 'wosew16td91',
|
||||||
name: 'p82ihvtkxtf',
|
name: 'p82ihvtkxtf',
|
||||||
'x-component-props': { multiple: true, action: 'attachments:upload' },
|
'x-component-props': { multiple: true, action: 'attachments:create' },
|
||||||
type: 'array',
|
type: 'array',
|
||||||
'x-component': 'Upload.Attachment',
|
'x-component': 'Upload.Attachment',
|
||||||
title: '附件',
|
title: '附件',
|
||||||
|
@ -9,7 +9,7 @@ const mock = new MockAdapter(apiClient.axios);
|
|||||||
|
|
||||||
const sleep = (value: number) => new Promise((resolve) => setTimeout(resolve, value));
|
const sleep = (value: number) => new Promise((resolve) => setTimeout(resolve, value));
|
||||||
|
|
||||||
mock.onPost('/attachments:upload').reply(async (config) => {
|
mock.onPost('/attachments:create').reply(async (config) => {
|
||||||
const total = 1024; // mocked file size
|
const total = 1024; // mocked file size
|
||||||
for (const progress of [0, 0.2, 0.4, 0.6, 0.8, 1]) {
|
for (const progress of [0, 0.2, 0.4, 0.6, 0.8, 1]) {
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
@ -16,7 +16,7 @@ const schema = {
|
|||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Preview',
|
'x-component': 'Preview',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
action: 'attachments:upload',
|
action: 'attachments:create',
|
||||||
// multiple: true,
|
// multiple: true,
|
||||||
},
|
},
|
||||||
'x-reactions': {
|
'x-reactions': {
|
||||||
|
@ -59,7 +59,7 @@ const schema = {
|
|||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Preview',
|
'x-component': 'Preview',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
action: 'attachments:upload',
|
action: 'attachments:create',
|
||||||
multiple: true,
|
multiple: true,
|
||||||
},
|
},
|
||||||
'x-reactions': [
|
'x-reactions': [
|
||||||
|
@ -180,9 +180,9 @@ const InternalRemoteSelect = connect(
|
|||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
if (!data?.data?.length) {
|
if (!data?.data?.length) {
|
||||||
return value !== undefined && value !== null ? (Array.isArray(value) ? value : [value]) : [];
|
return value != null ? (Array.isArray(value) ? value : [value]) : [];
|
||||||
}
|
}
|
||||||
const valueOptions = (value !== undefined && value !== null && (Array.isArray(value) ? value : [value])) || [];
|
const valueOptions = (value != null && (Array.isArray(value) ? value : [value])) || [];
|
||||||
return uniqBy(data?.data?.concat(valueOptions) || [], fieldNames.value);
|
return uniqBy(data?.data?.concat(valueOptions) || [], fieldNames.value);
|
||||||
}, [data?.data, getOptionsByFieldNames, normalizeOptions, value]);
|
}, [data?.data, getOptionsByFieldNames, normalizeOptions, value]);
|
||||||
const onDropdownVisibleChange = () => {
|
const onDropdownVisibleChange = () => {
|
||||||
|
@ -270,7 +270,7 @@ export default {
|
|||||||
uiSchema: {
|
uiSchema: {
|
||||||
'x-uid': '5xf7izxjny5',
|
'x-uid': '5xf7izxjny5',
|
||||||
name: '9az9003ijcm',
|
name: '9az9003ijcm',
|
||||||
'x-component-props': { multiple: true, action: 'attachments:upload' },
|
'x-component-props': { multiple: true, action: 'attachments:create' },
|
||||||
type: 'array',
|
type: 'array',
|
||||||
'x-component': 'Upload.Attachment',
|
'x-component': 'Upload.Attachment',
|
||||||
title: '附件',
|
title: '附件',
|
||||||
@ -660,7 +660,7 @@ export default {
|
|||||||
uiSchema: {
|
uiSchema: {
|
||||||
'x-uid': 'wosew16td91',
|
'x-uid': 'wosew16td91',
|
||||||
name: 'p82ihvtkxtf',
|
name: 'p82ihvtkxtf',
|
||||||
'x-component-props': { multiple: true, action: 'attachments:upload' },
|
'x-component-props': { multiple: true, action: 'attachments:create' },
|
||||||
type: 'array',
|
type: 'array',
|
||||||
'x-component': 'Upload.Attachment',
|
'x-component': 'Upload.Attachment',
|
||||||
title: '附件',
|
title: '附件',
|
||||||
|
@ -9,7 +9,7 @@ const mock = new MockAdapter(apiClient.axios);
|
|||||||
|
|
||||||
const sleep = (value: number) => new Promise((resolve) => setTimeout(resolve, value));
|
const sleep = (value: number) => new Promise((resolve) => setTimeout(resolve, value));
|
||||||
|
|
||||||
mock.onPost('/attachments:upload').reply(async (config) => {
|
mock.onPost('/attachments:create').reply(async (config) => {
|
||||||
const total = 1024; // mocked file size
|
const total = 1024; // mocked file size
|
||||||
for (const progress of [0, 0.2, 0.4, 0.6, 0.8, 1]) {
|
for (const progress of [0, 0.2, 0.4, 0.6, 0.8, 1]) {
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
@ -15,7 +15,7 @@ const schema = {
|
|||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Upload.Attachment',
|
'x-component': 'Upload.Attachment',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
action: 'attachments:upload',
|
action: 'attachments:create',
|
||||||
// multiple: true,
|
// multiple: true,
|
||||||
},
|
},
|
||||||
'x-reactions': {
|
'x-reactions': {
|
||||||
|
@ -58,7 +58,7 @@ const schema = {
|
|||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Upload.Attachment',
|
'x-component': 'Upload.Attachment',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
action: 'attachments:upload',
|
action: 'attachments:create',
|
||||||
multiple: true,
|
multiple: true,
|
||||||
},
|
},
|
||||||
'x-reactions': [
|
'x-reactions': [
|
||||||
|
@ -94,7 +94,7 @@ const schema: ISchema = {
|
|||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Upload.Attachment',
|
'x-component': 'Upload.Attachment',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
action: 'attachments:upload',
|
action: 'attachments:create',
|
||||||
multiple: false,
|
multiple: false,
|
||||||
// accept: 'jpg,png'
|
// accept: 'jpg,png'
|
||||||
},
|
},
|
||||||
@ -187,7 +187,7 @@ const schema2: ISchema = {
|
|||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Upload.Attachment',
|
'x-component': 'Upload.Attachment',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
action: 'attachments:upload',
|
action: 'attachments:create',
|
||||||
multiple: false,
|
multiple: false,
|
||||||
// accept: 'jpg,png'
|
// accept: 'jpg,png'
|
||||||
},
|
},
|
||||||
|
@ -113,15 +113,19 @@ export class MockServer extends Application {
|
|||||||
|
|
||||||
const queryString = qs.stringify(restParams, { arrayFormat: 'brackets' });
|
const queryString = qs.stringify(restParams, { arrayFormat: 'brackets' });
|
||||||
|
|
||||||
|
let request;
|
||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'upload':
|
|
||||||
return agent.post(`${url}?${queryString}`).attach('file', file).field(values);
|
|
||||||
case 'list':
|
case 'list':
|
||||||
case 'get':
|
case 'get':
|
||||||
return agent.get(`${url}?${queryString}`);
|
request = agent.get(`${url}?${queryString}`);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return agent.post(`${url}?${queryString}`).send(values);
|
request = agent.post(`${url}?${queryString}`);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return file ? request.attach('file', file).field(values) : request.send(values);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { SchemaComponent } from '@nocobase/client';
|
|
||||||
import { Card } from 'antd';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Card } from 'antd';
|
||||||
|
|
||||||
|
import { SchemaComponent } from '@nocobase/client';
|
||||||
|
|
||||||
import { StorageOptions } from './StorageOptions';
|
import { StorageOptions } from './StorageOptions';
|
||||||
import { storageSchema } from './schemas/storage';
|
import { storageSchema } from './schemas/storage';
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ const schema = {
|
|||||||
[uid()]: {
|
[uid()]: {
|
||||||
'x-component': 'Action.Drawer',
|
'x-component': 'Action.Drawer',
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: '{{t("File storages")}}',
|
title: '{{t("File manager")}}',
|
||||||
properties: {
|
properties: {
|
||||||
storageSchema,
|
storageSchema,
|
||||||
},
|
},
|
||||||
@ -31,7 +31,7 @@ export const FileStorageShortcut = () => {
|
|||||||
setVisible(true);
|
setVisible(true);
|
||||||
}}
|
}}
|
||||||
icon={<FileOutlined />}
|
icon={<FileOutlined />}
|
||||||
title={t('File storages')}
|
title={t('File manager')}
|
||||||
/>
|
/>
|
||||||
<SchemaComponent components={{ StorageOptions }} schema={schema} />
|
<SchemaComponent components={{ StorageOptions }} schema={schema} />
|
||||||
</ActionContext.Provider>
|
</ActionContext.Provider>
|
||||||
|
@ -2,22 +2,23 @@ import { FormLayout } from '@formily/antd';
|
|||||||
import { Field } from '@formily/core';
|
import { Field } from '@formily/core';
|
||||||
import { observer, RecursionField, Schema, useField, useForm } from '@formily/react';
|
import { observer, RecursionField, Schema, useField, useForm } from '@formily/react';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { NAMESPACE } from './locale';
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
local: {
|
local: {
|
||||||
properties: {
|
properties: {
|
||||||
documentRoot: {
|
documentRoot: {
|
||||||
title: '{{t("Destination")}}',
|
title: `{{t("Destination", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
required: true,
|
default: 'uploads',
|
||||||
},
|
},
|
||||||
serve: {
|
serve: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Checkbox',
|
'x-component': 'Checkbox',
|
||||||
'x-content': '{{t("Use the built-in static file server")}}',
|
'x-content': `{{t("Use the built-in static file server", { ns: "${NAMESPACE}" })}}`,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -25,28 +26,28 @@ const schema = {
|
|||||||
'ali-oss': {
|
'ali-oss': {
|
||||||
properties: {
|
properties: {
|
||||||
region: {
|
region: {
|
||||||
title: '{{t("Region")}}',
|
title: `{{t("Region", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
accessKeyId: {
|
accessKeyId: {
|
||||||
title: '{{t("AccessKey ID")}}',
|
title: `{{t("AccessKey ID", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
accessKeySecret: {
|
accessKeySecret: {
|
||||||
title: '{{t("AccessKey Secret")}}',
|
title: `{{t("AccessKey Secret", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Password',
|
'x-component': 'Password',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
bucket: {
|
bucket: {
|
||||||
title: '{{t("Bucket")}}',
|
title: `{{t("Bucket", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
@ -57,28 +58,28 @@ const schema = {
|
|||||||
'tx-cos': {
|
'tx-cos': {
|
||||||
properties: {
|
properties: {
|
||||||
Region: {
|
Region: {
|
||||||
title: '{{t("Region")}}',
|
title: `{{t("Region", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
SecretId: {
|
SecretId: {
|
||||||
title: '{{t("SecretId")}}',
|
title: `{{t("SecretId", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
SecretKey: {
|
SecretKey: {
|
||||||
title: '{{t("SecretKey")}}',
|
title: `{{t("SecretKey", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Password',
|
'x-component': 'Password',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
Bucket: {
|
Bucket: {
|
||||||
title: '{{t("Bucket")}}',
|
title: `{{t("Bucket", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
@ -89,39 +90,33 @@ const schema = {
|
|||||||
s3: {
|
s3: {
|
||||||
properties: {
|
properties: {
|
||||||
region: {
|
region: {
|
||||||
title: '{{t("Region")}}',
|
title: `{{t("Region", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
accessKeyId: {
|
accessKeyId: {
|
||||||
title: '{{t("AccessKey ID")}}',
|
title: `{{t("AccessKey ID", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
secretAccessKey: {
|
secretAccessKey: {
|
||||||
title: '{{t("AccessKey Secret")}}',
|
title: `{{t("AccessKey Secret", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Password',
|
'x-component': 'Password',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
bucket: {
|
bucket: {
|
||||||
title: '{{t("Bucket")}}',
|
title: `{{t("Bucket", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
endpoint: {
|
|
||||||
title: '{{t("Endpoint")}}',
|
|
||||||
type: 'string',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'Input',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
|
CollectionManagerProvider,
|
||||||
PluginManagerContext,
|
PluginManagerContext,
|
||||||
SchemaComponentOptions,
|
SchemaComponentOptions,
|
||||||
SchemaInitializerContext,
|
SchemaInitializerContext,
|
||||||
SchemaInitializerProvider,
|
SchemaInitializerProvider,
|
||||||
SettingsCenterProvider,
|
SettingsCenterProvider,
|
||||||
|
registerField,
|
||||||
registerTemplate,
|
registerTemplate,
|
||||||
useCollection,
|
useCollection,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
@ -14,12 +16,16 @@ import { FileStorageShortcut } from './FileStorageShortcut';
|
|||||||
import * as hooks from './hooks';
|
import * as hooks from './hooks';
|
||||||
import * as initializers from './initializers';
|
import * as initializers from './initializers';
|
||||||
import * as templates from './templates';
|
import * as templates from './templates';
|
||||||
|
import { NAMESPACE } from './locale';
|
||||||
|
import { attachment } from './interfaces/attachment';
|
||||||
|
|
||||||
// 注册之后就可以在 Crete collection 按钮中选择创建了
|
// 注册之后就可以在 Crete collection 按钮中选择创建了
|
||||||
forEach(templates, (template, key: string) => {
|
forEach(templates, (template, key: string) => {
|
||||||
registerTemplate(key, template);
|
registerTemplate(key, template);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerField(attachment.group, 'attachment', attachment);
|
||||||
|
|
||||||
export default function (props) {
|
export default function (props) {
|
||||||
const initializes = useContext(SchemaInitializerContext);
|
const initializes = useContext(SchemaInitializerContext);
|
||||||
const hasUploadAction = initializes.TableActionInitializers.items[0].children.some(
|
const hasUploadAction = initializes.TableActionInitializers.items[0].children.some(
|
||||||
@ -38,6 +44,7 @@ export default function (props) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
visible: () => {
|
visible: () => {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return collection.template === 'file';
|
return collection.template === 'file';
|
||||||
},
|
},
|
||||||
@ -49,11 +56,11 @@ export default function (props) {
|
|||||||
<SettingsCenterProvider
|
<SettingsCenterProvider
|
||||||
settings={{
|
settings={{
|
||||||
'file-manager': {
|
'file-manager': {
|
||||||
title: '{{t("File manager")}}',
|
title: `{{t("File manager", { ns: "${NAMESPACE}" })}}`,
|
||||||
icon: 'FileOutlined',
|
icon: 'FileOutlined',
|
||||||
tabs: {
|
tabs: {
|
||||||
storages: {
|
storages: {
|
||||||
title: '{{t("File storages")}}',
|
title: `{{t("File storage", { ns: "${NAMESPACE}" })}}`,
|
||||||
component: FileStoragePane,
|
component: FileStoragePane,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -68,9 +75,11 @@ export default function (props) {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SchemaComponentOptions scope={hooks}>
|
<CollectionManagerProvider interfaces={{ attachment }}>
|
||||||
<SchemaInitializerProvider components={initializers}>{props.children}</SchemaInitializerProvider>
|
<SchemaComponentOptions scope={hooks}>
|
||||||
</SchemaComponentOptions>
|
<SchemaInitializerProvider components={initializers}>{props.children}</SchemaInitializerProvider>
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
</CollectionManagerProvider>
|
||||||
</PluginManagerContext.Provider>
|
</PluginManagerContext.Provider>
|
||||||
</SettingsCenterProvider>
|
</SettingsCenterProvider>
|
||||||
);
|
);
|
||||||
|
@ -17,7 +17,7 @@ export const UploadActionInitializer = (props) => {
|
|||||||
icon: 'UploadOutlined',
|
icon: 'UploadOutlined',
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
modal: {
|
drawer: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: '{{ t("Upload files") }}',
|
title: '{{ t("Upload files") }}',
|
||||||
'x-component': 'Action.Container',
|
'x-component': 'Action.Container',
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { ISchema } from '@formily/react';
|
import { ISchema } from '@formily/react';
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import { defaultProps, operators } from './properties';
|
import { IField, interfacesProperties } from '@nocobase/client';
|
||||||
import { IField } from './types';
|
import { NAMESPACE } from '../locale';
|
||||||
|
|
||||||
export const attachment: IField = {
|
export const attachment: IField = {
|
||||||
name: 'attachment',
|
name: 'attachment',
|
||||||
type: 'object',
|
type: 'object',
|
||||||
group: 'media',
|
group: 'media',
|
||||||
title: '{{t("Attachment")}}',
|
title: `{{t("Attachment", { ns: "${NAMESPACE}" })}}`,
|
||||||
isAssociation: true,
|
isAssociation: true,
|
||||||
default: {
|
default: {
|
||||||
type: 'belongsToMany',
|
type: 'belongsToMany',
|
||||||
@ -17,17 +17,19 @@ export const attachment: IField = {
|
|||||||
type: 'array',
|
type: 'array',
|
||||||
// title,
|
// title,
|
||||||
'x-component': 'Upload.Attachment',
|
'x-component': 'Upload.Attachment',
|
||||||
'x-component-props': {
|
'x-component-props': {},
|
||||||
action: 'attachments:upload',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
availableTypes: ['belongsToMany'],
|
availableTypes: ['belongsToMany'],
|
||||||
schemaInitialize(schema: ISchema, { block }) {
|
schemaInitialize(schema: ISchema, { block, field }) {
|
||||||
if (['Table', 'Kanban'].includes(block)) {
|
if (['Table', 'Kanban'].includes(block)) {
|
||||||
schema['x-component-props'] = schema['x-component-props'] || {};
|
schema['x-component-props'] = schema['x-component-props'] || {};
|
||||||
schema['x-component-props']['size'] = 'small';
|
schema['x-component-props']['size'] = 'small';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
schema['x-component-props']['action'] = `${field.target}:create${
|
||||||
|
field.storage ? `?attachementField=${field.collectionName}.${field.name}` : ''
|
||||||
|
}`;
|
||||||
},
|
},
|
||||||
initialize: (values: any) => {
|
initialize: (values: any) => {
|
||||||
if (!values.through) {
|
if (!values.through) {
|
||||||
@ -47,21 +49,42 @@ export const attachment: IField = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
...defaultProps,
|
...interfacesProperties.defaultProps,
|
||||||
'uiSchema.x-component-props.accept': {
|
'uiSchema.x-component-props.accept': {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
title: '{{t("Accept")}}',
|
title: `{{t("MIME type", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
description: 'Example: .doc,.docx',
|
description: 'Example: image/png',
|
||||||
|
default: 'image/*',
|
||||||
},
|
},
|
||||||
'uiSchema.x-component-props.multiple': {
|
'uiSchema.x-component-props.multiple': {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
'x-content': "{{t('Allow uploading multiple files')}}",
|
'x-content': `{{t('Allow uploading multiple files', { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Checkbox',
|
'x-component': 'Checkbox',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
storage: {
|
||||||
|
type: 'string',
|
||||||
|
title: `{{t("Storage", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
description: `{{t('Default storage will be used when not selected', { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'RemoteSelect',
|
||||||
|
'x-component-props': {
|
||||||
|
service: {
|
||||||
|
resource: 'storages',
|
||||||
|
params: {
|
||||||
|
// pageSize: -1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
manual: false,
|
||||||
|
fieldNames: {
|
||||||
|
label: 'title',
|
||||||
|
value: 'name',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
filterable: {
|
filterable: {
|
||||||
children: [
|
children: [
|
||||||
@ -80,10 +103,10 @@ export const attachment: IField = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'filename',
|
name: 'filename',
|
||||||
title: '{{t("Filename")}}',
|
title: `{{t("Filename", { ns: "${NAMESPACE}" })}}`,
|
||||||
operators: operators.string,
|
operators: interfacesProperties.operators.string,
|
||||||
schema: {
|
schema: {
|
||||||
title: '{{t("Filename")}}',
|
title: `{{t("Filename", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
},
|
},
|
21
packages/plugins/file-manager/src/client/locale/en-US.ts
Normal file
21
packages/plugins/file-manager/src/client/locale/en-US.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export default {
|
||||||
|
'File manager': 'File manager',
|
||||||
|
'Attachment': 'Attachment',
|
||||||
|
'MIME type': 'MIME type',
|
||||||
|
'Storage display name': 'Storage display name',
|
||||||
|
'Storage name': 'Storage name',
|
||||||
|
'Storage type': 'Storage type',
|
||||||
|
'Default storage': 'Default storage',
|
||||||
|
'Storage base URL': 'Storage base URL',
|
||||||
|
'Destination': 'Destination',
|
||||||
|
'Use the built-in static file server': 'Use the built-in static file server',
|
||||||
|
'Local storage': 'Local storage',
|
||||||
|
'Aliyun OSS': 'Aliyun OSS',
|
||||||
|
'Tencent COS': 'Tencent COS',
|
||||||
|
'Amazon S3': 'Amazon S3',
|
||||||
|
'Region': 'Region',
|
||||||
|
'Bucket': 'Bucket',
|
||||||
|
'Path': 'Path',
|
||||||
|
'Filename': 'Filename',
|
||||||
|
'Will be used for API': 'Will be used for API',
|
||||||
|
};
|
@ -1,11 +1,7 @@
|
|||||||
import { i18n } from '@nocobase/client';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import zhCN from './zh-CN';
|
|
||||||
|
|
||||||
export const NAMESPACE = 'file-manager';
|
export const NAMESPACE = 'file-manager';
|
||||||
|
|
||||||
i18n.addResources('zh-CN', NAMESPACE, zhCN);
|
|
||||||
|
|
||||||
export function useFmTranslation() {
|
export function useFmTranslation() {
|
||||||
return useTranslation(NAMESPACE);
|
return useTranslation(NAMESPACE);
|
||||||
}
|
}
|
||||||
|
18
packages/plugins/file-manager/src/client/locale/ja-JP.ts
Normal file
18
packages/plugins/file-manager/src/client/locale/ja-JP.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export default {
|
||||||
|
'File manager': 'ファイルストレージ',
|
||||||
|
'Attachment': '添付ファイル',
|
||||||
|
'MIME type': 'ファイル形式',
|
||||||
|
'Allow uploading multiple files': '複数ファイルのアップロードを許可する',
|
||||||
|
'Storage display name': 'ファイルストレージ名',
|
||||||
|
'Storage name': 'ファイルストレージ識別子',
|
||||||
|
'Storage type': 'ストレージタイプ',
|
||||||
|
'Default storage': 'デフォルトストレージ',
|
||||||
|
'Storage base URL': 'Storage base URL',
|
||||||
|
'Destination': 'ファイルパス',
|
||||||
|
'Use the built-in static file server': '組み込みの静的ファイル サービスを使用する',
|
||||||
|
'Local storage': 'ローカルストレージ',
|
||||||
|
'Aliyun OSS': 'Aliyun OSS',
|
||||||
|
'Tencent COS': 'Tencent COS',
|
||||||
|
'Amazon S3': 'Amazon S3',
|
||||||
|
'Filename': 'ファイル名',
|
||||||
|
};
|
18
packages/plugins/file-manager/src/client/locale/ru-RU.ts
Normal file
18
packages/plugins/file-manager/src/client/locale/ru-RU.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export default {
|
||||||
|
'File manager': 'Файловые хранилища',
|
||||||
|
'Attachment': 'Вложение',
|
||||||
|
'MIME type': 'Подтвердить',
|
||||||
|
'Allow uploading multiple files': 'Позволить загружать много файлов',
|
||||||
|
'Storage display name': 'Имя храшилища на экране',
|
||||||
|
'Storage name': 'Имя хранилища',
|
||||||
|
'Storage type': 'Тип Хранилища',
|
||||||
|
'Default storage': 'Хранилище по умолчанию',
|
||||||
|
'Storage base URL': 'Базовый URL хранилища',
|
||||||
|
'Destination': 'Назначение',
|
||||||
|
'Use the built-in static file server': 'Использовать встроенный статический файл-сервер',
|
||||||
|
'Local storage': 'Локальное хранилище',
|
||||||
|
'Aliyun OSS': 'Aliyun OSS',
|
||||||
|
'Amazon S3': 'Amazon S3',
|
||||||
|
'Tencent COS': 'Tencent COS',
|
||||||
|
'Filename': 'Имя файла',
|
||||||
|
}
|
17
packages/plugins/file-manager/src/client/locale/tr-TR.ts
Normal file
17
packages/plugins/file-manager/src/client/locale/tr-TR.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export default {
|
||||||
|
'File manager': 'Dosya depoları',
|
||||||
|
'Attachment': 'Dosya eki',
|
||||||
|
'MIME type': 'Kabul et',
|
||||||
|
'Allow uploading multiple files': 'Birden çok dosya yüklemeye izin ver',
|
||||||
|
'Storage name': 'Depo adı',
|
||||||
|
'Storage type': 'Depolama türü',
|
||||||
|
'Default storage': 'Varsayılan depo',
|
||||||
|
'Storage base URL': 'Depolama temel URLsi',
|
||||||
|
'Destination': 'Hedef',
|
||||||
|
'Use the built-in static file server': 'Yerleşik statik dosya sunucusunu kullanın',
|
||||||
|
'Local storage': 'Lokal depolama',
|
||||||
|
'Aliyun OSS': 'Aliyun OSS',
|
||||||
|
'Amazon S3': 'Amazon S3',
|
||||||
|
'Tencent COS': 'Tencent COS',
|
||||||
|
'Filename': 'Dosya adı',
|
||||||
|
};
|
@ -1,11 +1,30 @@
|
|||||||
const locale = {
|
export default {
|
||||||
'File collection': '文件数据表',
|
'File collection': '文件数据表',
|
||||||
'File name': '文件名',
|
'File name': '文件名',
|
||||||
'Extension name': '扩展名',
|
'Extension name': '扩展名',
|
||||||
Size: '文件大小',
|
Size: '文件大小',
|
||||||
'Mime Type': 'Mime 类型',
|
'MIME type': 'MIME 类型',
|
||||||
URL: 'URL',
|
URL: 'URL',
|
||||||
'File storage': '文件存储',
|
'File storage': '文件存储',
|
||||||
|
'File manager': '文件管理器',
|
||||||
|
Attachment: '附件',
|
||||||
|
'Allow uploading multiple files': '允许上传多个文件',
|
||||||
|
Storage: '存储空间',
|
||||||
|
Storages: '存储空间',
|
||||||
|
'Storage name': '存储空间标识',
|
||||||
|
'Storage type': '存储类型',
|
||||||
|
'Default storage': '默认存储空间',
|
||||||
|
'Storage base URL': '访问 URL 基础',
|
||||||
|
Destination: '上传目标文件夹',
|
||||||
|
'Use the built-in static file server': '使用内置静态文件服务',
|
||||||
|
'Local storage': '本地存储',
|
||||||
|
'Aliyun OSS': '阿里云 OSS',
|
||||||
|
'Amazon S3': '亚马逊 S3',
|
||||||
|
'Tencent COS': '腾讯云 COS',
|
||||||
|
Region: '区域',
|
||||||
|
Bucket: '存储桶',
|
||||||
|
Path: '相对路径',
|
||||||
|
Filename: '文件名',
|
||||||
|
'Will be used for API': '将用于 API',
|
||||||
|
'Default storage will be used when not selected': '留空将使用默认存储空间',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default locale;
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ISchema } from '@formily/react';
|
import { ISchema } from '@formily/react';
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import { useActionContext, useRequest } from '@nocobase/client';
|
import { useActionContext, useRequest } from '@nocobase/client';
|
||||||
|
import { NAMESPACE } from '../locale';
|
||||||
|
|
||||||
const collection = {
|
const collection = {
|
||||||
name: 'storages',
|
name: 'storages',
|
||||||
@ -10,7 +11,7 @@ const collection = {
|
|||||||
name: 'title',
|
name: 'title',
|
||||||
interface: 'input',
|
interface: 'input',
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
title: '{{t("Storage display name")}}',
|
title: '{{t("Title")}}',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
required: true,
|
required: true,
|
||||||
@ -21,7 +22,8 @@ const collection = {
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
interface: 'input',
|
interface: 'input',
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
title: '{{t("Storage name")}}',
|
title: `{{t("Storage name", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
descriptions: `{{t("Will be used for API", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
} as ISchema,
|
} as ISchema,
|
||||||
@ -31,15 +33,15 @@ const collection = {
|
|||||||
name: 'type',
|
name: 'type',
|
||||||
interface: 'select',
|
interface: 'select',
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
title: '{{t("Storage type")}}',
|
title: `{{t("Storage type", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-component': 'Select',
|
'x-component': 'Select',
|
||||||
required: true,
|
required: true,
|
||||||
enum: [
|
enum: [
|
||||||
{ label: '{{t("Local storage")}}', value: 'local' },
|
{ label: `{{t("Local storage", { ns: "${NAMESPACE}" })}}`, value: 'local' },
|
||||||
{ label: '{{t("Aliyun OSS")}}', value: 'ali-oss' },
|
{ label: `{{t("Aliyun OSS", { ns: "${NAMESPACE}" })}}`, value: 'ali-oss' },
|
||||||
{ label: '{{t("Amazon S3")}}', value: 's3' },
|
{ label: `{{t("Amazon S3", { ns: "${NAMESPACE}" })}}`, value: 's3' },
|
||||||
{ label: '{{t("Tencent COS")}}', value: 'tx-cos' },
|
{ label: `{{t("Tencent COS", { ns: "${NAMESPACE}" })}}`, value: 'tx-cos' },
|
||||||
],
|
],
|
||||||
} as ISchema,
|
} as ISchema,
|
||||||
},
|
},
|
||||||
@ -48,7 +50,7 @@ const collection = {
|
|||||||
name: 'baseUrl',
|
name: 'baseUrl',
|
||||||
interface: 'input',
|
interface: 'input',
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
title: '{{t("Storage base URL")}}',
|
title: `{{t("Storage base URL", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
required: true,
|
required: true,
|
||||||
@ -59,7 +61,7 @@ const collection = {
|
|||||||
name: 'path',
|
name: 'path',
|
||||||
interface: 'input',
|
interface: 'input',
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
title: '{{t("Path")}}',
|
title: `{{t("Path", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
} as ISchema,
|
} as ISchema,
|
||||||
@ -69,7 +71,7 @@ const collection = {
|
|||||||
name: 'default',
|
name: 'default',
|
||||||
interface: 'boolean',
|
interface: 'boolean',
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
title: '{{t("Default storage")}}',
|
title: `{{t("Default storage", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
'x-component': 'Checkbox',
|
'x-component': 'Checkbox',
|
||||||
} as ISchema,
|
} as ISchema,
|
||||||
@ -117,14 +119,14 @@ export const storageSchema: ISchema = {
|
|||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
useAction: '{{ cm.useBulkDestroyAction }}',
|
useAction: '{{ cm.useBulkDestroyAction }}',
|
||||||
confirm: {
|
confirm: {
|
||||||
title: "{{t('Delete storage')}}",
|
title: "{{t('Delete')}}",
|
||||||
content: "{{t('Are you sure you want to delete it?')}}",
|
content: "{{t('Are you sure you want to delete it?')}}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: '{{t("Add storage")}}',
|
title: '{{t("Add new")}}',
|
||||||
'x-component': 'Action',
|
'x-component': 'Action',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
@ -148,7 +150,7 @@ export const storageSchema: ISchema = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
title: '{{t("Add storage")}}',
|
title: '{{t("Add new")}}',
|
||||||
properties: {
|
properties: {
|
||||||
title: {
|
title: {
|
||||||
'x-component': 'CollectionField',
|
'x-component': 'CollectionField',
|
||||||
@ -180,7 +182,7 @@ export const storageSchema: ISchema = {
|
|||||||
'x-component': 'CollectionField',
|
'x-component': 'CollectionField',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
title: '',
|
title: '',
|
||||||
'x-content': '{{t("Default storage")}}',
|
'x-content': `{{t("Default storage", { ns: "${NAMESPACE}" })}}`,
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
@ -284,7 +286,7 @@ export const storageSchema: ISchema = {
|
|||||||
'x-decorator-props': {
|
'x-decorator-props': {
|
||||||
useValues: '{{ cm.useValuesFromRecord }}',
|
useValues: '{{ cm.useValuesFromRecord }}',
|
||||||
},
|
},
|
||||||
title: '{{t("Edit storage")}}',
|
title: '{{t("Edit")}}',
|
||||||
properties: {
|
properties: {
|
||||||
title: {
|
title: {
|
||||||
'x-component': 'CollectionField',
|
'x-component': 'CollectionField',
|
||||||
@ -349,8 +351,8 @@ export const storageSchema: ISchema = {
|
|||||||
'x-component': 'Action.Link',
|
'x-component': 'Action.Link',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
confirm: {
|
confirm: {
|
||||||
title: "{{t('Delete role')}}",
|
title: '{{t("Delete")}}',
|
||||||
content: "{{t('Are you sure you want to delete it?')}}",
|
content: '{{t("Are you sure you want to delete it?")}}',
|
||||||
},
|
},
|
||||||
useAction: '{{cm.useDestroyAction}}',
|
useAction: '{{cm.useDestroyAction}}',
|
||||||
},
|
},
|
||||||
|
@ -72,7 +72,7 @@ export const file = {
|
|||||||
deletable: false,
|
deletable: false,
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
title: `{{t("Mime type", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("MIME type", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
'x-read-pretty': true,
|
'x-read-pretty': true,
|
||||||
},
|
},
|
||||||
|
@ -3,14 +3,15 @@ import path from 'path';
|
|||||||
import { getApp } from '.';
|
import { getApp } from '.';
|
||||||
import { FILE_FIELD_NAME, STORAGE_TYPE_LOCAL } from '../constants';
|
import { FILE_FIELD_NAME, STORAGE_TYPE_LOCAL } from '../constants';
|
||||||
|
|
||||||
const { LOCAL_STORAGE_BASE_URL, APP_PORT = '13000' } = process.env;
|
const { LOCAL_STORAGE_BASE_URL, LOCAL_STORAGE_DEST = 'storage/uploads', APP_PORT = '13000' } = process.env;
|
||||||
|
|
||||||
const DEFAULT_LOCAL_BASE_URL = LOCAL_STORAGE_BASE_URL || `http://localhost:${APP_PORT}/uploads`;
|
const DEFAULT_LOCAL_BASE_URL = LOCAL_STORAGE_BASE_URL || `http://localhost:${APP_PORT}/storage/uploads`;
|
||||||
|
|
||||||
describe('action', () => {
|
describe('action', () => {
|
||||||
let app;
|
let app;
|
||||||
let agent;
|
let agent;
|
||||||
let db;
|
let db;
|
||||||
|
let StorageModel;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
app = await getApp({
|
app = await getApp({
|
||||||
@ -19,12 +20,14 @@ describe('action', () => {
|
|||||||
agent = app.agent();
|
agent = app.agent();
|
||||||
db = app.db;
|
db = app.db;
|
||||||
|
|
||||||
const Storage = db.getCollection('storages').model;
|
StorageModel = db.getCollection('storages').model;
|
||||||
await Storage.create({
|
await StorageModel.create({
|
||||||
name: `local1_${db.getTablePrefix()}`,
|
name: 'local1',
|
||||||
type: STORAGE_TYPE_LOCAL,
|
type: STORAGE_TYPE_LOCAL,
|
||||||
baseUrl: DEFAULT_LOCAL_BASE_URL,
|
baseUrl: DEFAULT_LOCAL_BASE_URL,
|
||||||
default: true,
|
rules: {
|
||||||
|
size: 1024,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -32,9 +35,9 @@ describe('action', () => {
|
|||||||
await db.close();
|
await db.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('direct attachment', () => {
|
describe('default storage', () => {
|
||||||
it('upload file should be ok', async () => {
|
it('upload file should be ok', async () => {
|
||||||
const { body } = await agent.resource('attachments').upload({
|
const { body } = await agent.resource('attachments').create({
|
||||||
[FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt'),
|
[FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt'),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -45,6 +48,7 @@ describe('action', () => {
|
|||||||
size: 13,
|
size: 13,
|
||||||
mimetype: 'text/plain',
|
mimetype: 'text/plain',
|
||||||
meta: {},
|
meta: {},
|
||||||
|
storageId: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 文件上传和解析是否正常
|
// 文件上传和解析是否正常
|
||||||
@ -52,25 +56,26 @@ describe('action', () => {
|
|||||||
// 文件的 url 是否正常生成
|
// 文件的 url 是否正常生成
|
||||||
expect(body.data.url).toBe(`${DEFAULT_LOCAL_BASE_URL}${body.data.path}/${body.data.filename}`);
|
expect(body.data.url).toBe(`${DEFAULT_LOCAL_BASE_URL}${body.data.path}/${body.data.filename}`);
|
||||||
|
|
||||||
const Attachment = db.getCollection('attachments').model;
|
const Attachment = db.getModel('attachments');
|
||||||
const attachment = await Attachment.findOne({
|
const attachment = await Attachment.findOne({
|
||||||
where: { id: body.data.id },
|
where: { id: body.data.id },
|
||||||
include: ['storage'],
|
include: ['storage'],
|
||||||
});
|
});
|
||||||
const storage = attachment.get('storage');
|
|
||||||
// 文件的数据是否正常保存
|
// 文件的数据是否正常保存
|
||||||
expect(attachment).toMatchObject(matcher);
|
expect(attachment).toMatchObject(matcher);
|
||||||
|
|
||||||
// 关联的存储引擎是否正确
|
// 关联的存储引擎是否正确
|
||||||
|
const storage = await attachment.getStorage();
|
||||||
expect(storage).toMatchObject({
|
expect(storage).toMatchObject({
|
||||||
type: 'local',
|
type: 'local',
|
||||||
options: {},
|
options: { documentRoot: LOCAL_STORAGE_DEST },
|
||||||
rules: {},
|
rules: {},
|
||||||
path: '',
|
path: '',
|
||||||
baseUrl: DEFAULT_LOCAL_BASE_URL,
|
baseUrl: DEFAULT_LOCAL_BASE_URL,
|
||||||
default: true,
|
default: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { documentRoot = 'uploads' } = storage.options || {};
|
const { documentRoot = 'storage/uploads' } = storage.options || {};
|
||||||
const destPath = path.resolve(
|
const destPath = path.resolve(
|
||||||
path.isAbsolute(documentRoot) ? documentRoot : path.join(process.cwd(), documentRoot),
|
path.isAbsolute(documentRoot) ? documentRoot : path.join(process.cwd(), documentRoot),
|
||||||
storage.path,
|
storage.path,
|
||||||
@ -86,59 +91,70 @@ describe('action', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.skip('belongsTo attachment', () => {
|
describe('specific storage', () => {
|
||||||
it('upload with associatedIndex, fail as 400 because file mimetype does not match', async () => {
|
it('fail as 400 because file size greater than rules', async () => {
|
||||||
const User = db.getCollection('users').model;
|
db.collection({
|
||||||
const user = await User.create();
|
name: 'customers',
|
||||||
const response = await agent.resource('users.avatar').upload({
|
fields: [
|
||||||
associatedIndex: user.id,
|
{
|
||||||
file: path.resolve(__dirname, './files/text.txt'),
|
name: 'avatar',
|
||||||
|
type: 'belongsTo',
|
||||||
|
target: 'attachments',
|
||||||
|
storage: 'local1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await agent.resource('attachments').create({
|
||||||
|
attachmentField: 'customers.avatar',
|
||||||
|
file: path.resolve(__dirname, './files/image.jpg'),
|
||||||
});
|
});
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('upload with associatedIndex', async () => {
|
it('fail as 400 because file mimetype does not match', async () => {
|
||||||
const User = db.getCollection('users').model;
|
const textStorage = await StorageModel.create({
|
||||||
const user = await User.create();
|
name: 'local2',
|
||||||
|
type: STORAGE_TYPE_LOCAL,
|
||||||
const { body } = await agent.resource('users.avatar').upload({
|
baseUrl: DEFAULT_LOCAL_BASE_URL,
|
||||||
associatedIndex: user.id,
|
rules: {
|
||||||
file: path.resolve(__dirname, './files/image.png'),
|
mimetype: ['text/*'],
|
||||||
values: { width: 100, height: 100 },
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const matcher = {
|
db.collection({
|
||||||
title: 'image',
|
name: 'customers',
|
||||||
extname: '.png',
|
fields: [
|
||||||
path: '',
|
{
|
||||||
size: 255,
|
name: 'avatar',
|
||||||
mimetype: 'image/png',
|
type: 'belongsTo',
|
||||||
// TODO(optimize): 可以考虑使用 qs 的 decoder 来进行类型解析
|
target: 'attachments',
|
||||||
// see: https://github.com/ljharb/qs/issues/91
|
storage: textStorage.name,
|
||||||
// 或考虑使用 query-string 库的 parseNumbers 等配置项
|
},
|
||||||
meta: { width: '100', height: '100' },
|
],
|
||||||
};
|
|
||||||
// 上传正常返回
|
|
||||||
expect(body.data).toMatchObject(matcher);
|
|
||||||
|
|
||||||
// 由于初始没有外键,无法获取
|
|
||||||
// await user.getAvatar()
|
|
||||||
const updatedUser = await User.findByPk(user.id, {
|
|
||||||
include: ['avatar'],
|
|
||||||
});
|
});
|
||||||
// 外键更新正常
|
|
||||||
expect(updatedUser.get('avatar').id).toBe(body.data.id);
|
// await db.sync();
|
||||||
|
|
||||||
|
const response = await agent.resource('attachments').create({
|
||||||
|
attachmentField: 'customers.avatar',
|
||||||
|
file: path.resolve(__dirname, './files/image.jpg'),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('upload to assoiciated field and storage with full base url should be ok', async () => {
|
it('upload to storage which is not default', async () => {
|
||||||
const BASE_URL = `http://localhost:${APP_PORT}/another-uploads`;
|
const BASE_URL = `http://localhost:${APP_PORT}/another-uploads`;
|
||||||
const storageName = 'local_private';
|
|
||||||
const urlPath = 'test/path';
|
const urlPath = 'test/path';
|
||||||
const Storage = db.getCollection('storages').model;
|
|
||||||
// 动态添加 storage
|
// 动态添加 storage
|
||||||
await Storage.create({
|
const storage = await StorageModel.create({
|
||||||
name: storageName,
|
name: 'local_private',
|
||||||
type: STORAGE_TYPE_LOCAL,
|
type: STORAGE_TYPE_LOCAL,
|
||||||
|
rules: {
|
||||||
|
mimetype: ['text/*'],
|
||||||
|
},
|
||||||
path: urlPath,
|
path: urlPath,
|
||||||
baseUrl: BASE_URL,
|
baseUrl: BASE_URL,
|
||||||
options: {
|
options: {
|
||||||
@ -146,12 +162,21 @@ describe('action', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const User = db.getCollection('users').model;
|
db.collection({
|
||||||
const user = await User.create();
|
name: 'customers',
|
||||||
const { body } = await agent.resource('users.pubkeys').upload({
|
fields: [
|
||||||
associatedIndex: user.id,
|
{
|
||||||
|
name: 'file',
|
||||||
|
type: 'belongsTo',
|
||||||
|
target: 'attachments',
|
||||||
|
storage: storage.name,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { body } = await agent.resource('attachments').create({
|
||||||
|
attachmentField: 'customers.file',
|
||||||
file: path.resolve(__dirname, './files/text.txt'),
|
file: path.resolve(__dirname, './files/text.txt'),
|
||||||
values: {},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 文件的 url 是否正常生成
|
// 文件的 url 是否正常生成
|
||||||
@ -161,27 +186,5 @@ describe('action', () => {
|
|||||||
const content = await agent.get(url);
|
const content = await agent.get(url);
|
||||||
expect(content.text).toBe('Hello world!\n');
|
expect(content.text).toBe('Hello world!\n');
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO(bug): 没有 associatedIndex 时路径解析资源名称不对,无法进入 action
|
|
||||||
it.skip('upload without associatedIndex', async () => {
|
|
||||||
const { body } = await agent.resource('users.avatar').upload({
|
|
||||||
file: path.resolve(__dirname, './files/image.png'),
|
|
||||||
values: { width: 100, height: 100 },
|
|
||||||
});
|
|
||||||
const matcher = {
|
|
||||||
title: 'image',
|
|
||||||
extname: '.png',
|
|
||||||
path: '',
|
|
||||||
size: 255,
|
|
||||||
mimetype: 'image/png',
|
|
||||||
// TODO(optimize): 可以考虑使用 qs 的 decoder 来进行类型解析
|
|
||||||
// @see: https://github.com/ljharb/qs/issues/91
|
|
||||||
// 或考虑使用 query-string 库的 parseNumbers 等配置项
|
|
||||||
meta: { width: '100', height: '100' },
|
|
||||||
storage_id: 1,
|
|
||||||
};
|
|
||||||
// 上传返回正常
|
|
||||||
expect(body.data).toMatchObject(matcher);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -21,7 +21,7 @@ describe('storage:ali-oss', () => {
|
|||||||
const Storage = db.getCollection('storages').model;
|
const Storage = db.getCollection('storages').model;
|
||||||
storage = await Storage.create({
|
storage = await Storage.create({
|
||||||
...aliossStorage.defaults(),
|
...aliossStorage.defaults(),
|
||||||
name: `ali-oss_${db.getTablePrefix()}`,
|
name: 'ali-oss',
|
||||||
default: true,
|
default: true,
|
||||||
path: 'test/path',
|
path: 'test/path',
|
||||||
});
|
});
|
||||||
@ -33,7 +33,7 @@ describe('storage:ali-oss', () => {
|
|||||||
|
|
||||||
describe('direct attachment', () => {
|
describe('direct attachment', () => {
|
||||||
itif('upload file should be ok', async () => {
|
itif('upload file should be ok', async () => {
|
||||||
const { body } = await agent.resource('attachments').upload({
|
const { body } = await agent.resource('attachments').create({
|
||||||
[FILE_FIELD_NAME]: path.resolve(__dirname, '../files/text.txt'),
|
[FILE_FIELD_NAME]: path.resolve(__dirname, '../files/text.txt'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ describe('storage:s3', () => {
|
|||||||
const Storage = db.getCollection('storages').model;
|
const Storage = db.getCollection('storages').model;
|
||||||
storage = await Storage.create({
|
storage = await Storage.create({
|
||||||
...s3Storage.defaults(),
|
...s3Storage.defaults(),
|
||||||
name: `s3_${db.getTablePrefix()}`,
|
name: 's3',
|
||||||
default: true,
|
default: true,
|
||||||
path: 'test/path',
|
path: 'test/path',
|
||||||
});
|
});
|
||||||
@ -33,7 +33,7 @@ describe('storage:s3', () => {
|
|||||||
|
|
||||||
describe('direct attachment', () => {
|
describe('direct attachment', () => {
|
||||||
itif('upload file should be ok', async () => {
|
itif('upload file should be ok', async () => {
|
||||||
const { body } = await agent.resource('attachments').upload({
|
const { body } = await agent.resource('attachments').create({
|
||||||
[FILE_FIELD_NAME]: path.resolve(__dirname, '../files/text.txt'),
|
[FILE_FIELD_NAME]: path.resolve(__dirname, '../files/text.txt'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ describe('storage:tx-cos', () => {
|
|||||||
const Storage = db.getCollection('storages').model;
|
const Storage = db.getCollection('storages').model;
|
||||||
storage = await Storage.create({
|
storage = await Storage.create({
|
||||||
...txStorage.defaults(),
|
...txStorage.defaults(),
|
||||||
name: `tx-cos_${db.getTablePrefix()}`,
|
name: 'tx-cos',
|
||||||
default: true,
|
default: true,
|
||||||
path: 'test/path',
|
path: 'test/path',
|
||||||
});
|
});
|
||||||
@ -33,7 +33,7 @@ describe('storage:tx-cos', () => {
|
|||||||
|
|
||||||
describe('direct attachment', () => {
|
describe('direct attachment', () => {
|
||||||
itif('upload file should be ok', async () => {
|
itif('upload file should be ok', async () => {
|
||||||
const { body } = await agent.resource('attachments').upload({
|
const { body } = await agent.resource('attachments').create({
|
||||||
[FILE_FIELD_NAME]: path.resolve(__dirname, '../files/text.txt'),
|
[FILE_FIELD_NAME]: path.resolve(__dirname, '../files/text.txt'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -11,35 +11,16 @@ export default {
|
|||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
name: 'avatar',
|
name: 'avatar',
|
||||||
target: 'attachments',
|
target: 'attachments',
|
||||||
attachment: {
|
|
||||||
// storage 为配置的默认引擎
|
|
||||||
rules: {
|
|
||||||
size: 1024 * 10,
|
|
||||||
mimetype: ['image/png'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'belongsToMany',
|
type: 'belongsToMany',
|
||||||
name: 'pubkeys',
|
name: 'pubkeys',
|
||||||
target: 'attachments',
|
target: 'attachments',
|
||||||
attachment: {
|
|
||||||
storage: 'local_private',
|
|
||||||
rules: {
|
|
||||||
mimetype: ['text/*'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'belongsToMany',
|
type: 'belongsToMany',
|
||||||
name: 'photos',
|
name: 'photos',
|
||||||
target: 'attachments',
|
target: 'attachments',
|
||||||
attachment: {
|
|
||||||
rules: {
|
|
||||||
size: 1024 * 100,
|
|
||||||
mimetype: ['image/*'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as CollectionOptions;
|
} as CollectionOptions;
|
||||||
|
111
packages/plugins/file-manager/src/server/actions/attachments.ts
Normal file
111
packages/plugins/file-manager/src/server/actions/attachments.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import multer from '@koa/multer';
|
||||||
|
import { Context, Next } from '@nocobase/actions';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { DEFAULT_MAX_FILE_SIZE, FILE_FIELD_NAME, LIMIT_FILES } from '../constants';
|
||||||
|
import * as Rules from '../rules';
|
||||||
|
import { getStorageConfig } from '../storages';
|
||||||
|
|
||||||
|
// TODO(optimize): 需要优化错误处理,计算失败后需要抛出对应错误,以便程序处理
|
||||||
|
function getFileFilter(storage) {
|
||||||
|
return (req, file, cb) => {
|
||||||
|
// size 交给 limits 处理
|
||||||
|
const { size, ...rules } = storage.rules;
|
||||||
|
const ruleKeys = Object.keys(rules);
|
||||||
|
const result =
|
||||||
|
!ruleKeys.length || !ruleKeys.some((key) => typeof Rules[key] !== 'function' || !Rules[key](file, rules[key]));
|
||||||
|
cb(null, result);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileData(ctx: Context) {
|
||||||
|
const { [FILE_FIELD_NAME]: file, storage } = ctx;
|
||||||
|
if (!file) {
|
||||||
|
return ctx.throw(400, 'file validation failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const storageConfig = getStorageConfig(storage.type);
|
||||||
|
const { [storageConfig.filenameKey || 'filename']: name } = file;
|
||||||
|
// make compatible filename across cloud service (with path)
|
||||||
|
const filename = path.basename(name);
|
||||||
|
const extname = path.extname(filename);
|
||||||
|
const urlPath = storage.path ? storage.path.replace(/^([^/])/, '/$1') : '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: file.originalname.replace(extname, ''),
|
||||||
|
filename,
|
||||||
|
extname,
|
||||||
|
// TODO(feature): 暂时两者相同,后面 storage.path 模版化以后,这里只是 file 实际的 path
|
||||||
|
path: storage.path,
|
||||||
|
size: file.size,
|
||||||
|
// 直接缓存起来
|
||||||
|
url: `${storage.baseUrl}${urlPath}/${filename}`,
|
||||||
|
mimetype: file.mimetype,
|
||||||
|
// @ts-ignore
|
||||||
|
meta: ctx.request.body,
|
||||||
|
storageId: storage.id,
|
||||||
|
...(storageConfig.getFileData ? storageConfig.getFileData(file) : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function middleware(ctx: Context, next: Next) {
|
||||||
|
const { resourceName, actionName } = ctx.action;
|
||||||
|
const { attachmentField } = ctx.action.params;
|
||||||
|
const collection = ctx.db.getCollection(resourceName);
|
||||||
|
|
||||||
|
if (collection?.options?.template !== 'file' || !['upload', 'create'].includes(actionName)) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const storageName = ctx.db.getFieldByPath(attachmentField)?.options?.storage || collection.options.storage;
|
||||||
|
const StorageRepo = ctx.db.getRepository('storages');
|
||||||
|
const storage = await StorageRepo.findOne({ filter: storageName ? { name: storageName } : { default: true } });
|
||||||
|
|
||||||
|
ctx.storage = storage;
|
||||||
|
|
||||||
|
await multipart(ctx, async () => {
|
||||||
|
const values = getFileData(ctx);
|
||||||
|
|
||||||
|
ctx.action.mergeParams({
|
||||||
|
values,
|
||||||
|
});
|
||||||
|
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function multipart(ctx: Context, next: Next) {
|
||||||
|
const { storage } = ctx;
|
||||||
|
if (!storage) {
|
||||||
|
console.error('[file-manager] no linked or default storage provided');
|
||||||
|
return ctx.throw(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
const storageConfig = getStorageConfig(storage.type);
|
||||||
|
if (!storageConfig) {
|
||||||
|
console.error(`[file-manager] storage type "${storage.type}" is not defined`);
|
||||||
|
return ctx.throw(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
const multerOptions = {
|
||||||
|
fileFilter: getFileFilter(storage),
|
||||||
|
limits: {
|
||||||
|
fileSize: storage.rules.size ?? DEFAULT_MAX_FILE_SIZE,
|
||||||
|
// 每次只允许提交一个文件
|
||||||
|
files: LIMIT_FILES,
|
||||||
|
},
|
||||||
|
storage: storageConfig.make(storage),
|
||||||
|
};
|
||||||
|
const upload = multer(multerOptions).single(FILE_FIELD_NAME);
|
||||||
|
try {
|
||||||
|
// NOTE: empty next and invoke after success
|
||||||
|
await upload(ctx, () => {});
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name === 'MulterError') {
|
||||||
|
return ctx.throw(400, err);
|
||||||
|
}
|
||||||
|
return ctx.throw(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import actions from '@nocobase/actions';
|
||||||
|
import { middleware } from './attachments';
|
||||||
|
|
||||||
|
export default function ({ app }) {
|
||||||
|
app.resourcer.use(middleware);
|
||||||
|
app.resourcer.registerActionHandler('upload', actions.create);
|
||||||
|
}
|
@ -1,169 +0,0 @@
|
|||||||
import multer from '@koa/multer';
|
|
||||||
import { Context, Next } from '@nocobase/actions';
|
|
||||||
import path from 'path';
|
|
||||||
import { FILE_FIELD_NAME, LIMIT_FILES, LIMIT_MAX_FILE_SIZE } from '../constants';
|
|
||||||
import * as Rules from '../rules';
|
|
||||||
import { getStorageConfig } from '../storages';
|
|
||||||
|
|
||||||
function getRules(ctx: Context) {
|
|
||||||
const { resourceField } = ctx;
|
|
||||||
if (!resourceField) {
|
|
||||||
return ctx.storage.rules;
|
|
||||||
}
|
|
||||||
const { rules = {} } = resourceField.options.attachment || {};
|
|
||||||
return Object.assign({}, ctx.storage.rules, rules);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(optimize): 需要优化错误处理,计算失败后需要抛出对应错误,以便程序处理
|
|
||||||
function getFileFilter(ctx: Context) {
|
|
||||||
return (req, file, cb) => {
|
|
||||||
// size 交给 limits 处理
|
|
||||||
const { size, ...rules } = getRules(ctx);
|
|
||||||
const ruleKeys = Object.keys(rules);
|
|
||||||
const result =
|
|
||||||
!ruleKeys.length ||
|
|
||||||
!ruleKeys.some((key) => typeof Rules[key] !== 'function' || !Rules[key](file, rules[key], ctx));
|
|
||||||
cb(null, result);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUploadAction = (ctx: Context) => {
|
|
||||||
const { resourceName, actionName } = ctx.action;
|
|
||||||
if (actionName === 'upload' && resourceName === 'attachments') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const collection = ctx.db.getCollection(resourceName);
|
|
||||||
if (collection?.options?.template === 'file' && ['upload', 'create'].includes(actionName)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function middleware(ctx: Context, next: Next) {
|
|
||||||
const { resourceName } = ctx.action;
|
|
||||||
const collection = ctx.db.getCollection(resourceName);
|
|
||||||
|
|
||||||
if (!isUploadAction(ctx)) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
const Storage = ctx.db.getCollection('storages');
|
|
||||||
let storage;
|
|
||||||
|
|
||||||
if (collection.options.storage) {
|
|
||||||
storage = await Storage.repository.findOne({ filter: { name: collection.options.storage } });
|
|
||||||
} else {
|
|
||||||
storage = await Storage.repository.findOne({ filter: { default: true } });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!storage) {
|
|
||||||
console.error('[file-manager] no default or linked storage provided');
|
|
||||||
return ctx.throw(500);
|
|
||||||
}
|
|
||||||
// 传递已取得的存储引擎,避免重查
|
|
||||||
ctx.storage = storage;
|
|
||||||
|
|
||||||
const storageConfig = getStorageConfig(storage.type);
|
|
||||||
if (!storageConfig) {
|
|
||||||
console.error(`[file-manager] storage type "${storage.type}" is not defined`);
|
|
||||||
return ctx.throw(500);
|
|
||||||
}
|
|
||||||
const multerOptions = {
|
|
||||||
fileFilter: getFileFilter(ctx),
|
|
||||||
limits: {
|
|
||||||
fileSize: Math.min(getRules(ctx).size || LIMIT_MAX_FILE_SIZE, LIMIT_MAX_FILE_SIZE),
|
|
||||||
// 每次只允许提交一个文件
|
|
||||||
files: LIMIT_FILES,
|
|
||||||
},
|
|
||||||
storage: storageConfig.make(storage),
|
|
||||||
};
|
|
||||||
const upload = multer(multerOptions).single(FILE_FIELD_NAME);
|
|
||||||
return upload(ctx, next);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createAction(ctx: Context, next: Next) {
|
|
||||||
if (!isUploadAction(ctx)) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { [FILE_FIELD_NAME]: file, storage } = ctx;
|
|
||||||
if (!file) {
|
|
||||||
return ctx.throw(400, 'file validation failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const storageConfig = getStorageConfig(storage.type);
|
|
||||||
const { [storageConfig.filenameKey || 'filename']: name } = file;
|
|
||||||
// make compatible filename across cloud service (with path)
|
|
||||||
const filename = path.basename(name);
|
|
||||||
const extname = path.extname(filename);
|
|
||||||
const urlPath = storage.path ? storage.path.replace(/^([^\/])/, '/$1') : '';
|
|
||||||
|
|
||||||
const values = {
|
|
||||||
title: file.originalname.replace(extname, ''),
|
|
||||||
filename,
|
|
||||||
extname,
|
|
||||||
// TODO(feature): 暂时两者相同,后面 storage.path 模版化以后,这里只是 file 实际的 path
|
|
||||||
path: storage.path,
|
|
||||||
size: file.size,
|
|
||||||
// 直接缓存起来
|
|
||||||
url: `${storage.baseUrl}${urlPath}/${filename}`,
|
|
||||||
mimetype: file.mimetype,
|
|
||||||
storageId: storage.id,
|
|
||||||
// @ts-ignore
|
|
||||||
meta: ctx.request.body,
|
|
||||||
...(storageConfig.getFileData ? storageConfig.getFileData(file) : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
ctx.action.mergeParams({
|
|
||||||
values,
|
|
||||||
});
|
|
||||||
|
|
||||||
await next();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function uploadAction(ctx: Context, next: Next) {
|
|
||||||
const { [FILE_FIELD_NAME]: file, storage } = ctx;
|
|
||||||
if (!file) {
|
|
||||||
return ctx.throw(400, 'file validation failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const storageConfig = getStorageConfig(storage.type);
|
|
||||||
const { [storageConfig.filenameKey || 'filename']: name } = file;
|
|
||||||
// make compatible filename across cloud service (with path)
|
|
||||||
const filename = path.basename(name);
|
|
||||||
const extname = path.extname(filename);
|
|
||||||
const urlPath = storage.path ? storage.path.replace(/^([^\/])/, '/$1') : '';
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
title: file.originalname.replace(extname, ''),
|
|
||||||
filename,
|
|
||||||
extname,
|
|
||||||
// TODO(feature): 暂时两者相同,后面 storage.path 模版化以后,这里只是 file 实际的 path
|
|
||||||
path: storage.path,
|
|
||||||
size: file.size,
|
|
||||||
// 直接缓存起来
|
|
||||||
url: `${storage.baseUrl}${urlPath}/${filename}`,
|
|
||||||
mimetype: file.mimetype,
|
|
||||||
storageId: storage.id,
|
|
||||||
// @ts-ignore
|
|
||||||
meta: ctx.request.body,
|
|
||||||
...(storageConfig.getFileData ? storageConfig.getFileData(file) : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const fileData = await ctx.db.sequelize.transaction(async (transaction) => {
|
|
||||||
const { resourceName } = ctx.action;
|
|
||||||
const repository = ctx.db.getRepository(resourceName);
|
|
||||||
|
|
||||||
const result = await repository.create({
|
|
||||||
values: {
|
|
||||||
...data,
|
|
||||||
},
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.body = fileData;
|
|
||||||
|
|
||||||
await next();
|
|
||||||
}
|
|
@ -7,6 +7,7 @@ export default {
|
|||||||
title: '文件管理器',
|
title: '文件管理器',
|
||||||
createdBy: true,
|
createdBy: true,
|
||||||
updatedBy: true,
|
updatedBy: true,
|
||||||
|
template: 'file',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
comment: '用户文件名(不含扩展名)',
|
comment: '用户文件名(不含扩展名)',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export const FILE_FIELD_NAME = 'file';
|
export const FILE_FIELD_NAME = 'file';
|
||||||
export const LIMIT_FILES = 1;
|
export const LIMIT_FILES = 1;
|
||||||
export const LIMIT_MAX_FILE_SIZE = 1024 * 1024 * 1024;
|
export const DEFAULT_MAX_FILE_SIZE = 1024 * 1024 * 1024;
|
||||||
|
|
||||||
export const STORAGE_TYPE_LOCAL = 'local';
|
export const STORAGE_TYPE_LOCAL = 'local';
|
||||||
export const STORAGE_TYPE_ALI_OSS = 'ali-oss';
|
export const STORAGE_TYPE_ALI_OSS = 'ali-oss';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import match from 'mime-match';
|
import match from 'mime-match';
|
||||||
|
|
||||||
export default function (file, options: string | string[] = '*', ctx): boolean {
|
export default function (file, options: string | string[] = '*'): boolean {
|
||||||
return options.toString().split(',').some(match(file.mimetype));
|
return options.toString().split(',').some(match(file.mimetype));
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Plugin } from '@nocobase/server';
|
import { Plugin } from '@nocobase/server';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { createAction, uploadAction, middleware as uploadMiddleware } from './actions/upload';
|
import initActions from './actions';
|
||||||
import { STORAGE_TYPE_LOCAL } from './constants';
|
import { STORAGE_TYPE_LOCAL } from './constants';
|
||||||
import { getStorageConfig } from './storages';
|
import { getStorageConfig } from './storages';
|
||||||
|
|
||||||
export default class PluginFileManager extends Plugin {
|
export default class PluginFileManager extends Plugin {
|
||||||
storageType() {
|
storageType() {
|
||||||
return process.env.DEFAULT_STORAGE_TYPE;
|
return process.env.DEFAULT_STORAGE_TYPE ?? 'local';
|
||||||
}
|
}
|
||||||
|
|
||||||
async install() {
|
async install() {
|
||||||
@ -41,14 +41,16 @@ export default class PluginFileManager extends Plugin {
|
|||||||
actions: ['storages:*'],
|
actions: ['storages:*'],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.app.acl.allow('attachments', 'upload', 'loggedIn');
|
initActions(this);
|
||||||
|
|
||||||
this.app.resourcer.use(uploadMiddleware);
|
// this.app.acl.allow('attachments', 'upload', 'loggedIn');
|
||||||
this.app.resourcer.use(createAction);
|
|
||||||
this.app.resourcer.registerActionHandler('upload', uploadAction);
|
// this.app.resourcer.use(uploadMiddleware);
|
||||||
|
// this.app.resourcer.use(createAction);
|
||||||
|
// this.app.resourcer.registerActionHandler('upload', uploadAction);
|
||||||
|
|
||||||
if (process.env.APP_ENV !== 'production') {
|
if (process.env.APP_ENV !== 'production') {
|
||||||
await getStorageConfig(STORAGE_TYPE_LOCAL).middleware(this.app);
|
await getStorageConfig(STORAGE_TYPE_LOCAL).middleware!(this.app);
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultStorageName = getStorageConfig(this.storageType()).defaults().name;
|
const defaultStorageName = getStorageConfig(this.storageType()).defaults().name;
|
||||||
|
@ -49,7 +49,7 @@ function createLocalServerUpdateHook(app, storages) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDocumentRoot(storage): string {
|
function getDocumentRoot(storage): string {
|
||||||
const { documentRoot = 'uploads' } = storage.options || {};
|
const { documentRoot = 'storage/uploads' } = storage.options || {};
|
||||||
// TODO(feature): 后面考虑以字符串模板的方式使用,可注入 req/action 相关变量,以便于区分文件夹
|
// TODO(feature): 后面考虑以字符串模板的方式使用,可注入 req/action 相关变量,以便于区分文件夹
|
||||||
return path.resolve(path.isAbsolute(documentRoot) ? documentRoot : path.join(process.cwd(), documentRoot));
|
return path.resolve(path.isAbsolute(documentRoot) ? documentRoot : path.join(process.cwd(), documentRoot));
|
||||||
}
|
}
|
||||||
@ -126,7 +126,7 @@ export default {
|
|||||||
},
|
},
|
||||||
defaults() {
|
defaults() {
|
||||||
const { LOCAL_STORAGE_DEST, LOCAL_STORAGE_BASE_URL, APP_PORT } = process.env;
|
const { LOCAL_STORAGE_DEST, LOCAL_STORAGE_BASE_URL, APP_PORT } = process.env;
|
||||||
const documentRoot = LOCAL_STORAGE_DEST || 'uploads';
|
const documentRoot = LOCAL_STORAGE_DEST || 'storage/uploads';
|
||||||
return {
|
return {
|
||||||
title: '本地存储',
|
title: '本地存储',
|
||||||
type: STORAGE_TYPE_LOCAL,
|
type: STORAGE_TYPE_LOCAL,
|
||||||
|
Loading…
Reference in New Issue
Block a user