feat: file storages (#314)

* feat: file storages

* feat: storageOptions

* feat: radio field

* fix: documentRoot
This commit is contained in:
chenos 2022-04-24 10:12:12 +08:00 committed by GitHub
parent fefa1fc0c9
commit 2fd27ea9f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 557 additions and 2 deletions

View File

@ -14,6 +14,7 @@ import {
compose,
DesignableSwitch,
DocumentTitleProvider,
FileStorageShortcut,
i18n,
MenuItemInitializers,
PluginManagerProvider,
@ -85,6 +86,7 @@ const providers = [
WorkflowShortcut,
SystemSettingsShortcut,
SchemaTemplateShortcut,
FileStorageShortcut,
},
},
],

View File

@ -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>
);
};

View 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>
);
});

View File

@ -0,0 +1 @@
export * from './FileStorageShortcut';

View 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}}',
},
},
},
},
},
},
},
},
},
},
},
};

View File

@ -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';

View File

@ -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',
}

View File

@ -134,6 +134,7 @@ const InternalAdminLayout = (props: any) => {
{ component: 'WorkflowShortcut', pin: true },
{ component: 'SchemaTemplateShortcut', pin: true },
{ component: 'SystemSettingsShortcut' },
{ component: 'FileStorageShortcut' },
]}
/>
</ACLAllowConfigure>

View File

@ -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

View 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));
}
}

View File

@ -49,7 +49,7 @@ export default {
// TODO(feature): 需要使用一个实现了可设置默认值的字段
{
comment: '默认引擎',
type: 'boolean',
type: 'radio',
name: 'default',
defaultValue: false,
},

View File

@ -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;
}