feat: collection options & hooks (#21)
* feat: collection hooks * export action middlewares * add associated middleware * cleanup * add field interface options * 调整配置参数 * 补充字段类型 options * 继续调整配置参数 * 支持排序 * filterable & sortable & draggable * feat: add random name for creating table (#23) * feat: add random name for creating table * fix: random number * Feature: collections field (#24) * feat: add random name for field and update table options * fix: make field name required * fix: this declaration * showInXX 参数调整 * showInXX 放 component 里 * 继续调整参数 * 字段分组、pages 表配置参数等 * change date to datetime * 选择类型字段的 options 改为 dataSource * feat: refactor hooks initialization and add field options by interface (#25) * feat: refactor hooks initialization and add field options by interface * refactor: use model.set to build input values * refactor: extend setter/getter to adapt field options * fix: try to fix virtual field * refactor: setter/getter of FieldModel * 改进自定义 model 等细节 * 补充注释 * bugfix Co-authored-by: Junyi <mytharcher@users.noreply.github.com>
This commit is contained in:
parent
d9e6d2e614
commit
b5ddd6a6ba
@ -384,7 +384,7 @@ export async function sort(ctx: Context, next: Next) {
|
|||||||
const Model = ctx.db.getModel(resourceName);
|
const Model = ctx.db.getModel(resourceName);
|
||||||
const table = ctx.db.getTable(resourceName);
|
const table = ctx.db.getTable(resourceName);
|
||||||
|
|
||||||
if (!table.getOptions().sortable || !values.offset) {
|
if (!values.offset) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
const [primaryField] = Model.primaryKeyAttributes;
|
const [primaryField] = Model.primaryKeyAttributes;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as actions from './actions';
|
import * as actions from './actions';
|
||||||
|
|
||||||
export * from './middleware';
|
export * as actions from './actions';
|
||||||
|
export * as middlewares from './middlewares';
|
||||||
export default actions;
|
export default actions;
|
||||||
|
2
packages/actions/src/middlewares/index.ts
Normal file
2
packages/actions/src/middlewares/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './associated';
|
||||||
|
export * from './json-reponse';
|
@ -1,31 +1,32 @@
|
|||||||
import { request } from 'umi';
|
import { request } from 'umi';
|
||||||
|
|
||||||
interface ResourceProxyConstructor {
|
interface ActionParams {
|
||||||
new <T, H extends object>(target: T, handler: ProxyHandler<H>): H
|
|
||||||
}
|
|
||||||
|
|
||||||
const ResourceProxy = Proxy as ResourceProxyConstructor;
|
|
||||||
|
|
||||||
interface Params {
|
|
||||||
resourceKey?: string | number;
|
resourceKey?: string | number;
|
||||||
// resourceName?: string;
|
// resourceName?: string;
|
||||||
// associatedName?: string;
|
// associatedName?: string;
|
||||||
associatedKey?: string | number;
|
associatedKey?: string | number;
|
||||||
fields?: any;
|
fields?: any;
|
||||||
filter?: any;
|
filter?: any;
|
||||||
|
values?: any;
|
||||||
|
page?: any;
|
||||||
|
perPage?: any;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Handler {
|
interface Resource {
|
||||||
[name: string]: (params?: Params) => Promise<any>;
|
get: (params?: ActionParams) => Promise<any>;
|
||||||
|
list: (params?: ActionParams) => Promise<any>;
|
||||||
|
create: (params?: ActionParams) => Promise<any>;
|
||||||
|
update: (params?: ActionParams) => Promise<any>;
|
||||||
|
destroy: (params?: ActionParams) => Promise<any>;
|
||||||
|
[name: string]: (params?: ActionParams) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class APIClient {
|
class ApiClient {
|
||||||
resource(name: string) {
|
resource(name: string): Resource {
|
||||||
return new ResourceProxy<object, Handler>({}, {
|
const proxy: any = new Proxy({}, {
|
||||||
get(target, method, receiver) {
|
get(target, method, receiver) {
|
||||||
return (params: Params = {}) => {
|
return (params: ActionParams = {}) => {
|
||||||
console.log(params);
|
|
||||||
const { associatedKey, resourceKey, ...restParams } = params;
|
const { associatedKey, resourceKey, ...restParams } = params;
|
||||||
let url = `/${name}`;
|
let url = `/${name}`;
|
||||||
let options: any = {};
|
let options: any = {};
|
||||||
@ -49,9 +50,10 @@ class APIClient {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return proxy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const api = new APIClient();
|
const api = new ApiClient();
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
@ -36,7 +36,7 @@ const api = Api.create({
|
|||||||
});
|
});
|
||||||
|
|
||||||
api.resourcer.use(associated);
|
api.resourcer.use(associated);
|
||||||
api.resourcer.registerHandlers(actions.associate);
|
api.resourcer.registerHandlers({...actions.common, ...actions.associate});
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
title: '后台应用',
|
title: '后台应用',
|
||||||
@ -158,159 +158,21 @@ const data = {
|
|||||||
|
|
||||||
const database: Database = api.database;
|
const database: Database = api.database;
|
||||||
|
|
||||||
await database.sync();
|
await database.sync({
|
||||||
|
// tables: ['collections', 'fields', 'actions', 'views', 'tabs'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const Collection = database.getModel('collections');
|
||||||
|
const tables = database.getTables([]);
|
||||||
|
|
||||||
|
for (let table of tables) {
|
||||||
|
await Collection.import(table.getOptions(), { hooks: false });
|
||||||
|
}
|
||||||
|
|
||||||
const Page = database.getModel('pages');
|
const Page = database.getModel('pages');
|
||||||
const page = await Page.create(data);
|
const page = await Page.create(data);
|
||||||
await page.updateAssociations(data);
|
await page.updateAssociations(data);
|
||||||
|
|
||||||
const [Collection, View, Action, Tab] = database.getModels(['collections', 'views', 'actions', 'tabs']);
|
|
||||||
const tables = database.getTables([]);
|
|
||||||
|
|
||||||
for (let table of tables) {
|
|
||||||
const options = table.getOptions();
|
|
||||||
const collection = await Collection.create(options);
|
|
||||||
// console.log(options);
|
|
||||||
const associations: any = {};
|
|
||||||
if (options.fields) {
|
|
||||||
associations['fields'] = options.fields.map((item, sort) => ({
|
|
||||||
...item,
|
|
||||||
options: item,
|
|
||||||
sort,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
if (options.tabs) {
|
|
||||||
associations['tabs'] = options.tabs.map((item, sort) => ({
|
|
||||||
...item,
|
|
||||||
options: item,
|
|
||||||
sort,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
if (options.actions) {
|
|
||||||
associations['actions'] = options.actions.map((item, sort) => ({
|
|
||||||
...item,
|
|
||||||
options: item,
|
|
||||||
sort,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
if (options.views) {
|
|
||||||
associations['views'] = options.views.map((item, sort) => ({
|
|
||||||
...item,
|
|
||||||
options: item,
|
|
||||||
sort,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
await collection.updateAssociations(associations);
|
|
||||||
}
|
|
||||||
|
|
||||||
// const actions = await Action.findAll();
|
|
||||||
|
|
||||||
// for (const action of actions) {
|
|
||||||
// const viewName = action.options.viewName;
|
|
||||||
// console.log({viewName});
|
|
||||||
// if (viewName) {
|
|
||||||
// const view = await View.findOne({
|
|
||||||
// where: {
|
|
||||||
// name: viewName,
|
|
||||||
// collection_name: action.collection_name
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// if (view) {
|
|
||||||
// action.options.viewId = view.id;
|
|
||||||
// console.log(action.options);
|
|
||||||
// action.setDataValue('options', action.options);
|
|
||||||
// action.changed('options', true);
|
|
||||||
// await action.save();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// const tabs = await Tab.findAll();
|
|
||||||
|
|
||||||
// for (const tab of tabs) {
|
|
||||||
// const viewName = tab.options.viewName;
|
|
||||||
// if (!viewName) {
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
// let view: any;
|
|
||||||
// if (tab.type === 'association') {
|
|
||||||
// view = await View.findOne({
|
|
||||||
// where: {
|
|
||||||
// name: viewName,
|
|
||||||
// collection_name: tab.options.association,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// } else {
|
|
||||||
// view = await View.findOne({
|
|
||||||
// where: {
|
|
||||||
// name: viewName,
|
|
||||||
// collection_name: tab.collection_name,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// if (view) {
|
|
||||||
// tab.options.viewId = view.id;
|
|
||||||
// tab.setDataValue('options', tab.options);
|
|
||||||
// tab.changed('options', true);
|
|
||||||
// await tab.save();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// const views = await View.findAll();
|
|
||||||
// for (const view of views) {
|
|
||||||
// const detailsViewName = view.options.detailsViewName;
|
|
||||||
// if (detailsViewName) {
|
|
||||||
// const v = await View.findOne({
|
|
||||||
// where: {
|
|
||||||
// name: detailsViewName,
|
|
||||||
// collection_name: view.collection_name
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// if (v) {
|
|
||||||
// view.options.detailsViewId = v.id;
|
|
||||||
// view.setDataValue('options', view.options);
|
|
||||||
// view.changed('options', true);
|
|
||||||
// await view.save();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// const updateViewName = view.options.updateViewName;
|
|
||||||
// if (updateViewName) {
|
|
||||||
// const v = await View.findOne({
|
|
||||||
// where: {
|
|
||||||
// name: updateViewName,
|
|
||||||
// collection_name: view.collection_name
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// if (v) {
|
|
||||||
// view.options.updateViewId = v.id;
|
|
||||||
// view.setDataValue('options', view.options);
|
|
||||||
// view.changed('options', true);
|
|
||||||
// await view.save();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// console.log({detailsViewName, updateViewName});
|
|
||||||
// }
|
|
||||||
|
|
||||||
// for (let table of tables) {
|
|
||||||
// const options = table.getOptions();
|
|
||||||
// const collection = await Collection.findOne({
|
|
||||||
// where: {
|
|
||||||
// name: options.name,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// const tabs = await collection.getTabs() as Model[];
|
|
||||||
// const actions = await collection.getActions() as Model[];
|
|
||||||
// const views = await collection.getViews() as Model[];
|
|
||||||
// for (const tab of tabs) {
|
|
||||||
// tab.options.viewName;
|
|
||||||
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const collections = await Collection.findAll();
|
|
||||||
|
|
||||||
// await Promise.all(collections.map(async (collection) => {
|
|
||||||
// return await collection.modelInit();
|
|
||||||
// }));
|
|
||||||
|
|
||||||
await Page.create({
|
await Page.create({
|
||||||
title: '登录页面',
|
title: '登录页面',
|
||||||
path: '/login',
|
path: '/login',
|
||||||
|
@ -20,10 +20,11 @@ export const setup = () => {
|
|||||||
timerange: TimePicker.RangePicker,
|
timerange: TimePicker.RangePicker,
|
||||||
transfer: Transfer,
|
transfer: Transfer,
|
||||||
boolean: Switch,
|
boolean: Switch,
|
||||||
|
checkbox: Switch,
|
||||||
array: ArrayCards,
|
array: ArrayCards,
|
||||||
cards: ArrayCards,
|
cards: ArrayCards,
|
||||||
table: ArrayTable,
|
table: ArrayTable,
|
||||||
checkbox: Checkbox.Group,
|
checkboxes: Checkbox.Group,
|
||||||
date: DatePicker,
|
date: DatePicker,
|
||||||
daterange: DatePicker.RangePicker,
|
daterange: DatePicker.RangePicker,
|
||||||
year: DatePicker.YearPicker,
|
year: DatePicker.YearPicker,
|
||||||
|
@ -29,9 +29,9 @@ export function SimpleTable(props: SimpleTableProps) {
|
|||||||
const { rowKey = 'id', fields = [], rowViewName, actions = [], paginated = true, defaultPageSize = 10 } = schema;
|
const { rowKey = 'id', fields = [], rowViewName, actions = [], paginated = true, defaultPageSize = 10 } = schema;
|
||||||
const { sourceKey = 'id' } = activeTab.field||{};
|
const { sourceKey = 'id' } = activeTab.field||{};
|
||||||
const drawerRef = useRef<any>();
|
const drawerRef = useRef<any>();
|
||||||
const { data, loading, pagination, mutate } = useRequest((params = {}) => {
|
const name = associatedName ? `${associatedName}.${resourceName}` : resourceName;
|
||||||
|
const { data, loading, pagination, mutate, refresh } = useRequest((params = {}) => {
|
||||||
const { current, pageSize, ...restParams } = params;
|
const { current, pageSize, ...restParams } = params;
|
||||||
const name = associatedName ? `${associatedName}.${resourceName}` : resourceName;
|
|
||||||
return api.resource(name).list({
|
return api.resource(name).list({
|
||||||
associatedKey,
|
associatedKey,
|
||||||
page: paginated ? current : 1,
|
page: paginated ? current : 1,
|
||||||
@ -75,7 +75,21 @@ export function SimpleTable(props: SimpleTableProps) {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
columns={fields2columns(fields)}
|
columns={fields2columns(fields)}
|
||||||
dataSource={data?.list||(data as any)}
|
dataSource={data?.list||(data as any)}
|
||||||
components={components({data, mutate})}
|
components={components({
|
||||||
|
data,
|
||||||
|
mutate,
|
||||||
|
rowKey,
|
||||||
|
onMoved: async ({resourceKey, offset}) => {
|
||||||
|
await api.resource(name).sort({
|
||||||
|
associatedKey,
|
||||||
|
resourceKey,
|
||||||
|
field: 'sort',
|
||||||
|
offset,
|
||||||
|
});
|
||||||
|
await refresh();
|
||||||
|
console.log({resourceKey, offset});
|
||||||
|
}
|
||||||
|
})}
|
||||||
onRow={(record) => ({
|
onRow={(record) => ({
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
drawerRef.current.setVisible(true);
|
drawerRef.current.setVisible(true);
|
||||||
|
@ -14,20 +14,30 @@ export const DragHandle = sortableHandle(() => (
|
|||||||
<MenuOutlined style={{ cursor: 'pointer', color: '#999' }} />
|
<MenuOutlined style={{ cursor: 'pointer', color: '#999' }} />
|
||||||
));
|
));
|
||||||
|
|
||||||
export const components = ({data = {}, mutate}: {data: any, mutate: any}) => {
|
interface Props {
|
||||||
|
data: any,
|
||||||
|
mutate: any,
|
||||||
|
rowKey: any,
|
||||||
|
onMoved: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const components = ({data = {}, rowKey, mutate, onMoved}: Props) => {
|
||||||
return {
|
return {
|
||||||
body: {
|
body: {
|
||||||
wrapper: props => (
|
wrapper: props => (
|
||||||
<SortableContainer
|
<SortableContainer
|
||||||
useDragHandle
|
useDragHandle
|
||||||
helperClass="row-dragging"
|
helperClass="row-dragging"
|
||||||
onSortEnd={({ oldIndex, newIndex }) => {
|
onSortEnd={async ({ oldIndex, newIndex, ...restProps }) => {
|
||||||
if (oldIndex !== newIndex) {
|
if (oldIndex !== newIndex) {
|
||||||
const list = arrayMove([].concat(data.list), oldIndex, newIndex).filter(el => !!el);
|
const list = arrayMove([].concat(data.list), oldIndex, newIndex).filter(el => !!el);
|
||||||
|
console.log({oldIndex, newIndex, list});
|
||||||
mutate({
|
mutate({
|
||||||
...data,
|
...data,
|
||||||
list,
|
list,
|
||||||
});
|
});
|
||||||
|
const resourceKey = get(list, [newIndex, rowKey]);
|
||||||
|
await onMoved({resourceKey, offset: newIndex - oldIndex});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
@ -35,7 +45,7 @@ export const components = ({data = {}, mutate}: {data: any, mutate: any}) => {
|
|||||||
),
|
),
|
||||||
row: ({ className, style, ...restProps }) => {
|
row: ({ className, style, ...restProps }) => {
|
||||||
// function findIndex base on Table rowKey props and should always be a right array index
|
// function findIndex base on Table rowKey props and should always be a right array index
|
||||||
const index = findIndex(data.list, (x: any) => x.id === restProps['data-row-key']);
|
const index = findIndex(data.list, (x: any) => x[rowKey] === restProps['data-row-key']);
|
||||||
return <SortableItem index={index} {...restProps} />;
|
return <SortableItem index={index} {...restProps} />;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -42,13 +42,12 @@ export function Table(props: TableProps) {
|
|||||||
associatedKey,
|
associatedKey,
|
||||||
} = props;
|
} = props;
|
||||||
const { fields, defaultTabName, rowKey = 'id', actions = [], paginated = true, defaultPageSize = 10 } = schema;
|
const { fields, defaultTabName, rowKey = 'id', actions = [], paginated = true, defaultPageSize = 10 } = schema;
|
||||||
const name = associatedName ? `${associatedName}.${resourceName}` : resourceName;
|
|
||||||
// const { data, mutate } = useRequest(() => api.resource(name).list({
|
// const { data, mutate } = useRequest(() => api.resource(name).list({
|
||||||
// associatedKey,
|
// associatedKey,
|
||||||
// }));
|
// }));
|
||||||
const { data, loading, pagination, mutate } = useRequest((params = {}) => {
|
const name = associatedName ? `${associatedName}.${resourceName}` : resourceName;
|
||||||
|
const { data, loading, pagination, mutate, refresh } = useRequest((params = {}) => {
|
||||||
const { current, pageSize, ...restParams } = params;
|
const { current, pageSize, ...restParams } = params;
|
||||||
const name = associatedName ? `${associatedName}.${resourceName}` : resourceName;
|
|
||||||
return api.resource(name).list({
|
return api.resource(name).list({
|
||||||
associatedKey,
|
associatedKey,
|
||||||
page: paginated ? current : 1,
|
page: paginated ? current : 1,
|
||||||
@ -86,7 +85,21 @@ export function Table(props: TableProps) {
|
|||||||
rowKey={rowKey}
|
rowKey={rowKey}
|
||||||
columns={fields2columns(fields)}
|
columns={fields2columns(fields)}
|
||||||
dataSource={data?.list||(data as any)}
|
dataSource={data?.list||(data as any)}
|
||||||
components={components({data, mutate})}
|
components={components({
|
||||||
|
data,
|
||||||
|
mutate,
|
||||||
|
rowKey,
|
||||||
|
onMoved: async ({resourceKey, offset}) => {
|
||||||
|
await api.resource(name).sort({
|
||||||
|
associatedKey,
|
||||||
|
resourceKey,
|
||||||
|
field: 'sort',
|
||||||
|
offset,
|
||||||
|
});
|
||||||
|
await refresh();
|
||||||
|
console.log({resourceKey, offset});
|
||||||
|
}
|
||||||
|
})}
|
||||||
onRow={(data) => ({
|
onRow={(data) => ({
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
redirectTo({
|
redirectTo({
|
||||||
|
23
packages/database/src/__tests__/model/custom.test.ts
Normal file
23
packages/database/src/__tests__/model/custom.test.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { registerModels } from '../..';
|
||||||
|
import { getDatabase } from '../';
|
||||||
|
import Model from '../../model';
|
||||||
|
|
||||||
|
describe('custom model', () => {
|
||||||
|
it('custom model', async () => {
|
||||||
|
class BaseModel extends Model {};
|
||||||
|
const database = getDatabase();
|
||||||
|
registerModels({BaseModel});
|
||||||
|
database.table({
|
||||||
|
name: 'tests',
|
||||||
|
model: 'BaseModel',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await database.sync();
|
||||||
|
await database.close();
|
||||||
|
});
|
||||||
|
});
|
@ -16,6 +16,28 @@ import {
|
|||||||
import Database from './database';
|
import Database from './database';
|
||||||
import { Model, ModelCtor } from './model';
|
import { Model, ModelCtor } from './model';
|
||||||
|
|
||||||
|
const registeredModels = new Map<string, any>();
|
||||||
|
|
||||||
|
export function registerModel(key: string, model: any) {
|
||||||
|
registeredModels.set(key, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerModels(models) {
|
||||||
|
for (const key in models) {
|
||||||
|
if (models.hasOwnProperty(key)) {
|
||||||
|
registerModel(key, models[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 判断如果 key 是 model 直接返回
|
||||||
|
export function getRegisteredModel(key) {
|
||||||
|
if (typeof key === 'string') {
|
||||||
|
return registeredModels.get(key);
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TableOptions extends Omit<ModelOptions<Model>, 'name'|'modelName'> {
|
export interface TableOptions extends Omit<ModelOptions<Model>, 'name'|'modelName'> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,7 +54,7 @@ export interface TableOptions extends Omit<ModelOptions<Model>, 'name'|'modelNam
|
|||||||
/**
|
/**
|
||||||
* 自定义 model
|
* 自定义 model
|
||||||
*/
|
*/
|
||||||
model?: ModelCtor<Model>;
|
model?: ModelCtor<Model> | string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字段配置
|
* 字段配置
|
||||||
@ -95,6 +117,8 @@ export class Table {
|
|||||||
|
|
||||||
protected Model: ModelCtor<Model>;
|
protected Model: ModelCtor<Model>;
|
||||||
|
|
||||||
|
protected defaultModel: ModelCtor<Model>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否是中间表
|
* 是否是中间表
|
||||||
*/
|
*/
|
||||||
@ -108,6 +132,7 @@ export class Table {
|
|||||||
name,
|
name,
|
||||||
fields = [],
|
fields = [],
|
||||||
indexes = [],
|
indexes = [],
|
||||||
|
model,
|
||||||
...restOptions
|
...restOptions
|
||||||
} = options;
|
} = options;
|
||||||
this.options = options;
|
this.options = options;
|
||||||
@ -118,6 +143,8 @@ export class Table {
|
|||||||
sequelize: database.sequelize,
|
sequelize: database.sequelize,
|
||||||
...restOptions,
|
...restOptions,
|
||||||
};
|
};
|
||||||
|
// 初始化的时候获取
|
||||||
|
this.defaultModel = getRegisteredModel(model);
|
||||||
this.modelAttributes = {};
|
this.modelAttributes = {};
|
||||||
// 在 set fields 之前 model init 的原因是因为关系字段可能需要用到 model 的相关配置
|
// 在 set fields 之前 model init 的原因是因为关系字段可能需要用到 model 的相关配置
|
||||||
this.addIndexes(indexes, 'modelOnly');
|
this.addIndexes(indexes, 'modelOnly');
|
||||||
@ -127,7 +154,7 @@ export class Table {
|
|||||||
|
|
||||||
public modelInit(reinitialize: Reinitialize = false) {
|
public modelInit(reinitialize: Reinitialize = false) {
|
||||||
if (reinitialize || !this.Model) {
|
if (reinitialize || !this.Model) {
|
||||||
this.Model = this.options.model || class extends Model {};
|
this.Model = this.defaultModel || class extends Model {};
|
||||||
this.Model.database = this.database;
|
this.Model.database = this.database;
|
||||||
// 关系的建立是在 model.init 之后,在配置中表字段(Column)和关系(Relation)都在 fields,
|
// 关系的建立是在 model.init 之后,在配置中表字段(Column)和关系(Relation)都在 fields,
|
||||||
// 所以需要单独提炼出 associations 字段,并在 Model.init 之后执行 Model.associate
|
// 所以需要单独提炼出 associations 字段,并在 Model.init 之后执行 Model.associate
|
||||||
@ -250,7 +277,17 @@ export class Table {
|
|||||||
sourceTable: this,
|
sourceTable: this,
|
||||||
database: this.database,
|
database: this.database,
|
||||||
});
|
});
|
||||||
|
// 添加字段后 table.options 中的 fields 并不会更新,这导致 table.getOptions() 拿不到最新的字段配置
|
||||||
|
// 所以在同时更新 table.options.fields 数组
|
||||||
|
const existIndex = this.options.fields.findIndex(field => field.name === name);
|
||||||
|
if (existIndex !== -1) {
|
||||||
|
this.options.fields.splice(existIndex, 1, options);
|
||||||
|
} else {
|
||||||
|
this.options.fields.push(options);
|
||||||
|
}
|
||||||
|
|
||||||
this.fields.set(name, field);
|
this.fields.set(name, field);
|
||||||
|
|
||||||
if (field instanceof Relation) {
|
if (field instanceof Relation) {
|
||||||
// 关系字段先放到 associating 里待处理,等相关 target model 初始化之后,再通过 associate 建立关系
|
// 关系字段先放到 associating 里待处理,等相关 target model 初始化之后,再通过 associate 建立关系
|
||||||
this.associating.set(name, field);
|
this.associating.set(name, field);
|
||||||
|
@ -4,8 +4,11 @@
|
|||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nocobase/server": "^0.3.0-alpha.0",
|
|
||||||
"@nocobase/database": "^0.3.0-alpha.0",
|
"@nocobase/database": "^0.3.0-alpha.0",
|
||||||
"@nocobase/resourcer": "^0.3.0-alpha.0"
|
"@nocobase/resourcer": "^0.3.0-alpha.0",
|
||||||
|
"@nocobase/server": "^0.3.0-alpha.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nocobase/actions": "^0.3.0-alpha.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
146
packages/plugin-collections/src/__tests__/base-model.test.ts
Normal file
146
packages/plugin-collections/src/__tests__/base-model.test.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import Database, { ModelCtor } from '@nocobase/database';
|
||||||
|
import { getDatabase } from '.';
|
||||||
|
import BaseModel from '../models/base';
|
||||||
|
|
||||||
|
describe('base model', () => {
|
||||||
|
let database: Database;
|
||||||
|
let TestModel: ModelCtor<BaseModel>;
|
||||||
|
let test: BaseModel;
|
||||||
|
beforeEach(async () => {
|
||||||
|
database = getDatabase();
|
||||||
|
database.table({
|
||||||
|
name: 'tests',
|
||||||
|
model: BaseModel,
|
||||||
|
additionalAttribute: 'options',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'json',
|
||||||
|
name: 'component',
|
||||||
|
defaultValue: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'json',
|
||||||
|
name: 'options',
|
||||||
|
defaultValue: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await database.sync();
|
||||||
|
TestModel = database.getModel('tests') as ModelCtor<BaseModel>;
|
||||||
|
test = await TestModel.create({
|
||||||
|
name: '123',
|
||||||
|
abc: {aa: 'aa'},
|
||||||
|
'abc.bb': 'bb',
|
||||||
|
component: {
|
||||||
|
a: 'a',
|
||||||
|
},
|
||||||
|
'component.b': 'b',
|
||||||
|
options: {
|
||||||
|
bcd: 'bbb',
|
||||||
|
},
|
||||||
|
arr: [{a: 'a'}, {b: 'b'}],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => database.close());
|
||||||
|
|
||||||
|
it('get all attribute', async () => {
|
||||||
|
// 获取所有字段
|
||||||
|
expect(test.get()).toMatchObject({
|
||||||
|
abc: { aa: 'aa', bb: 'bb' },
|
||||||
|
bcd: 'bbb',
|
||||||
|
name: '123',
|
||||||
|
component: { a: 'a', b: 'b' },
|
||||||
|
arr: [{a: 'a'}, {b: 'b'}],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('get options attribute', async () => {
|
||||||
|
// 直接取 options 字段
|
||||||
|
expect(test.get('options')).toEqual({
|
||||||
|
abc: {
|
||||||
|
aa: 'aa',
|
||||||
|
bb: 'bb',
|
||||||
|
},
|
||||||
|
bcd: 'bbb',
|
||||||
|
arr: [{a: 'a'}, {b: 'b'}],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('get component attribute', async () => {
|
||||||
|
expect(test.get('component')).toEqual({ a: 'a', b: 'b' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('set component attribute with dot key', async () => {
|
||||||
|
test.set('component.c', 'c');
|
||||||
|
await test.save();
|
||||||
|
expect(test.get()).toMatchObject({
|
||||||
|
abc: { aa: 'aa', bb: 'bb' },
|
||||||
|
bcd: 'bbb',
|
||||||
|
name: '123',
|
||||||
|
component: { a: 'a', b: 'b' },
|
||||||
|
arr: [{a: 'a'}, {b: 'b'}],
|
||||||
|
});
|
||||||
|
expect(test.get('component')).toEqual({ a: 'a', b: 'b', c: 'c' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('set options attribute with dot key', async () => {
|
||||||
|
test.set('options.cccc', 'cccc');
|
||||||
|
await test.save();
|
||||||
|
expect(test.get()).toMatchObject({
|
||||||
|
abc: { aa: 'aa', bb: 'bb' },
|
||||||
|
bcd: 'bbb',
|
||||||
|
name: '123',
|
||||||
|
cccc: 'cccc',
|
||||||
|
component: { a: 'a', b: 'b' },
|
||||||
|
arr: [{a: 'a'}, {b: 'b'}],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('set options attribute without options prefix', async () => {
|
||||||
|
test.set('dddd', 'dddd');
|
||||||
|
await test.save();
|
||||||
|
expect(test.get()).toMatchObject({
|
||||||
|
abc: { aa: 'aa', bb: 'bb' },
|
||||||
|
bcd: 'bbb',
|
||||||
|
name: '123',
|
||||||
|
dddd: 'dddd',
|
||||||
|
component: { a: 'a', b: 'b' },
|
||||||
|
arr: [{a: 'a'}, {b: 'b'}],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refind', async () => {
|
||||||
|
test.set('component.c', 'c');
|
||||||
|
await test.save();
|
||||||
|
// 重新查询
|
||||||
|
const test2 = await TestModel.findByPk(test.id);
|
||||||
|
expect(test2.get()).toMatchObject({
|
||||||
|
abc: { aa: 'aa', bb: 'bb' },
|
||||||
|
bcd: 'bbb',
|
||||||
|
name: '123',
|
||||||
|
component: { a: 'a', b: 'b', c: 'c' },
|
||||||
|
arr: [{a: 'a'}, {b: 'b'}],
|
||||||
|
});
|
||||||
|
expect(test2.get('component')).toEqual({ a: 'a', b: 'b', c: 'c' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update', async () => {
|
||||||
|
await test.update({
|
||||||
|
'name123': 'xxx',
|
||||||
|
'component.d': 'd',
|
||||||
|
});
|
||||||
|
expect(test.get()).toMatchObject({
|
||||||
|
abc: { aa: 'aa', bb: 'bb' },
|
||||||
|
bcd: 'bbb',
|
||||||
|
name: '123',
|
||||||
|
name123: 'xxx',
|
||||||
|
component: { a: 'a', b: 'b', d: 'd' },
|
||||||
|
arr: [{a: 'a'}, {b: 'b'}],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
159
packages/plugin-collections/src/__tests__/collections.test.ts
Normal file
159
packages/plugin-collections/src/__tests__/collections.test.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import { Agent, getAgent, getApp } from '.';
|
||||||
|
import { Application } from '@nocobase/server';
|
||||||
|
import * as types from '../interfaces/types';
|
||||||
|
|
||||||
|
describe('collection hooks', () => {
|
||||||
|
let app: Application;
|
||||||
|
let agent: Agent;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = await getApp();
|
||||||
|
agent = getAgent(app);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => app.database.close());
|
||||||
|
|
||||||
|
it('create table', async () => {
|
||||||
|
const response = await agent.resource('collections').create({
|
||||||
|
values: {
|
||||||
|
name: 'tests',
|
||||||
|
title: 'tests',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const table = app.database.getTable('tests');
|
||||||
|
expect(table).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create table without name', async () => {
|
||||||
|
const created = await agent.resource('collections').create({
|
||||||
|
values: {
|
||||||
|
title: 'tests',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { name } = created.body;
|
||||||
|
const table = app.database.getTable(name);
|
||||||
|
expect(table).toBeDefined();
|
||||||
|
expect(table.getOptions().title).toBe('tests');
|
||||||
|
|
||||||
|
const list = await agent.resource('collections').list();
|
||||||
|
expect(list.body.rows.length).toBe(1);
|
||||||
|
|
||||||
|
await table.getModel().drop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('list fields', async () => {
|
||||||
|
const response = await agent.resource('collections.fields').list({
|
||||||
|
associatedKey: 'tests',
|
||||||
|
// values: {
|
||||||
|
// type: 'string',
|
||||||
|
// name: 'title',
|
||||||
|
// title: '标题',
|
||||||
|
// },
|
||||||
|
});
|
||||||
|
// console.log(response.body);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create field', async () => {
|
||||||
|
await agent.resource('collections').create({
|
||||||
|
values: {
|
||||||
|
name: 'tests',
|
||||||
|
title: 'tests',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await agent.resource('collections.fields').create({
|
||||||
|
associatedKey: 'tests',
|
||||||
|
values: {
|
||||||
|
type: 'string',
|
||||||
|
name: 'name',
|
||||||
|
options: {
|
||||||
|
type: 'string',
|
||||||
|
name: 'name',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const table = app.database.getTable('tests');
|
||||||
|
expect(table.getField('name')).toBeDefined();
|
||||||
|
|
||||||
|
const { body } = await agent.resource('tests').create({
|
||||||
|
values: { name: 'a' }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(body.name).toBe('a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create field without name', async () => {
|
||||||
|
await agent.resource('collections').create({
|
||||||
|
values: {
|
||||||
|
name: 'tests',
|
||||||
|
title: 'tests',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createdField = await agent.resource('collections.fields').create({
|
||||||
|
associatedKey: 'tests',
|
||||||
|
values: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { name: createdFieldName } = createdField.body;
|
||||||
|
|
||||||
|
const table = app.database.getTable('tests');
|
||||||
|
expect(table.getField(createdFieldName)).toBeDefined();
|
||||||
|
|
||||||
|
const createdRow = await agent.resource('tests').create({
|
||||||
|
values: { [createdFieldName]: 'a' }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createdRow.body[createdFieldName]).toBe('a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create string field by interface', async () => {
|
||||||
|
await agent.resource('collections').create({
|
||||||
|
values: {
|
||||||
|
name: 'tests',
|
||||||
|
title: 'tests',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const values = {
|
||||||
|
interface: 'string',
|
||||||
|
title: '名称',
|
||||||
|
name: 'name',
|
||||||
|
required: true,
|
||||||
|
viewable: true,
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
'component.tooltip': 'test'
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdField = await agent.resource('collections.fields').create({
|
||||||
|
associatedKey: 'tests',
|
||||||
|
values,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createdField.body).toMatchObject({
|
||||||
|
...{
|
||||||
|
interface: 'string',
|
||||||
|
title: '名称',
|
||||||
|
name: 'name',
|
||||||
|
required: true,
|
||||||
|
viewable: true,
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
},
|
||||||
|
...types['string'].options,
|
||||||
|
sort: 1,
|
||||||
|
collection_name: 'tests',
|
||||||
|
});
|
||||||
|
|
||||||
|
const gotField = await agent.resource('fields').get({
|
||||||
|
resourceKey: createdField.body.id
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(gotField.body).toEqual(createdField.body);
|
||||||
|
});
|
||||||
|
});
|
144
packages/plugin-collections/src/__tests__/index.ts
Normal file
144
packages/plugin-collections/src/__tests__/index.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import qs from 'qs';
|
||||||
|
import plugin from '../server';
|
||||||
|
import supertest from 'supertest';
|
||||||
|
import bodyParser from 'koa-bodyparser';
|
||||||
|
import { Dialect } from 'sequelize';
|
||||||
|
import Database from '@nocobase/database';
|
||||||
|
import { actions, middlewares } from '@nocobase/actions';
|
||||||
|
import { Application, middleware } from '@nocobase/server';
|
||||||
|
|
||||||
|
function getTestKey() {
|
||||||
|
const { id } = require.main;
|
||||||
|
const key = id
|
||||||
|
.replace(`${process.env.PWD}/packages`, '')
|
||||||
|
.replace('.test.ts', '')
|
||||||
|
.replace(/[^\w]/g, '_')
|
||||||
|
.replace(/_+/g, '_');
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
username: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_DATABASE,
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: Number.parseInt(process.env.DB_PORT, 10),
|
||||||
|
dialect: process.env.DB_DIALECT as Dialect,
|
||||||
|
define: {
|
||||||
|
hooks: {
|
||||||
|
beforeCreate(model, options) {
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logging: false,
|
||||||
|
sync: {
|
||||||
|
force: true,
|
||||||
|
alter: {
|
||||||
|
drop: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getApp() {
|
||||||
|
const app = new Application({
|
||||||
|
database: {
|
||||||
|
...config,
|
||||||
|
hooks: {
|
||||||
|
beforeDefine(columns, model) {
|
||||||
|
model.tableName = `${getTestKey()}_${model.tableName || model.name.plural}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resourcer: {
|
||||||
|
prefix: '/api',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
app.resourcer.use(middlewares.associated);
|
||||||
|
app.resourcer.registerHandlers({...actions.associate, ...actions.common});
|
||||||
|
await app.plugins([plugin]);
|
||||||
|
await app.database.sync({
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
// 表配置信息存到数据库里
|
||||||
|
// const tables = app.database.getTables([]);
|
||||||
|
// for (const table of tables) {
|
||||||
|
// const Collection = app.database.getModel('collections');
|
||||||
|
// await Collection.import(table.getOptions(), { hooks: false });
|
||||||
|
// }
|
||||||
|
app.use(async (ctx, next) => {
|
||||||
|
ctx.db = app.database;
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
app.use(bodyParser());
|
||||||
|
app.use(middleware({
|
||||||
|
prefix: '/api',
|
||||||
|
resourcer: app.resourcer,
|
||||||
|
database: app.database,
|
||||||
|
}));
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionParams {
|
||||||
|
resourceKey?: string | number;
|
||||||
|
// resourceName?: string;
|
||||||
|
// associatedName?: string;
|
||||||
|
associatedKey?: string | number;
|
||||||
|
fields?: any;
|
||||||
|
filter?: any;
|
||||||
|
values?: any;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Handler {
|
||||||
|
get: (params?: ActionParams) => Promise<supertest.Response>;
|
||||||
|
list: (params?: ActionParams) => Promise<supertest.Response>;
|
||||||
|
create: (params?: ActionParams) => Promise<supertest.Response>;
|
||||||
|
update: (params?: ActionParams) => Promise<supertest.Response>;
|
||||||
|
destroy: (params?: ActionParams) => Promise<supertest.Response>;
|
||||||
|
[name: string]: (params?: ActionParams) => Promise<supertest.Response>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Agent {
|
||||||
|
resource: (name: string) => Handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAgent(app: Application): Agent {
|
||||||
|
const agent = supertest.agent(app.callback());
|
||||||
|
return {
|
||||||
|
resource(name: string): any {
|
||||||
|
return new Proxy({}, {
|
||||||
|
get(target, method, receiver) {
|
||||||
|
return (params: ActionParams = {}) => {
|
||||||
|
const { associatedKey, resourceKey, values = {}, ...restParams } = params;
|
||||||
|
let url = `/api/${name}`;
|
||||||
|
if (associatedKey) {
|
||||||
|
url = `/api/${name.split('.').join(`/${associatedKey}/`)}`;
|
||||||
|
}
|
||||||
|
url += `:${method as string}`;
|
||||||
|
if (resourceKey) {
|
||||||
|
url += `/${resourceKey}`;
|
||||||
|
}
|
||||||
|
console.log(url);
|
||||||
|
if (['list', 'get'].indexOf(method as string) !== -1) {
|
||||||
|
return agent.get(`${url}?${qs.stringify(restParams)}`);
|
||||||
|
} else {
|
||||||
|
return agent.post(`${url}?${qs.stringify(restParams)}`).send(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDatabase() {
|
||||||
|
return new Database({
|
||||||
|
...config,
|
||||||
|
hooks: {
|
||||||
|
beforeDefine(columns, model) {
|
||||||
|
model.tableName = `${getTestKey()}_${model.tableName || model.name.plural}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -3,8 +3,11 @@ import { TableOptions } from '@nocobase/database';
|
|||||||
export default {
|
export default {
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
title: '操作配置',
|
title: '操作配置',
|
||||||
|
draggable: true,
|
||||||
|
model: 'ActionModel',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
interface: 'sort',
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
name: 'sort',
|
name: 'sort',
|
||||||
title: '排序',
|
title: '排序',
|
||||||
@ -12,36 +15,66 @@ export default {
|
|||||||
type: 'sort',
|
type: 'sort',
|
||||||
className: 'drag-visible',
|
className: 'drag-visible',
|
||||||
width: 60,
|
width: 60,
|
||||||
|
showInTable: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'string',
|
||||||
|
type: 'string',
|
||||||
|
name: 'title',
|
||||||
|
title: '名称',
|
||||||
|
component: {
|
||||||
|
type: 'string',
|
||||||
|
className: 'drag-visible',
|
||||||
|
showInForm: true,
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'string',
|
||||||
|
type: 'string',
|
||||||
|
name: 'name',
|
||||||
|
title: '标识',
|
||||||
|
component: {
|
||||||
|
type: 'string',
|
||||||
|
showInForm: true,
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'string',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'type',
|
name: 'type',
|
||||||
title: '类型',
|
title: '类型',
|
||||||
component: {
|
component: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
className: 'drag-visible',
|
showInForm: true,
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'string',
|
interface: 'linkTo',
|
||||||
name: 'name',
|
|
||||||
title: '标识',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'string',
|
|
||||||
name: 'title',
|
|
||||||
title: '名称',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'json',
|
|
||||||
name: 'options',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
name: 'collection',
|
name: 'collection',
|
||||||
|
title: '所属数据表',
|
||||||
target: 'collections',
|
target: 'collections',
|
||||||
targetKey: 'name',
|
targetKey: 'name',
|
||||||
|
component: {
|
||||||
|
type: 'drawerSelect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'json',
|
||||||
|
type: 'json',
|
||||||
|
name: 'options',
|
||||||
|
title: '配置信息',
|
||||||
|
defaultValue: {},
|
||||||
|
component: {
|
||||||
|
type: 'hidden',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { TableOptions } from '@nocobase/database';
|
import { TableOptions } from '@nocobase/database';
|
||||||
import CollectionModel from '../models/collection';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'collections',
|
name: 'collections',
|
||||||
title: '数据表配置',
|
title: '数据表配置',
|
||||||
model: CollectionModel,
|
sortable: true,
|
||||||
|
draggable: true,
|
||||||
|
model: 'CollectionModel',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
interface: 'sort',
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
name: 'sort',
|
name: 'sort',
|
||||||
title: '排序',
|
title: '排序',
|
||||||
@ -14,20 +16,25 @@ export default {
|
|||||||
type: 'sort',
|
type: 'sort',
|
||||||
className: 'drag-visible',
|
className: 'drag-visible',
|
||||||
width: 60,
|
width: 60,
|
||||||
|
showInTable: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'string',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'title',
|
name: 'title',
|
||||||
title: '名称',
|
title: '数据表名称',
|
||||||
showInTable: true,
|
|
||||||
required: true,
|
required: true,
|
||||||
component: {
|
component: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
className: 'drag-visible',
|
className: 'drag-visible',
|
||||||
|
showInTable: true,
|
||||||
|
showInForm: true,
|
||||||
|
showInDetail: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'string',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
title: '标识',
|
title: '标识',
|
||||||
@ -35,65 +42,175 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
component: {
|
component: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-rules': [
|
showInTable: true,
|
||||||
{
|
showInForm: true,
|
||||||
format: 'slug',
|
showInDetail: true,
|
||||||
message: '只允许英文数字和下划线',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'string',
|
interface: 'string',
|
||||||
name: 'description',
|
type: 'virtual',
|
||||||
title: '描述',
|
name: 'options.icon',
|
||||||
|
title: '图标',
|
||||||
component: {
|
component: {
|
||||||
type: 'textarea',
|
type: 'string',
|
||||||
|
showInTable: true,
|
||||||
|
showInForm: true,
|
||||||
|
showInDetail: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'radio',
|
||||||
|
type: 'virtual',
|
||||||
|
name: 'options.defaultView',
|
||||||
|
title: '默认视图',
|
||||||
|
defaultValue: 'table',
|
||||||
|
dataSource: [
|
||||||
|
{label: '表格', value: 'table'},
|
||||||
|
{label: '看板', value: 'kanban', disabled: true},
|
||||||
|
{label: '日历', value: 'calendar', disabled: true},
|
||||||
|
],
|
||||||
|
component: {
|
||||||
|
type: 'radio',
|
||||||
|
showInTable: true,
|
||||||
|
showInForm: true,
|
||||||
|
showInDetail: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'radio',
|
||||||
|
type: 'virtual',
|
||||||
|
name: 'options.mode',
|
||||||
|
title: '表格模式',
|
||||||
|
defaultValue: 'default',
|
||||||
|
dataSource: [
|
||||||
|
{label: '常规模式', value: 'default'},
|
||||||
|
{label: '简易模式', value: 'simple'},
|
||||||
|
],
|
||||||
|
component: {
|
||||||
|
type: 'radio',
|
||||||
|
tooltip: `
|
||||||
|
<p>常规模式:点击数据进入详情页进行各项查看和操作;<br/>简易模式:点击数据直接打开编辑表单</p>
|
||||||
|
`,
|
||||||
|
showInForm: true,
|
||||||
|
showInDetail: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'radio',
|
||||||
|
type: 'virtual',
|
||||||
|
name: 'options.defaultPerPage',
|
||||||
|
title: '每页显示几行数据',
|
||||||
|
defaultValue: 50,
|
||||||
|
dataSource: [
|
||||||
|
{label: '20', value: 20},
|
||||||
|
{label: '50', value: 50},
|
||||||
|
{label: '100', value: 100},
|
||||||
|
],
|
||||||
|
component: {
|
||||||
|
type: 'radio',
|
||||||
|
showInForm: true,
|
||||||
|
showInDetail: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'boolean',
|
||||||
|
type: 'virtual',
|
||||||
|
name: 'options.draggable',
|
||||||
|
title: '支持拖拽数据排序',
|
||||||
|
showInForm: true,
|
||||||
|
showInDetail: true,
|
||||||
|
component: {
|
||||||
|
type: 'checkbox',
|
||||||
|
showInForm: true,
|
||||||
|
showInDetail: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'boolean',
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'showInDataMenu',
|
||||||
|
title: '显示在“数据”菜单里',
|
||||||
|
component: {
|
||||||
|
type: 'checkbox',
|
||||||
|
showInTable: true,
|
||||||
|
showInForm: true,
|
||||||
|
showInDetail: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'json',
|
||||||
type: 'json',
|
type: 'json',
|
||||||
name: 'options',
|
name: 'options',
|
||||||
|
title: '配置信息',
|
||||||
|
defaultValue: {},
|
||||||
|
component: {
|
||||||
|
type: 'hidden',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'linkTo',
|
||||||
type: 'hasMany',
|
type: 'hasMany',
|
||||||
name: 'fields',
|
name: 'fields',
|
||||||
|
title: '字段',
|
||||||
sourceKey: 'name',
|
sourceKey: 'name',
|
||||||
|
draggable: true,
|
||||||
actions: {
|
actions: {
|
||||||
list: {
|
list: {
|
||||||
sort: 'sort',
|
sort: 'sort',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
component: {
|
||||||
|
type: 'drawerSelect',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'linkTo',
|
||||||
type: 'hasMany',
|
type: 'hasMany',
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
|
title: '动作',
|
||||||
sourceKey: 'name',
|
sourceKey: 'name',
|
||||||
|
draggable: true,
|
||||||
actions: {
|
actions: {
|
||||||
list: {
|
list: {
|
||||||
sort: 'sort',
|
sort: 'sort',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
component: {
|
||||||
|
type: 'drawerSelect',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'linkTo',
|
||||||
type: 'hasMany',
|
type: 'hasMany',
|
||||||
name: 'tabs',
|
name: 'tabs',
|
||||||
|
title: '标签页',
|
||||||
sourceKey: 'name',
|
sourceKey: 'name',
|
||||||
|
draggable: true,
|
||||||
actions: {
|
actions: {
|
||||||
list: {
|
list: {
|
||||||
sort: 'sort',
|
sort: 'sort',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
component: {
|
||||||
|
type: 'drawerSelect',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'linkTo',
|
||||||
type: 'hasMany',
|
type: 'hasMany',
|
||||||
name: 'views',
|
name: 'views',
|
||||||
|
title: '视图',
|
||||||
sourceKey: 'name',
|
sourceKey: 'name',
|
||||||
|
draggable: true,
|
||||||
actions: {
|
actions: {
|
||||||
list: {
|
list: {
|
||||||
sort: 'sort',
|
sort: 'sort',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
component: {
|
||||||
|
type: 'drawerSelect',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
|
@ -1,58 +1,230 @@
|
|||||||
import { TableOptions } from '@nocobase/database';
|
import { TableOptions } from '@nocobase/database';
|
||||||
|
import { options } from '../interfaces';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'fields',
|
name: 'fields',
|
||||||
title: '字段配置',
|
title: '字段配置',
|
||||||
|
draggable: true,
|
||||||
|
model: 'FieldModel',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
interface: 'sort',
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
name: 'sort',
|
name: 'sort',
|
||||||
title: '排序',
|
title: '排序',
|
||||||
|
defaultValue: 1,
|
||||||
component: {
|
component: {
|
||||||
type: 'sort',
|
type: 'sort',
|
||||||
className: 'drag-visible',
|
className: 'drag-visible',
|
||||||
width: 60,
|
width: 60,
|
||||||
|
showInTable: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'string',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'type',
|
name: 'title',
|
||||||
title: '类型',
|
title: '字段名称',
|
||||||
component: {
|
component: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
className: 'drag-visible',
|
className: 'drag-visible',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'string',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
title: '标识',
|
title: '标识',
|
||||||
|
required: true,
|
||||||
|
component: {
|
||||||
|
type: 'string',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'select',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'title',
|
name: 'interface',
|
||||||
title: '名称',
|
title: '字段类型',
|
||||||
|
dataSource: options,
|
||||||
|
component: {
|
||||||
|
type: 'select',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'subTable',
|
||||||
|
type: 'virtual',
|
||||||
|
name: 'options.dataSource',
|
||||||
|
title: '可选项',
|
||||||
|
component: {
|
||||||
|
type: 'table',
|
||||||
|
// showInTable: true,
|
||||||
|
// showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
value: {
|
||||||
|
type: "string",
|
||||||
|
title: "值",
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: "string",
|
||||||
|
title: "选项",
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'string',
|
||||||
|
type: 'string',
|
||||||
|
name: 'type',
|
||||||
|
title: '数据类型',
|
||||||
|
component: {
|
||||||
|
type: 'string',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'number',
|
||||||
|
type: 'integer',
|
||||||
|
name: 'parent_id',
|
||||||
|
title: '所属分组',
|
||||||
|
component: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'linkTo',
|
||||||
|
multiple: false,
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'parent',
|
||||||
|
title: '所属分组',
|
||||||
|
target: 'fields',
|
||||||
|
foreignKey: 'parent_id',
|
||||||
|
targetKey: 'id',
|
||||||
|
component: {
|
||||||
|
type: 'drawerSelect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'linkTo',
|
||||||
|
multiple: true,
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'children',
|
||||||
|
title: '子字段',
|
||||||
|
target: 'fields',
|
||||||
|
foreignKey: 'parent_id',
|
||||||
|
sourceKey: 'id',
|
||||||
|
component: {
|
||||||
|
type: 'drawerSelect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'string',
|
||||||
|
type: 'virtual',
|
||||||
|
name: 'component.tooltip',
|
||||||
|
title: '提示信息',
|
||||||
|
component: {
|
||||||
|
type: 'string',
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'boolean',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
name: 'showInListAction',
|
name: 'required',
|
||||||
title: '显示在表格里',
|
title: '必填项',
|
||||||
|
component: {
|
||||||
|
type: 'checkbox',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'boolean',
|
interface: 'boolean',
|
||||||
name: 'showInGetAction',
|
type: 'virtual',
|
||||||
title: '显示在详情里',
|
name: 'component.showInTable',
|
||||||
|
title: '显示在表格中',
|
||||||
|
component: {
|
||||||
|
type: 'checkbox',
|
||||||
|
tooltip: '若勾选,该字段将作为一列显示在数据表里',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'json',
|
interface: 'boolean',
|
||||||
name: 'options',
|
type: 'virtual',
|
||||||
|
name: 'component.showInForm',
|
||||||
|
title: '显示在表单中',
|
||||||
|
component: {
|
||||||
|
type: 'checkbox',
|
||||||
|
tooltip: '若勾选,该字段将出现在表单中',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'boolean',
|
||||||
|
type: 'virtual',
|
||||||
|
name: 'component.showInDetail',
|
||||||
|
title: '显示在详情中',
|
||||||
|
component: {
|
||||||
|
type: 'checkbox',
|
||||||
|
tooltip: '若勾选,该字段将出现在详情中',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'linkTo',
|
||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
name: 'collection',
|
name: 'collection',
|
||||||
title: '所属数据表',
|
title: '所属数据表',
|
||||||
target: 'collections',
|
target: 'collections',
|
||||||
targetKey: 'name',
|
targetKey: 'name',
|
||||||
|
component: {
|
||||||
|
type: 'drawerSelect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'json',
|
||||||
|
type: 'json',
|
||||||
|
name: 'component',
|
||||||
|
title: '前端组件',
|
||||||
|
defaultValue: {},
|
||||||
|
component: {
|
||||||
|
type: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'json',
|
||||||
|
type: 'json',
|
||||||
|
name: 'options',
|
||||||
|
title: '配置信息',
|
||||||
|
defaultValue: {},
|
||||||
|
component: {
|
||||||
|
type: 'hidden',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
|
@ -3,8 +3,11 @@ import { TableOptions } from '@nocobase/database';
|
|||||||
export default {
|
export default {
|
||||||
name: 'tabs',
|
name: 'tabs',
|
||||||
title: '标签配置',
|
title: '标签配置',
|
||||||
|
sortable: true,
|
||||||
|
model: 'TabModel',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
interface: 'sort',
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
name: 'sort',
|
name: 'sort',
|
||||||
title: '排序',
|
title: '排序',
|
||||||
@ -12,42 +15,108 @@ export default {
|
|||||||
type: 'sort',
|
type: 'sort',
|
||||||
className: 'drag-visible',
|
className: 'drag-visible',
|
||||||
width: 60,
|
width: 60,
|
||||||
|
showInTable: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'string',
|
interface: 'string',
|
||||||
name: 'type',
|
|
||||||
title: '类型',
|
|
||||||
component: {
|
|
||||||
type: 'string',
|
|
||||||
className: 'drag-visible',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'string',
|
|
||||||
name: 'name',
|
|
||||||
title: '标识',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'title',
|
name: 'title',
|
||||||
title: '名称',
|
title: '名称',
|
||||||
|
component: {
|
||||||
|
type: 'string',
|
||||||
|
className: 'drag-visible',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'string',
|
||||||
|
type: 'string',
|
||||||
|
name: 'name',
|
||||||
|
title: '标识',
|
||||||
|
component: {
|
||||||
|
type: 'string',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'radio',
|
||||||
|
type: 'string',
|
||||||
|
name: 'type',
|
||||||
|
title: '类型',
|
||||||
|
dataSource: [
|
||||||
|
{ label: '详情数据', value: 'details' },
|
||||||
|
{ label: '相关数据', value: 'association' },
|
||||||
|
{ label: '模块组合', value: 'module' },
|
||||||
|
],
|
||||||
|
component: {
|
||||||
|
type: 'radio',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'string',
|
||||||
|
type: 'virtual',
|
||||||
|
name: 'options.association',
|
||||||
|
title: '相关数据表',
|
||||||
|
component: {
|
||||||
|
type: 'string',
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'boolean',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
name: 'default',
|
name: 'default',
|
||||||
title: '默认标签页',
|
title: '默认标签页',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
|
component: {
|
||||||
|
type: 'checkbox',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'json',
|
interface: 'boolean',
|
||||||
name: 'options',
|
type: 'boolean',
|
||||||
|
name: 'enabled',
|
||||||
|
title: '启动',
|
||||||
|
defaultValue: false,
|
||||||
|
component: {
|
||||||
|
type: 'checkbox',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'linkTo',
|
||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
name: 'collection',
|
name: 'collection',
|
||||||
|
title: '所属数据表',
|
||||||
target: 'collections',
|
target: 'collections',
|
||||||
targetKey: 'name',
|
targetKey: 'name',
|
||||||
|
component: {
|
||||||
|
type: 'drawerSelect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'json',
|
||||||
|
type: 'json',
|
||||||
|
name: 'options',
|
||||||
|
title: '配置信息',
|
||||||
|
defaultValue: {},
|
||||||
|
component: {
|
||||||
|
type: 'hidden',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
|
@ -3,8 +3,11 @@ import { TableOptions } from '@nocobase/database';
|
|||||||
export default {
|
export default {
|
||||||
name: 'views',
|
name: 'views',
|
||||||
title: '视图配置',
|
title: '视图配置',
|
||||||
|
sortable: true,
|
||||||
|
model: 'ViewModel',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
interface: 'sort',
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
name: 'sort',
|
name: 'sort',
|
||||||
title: '排序',
|
title: '排序',
|
||||||
@ -12,56 +15,126 @@ export default {
|
|||||||
type: 'sort',
|
type: 'sort',
|
||||||
className: 'drag-visible',
|
className: 'drag-visible',
|
||||||
width: 60,
|
width: 60,
|
||||||
|
showInTable: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'string',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'type',
|
name: 'title',
|
||||||
title: '类型',
|
title: '视图名称',
|
||||||
component: {
|
component: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
className: 'drag-visible',
|
className: 'drag-visible',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'string',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
title: '标识',
|
title: '标识',
|
||||||
|
component: {
|
||||||
|
type: 'string',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'radio',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'title',
|
name: 'type',
|
||||||
title: '名称',
|
title: '视图类型',
|
||||||
|
dataSource: [
|
||||||
|
{ label: '表格', value: 'table' },
|
||||||
|
{ label: '看板', value: 'kanban', disabled: true },
|
||||||
|
{ label: '日历', value: 'calendar', disabled: true },
|
||||||
|
{ label: '地图', value: 'map', disabled: true },
|
||||||
|
],
|
||||||
|
component: {
|
||||||
|
type: 'radio',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'string',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'template',
|
name: 'template',
|
||||||
title: '模板',
|
title: '模板',
|
||||||
|
component: {
|
||||||
|
type: 'string',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'boolean',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
name: 'default',
|
name: 'default',
|
||||||
title: '默认视图',
|
title: '默认视图',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
|
component: {
|
||||||
|
type: 'checkbox',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'json',
|
interface: 'boolean',
|
||||||
name: 'options',
|
type: 'boolean',
|
||||||
|
name: 'showInDataMenu',
|
||||||
|
title: '作为数据表子菜单',
|
||||||
|
defaultValue: false,
|
||||||
|
component: {
|
||||||
|
type: 'checkbox',
|
||||||
|
showInTable: true,
|
||||||
|
showInDetail: true,
|
||||||
|
showInForm: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'linkTo',
|
||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
name: 'collection',
|
name: 'collection',
|
||||||
|
title: '所属数据表',
|
||||||
target: 'collections',
|
target: 'collections',
|
||||||
targetKey: 'name',
|
targetKey: 'name',
|
||||||
|
component: {
|
||||||
|
type: 'drawerSelect',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'belongsToMany',
|
interface: 'json',
|
||||||
name: 'fields',
|
type: 'json',
|
||||||
},
|
name: 'options',
|
||||||
{
|
title: '配置信息',
|
||||||
type: 'belongsToMany',
|
defaultValue: {},
|
||||||
name: 'actions',
|
component: {
|
||||||
|
type: 'hidden',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
// 以下暂不考虑
|
||||||
|
// {
|
||||||
|
// type: 'belongsToMany',
|
||||||
|
// name: 'fields',
|
||||||
|
// component: {
|
||||||
|
// type: 'drawerSelect',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: 'belongsToMany',
|
||||||
|
// name: 'actions',
|
||||||
|
// component: {
|
||||||
|
// type: 'drawerSelect',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
import CollectionModel from '../models/collection';
|
||||||
|
|
||||||
|
export default async function (model: CollectionModel) {
|
||||||
|
await model.migrate();
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import CollectionModel from '../models/collection';
|
||||||
|
|
||||||
|
export default async function (model: CollectionModel) {
|
||||||
|
if (!model.get('name')) {
|
||||||
|
model.setDataValue('name', this.generateName());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
import FieldModel from '../models/field';
|
||||||
|
|
||||||
|
export default async function (model: FieldModel) {
|
||||||
|
// console.log('afterCreate', model.toJSON());
|
||||||
|
await model.migrate();
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import FieldModel from '../models/field';
|
||||||
|
import * as types from '../interfaces/types';
|
||||||
|
|
||||||
|
export default async function (model: FieldModel) {
|
||||||
|
const values = model.get();
|
||||||
|
if (!values.name) {
|
||||||
|
values.name = this.generateName();
|
||||||
|
}
|
||||||
|
if (values.interface) {
|
||||||
|
const { options } = types[values.interface];
|
||||||
|
Object.keys(options).forEach(key => {
|
||||||
|
switch (typeof values[key]) {
|
||||||
|
case 'undefined':
|
||||||
|
values[key] = options[key];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'object':
|
||||||
|
values[key] = {
|
||||||
|
...options[key],
|
||||||
|
...values[key]
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
model.set(values, { raw: true });
|
||||||
|
}
|
16
packages/plugin-collections/src/hooks/index.ts
Normal file
16
packages/plugin-collections/src/hooks/index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import collectionsBeforeValidate from './collections-before-validate';
|
||||||
|
import collectionsAfterCreate from './collections-after-create';
|
||||||
|
|
||||||
|
import fieldsBeforeValidate from './fields-before-validate';
|
||||||
|
import fieldsAfterCreate from './fields-after-create';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
collections: {
|
||||||
|
beforeValidate: collectionsBeforeValidate,
|
||||||
|
afterCreate: collectionsAfterCreate,
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
beforeValidate: fieldsBeforeValidate,
|
||||||
|
afterCreate: fieldsAfterCreate
|
||||||
|
}
|
||||||
|
};
|
76
packages/plugin-collections/src/interfaces/index.ts
Normal file
76
packages/plugin-collections/src/interfaces/index.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* 考虑到 Interface 的参数模板还不固定,暂时先放这里了,便于后续修改
|
||||||
|
*/
|
||||||
|
import * as types from './types';
|
||||||
|
export * as types from './types';
|
||||||
|
|
||||||
|
export const options = [
|
||||||
|
{
|
||||||
|
title: '基本类型',
|
||||||
|
children: [
|
||||||
|
types.string,
|
||||||
|
types.textarea,
|
||||||
|
types.phone,
|
||||||
|
types.email,
|
||||||
|
types.number,
|
||||||
|
types.percent,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '多媒体类型',
|
||||||
|
children: [
|
||||||
|
types.wysiwyg,
|
||||||
|
types.attachment,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '选择类型',
|
||||||
|
children: [
|
||||||
|
types.boolean,
|
||||||
|
types.select,
|
||||||
|
types.multipleSelect,
|
||||||
|
types.radio,
|
||||||
|
types.checkboxes,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '日期和时间',
|
||||||
|
children: [
|
||||||
|
types.datetime,
|
||||||
|
types.time,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '关系类型',
|
||||||
|
children: [
|
||||||
|
types.subTable,
|
||||||
|
types.linkTo,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '系统信息',
|
||||||
|
children: [
|
||||||
|
types.createdAt,
|
||||||
|
types.createdBy,
|
||||||
|
types.updatedAt,
|
||||||
|
types.updatedBy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '开发者模式',
|
||||||
|
children: [
|
||||||
|
types.primaryKey,
|
||||||
|
types.sort,
|
||||||
|
types.password,
|
||||||
|
types.json,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
].map(({title, children}) => ({
|
||||||
|
label: title,
|
||||||
|
children: children.map(child => ({
|
||||||
|
label: child.title,
|
||||||
|
value: child.options.interface,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default options;
|
460
packages/plugin-collections/src/interfaces/types.ts
Normal file
460
packages/plugin-collections/src/interfaces/types.ts
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
// merge:interface 模板,旧数据,用户数据
|
||||||
|
// TODO: 删除的情况怎么处理
|
||||||
|
// 联动的原则:尽量减少干预,尤其是尽量少改动 type,type 兼容
|
||||||
|
// 参数的优先级:
|
||||||
|
// 1、interface,type 尽量只随 interface 变动,而不受别的字段影响(特殊情况除外)
|
||||||
|
// 2、
|
||||||
|
// TODO: interface 的修改
|
||||||
|
export const string = {
|
||||||
|
title: '单行文本',
|
||||||
|
options: {
|
||||||
|
interface: 'string',
|
||||||
|
type: 'string',
|
||||||
|
component: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const textarea = {
|
||||||
|
title: '多行文本',
|
||||||
|
options: {
|
||||||
|
interface: 'textarea',
|
||||||
|
type: 'text',
|
||||||
|
filterable: true,
|
||||||
|
component: {
|
||||||
|
type: 'textarea',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const phone = {
|
||||||
|
title: '手机号码',
|
||||||
|
options: {
|
||||||
|
interface: 'phone',
|
||||||
|
type: 'string',
|
||||||
|
filterable: true,
|
||||||
|
format: 'phone', // 验证的问题
|
||||||
|
component: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const email = {
|
||||||
|
title: '邮箱',
|
||||||
|
options: {
|
||||||
|
interface: 'email',
|
||||||
|
type: 'string',
|
||||||
|
filterable: true,
|
||||||
|
format: 'email',
|
||||||
|
component: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 precision 控制精确度
|
||||||
|
*/
|
||||||
|
export const number = {
|
||||||
|
title: '数字',
|
||||||
|
options: {
|
||||||
|
interface: 'number',
|
||||||
|
type: 'integer',
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
precision: 0, // 需要考虑
|
||||||
|
component: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 precision 控制精确度
|
||||||
|
* 百分比转化是前端处理还是后端处理
|
||||||
|
*/
|
||||||
|
export const percent = {
|
||||||
|
title: '百分比',
|
||||||
|
options: {
|
||||||
|
interface: 'percent',
|
||||||
|
type: 'integer',
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
precision: 0,
|
||||||
|
component: {
|
||||||
|
type: 'number',
|
||||||
|
suffix: '%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const wysiwyg = {
|
||||||
|
title: '可视化编辑器',
|
||||||
|
options: {
|
||||||
|
interface: 'wysiwyg',
|
||||||
|
type: 'text',
|
||||||
|
component: {
|
||||||
|
type: 'wysiwyg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 特殊的关系字段
|
||||||
|
*/
|
||||||
|
export const attachment = {
|
||||||
|
title: '附件',
|
||||||
|
options: {
|
||||||
|
interface: 'attachment',
|
||||||
|
type: 'belongsToMany',
|
||||||
|
filterable: true,
|
||||||
|
target: 'attachments',
|
||||||
|
component: {
|
||||||
|
type: 'fileManager',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const select = {
|
||||||
|
title: '下拉选择(单选)',
|
||||||
|
options: {
|
||||||
|
interface: 'select',
|
||||||
|
type: 'string',
|
||||||
|
filterable: true,
|
||||||
|
dataSource: [],
|
||||||
|
component: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* type 怎么处理
|
||||||
|
* 暂时 json 处理
|
||||||
|
* 后续:扩展 type=array 的字段
|
||||||
|
* array 的情况怎么兼容
|
||||||
|
* filter 要处理
|
||||||
|
* 不能处理 json 搜索的数据库可以用 hasMany 转化
|
||||||
|
*
|
||||||
|
* 思考:🤔 如果 select合并成一个 interface,multiple 会影响 type
|
||||||
|
*/
|
||||||
|
export const multipleSelect = {
|
||||||
|
title: '下拉选择(多选)',
|
||||||
|
options: {
|
||||||
|
interface: 'multipleSelect',
|
||||||
|
type: 'json', // json 过滤
|
||||||
|
filterable: true,
|
||||||
|
dataSource: [],
|
||||||
|
multiple: true, // 需要重点考虑
|
||||||
|
component: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const radio = {
|
||||||
|
title: '单选框',
|
||||||
|
options: {
|
||||||
|
interface: 'radio',
|
||||||
|
type: 'string',
|
||||||
|
filterable: true,
|
||||||
|
dataSource: [],
|
||||||
|
component: {
|
||||||
|
type: 'radio',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkboxes = {
|
||||||
|
title: '多选框',
|
||||||
|
options: {
|
||||||
|
interface: 'checkboxes',
|
||||||
|
type: 'json',
|
||||||
|
filterable: true,
|
||||||
|
dataSource: [],
|
||||||
|
component: {
|
||||||
|
type: 'checkboxes',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const boolean = {
|
||||||
|
title: '是/否',
|
||||||
|
options: {
|
||||||
|
interface: 'boolean',
|
||||||
|
type: 'boolean',
|
||||||
|
filterable: true,
|
||||||
|
component: {
|
||||||
|
type: 'checkbox', // switch
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* dateonly 要不要变 type
|
||||||
|
* 如果是 dateonly 时间怎么办?
|
||||||
|
*/
|
||||||
|
export const datetime = {
|
||||||
|
title: '日期',
|
||||||
|
options: {
|
||||||
|
interface: 'datetime',
|
||||||
|
type: 'date',
|
||||||
|
dateonly: false, // dateonly
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
component: {
|
||||||
|
type: 'date',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const time = {
|
||||||
|
title: '时间',
|
||||||
|
options: {
|
||||||
|
interface: 'time',
|
||||||
|
type: 'time',
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
format: 'HH:mm:ss',
|
||||||
|
component: {
|
||||||
|
type: 'time',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重点:
|
||||||
|
* 初始化子表和子字段
|
||||||
|
* hasMany 相关的设置参数
|
||||||
|
* fields 是子字段
|
||||||
|
*
|
||||||
|
* 分组字段 - virtual:不考虑字段分组
|
||||||
|
* 子表格 - hasMany
|
||||||
|
* - 子字段只属于子表格字段关联的表(target),不属于当前表(source)
|
||||||
|
*/
|
||||||
|
// database.table({
|
||||||
|
// name: 'tablename',
|
||||||
|
// fields: [
|
||||||
|
// {
|
||||||
|
// type: 'hasMany',
|
||||||
|
// name: 'foos',
|
||||||
|
// target: 'foos',
|
||||||
|
// fields: [
|
||||||
|
// {
|
||||||
|
// type: 'string',
|
||||||
|
// name: 'xxx',
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// });
|
||||||
|
// database.table({
|
||||||
|
// name: 'foos',
|
||||||
|
// fields: [
|
||||||
|
// {
|
||||||
|
// type: 'string',
|
||||||
|
// name: 'xxx',
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// });
|
||||||
|
export const subTable = {
|
||||||
|
title: '子表格',
|
||||||
|
options: {
|
||||||
|
interface: 'subTable',
|
||||||
|
type: 'hasMany',
|
||||||
|
// fields: [],
|
||||||
|
component: {
|
||||||
|
type: 'subTable',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尽量减少更新 multiple 造成的影响
|
||||||
|
* 同步生成配对的关系字段
|
||||||
|
*
|
||||||
|
* 只传 name 没有 target,可以通过 addField 处理,找到 target
|
||||||
|
* 没有 name 但是有 target,name 随机生成
|
||||||
|
* 有 name 也有 target
|
||||||
|
*/
|
||||||
|
|
||||||
|
// database.table({
|
||||||
|
// name: 'foos',
|
||||||
|
// fields: [
|
||||||
|
// {
|
||||||
|
// type: 'hasMany',
|
||||||
|
// name: 'bars',
|
||||||
|
// // target: 'bars',
|
||||||
|
// // sourceKey: 'id',
|
||||||
|
// // foreignKey: 'foo_id',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: 'hasMany',
|
||||||
|
// name: 'xxxxx', // 如果没有随机生成
|
||||||
|
// target: 'bars',
|
||||||
|
// // sourceKey: 'id',
|
||||||
|
// // foreignKey: 'foo_id',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: 'hasMany',
|
||||||
|
// name: 'xxxxx', // 如果没有随机生成
|
||||||
|
// target: 'bars',
|
||||||
|
// sourceKey: 'id',
|
||||||
|
// foreignKey: 'foo_id',
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const field = table.addField({
|
||||||
|
// type: 'hasMany',
|
||||||
|
// name: 'xxx', // xxx
|
||||||
|
// });
|
||||||
|
|
||||||
|
export const linkTo = {
|
||||||
|
title: '关联数据',
|
||||||
|
options: {
|
||||||
|
interface: 'linkTo',
|
||||||
|
multiple: true, // 可能影响 type
|
||||||
|
type: 'belongsToMany',
|
||||||
|
// name,
|
||||||
|
// target: '关联表', // 用户会输入
|
||||||
|
filterable: true,
|
||||||
|
component: {
|
||||||
|
type: 'drawerSelect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createdBy = {
|
||||||
|
title: '创建者',
|
||||||
|
options: {
|
||||||
|
interface: 'createdBy',
|
||||||
|
type: 'belongsTo',
|
||||||
|
filterable: true,
|
||||||
|
component: {
|
||||||
|
type: 'drawerSelect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createdAt = {
|
||||||
|
title: '创建时间',
|
||||||
|
options: {
|
||||||
|
interface: 'createdAt',
|
||||||
|
type: 'date',
|
||||||
|
required: true,
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
component: {
|
||||||
|
type: 'date',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updatedBy = {
|
||||||
|
title: '更新人',
|
||||||
|
options: {
|
||||||
|
interface: 'updatedBy',
|
||||||
|
type: 'belongsTo',
|
||||||
|
filterable: true,
|
||||||
|
component: {
|
||||||
|
type: 'drawerSelect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updatedAt = {
|
||||||
|
title: '更新日期',
|
||||||
|
options: {
|
||||||
|
interface: 'updatedAt',
|
||||||
|
type: 'date',
|
||||||
|
required: true,
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
component: {
|
||||||
|
type: 'date',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字段分组(暂缓)
|
||||||
|
*
|
||||||
|
* 影响数据输出结构,树形结构输出
|
||||||
|
*/
|
||||||
|
export const group = {
|
||||||
|
title: '字段组',
|
||||||
|
options: {
|
||||||
|
interface: 'group',
|
||||||
|
// name: 'id',
|
||||||
|
type: 'virtual',
|
||||||
|
component: {
|
||||||
|
type: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键(暂缓)
|
||||||
|
*/
|
||||||
|
export const primaryKey = {
|
||||||
|
title: '主键',
|
||||||
|
options: {
|
||||||
|
interface: 'primaryKey',
|
||||||
|
name: 'id',
|
||||||
|
type: 'integer',
|
||||||
|
required: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
filterable: true,
|
||||||
|
component: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自增长
|
||||||
|
* scope 的问题
|
||||||
|
*/
|
||||||
|
export const sort = {
|
||||||
|
title: '排序',
|
||||||
|
options: {
|
||||||
|
interface: 'sort',
|
||||||
|
type: 'integer',
|
||||||
|
required: true,
|
||||||
|
component: {
|
||||||
|
type: 'sort',
|
||||||
|
showInTable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const password = {
|
||||||
|
title: '密码',
|
||||||
|
options: {
|
||||||
|
interface: 'password',
|
||||||
|
type: 'password',
|
||||||
|
hidden: true, // hidden 用来控制 api 不输出这个字段,但是可能这个字段显示在表单里 showInForm
|
||||||
|
component: {
|
||||||
|
type: 'password',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const json = {
|
||||||
|
title: 'JSON',
|
||||||
|
options: {
|
||||||
|
interface: 'json',
|
||||||
|
type: 'json',
|
||||||
|
component: {
|
||||||
|
type: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
6
packages/plugin-collections/src/models/action.ts
Normal file
6
packages/plugin-collections/src/models/action.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import BaseModel from './base';
|
||||||
|
|
||||||
|
export class ActionModel extends BaseModel {
|
||||||
|
|
||||||
|
}
|
102
packages/plugin-collections/src/models/base.ts
Normal file
102
packages/plugin-collections/src/models/base.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import { Model } from '@nocobase/database';
|
||||||
|
|
||||||
|
export class BaseModel extends Model {
|
||||||
|
get additionalAttribute() {
|
||||||
|
const tableOptions = this.database.getTable(this.constructor.name).getOptions();
|
||||||
|
return _.get(tableOptions, 'additionalAttribute') || 'options';
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key?: any, options?: any) {
|
||||||
|
if (typeof key === 'string') {
|
||||||
|
const [column, ...path] = key.split('.');
|
||||||
|
const attribute = this.rawAttributes[column];
|
||||||
|
if (attribute) {
|
||||||
|
const value = super.get(column, options);
|
||||||
|
if (path.length) {
|
||||||
|
return _.get(value, path);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return _.get(super.get(this.additionalAttribute, options) || {}, key);
|
||||||
|
}
|
||||||
|
const data = super.get();
|
||||||
|
return {
|
||||||
|
...(data[this.additionalAttribute]||{}),
|
||||||
|
..._.omit(data, [this.additionalAttribute]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getDataValue(key: any) {
|
||||||
|
const [column, ...path] = key.split('.');
|
||||||
|
const attribute = this.rawAttributes[column];
|
||||||
|
if (attribute) {
|
||||||
|
const value = super.getDataValue(column);
|
||||||
|
if (path.length) {
|
||||||
|
return _.get(value, path);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const options = super.getDataValue(this.additionalAttribute) || {};
|
||||||
|
return _.get(options, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key?: any, value?: any, options?: any) {
|
||||||
|
if (typeof key === 'string') {
|
||||||
|
// 不处理关系数据
|
||||||
|
// @ts-ignore
|
||||||
|
if (_.get(this.constructor.associations, key)) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
// 如果是 object 数据,merge 处理
|
||||||
|
if (_.isPlainObject(value)) {
|
||||||
|
value = _.merge(this.get(key)||{}, value);
|
||||||
|
}
|
||||||
|
const [column, ...path] = key.split('.');
|
||||||
|
this.changed(column, true);
|
||||||
|
const attribute = this.rawAttributes[column];
|
||||||
|
if (attribute) {
|
||||||
|
if (!path.length) {
|
||||||
|
return super.set(key, value, options);
|
||||||
|
}
|
||||||
|
const values = this.get(column, options) || {};
|
||||||
|
_.set(values, path, value);
|
||||||
|
return super.set(column, values, options);
|
||||||
|
}
|
||||||
|
// 如果未设置 attribute,存到 additionalAttribute 里
|
||||||
|
const opts = this.get(this.additionalAttribute, options) || {};
|
||||||
|
_.set(opts, key, value);
|
||||||
|
this.changed(this.additionalAttribute, true);
|
||||||
|
return super.set(this.additionalAttribute, opts, options);
|
||||||
|
}
|
||||||
|
return super.set(key, value, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDataValue(key: any, value: any) {
|
||||||
|
// 不处理关系数据
|
||||||
|
// @ts-ignore
|
||||||
|
if (_.get(this.constructor.associations, key)) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
if (_.isPlainObject(value)) {
|
||||||
|
value = _.merge(this.get(key)||{}, value);
|
||||||
|
}
|
||||||
|
const [column, ...path] = key.split('.');
|
||||||
|
this.changed(column, true);
|
||||||
|
const attribute = this.rawAttributes[column];
|
||||||
|
if (attribute) {
|
||||||
|
if (!path.length) {
|
||||||
|
return super.setDataValue(key, value);
|
||||||
|
}
|
||||||
|
const values = this.get(column) || {};
|
||||||
|
_.set(values, path, value);
|
||||||
|
return super.setDataValue(column, values);
|
||||||
|
}
|
||||||
|
const opts = this.get(this.additionalAttribute) || {};
|
||||||
|
_.set(opts, key, value);
|
||||||
|
this.changed(this.additionalAttribute, true);
|
||||||
|
return super.setDataValue(this.additionalAttribute, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BaseModel;
|
@ -1,22 +1,92 @@
|
|||||||
import { Model } from '@nocobase/database';
|
import _ from 'lodash';
|
||||||
|
import BaseModel from './base';
|
||||||
|
import { TableOptions } from '@nocobase/database';
|
||||||
|
import { SaveOptions, Utils } from 'sequelize';
|
||||||
|
|
||||||
export class CollectionModel extends Model {
|
export class CollectionModel extends BaseModel {
|
||||||
async modelInit() {
|
|
||||||
if (['collections', 'fields'].includes(this.get('name'))) {
|
/**
|
||||||
return;
|
* 通过 name 获取 collection
|
||||||
}
|
*
|
||||||
const Field = this.database.getModel('fields');
|
* @param name
|
||||||
const fields = await Field.findAll();
|
*/
|
||||||
this.database.table({
|
static async findByName(name: string) {
|
||||||
name: this.get('name'),
|
return this.findOne({ where: { name } });
|
||||||
fields: fields.map(field => {
|
}
|
||||||
return {
|
|
||||||
name: field.get('name'),
|
/**
|
||||||
type: field.get('type'),
|
* 生成随机数据库表名
|
||||||
};
|
*
|
||||||
}),
|
* 策略:暂时使用 3+2
|
||||||
|
* 1. 自增 id
|
||||||
|
* 2. 随机字母
|
||||||
|
* 3. 时间戳
|
||||||
|
* 4. 转拼音
|
||||||
|
* 5. 常见词翻译
|
||||||
|
*
|
||||||
|
* @param title 显示的名称
|
||||||
|
*/
|
||||||
|
static generateName(title?: string): string {
|
||||||
|
return `t_${Date.now().toString(36)}_${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迁移
|
||||||
|
*/
|
||||||
|
async migrate() {
|
||||||
|
const options = await this.getOptions();
|
||||||
|
const prevTable = this.database.getTable(this.get('name'));
|
||||||
|
const prevOptions = prevTable ? prevTable.getOptions() : {};
|
||||||
|
// table 是初始化和重新初始化
|
||||||
|
const table = this.database.table({...prevOptions, ...options});
|
||||||
|
return await table.sync({
|
||||||
|
force: false,
|
||||||
|
alter: {
|
||||||
|
drop: false,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
console.log('modelInit', this.get('name'));
|
}
|
||||||
|
|
||||||
|
async getFieldsOptions() {
|
||||||
|
const fieldsOptions = [];
|
||||||
|
const fields = await this.getFields();
|
||||||
|
for (const field of fields) {
|
||||||
|
fieldsOptions.push(await field.getOptions());
|
||||||
|
}
|
||||||
|
return fieldsOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOptions(): Promise<TableOptions> {
|
||||||
|
return {
|
||||||
|
...this.get('options'),
|
||||||
|
name: this.get('name'),
|
||||||
|
title: this.get('title'),
|
||||||
|
fields: await this.getFieldsOptions(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async import(data: TableOptions, options: SaveOptions = {}): Promise<CollectionModel> {
|
||||||
|
data = _.cloneDeep(data);
|
||||||
|
const collection = await this.create({
|
||||||
|
...data,
|
||||||
|
}, options);
|
||||||
|
const items: any = {};
|
||||||
|
const associations = ['fields', 'tabs', 'actions', 'views'];
|
||||||
|
for (const key of associations) {
|
||||||
|
if (!Array.isArray(data[key])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
items[key] = data[key].map((item, sort) => ({
|
||||||
|
...item,
|
||||||
|
sort,
|
||||||
|
}));
|
||||||
|
for (const item of items[key]) {
|
||||||
|
await collection[`create${_.upperFirst(Utils.singularize(key))}`](item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// updateAssociations 有 BUG
|
||||||
|
// await collection.updateAssociations(items, options);
|
||||||
|
return collection;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,30 @@
|
|||||||
import { Model } from '@nocobase/database';
|
import _ from 'lodash';
|
||||||
|
import BaseModel from './base';
|
||||||
|
import { FieldOptions } from '@nocobase/database';
|
||||||
|
|
||||||
export class FieldModel extends Model {
|
export class FieldModel extends BaseModel {
|
||||||
|
static generateName(title?: string): string {
|
||||||
|
return `f_${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOptions(): Promise<FieldOptions> {
|
||||||
|
return {
|
||||||
|
...this.get('options'),
|
||||||
|
type: this.get('type'),
|
||||||
|
name: this.get('name'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async migrate() {
|
||||||
|
const table = this.database.getTable(this.get('collection_name'));
|
||||||
|
table.addField(await this.getOptions());
|
||||||
|
await table.sync({
|
||||||
|
force: false,
|
||||||
|
alter: {
|
||||||
|
drop: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FieldModel;
|
export default FieldModel;
|
||||||
|
5
packages/plugin-collections/src/models/index.ts
Normal file
5
packages/plugin-collections/src/models/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from './action';
|
||||||
|
export * from './collection';
|
||||||
|
export * from './field';
|
||||||
|
export * from './tab';
|
||||||
|
export * from './view';
|
6
packages/plugin-collections/src/models/tab.ts
Normal file
6
packages/plugin-collections/src/models/tab.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import BaseModel from './base';
|
||||||
|
|
||||||
|
export class TabModel extends BaseModel {
|
||||||
|
|
||||||
|
}
|
6
packages/plugin-collections/src/models/view.ts
Normal file
6
packages/plugin-collections/src/models/view.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import BaseModel from './base';
|
||||||
|
|
||||||
|
export class ViewModel extends BaseModel {
|
||||||
|
|
||||||
|
}
|
@ -1,12 +1,28 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import Database, { ModelCtor } from '@nocobase/database';
|
import { Application } from '@nocobase/server';
|
||||||
import Resourcer from '@nocobase/resourcer';
|
import hooks from './hooks';
|
||||||
|
import { registerModels } from '@nocobase/database';
|
||||||
|
import * as models from './models';
|
||||||
|
|
||||||
export default async function (this: any, options = {}) {
|
export default async function (this: Application, options = {}) {
|
||||||
const database: Database = this.database;
|
const database = this.database;
|
||||||
const resourcer: Resourcer = this.resourcer;
|
const resourcer = this.resourcer;
|
||||||
|
// 提供全局的 models 注册机制
|
||||||
|
registerModels(models);
|
||||||
|
|
||||||
database.import({
|
database.import({
|
||||||
directory: path.resolve(__dirname, 'collections'),
|
directory: path.resolve(__dirname, 'collections'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Object.keys(hooks).forEach(modelName => {
|
||||||
|
const Model = database.getModel(modelName);
|
||||||
|
Object.keys(hooks[modelName]).forEach(hookKey => {
|
||||||
|
// TODO(types): 多层 map 映射类型定义较为复杂,暂时忽略
|
||||||
|
// @ts-ignore
|
||||||
|
Model.addHook(hookKey, hooks[modelName][hookKey]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载数据库表 collections 中已经保存的表配置
|
||||||
|
// await Collection.findAll();
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,9 @@ const transforms = {
|
|||||||
table: async (fields: Model[]) => {
|
table: async (fields: Model[]) => {
|
||||||
const arr = [];
|
const arr = [];
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
|
if (!get(field.component, 'showInTable')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
arr.push({
|
arr.push({
|
||||||
...field.toJSON(),
|
...field.toJSON(),
|
||||||
...field.options,
|
...field.options,
|
||||||
@ -17,9 +20,27 @@ const transforms = {
|
|||||||
form: async (fields: Model[]) => {
|
form: async (fields: Model[]) => {
|
||||||
const schema = {};
|
const schema = {};
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
schema[field.name] = {
|
if (!get(field.component, 'showInForm')) {
|
||||||
type: 'string',
|
continue;
|
||||||
|
}
|
||||||
|
const type = get(field.component, 'type', 'string');
|
||||||
|
const prop: any = {
|
||||||
|
type,
|
||||||
title: field.title||field.name,
|
title: field.title||field.name,
|
||||||
|
...(field.component||{}),
|
||||||
|
}
|
||||||
|
if (type === 'select') {
|
||||||
|
prop.type = 'string'
|
||||||
|
}
|
||||||
|
const defaultValue = get(field.options, 'defaultValue');
|
||||||
|
if (defaultValue) {
|
||||||
|
prop.default = defaultValue;
|
||||||
|
}
|
||||||
|
if (['radio', 'select', 'checkboxes'].includes(type)) {
|
||||||
|
prop.enum = get(field.options, 'dataSource', []);
|
||||||
|
}
|
||||||
|
schema[field.name] = {
|
||||||
|
...prop,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return schema;
|
return schema;
|
||||||
@ -27,6 +48,9 @@ const transforms = {
|
|||||||
details: async (fields: Model[]) => {
|
details: async (fields: Model[]) => {
|
||||||
const arr = [];
|
const arr = [];
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
|
if (!get(field.component, 'showInDetail')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
arr.push({
|
arr.push({
|
||||||
...field.toJSON(),
|
...field.toJSON(),
|
||||||
...field.options,
|
...field.options,
|
||||||
|
@ -5,6 +5,7 @@ export default {
|
|||||||
title: '页面配置',
|
title: '页面配置',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
interface: 'sort',
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
name: 'sort',
|
name: 'sort',
|
||||||
title: '排序',
|
title: '排序',
|
||||||
@ -12,66 +13,82 @@ export default {
|
|||||||
type: 'sort',
|
type: 'sort',
|
||||||
className: 'drag-visible',
|
className: 'drag-visible',
|
||||||
width: 60,
|
width: 60,
|
||||||
|
showInTable: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'string',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'title',
|
name: 'title',
|
||||||
title: '名称',
|
title: '名称',
|
||||||
showInTable: true,
|
|
||||||
isMainTitle: true,
|
|
||||||
component: {
|
component: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
className: 'drag-visible',
|
className: 'drag-visible',
|
||||||
|
showInTable: true,
|
||||||
|
showInForm: true,
|
||||||
|
showInDetail: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'number',
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
name: 'parent_id',
|
name: 'parent_id',
|
||||||
title: '父级页面',
|
title: '父级页面',
|
||||||
component: {
|
component: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
showInForm: true,
|
||||||
|
showInDetail: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'string',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'path',
|
name: 'path',
|
||||||
title: '路径',
|
title: '路径',
|
||||||
unique: true,
|
unique: true,
|
||||||
showInTable: true,
|
|
||||||
component: {
|
component: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
showInTable: true,
|
||||||
|
showInForm: true,
|
||||||
|
showInDetail: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'string',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'icon',
|
name: 'icon',
|
||||||
title: '图标',
|
title: '图标',
|
||||||
component: {
|
component: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
showInTable: true,
|
||||||
|
showInForm: true,
|
||||||
|
showInDetail: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'select',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'type',
|
name: 'type',
|
||||||
title: '类型',
|
title: '类型',
|
||||||
showInTable: true,
|
options: [
|
||||||
|
{
|
||||||
|
label: '页面',
|
||||||
|
value: 'page',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '布局',
|
||||||
|
value: 'layout',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '数据集',
|
||||||
|
value: 'collection',
|
||||||
|
},
|
||||||
|
],
|
||||||
component: {
|
component: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: [
|
showInTable: true,
|
||||||
{
|
showInForm: true,
|
||||||
label: '页面',
|
showInDetail: true,
|
||||||
value: 'page',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '布局',
|
|
||||||
value: 'layout',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '数据集',
|
|
||||||
value: 'collection',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'x-linkages': [
|
'x-linkages': [
|
||||||
{
|
{
|
||||||
"type": "value:visible",
|
"type": "value:visible",
|
||||||
@ -82,85 +99,84 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'select',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'collection',
|
name: 'collection',
|
||||||
title: '属于哪种数据集?',
|
title: '属于哪种数据集?',
|
||||||
component: {
|
component: {
|
||||||
type: 'string',
|
type: 'select',
|
||||||
|
showInForm: true,
|
||||||
|
showInDetail: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'select',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'template',
|
name: 'template',
|
||||||
title: '模板',
|
title: '模板',
|
||||||
showInTable: true,
|
options: [
|
||||||
|
{
|
||||||
|
label: '顶部菜单布局',
|
||||||
|
value: 'TopMenuLayout',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '左侧菜单布局',
|
||||||
|
value: 'SideMenuLayout',
|
||||||
|
},
|
||||||
|
],
|
||||||
component: {
|
component: {
|
||||||
type: 'string',
|
type: 'select',
|
||||||
enum: [
|
showInTable: true,
|
||||||
{
|
showInForm: true,
|
||||||
label: '顶部菜单布局',
|
showInDetail: true,
|
||||||
value: 'LayoutWithTopMenu',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '左侧菜单布局',
|
|
||||||
value: 'LayoutWithSideMenu',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '数据集(全部)',
|
|
||||||
value: 'collections',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '数据集(某种)',
|
|
||||||
value: 'collection',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '登录',
|
|
||||||
value: 'login',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '注册',
|
|
||||||
value: 'register',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '分析页',
|
|
||||||
value: 'analysis',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '工作区',
|
|
||||||
value: 'workplace',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'boolean',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
name: 'showInMenu',
|
name: 'showInMenu',
|
||||||
title: '在菜单里显示',
|
title: '在菜单里显示',
|
||||||
// showInTable: true,
|
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
component: {
|
component: {
|
||||||
type: 'boolean',
|
type: 'checkbox',
|
||||||
|
showInTable: true,
|
||||||
|
showInForm: true,
|
||||||
|
showInDetail: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'boolean',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
name: 'inherit',
|
name: 'inherit',
|
||||||
title: '继承父级页面内容',
|
title: '继承父级页面内容',
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
component: {
|
component: {
|
||||||
type: 'boolean',
|
type: 'checkbox',
|
||||||
|
showInTable: true,
|
||||||
|
showInForm: true,
|
||||||
|
showInDetail: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'linkTo',
|
||||||
type: 'hasMany',
|
type: 'hasMany',
|
||||||
name: 'children',
|
name: 'children',
|
||||||
|
title: '子页面',
|
||||||
target: 'pages',
|
target: 'pages',
|
||||||
foreignKey: 'parent_id',
|
foreignKey: 'parent_id',
|
||||||
sourceKey: 'id',
|
sourceKey: 'id',
|
||||||
|
component: {
|
||||||
|
type: 'drawerSelect',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
interface: 'json',
|
||||||
type: 'json',
|
type: 'json',
|
||||||
name: 'options',
|
name: 'options',
|
||||||
|
title: '元数据',
|
||||||
|
component: {
|
||||||
|
type: 'hidden',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
|
Loading…
Reference in New Issue
Block a user