feat: file storages (#314)
* feat: file storages * feat: storageOptions * feat: radio field * fix: documentRoot
This commit is contained in:
parent
fefa1fc0c9
commit
2fd27ea9f3
@ -14,6 +14,7 @@ import {
|
||||
compose,
|
||||
DesignableSwitch,
|
||||
DocumentTitleProvider,
|
||||
FileStorageShortcut,
|
||||
i18n,
|
||||
MenuItemInitializers,
|
||||
PluginManagerProvider,
|
||||
@ -85,6 +86,7 @@ const providers = [
|
||||
WorkflowShortcut,
|
||||
SystemSettingsShortcut,
|
||||
SchemaTemplateShortcut,
|
||||
FileStorageShortcut,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -0,0 +1,40 @@
|
||||
import { SettingOutlined } from '@ant-design/icons';
|
||||
import { uid } from '@formily/shared';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PluginManager } from '..';
|
||||
import { ActionContext, SchemaComponent } from '../schema-component';
|
||||
import { storageSchema } from './schemas/storage';
|
||||
import { StorageOptions } from './StorageOptions';
|
||||
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
'x-component': 'Action.Drawer',
|
||||
type: 'void',
|
||||
title: '{{t("File storages")}}',
|
||||
properties: {
|
||||
storageSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const FileStorageShortcut = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<ActionContext.Provider value={{ visible, setVisible }}>
|
||||
<PluginManager.Toolbar.Item
|
||||
eventKey={'FileStorage'}
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
}}
|
||||
icon={<SettingOutlined />}
|
||||
title={t('File storages')}
|
||||
/>
|
||||
<SchemaComponent components={{ StorageOptions }} schema={schema} />
|
||||
</ActionContext.Provider>
|
||||
);
|
||||
};
|
98
packages/core/client/src/file-manager/StorageOptions.tsx
Normal file
98
packages/core/client/src/file-manager/StorageOptions.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import { FormLayout } from '@formily/antd';
|
||||
import { Field } from '@formily/core';
|
||||
import { observer, RecursionField, Schema, useField, useForm } from '@formily/react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
const schema = {
|
||||
local: {
|
||||
properties: {
|
||||
documentRoot: {
|
||||
title: '{{t("Destination")}}',
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
},
|
||||
'ali-oss': {
|
||||
properties: {
|
||||
region: {
|
||||
title: '{{t("Region")}}',
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
},
|
||||
accessKeyId: {
|
||||
title: '{{t("AccessKey ID")}}',
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
},
|
||||
accessKeySecret: {
|
||||
title: '{{t("AccessKey Secret")}}',
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
},
|
||||
bucket: {
|
||||
title: '{{t("Bucket")}}',
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
s3: {
|
||||
properties: {
|
||||
region: {
|
||||
title: '{{t("Region")}}',
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
},
|
||||
accessKeyId: {
|
||||
title: '{{t("AccessKey ID")}}',
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
},
|
||||
secretAccessKey: {
|
||||
title: '{{t("AccessKey Secret")}}',
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
},
|
||||
bucket: {
|
||||
title: '{{t("Bucket")}}',
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const StorageOptions = observer((props) => {
|
||||
const form = useForm();
|
||||
const field = useField<Field>();
|
||||
const [s, setSchema] = useState(new Schema({}));
|
||||
useEffect(() => {
|
||||
form.clearFormGraph('options.*');
|
||||
setSchema(new Schema(schema[form.values.type] || {}));
|
||||
}, [form.values.type]);
|
||||
return (
|
||||
<div>
|
||||
<FormLayout layout={'vertical'}>
|
||||
<RecursionField key={form.values.type || 'local'} basePath={field.address} onlyRenderProperties schema={s} />
|
||||
</FormLayout>
|
||||
</div>
|
||||
);
|
||||
});
|
1
packages/core/client/src/file-manager/index.ts
Normal file
1
packages/core/client/src/file-manager/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './FileStorageShortcut';
|
350
packages/core/client/src/file-manager/schemas/storage.ts
Normal file
350
packages/core/client/src/file-manager/schemas/storage.ts
Normal file
@ -0,0 +1,350 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { useRequest } from '../../api-client';
|
||||
import { useActionContext } from '../../schema-component';
|
||||
|
||||
const collection = {
|
||||
name: 'storages',
|
||||
fields: [
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'title',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{t("Storage display name")}}',
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
} as ISchema,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{t("Storage name")}}',
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
} as ISchema,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'type',
|
||||
interface: 'select',
|
||||
uiSchema: {
|
||||
title: '{{t("Storage type")}}',
|
||||
type: 'string',
|
||||
'x-component': 'Select',
|
||||
required: true,
|
||||
enum: [
|
||||
{ label: '{{t("Local storage")}}', value: 'local' },
|
||||
{ label: '{{t("Aliyun OSS")}}', value: 'ali-oss' },
|
||||
{ label: '{{t("Amazon S3")}}', value: 's3' },
|
||||
],
|
||||
} as ISchema,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'baseUrl',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{t("Storage base URL")}}',
|
||||
type: 'string',
|
||||
'x-component': 'Input',
|
||||
} as ISchema,
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'default',
|
||||
interface: 'boolean',
|
||||
uiSchema: {
|
||||
title: '{{t("Default storage")}}',
|
||||
type: 'boolean',
|
||||
'x-component': 'Checkbox',
|
||||
} as ISchema,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const storageSchema: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
block1: {
|
||||
type: 'void',
|
||||
'x-decorator': 'ResourceActionProvider',
|
||||
'x-decorator-props': {
|
||||
collection,
|
||||
resourceName: 'storages',
|
||||
request: {
|
||||
resource: 'storages',
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 50,
|
||||
sort: ['id'],
|
||||
appends: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
'x-component': 'CollectionProvider',
|
||||
'x-component-props': {
|
||||
collection,
|
||||
},
|
||||
properties: {
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
delete: {
|
||||
type: 'void',
|
||||
title: '{{ t("Delete") }}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ cm.useBulkDestroyAction }}',
|
||||
confirm: {
|
||||
title: "{{t('Delete storage')}}",
|
||||
content: "{{t('Are you sure you want to delete it?')}}",
|
||||
},
|
||||
},
|
||||
},
|
||||
create: {
|
||||
type: 'void',
|
||||
title: '{{t("Add storage")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-decorator': 'Form',
|
||||
'x-decorator-props': {
|
||||
useValues(options) {
|
||||
const ctx = useActionContext();
|
||||
return useRequest(
|
||||
() =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
name: `s_${uid()}`,
|
||||
},
|
||||
}),
|
||||
{ ...options, refreshDeps: [ctx.visible] },
|
||||
);
|
||||
},
|
||||
},
|
||||
title: '{{t("Add storage")}}',
|
||||
properties: {
|
||||
title: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
name: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
description:
|
||||
'{{t("Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.")}}',
|
||||
},
|
||||
baseUrl: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
type: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
options: {
|
||||
type: 'object',
|
||||
'x-component': 'StorageOptions',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
default: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
title: '',
|
||||
'x-content': '{{t("Default storage")}}',
|
||||
},
|
||||
footer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer.Footer',
|
||||
properties: {
|
||||
cancel: {
|
||||
title: '{{t("Cancel")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ cm.useCancelAction }}',
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
title: '{{t("Submit")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
useAction: '{{ cm.useCreateAction }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
table: {
|
||||
type: 'void',
|
||||
'x-uid': 'input',
|
||||
'x-component': 'Table.Void',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
useDataSource: '{{ cm.useDataSourceFromRAC }}',
|
||||
},
|
||||
properties: {
|
||||
column1: {
|
||||
type: 'void',
|
||||
'x-decorator': 'Table.Column.Decorator',
|
||||
'x-component': 'Table.Column',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'number',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
column2: {
|
||||
type: 'void',
|
||||
'x-decorator': 'Table.Column.Decorator',
|
||||
'x-component': 'Table.Column',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
column3: {
|
||||
type: 'void',
|
||||
'x-decorator': 'Table.Column.Decorator',
|
||||
'x-component': 'Table.Column',
|
||||
properties: {
|
||||
default: {
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
column4: {
|
||||
type: 'void',
|
||||
title: '{{t("Actions")}}',
|
||||
'x-component': 'Table.Column',
|
||||
properties: {
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
split: '|',
|
||||
},
|
||||
properties: {
|
||||
update: {
|
||||
type: 'void',
|
||||
title: '{{t("Edit")}}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-decorator': 'Form',
|
||||
'x-decorator-props': {
|
||||
useValues: '{{ cm.useValuesFromRecord }}',
|
||||
},
|
||||
title: '{{t("Edit storage")}}',
|
||||
properties: {
|
||||
title: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
name: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-disabled': true,
|
||||
},
|
||||
baseUrl: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
type: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-disabled': true,
|
||||
},
|
||||
options: {
|
||||
type: 'object',
|
||||
'x-component': 'StorageOptions',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
default: {
|
||||
title: '',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-content': '{{t("Default storage")}}',
|
||||
},
|
||||
footer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer.Footer',
|
||||
properties: {
|
||||
cancel: {
|
||||
title: '{{t("Cancel")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ cm.useCancelAction }}',
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
title: '{{t("Submit")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
useAction: '{{ cm.useUpdateAction }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
type: 'void',
|
||||
title: '{{ t("Delete") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
confirm: {
|
||||
title: "{{t('Delete role')}}",
|
||||
content: "{{t('Are you sure you want to delete it?')}}",
|
||||
},
|
||||
useAction: '{{cm.useDestroyAction}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -11,6 +11,7 @@ export * from './board';
|
||||
export * from './china-region';
|
||||
export * from './collection-manager';
|
||||
export * from './document-title';
|
||||
export * from './file-manager';
|
||||
export * from './i18n';
|
||||
export * from './icon';
|
||||
export * from './plugin-manager';
|
||||
|
@ -382,4 +382,12 @@ export default {
|
||||
'Action permission': '操作权限',
|
||||
'Field permission': '字段权限',
|
||||
'Scope name': '数据范围名称',
|
||||
|
||||
'File storages': '文件存储',
|
||||
'Storage display name': '文件存储名称',
|
||||
'Storage name': '文件存储标识',
|
||||
'Default storage': '默认存储',
|
||||
'Add storage': '添加文件存储',
|
||||
'Edit storage': '编辑文件存储',
|
||||
'Storage base URL': 'Base URL',
|
||||
}
|
||||
|
@ -134,6 +134,7 @@ const InternalAdminLayout = (props: any) => {
|
||||
{ component: 'WorkflowShortcut', pin: true },
|
||||
{ component: 'SchemaTemplateShortcut', pin: true },
|
||||
{ component: 'SystemSettingsShortcut' },
|
||||
{ component: 'FileStorageShortcut' },
|
||||
]}
|
||||
/>
|
||||
</ACLAllowConfigure>
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
RealFieldOptions
|
||||
} from './number-field';
|
||||
import { PasswordFieldOptions } from './password-field';
|
||||
import { RadioFieldOptions } from './radio-field';
|
||||
import { SortFieldOptions } from './sort-field';
|
||||
import { StringFieldOptions } from './string-field';
|
||||
import { TextFieldOptions } from './text-field';
|
||||
@ -35,6 +36,7 @@ export * from './has-one-field';
|
||||
export * from './json-field';
|
||||
export * from './number-field';
|
||||
export * from './password-field';
|
||||
export * from './radio-field';
|
||||
export * from './relation-field';
|
||||
export * from './sort-field';
|
||||
export * from './string-field';
|
||||
@ -54,6 +56,7 @@ export type FieldOptions =
|
||||
| JsonFieldOptions
|
||||
| JsonbFieldOptions
|
||||
| BooleanFieldOptions
|
||||
| RadioFieldOptions
|
||||
| SortFieldOptions
|
||||
| TextFieldOptions
|
||||
| VirtualFieldOptions
|
||||
|
50
packages/core/database/src/fields/radio-field.ts
Normal file
50
packages/core/database/src/fields/radio-field.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
|
||||
export interface RadioFieldOptions extends BaseColumnFieldOptions {
|
||||
type: 'radio';
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂时只支持全局,不支持批量
|
||||
*/
|
||||
export class RadioField extends Field {
|
||||
get dataType() {
|
||||
return DataTypes.BOOLEAN;
|
||||
}
|
||||
|
||||
init() {
|
||||
const { name } = this.options;
|
||||
this.listener = async (model, { transaction }) => {
|
||||
if (!model.changed(name as any)) {
|
||||
return;
|
||||
}
|
||||
const value = model.get(name) as boolean;
|
||||
if (value) {
|
||||
const M = this.collection.model;
|
||||
await M.update(
|
||||
{ [name]: false },
|
||||
{
|
||||
where: {
|
||||
[name]: true,
|
||||
},
|
||||
transaction,
|
||||
hooks: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
bind() {
|
||||
super.bind();
|
||||
this.on('beforeCreate', this.listener.bind(this));
|
||||
this.on('beforeUpdate', this.listener.bind(this));
|
||||
}
|
||||
|
||||
unbind() {
|
||||
super.unbind();
|
||||
this.off('beforeCreate', this.listener.bind(this));
|
||||
this.off('beforeUpdate', this.listener.bind(this));
|
||||
}
|
||||
}
|
@ -49,7 +49,7 @@ export default {
|
||||
// TODO(feature): 需要使用一个实现了可设置默认值的字段
|
||||
{
|
||||
comment: '默认引擎',
|
||||
type: 'boolean',
|
||||
type: 'radio',
|
||||
name: 'default',
|
||||
defaultValue: false,
|
||||
},
|
||||
|
@ -82,11 +82,12 @@ async function middleware(app: Application, options?) {
|
||||
|
||||
// 以下情况才认为当前进程所应该提供静态服务
|
||||
// 否则都忽略,交给其他 server 来提供(如 nginx/cdn 等)
|
||||
if (url.origin && url.origin !== LOCALHOST) {
|
||||
if (url.origin && storage?.options?.serve === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const basePath = url.pathname.startsWith('/') ? url.pathname : `/${url.pathname}`;
|
||||
|
||||
if (!match(basePath, ctx.path)) {
|
||||
continue;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user