feat: improve code

This commit is contained in:
chenos 2021-09-11 18:53:26 +08:00
parent f300fd6ae9
commit c6b68f2b10
139 changed files with 418 additions and 13962 deletions

View File

@ -8,10 +8,11 @@ export default defineConfig({
define: {
'process.env.API_URL': process.env.API_URL,
},
includes: ['docs'],
// mfsu: {},
// ssr: {},
// exportStatic: {},
mode: 'site',
mode: 'doc',
logo: 'https://www.nocobase.com/dist/images/logo.png',
navs: {
'en-US': [

View File

@ -10,21 +10,15 @@ export default {
// 如使用代码作为 id 可能更节省,但由于代码数字最长为 12 字节,除非使用 bigint(64) 才够放置
{
name: 'code',
title: '代码',
interface: 'string',
type: 'string',
unique: true,
},
{
name: 'name',
title: '名称',
interface: 'string',
type: 'string',
},
{
name: 'parent',
title: '从属',
interface: 'linkTo',
type: 'belongsTo',
target: 'china_regions',
targetKey: 'code',
@ -32,8 +26,6 @@ export default {
},
{
name: 'children',
title: '下辖',
interface: 'linkTo',
type: 'hasMany',
target: 'china_regions',
sourceKey: 'code',
@ -41,7 +33,6 @@ export default {
},
{
name: 'level',
title: '层级',
type: 'integer'
}
]

View File

@ -1,11 +1,19 @@
import path from 'path';
import { registerModels } from '@nocobase/database';
import Database, { registerModels } from '@nocobase/database';
import { ChinaRegion } from './models/china-region';
import Application from '@nocobase/server';
export default async function (options = {}) {
registerModels({ ChinaRegion });
export default async function (this: Application, options = {}) {
const { database } = this;
registerModels({ ChinaRegion });
database.import({
directory: path.resolve(__dirname, 'collections'),
});
this.on('china-region.init', async () => {
const M = database.getModel('china_regions');
await M.importData();
});
}

View File

@ -1,16 +0,0 @@
{
"name": "@nocobase/plugin-collections-v04",
"version": "0.4.0-alpha.7",
"main": "lib/index.js",
"license": "MIT",
"dependencies": {
"@nocobase/database": "^0.4.0-alpha.7",
"@nocobase/resourcer": "^0.4.0-alpha.7",
"@nocobase/server": "^0.4.0-alpha.7",
"deepmerge": "^4.2.2"
},
"devDependencies": {
"@nocobase/actions": "^0.4.0-alpha.7"
},
"gitHead": "f0b335ac30f29f25c95d7d137655fa64d8d67f1e"
}

View File

@ -1,44 +0,0 @@
import { Agent, getAgent, getApp } from '..';
import { Application } from '@nocobase/server';
import { types } from '../../interfaces';
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 response = await agent.resource('collections').create({
values: {
title: 'tests',
},
});
const { name } = response.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();
});
});

View File

@ -1,144 +0,0 @@
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(/src\/__tests__/g, '')
.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.registerActionHandlers({ ...actions.associate, ...actions.common });
app.registerPlugin('collections', [plugin]);
await app.loadPlugins();
await app.database.sync();
// 表配置信息存到数据库里
// 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}`;
}
}
});
};

View File

@ -1,286 +0,0 @@
import Database, { ModelCtor } from '@nocobase/database';
import { getDatabase } from '../';
import BaseModel from '../../models/base';
import _ from 'lodash';
describe('models.base', () => {
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',
},
{
name: 'title',
type: 'virtual',
},
{
name: 'xyz',
type: 'virtual',
defaultValue: 'xyz1',
},
{
name: 'content',
type: 'virtual',
set(val) {
// 留空
}
},
{
name: 'key1',
type: 'virtual',
set(val) {
this.setDataValue('options.key1', `111${val}111`);
}
},
{
name: 'key2',
type: 'virtual',
get() {
return 'val2';
}
},
{
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',
xyz: "xyz1",
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' }],
});
});
it('update virtual attribute', async () => {
await test.update({
title: 'xxx', // 虚拟字段没 set 转存 options
content: 'content123', // set 留空,这个 key 什么也不做
key1: 'val1', // 走 set 方法
});
// 重新获取再验证
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' },
arr: [{ a: 'a' }, { b: 'b' }],
title: 'xxx',
key2: 'val2', // key2 为 get 方法取的
key1: '111val1111',
});
expect(test2.get('content')).toBeUndefined();
});
it('update', async () => {
const t = await TestModel.create({
name: 'name1',
// xyz: 'xyz',
});
await t.update({
abc: 'abc',
});
const t2 = await TestModel.findOne({
where: {
name: 'name1',
}
});
expect(t2.get()).toMatchObject({
xyz: 'xyz1',
abc: 'abc',
key2: 'val2',
id: 2,
name: 'name1',
});
await t2.update({
abc: 'abcdef',
});
const t3 = await TestModel.findOne({
where: {
name: 'name1',
}
});
// 查询之后更新再重新查询
expect(t3.get()).toMatchObject({
xyz: 'xyz1',
abc: 'abcdef',
key2: 'val2',
id: 2,
name: 'name1',
});
});
it('update', async () => {
const t = await TestModel.create({
name: 'name1',
xyz: 'xyz',
});
await t.update({
abc: 'abc',
});
const t2 = await TestModel.findOne({
where: {
name: 'name1',
}
});
expect(t2.get()).toMatchObject({
xyz: 'xyz',
abc: 'abc',
key2: 'val2',
id: 2,
name: 'name1',
});
});
it('component', async () => {
const t = await TestModel.create({
component: {
arr: [
{ a: 'a', aa: 'aa' },
{ b: 'b', bb: 'bb' },
{ c: 'c', cc: 'cc' },
],
},
});
t.set({
component: {
arr: [
{ a: 'aa' },
{ b: 'bb' },
],
}
});
await t.save();
expect(t.get('component')).toEqual({
arr: [
{ a: 'aa' },
{ b: 'bb' },
],
});
})
});

View File

@ -1,69 +0,0 @@
import { Agent, getAgent, getApp } from '../';
import { Application } from '@nocobase/server';
import * as types from '../../interfaces/types';
describe('models.collection', () => {
let app: Application;
let agent: Agent;
beforeEach(async () => {
app = await getApp();
agent = getAgent(app);
});
afterEach(() => app.database.close());
it('import all tables', async () => {
const tables = app.database.getTables([]);
for (const table of tables) {
const Collection = app.database.getModel('collections');
await Collection.import(table.getOptions(), { migrate: false });
}
});
it('import examples', async () => {
await app.database.getModel('collections').import({
title: '示例',
name: 'examples',
showInDataMenu: true,
statusable: false,
fields: [
{
interface: 'string',
title: '单行文本',
name: 'string',
component: {
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'textarea',
title: '多行文本',
name: 'textarea',
component: {
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
],
}, {
// migrate: false,
});
const table = app.database.getTable('examples');
expect(table).toBeDefined();
expect(table.getFields().size).toBe(2);
await table.sync();
const Example = app.database.getModel('examples');
const example = await Example.create({
string: 'string1',
textarea: 'textarea1',
});
expect(example.toJSON()).toMatchObject({
string: 'string1',
textarea: 'textarea1',
});
});
});

View File

@ -1,128 +0,0 @@
import { Agent, getAgent, getApp } from '../';
import { Application } from '@nocobase/server';
import { types } from '../../interfaces';
describe('models.field', () => {
let app: Application;
let agent: Agent;
beforeEach(async () => {
app = await getApp();
agent = getAgent(app);
});
afterEach(() => app.database.close());
it('updatedAt', async () => {
const Field = app.database.getModel('fields');
const field = new Field();
field.setInterface('updatedAt');
expect(field.get()).toMatchObject(types.updatedAt.options)
});
it('dataSource', async () => {
const Collection = app.database.getModel('collections');
// @ts-ignore
const collection = await Collection.create({
title: 'tests',
});
await collection.updateAssociations({
fields: [
{
title: 'xx',
name: 'xx',
interface: 'select',
type: 'virtual',
dataSource: [
{ label: 'xx', value: 'xx' },
],
component: {
type: 'string',
showInDetail: true,
showInForm: true,
},
}
],
});
const fields = await collection.getFields();
expect(fields[0].get('dataSource')).toEqual([
{ label: 'xx', value: 'xx' },
]);
});
it.skip('sub table field', async () => {
const [Collection, Field] = app.database.getModels(['collections', 'fields']);
const options = {
title: 'tests',
name: 'tests',
fields: [
{
interface: 'subTable',
title: '子表格',
name: 'subs',
children: [
{
interface: 'string',
title: '名称',
name: 'name',
},
],
},
],
};
const collection = await Collection.create(options);
await collection.updateAssociations(options);
const field = await Field.findOne({
where: {
title: '子表格',
},
});
await field.createChild({
interface: 'string',
title: '名称',
name: 'title',
});
const Test = app.database.getModel('tests');
const Sub = app.database.getModel('subs');
// console.log(Test.associations);
// console.log(Sub.rawAttributes);
const test = await Test.create({});
const sub = await test.createSub({ name: 'name1', title: 'title1' });
expect(sub.toJSON()).toMatchObject({ name: 'name1', title: 'title1' })
});
it('sub table field', async () => {
const [Collection, Field] = app.database.getModels(['collections', 'fields']);
// @ts-ignore
const options = {
title: 'tests',
name: 'tests',
fields: [
{
interface: 'subTable',
title: '子表格',
// name: 'subs',
children: [
{
interface: 'string',
title: '名称',
// name: 'name',
},
],
},
],
};
const collection = await Collection.create(options);
await collection.updateAssociations(options);
const field = await Field.findOne({
where: {
title: '子表格',
},
});
await field.createChild({
interface: 'string',
title: '名称',
name: 'title',
});
});
});

View File

@ -1,33 +0,0 @@
import { Model, ModelCtor } from '@nocobase/database';
import { ResourceOptions } from '@nocobase/resourcer';
import { get } from 'lodash';
export default async (ctx, next) => {
const { resourceName, resourceKey } = ctx.action.params;
const [Collection, Tab, View] = ctx.db.getModels(['collections', 'tabs', 'views']) as ModelCtor<Model>[];
const collection = await Collection.findOne(Collection.parseApiJson({
filter: {
name: resourceName,
},
// fields: {
// // appends: ['tabs'],
// },
}));
const views = await collection.getViews({
where: {
default: true,
},
});
collection.setDataValue('defaultViewId', get(views, [0, 'id']));
collection.setDataValue('defaultViewName', get(views, [0, 'name']));
const tabs = await collection.getTabs();
ctx.body = {
...collection.toJSON(),
tabs: tabs.map(tab => ({
...tab.toJSON(),
...tab.options,
viewCollectionName: tab.type == 'association' ? tab.options.association : tab.collection_name,
})),
};
await next();
}

View File

@ -1,45 +0,0 @@
import { ResourceOptions } from '@nocobase/resourcer';
import { Model, ModelCtor } from '@nocobase/database';
import { get } from 'lodash';
export default async (ctx, next) => {
const { resourceName, resourceKey } = ctx.action.params;
const [View, Field, Action] = ctx.db.getModels(['views', 'fields', 'actions']) as ModelCtor<Model>[];
const view = await View.findOne(View.parseApiJson({
filter: {
collection_name: resourceName,
name: resourceKey,
},
fields: {
appends: ['actions', 'fields'],
},
}));
const collection = await view.getCollection();
const fields = await collection.getFields();
const actions = await collection.getActions();
const actionNames = view.options.actionNames || [];
// console.log(view.options);
if (view.type === 'table') {
const defaultTabs = await collection.getTabs({
where: {
default: true,
},
});
view.setDataValue('defaultTabName', get(defaultTabs, [0, 'name']));
}
if (view.options.updateViewId) {
view.setDataValue('rowViewName', view.options.updateViewName);
}
view.setDataValue('viewCollectionName', view.collection_name);
ctx.body = {
...view.toJSON(),
...(view.options || {}),
fields,
actions: actions.filter(action => actionNames.includes(action.name)).map(action => ({
...action.toJSON(),
...action.options,
viewCollectionName: action.collection_name,
})),
};
await next();
};

View File

@ -1,117 +0,0 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'actions',
title: '操作配置',
internal: true,
draggable: true,
model: 'ActionModel',
developerMode: true,
fields: [
{
interface: 'sort',
type: 'sort',
name: 'sort',
scope: ['collection'],
title: '排序',
component: {
type: 'sort',
className: 'drag-visible',
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',
name: 'type',
title: '类型',
component: {
type: 'string',
showInForm: true,
showInTable: true,
showInDetail: true,
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'developerMode',
title: '开发者模式',
defaultValue: false,
component: {
type: 'boolean',
},
},
{
interface: 'linkTo',
type: 'belongsTo',
name: 'collection',
title: '所属数据表',
target: 'collections',
targetKey: 'name',
component: {
type: 'drawerSelect',
},
},
{
interface: 'json',
type: 'json',
name: 'options',
title: '配置信息',
defaultValue: {},
component: {
type: 'hidden',
},
},
],
actions: [
{
type: 'list',
name: 'list',
title: '查看',
},
{
type: 'destroy',
name: 'destroy',
title: '删除',
},
{
type: 'create',
name: 'create',
title: '新增',
viewName: 'form',
},
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
},
],
} as TableOptions;

View File

@ -1,379 +0,0 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'collections',
title: '数据表配置',
internal: true,
sortable: true,
draggable: true,
model: 'CollectionModel',
developerMode: true,
createdAt: 'createdTime',
updatedAt: 'updatedTime',
fields: [
// {
// interface: 'sort',
// type: 'sort',
// name: 'sort',
// title: '排序',
// component: {
// type: 'sort',
// className: 'drag-visible',
// width: 60,
// showInTable: true,
// },
// },
{
interface: 'string',
type: 'string',
name: 'title',
title: '数据表名称',
required: true,
component: {
type: 'string',
},
},
{
interface: 'string',
type: 'string',
name: 'name',
createOnly: true,
title: '标识',
unique: true,
required: true,
developerMode: true,
component: {
type: 'string',
},
},
{
interface: 'textarea',
type: 'text',
name: 'description',
title: '数据表描述',
component: {
type: 'textarea',
},
},
// {
// interface: 'boolean',
// type: 'virtual',
// name: 'createdAt',
// title: '记录创建时间',
// developerMode: true,
// defaultValue: true,
// component: {
// type: 'checkbox',
// default: true,
// showInForm: true,
// },
// },
// {
// interface: 'boolean',
// type: 'virtual',
// name: 'updatedAt',
// title: '记录修改时间',
// developerMode: true,
// defaultValue: true,
// component: {
// type: 'checkbox',
// default: true,
// showInForm: true,
// },
// },
{
interface: 'boolean',
type: 'virtual',
name: 'createdBy',
title: '记录创建人信息',
developerMode: true,
component: {
type: 'checkbox',
default: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'updatedBy',
title: '记录修改人信息',
developerMode: true,
component: {
type: 'checkbox',
default: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'developerMode',
title: '开发者模式',
developerMode: true,
defaultValue: false,
component: {
type: 'boolean',
},
},
{
interface: 'json',
type: 'json',
name: 'options',
title: '配置信息',
defaultValue: {},
component: {
type: 'hidden',
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'internal',
title: '系统内置',
defaultValue: false,
developerMode: true,
component: {
type: 'boolean',
},
},
{
interface: 'linkTo',
type: 'hasMany',
name: 'fields',
title: '字段',
sourceKey: 'name',
draggable: true,
actions: {
list: {
sort: 'sort',
},
get: {
fields: {
appends: ['children'],
},
},
},
component: {
type: 'drawerSelect',
},
},
{
interface: 'linkTo',
type: 'hasMany',
name: 'actions',
title: '动作',
sourceKey: 'name',
draggable: true,
actions: {
list: {
sort: 'sort',
},
},
component: {
type: 'drawerSelect',
},
},
{
interface: 'linkTo',
type: 'hasMany',
name: 'views_v2',
target: 'views_v2',
title: '视图',
sourceKey: 'name',
draggable: true,
// actions: {
// list: {
// sort: 'sort',
// },
// destroy: {
// filter: {
// default: false
// }
// }
// },
component: {
type: 'drawerSelect',
},
},
{
interface: 'linkTo',
type: 'hasMany',
name: 'scopes',
target: 'scopes',
title: '数据范围',
sourceKey: 'name',
actions: {
list: {
sort: 'id',
},
update: {
filter: {
locked: false
}
},
destroy: {
filter: {
locked: false
}
}
},
component: {
type: 'drawerSelect',
},
},
],
actions: [
{
type: 'list',
name: 'list',
title: '查看',
},
{
type: 'destroy',
name: 'destroy',
title: '删除',
},
{
type: 'create',
name: 'create',
title: '新增',
viewName: 'form',
},
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
},
],
views_v2: [
{
developerMode: true,
type: 'table',
name: 'table',
title: '全部数据',
labelField: 'title',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: ['title', 'description'],
detailsOpenMode: 'window', // window
details: ['descriptions', 'fields', 'views'],
sort: ['id'],
},
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: ['title', 'description'],
},
{
developerMode: true,
type: 'descriptions',
name: 'descriptions',
title: '详情',
fields: ['title', 'description'],
actions: [
{
name: 'update',
type: 'update',
title: '编辑',
viewName: 'form',
},
],
},
{
developerMode: true,
type: 'table',
name: 'permissions_table',
title: '权限表格',
labelField: 'title',
actions: [],
fields: ['title'],
detailsOpenMode: 'drawer', // window
details: ['permissions_form'],
sort: ['id'],
},
{
developerMode: true,
type: 'form',
name: 'permissions_form',
title: '权限表单',
fields: [
{
interface: 'json',
type: 'json',
title: '数据操作权限',
name: 'actions',
component: {
"type": "permissions.actions",
"title": "数据操作权限",
"x-linkages": [{
"type": "value:schema",
"target": "actions",
"schema": {
"x-component-props": {
"resourceKey": "{{ $form.values && $form.values.resourceKey }}"
}
}
}],
"x-component-props": {
"dataSource": []
}
},
},
{
interface: 'json',
type: 'json',
title: '字段权限',
name: 'fields',
component: {
"type": "permissions.fields",
"x-linkages": [{
"type": "value:schema",
"target": "fields",
"schema": {
"x-component-props": {
"resourceKey": "{{ $form.values && $form.values.resourceKey }}"
}
}
}],
"x-component-props": {
"dataSource": []
}
},
},
],
},
{
developerMode: true,
type: 'table',
dataSourceType: 'association',
name: 'fields',
title: '字段',
targetViewName: 'table2',
targetFieldName: 'fields',
},
{
developerMode: true,
type: 'table',
dataSourceType: 'association',
name: 'views',
title: '视图',
targetViewName: 'table',
targetFieldName: 'views_v2',
},
],
} as TableOptions;

View File

@ -1,690 +0,0 @@
import { TableOptions } from '@nocobase/database';
import { types, getOptions } from '../interfaces';
export default {
name: 'fields',
title: '字段配置',
internal: true,
draggable: true,
model: 'FieldModel',
developerMode: true,
fields: [
{
interface: 'sort',
type: 'sort',
name: 'sort',
scope: ['collection'],
title: '排序',
component: {
type: 'sort',
className: 'drag-visible',
width: 60,
showInTable: true,
},
},
{
interface: 'string',
type: 'string',
name: 'title',
title: '字段名称',
required: true,
component: {
type: 'string',
className: 'drag-visible',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'string',
type: 'string',
name: 'name',
title: '标识',
required: true,
createOnly: true,
developerMode: true,
component: {
type: 'string',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'select',
type: 'string',
name: 'interface',
title: '字段类型',
required: true,
dataSource: getOptions(),
createOnly: true,
component: {
type: 'select',
showInTable: true,
showInDetail: true,
showInForm: true,
"x-linkages": [
// TODO(draft): 统一解决字段类型和配置参数联动的一种方式
// {
// type: 'value:schema',
// target: 'options',
// schema: {
// 'x-component-props': {
// fields: '{{ $self.values[1].fields || [] }}'
// }
// },
// condition: '{{ !!$self.value }}'
// },
{
"type": "value:visible",
"target": "precision",
"condition": "{{ ['number', 'percent'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "dataSource",
"condition": "{{ ['select', 'multipleSelect', 'radio', 'checkboxes'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "dateFormat",
"condition": "{{ ['datetime', 'createdAt', 'updatedAt'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "showTime",
"condition": "{{ ['datetime', 'createdAt', 'updatedAt'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "timeFormat",
"condition": "{{ ['time'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "multiple",
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "target",
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
},
// {
// "type": "value:visible",
// "target": "labelField",
// "condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
// },
{
"type": "value:visible",
"target": "createable",
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "children",
"condition": "{{ ['subTable'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "component.showInTable",
"condition": "{{ ['subTable', 'description'].indexOf($self.value) === -1 }}"
},
{
"type": "value:visible",
"target": "component.showInForm",
"condition": "{{ ['createdAt', 'updatedAt', 'createdBy', 'updatedBy'].indexOf($self.value) === -1 }}"
},
{
"type": "value:visible",
"target": "required",
"condition": "{{ ['createdAt', 'updatedAt', 'createdBy', 'updatedBy'].indexOf($self.value) === -1 }}"
},
{
"type": "value:visible",
"target": "maxLevel",
"condition": "{{ ['chinaRegion'].includes($self.value) }}"
},
{
"type": "value:visible",
"target": "incompletely",
"condition": "{{ ['chinaRegion'].includes($self.value) }}"
},
],
},
},
// TODO(draft): 将 options 作为集合字段开放出来,可以动态的解决字段参数的配置表单联动问题
// {
// interface: 'json',
// type: 'json',
// name: 'options',
// title: '配置信息',
// defaultValue: {},
// component: {
// type: 'subFields',
// showInForm: true,
// },
// },
{
interface: 'subTable',
type: 'virtual',
name: 'dataSource',
title: '可选项',
component: {
type: 'table',
default: [{}],
// 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: '数据类型',
developerMode: true,
component: {
type: 'string',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'number',
type: 'integer',
name: 'parent_id',
title: '所属分组',
component: {
type: 'number',
},
},
{
interface: 'select',
type: 'virtual',
name: 'precision',
title: '精度',
dataSource: [
{ value: 0, label: '1' },
{ value: 1, label: '1.0' },
{ value: 2, label: '1.00' },
{ value: 3, label: '1.000' },
{ value: 4, label: '1.0000' },
],
component: {
type: 'number',
showInForm: true,
default: 0,
},
},
{
interface: 'select',
type: 'virtual',
name: 'dateFormat',
title: '日期格式',
dataSource: [
{ value: 'YYYY/MM/DD', label: '年/月/日' },
{ value: 'YYYY-MM-DD', label: '年-月-日' },
{ value: 'DD/MM/YYYY', label: '日/月/年' },
],
component: {
type: 'string',
showInForm: true,
default: 'YYYY-MM-DD',
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'showTime',
title: '显示时间',
component: {
type: 'boolean',
showInForm: true,
default: false,
"x-linkages": [
{
"type": "value:visible",
"target": "timeFormat",
"condition": "{{ ($form.values && $form.values.interface === 'time') || $self.value === true }}"
},
],
},
},
{
interface: 'select',
type: 'virtual',
name: 'timeFormat',
title: '时间格式',
dataSource: [
{ value: 'HH:mm:ss', label: '24小时制' },
{ value: 'hh:mm:ss a', label: '12小时制' },
],
component: {
type: 'string',
showInForm: true,
default: 'HH:mm:ss',
},
},
// TODO(refactor): 此部分类型相关的参数,后期应拆分出去
{
name: 'maxLevel',
title: '可选层级',
interface: 'radio',
type: 'virtual',
dataSource: [
{ value: 1, label: '省' },
{ value: 2, label: '市' },
{ value: 3, label: '区/县' },
{ value: 4, label: '乡镇/街道' },
{ value: 5, label: '村/居委会' },
],
component: {
showInForm: true,
default: 3
}
},
{
name: 'incompletely',
title: '可部分选择',
interface: 'boolean',
type: 'virtual',
component: {
showInForm: true,
}
},
{
interface: 'linkTo',
multiple: false,
type: 'belongsTo',
name: 'parent',
title: '所属分组',
target: 'fields',
foreignKey: 'parent_id',
targetKey: 'id',
component: {
type: 'drawerSelect',
},
},
{
interface: 'string',
type: 'virtual',
name: 'target',
title: '要关联的数据表',
required: true,
createOnly: true,
component: {
type: 'remoteSelect',
showInDetail: true,
showInForm: true,
'x-component-props': {
mode: 'simple',
resourceName: 'collections',
labelField: 'title',
valueField: 'name',
},
"x-linkages": [
{
type: "value:state",
target: "labelField",
condition: "{{ $self.inputed }}",
state: {
value: null,
}
},
{
"type": "value:visible",
"target": "labelField",
"condition": "{{ !!$self.value }}"
},
{
type: "value:schema",
target: "labelField",
// condition: "{{ $self.value }}",
schema: {
"x-component-props": {
"associatedKey": "{{ $self.value }}"
},
},
},
{
type: 'value:visible',
target: 'component.x-component-props.filter',
condition: '{{ !!$self.value }}'
},
{
type: "value:schema",
target: "component.x-component-props.filter",
schema: {
"x-component-props": {
"associatedKey": "{{ $self.value }}"
},
},
},
],
},
},
{
interface: 'string',
type: 'virtual',
name: 'labelField',
title: '要显示的字段',
required: true,
component: {
type: 'remoteSelect',
'x-component-props': {
mode: 'simple',
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
},
showInDetail: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'multiple',
title: '允许添加多条记录',
component: {
type: 'checkbox',
showInDetail: true,
showInForm: true,
default: true,
},
},
{
name: 'component.x-component-props.filter',
interface: 'json',
type: 'virtual',
title: '数据范围',
component: {
type: 'filter',
'x-component-props': {
resourceName: 'collections.fields',
},
showInForm: true,
}
},
{
interface: 'boolean',
type: 'virtual',
name: 'createable',
title: '允许直接在关联的数据表内新建数据',
component: {
type: 'checkbox',
showInDetail: true,
showInForm: true,
},
},
{
interface: 'subTable',
type: 'hasMany',
name: 'children',
target: 'fields',
sourceKey: 'id',
foreignKey: 'parent_id',
title: '子表格字段',
viewName: 'table2',
// visible: true,
component: {
type: 'subTable',
default: [],
// showInTable: true,
// showInDetail: true,
showInForm: true,
'x-linkages': [
{
type: 'value:schema',
target: 'children',
schema: {
'x-component-props': {
associatedKey: "{{ $form.values && $form.values.id }}"
},
},
},
],
},
},
// {
// interface: 'linkTo',
// multiple: true,
// type: 'hasMany',
// name: 'children',
// title: '子字段',
// target: 'fields',
// foreignKey: 'parent_id',
// sourceKey: 'id',
// component: {
// type: 'drawerSelect',
// },
// },
{
interface: 'textarea',
type: 'virtual',
name: 'component.tooltip',
title: '提示信息',
component: {
type: 'textarea',
showInDetail: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'required',
title: '必填项',
component: {
type: 'checkbox',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'component.showInTable',
title: '显示在表格中',
component: {
type: 'checkbox',
tooltip: '若勾选,该字段将作为一列显示在数据表里',
showInTable: true,
showInDetail: true,
showInForm: true,
default: true,
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'component.showInForm',
title: '显示在表单中',
component: {
type: 'checkbox',
tooltip: '若勾选,该字段将出现在表单中',
showInTable: true,
showInDetail: true,
showInForm: true,
default: true,
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'component.showInDetail',
title: '显示在详情中',
component: {
type: 'checkbox',
tooltip: '若勾选,该字段将出现在详情中',
showInTable: true,
showInDetail: true,
showInForm: true,
default: true,
},
},
{
interface: 'linkTo',
type: 'belongsTo',
name: 'collection',
title: '所属数据表',
target: 'collections',
targetKey: 'name',
labelField: 'title',
component: {
type: 'drawerSelect',
// showInTable: true,
'x-component-props': {
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
},
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'developerMode',
title: '开发者模式',
defaultValue: false,
component: {
type: 'boolean',
},
},
{
interface: 'json',
type: 'json',
name: 'component',
title: '前端组件',
defaultValue: {},
component: {
type: 'hidden',
},
},
{
interface: 'json',
type: 'json',
name: 'options',
title: '配置信息',
defaultValue: {},
component: {
type: 'hidden',
},
},
],
actions: [
{
type: 'list',
name: 'list',
title: '查看',
},
{
type: 'destroy',
name: 'destroy',
title: '删除',
},
{
type: 'create',
name: 'create',
title: '新增',
viewName: 'form',
},
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
},
],
views_v2: [
{
developerMode: true,
type: 'table',
name: 'table',
title: '关联的字段',
labelField: 'title',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: ['title', 'interface'],
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['sort'],
},
{
developerMode: true,
type: 'table',
name: 'table2',
title: '表格',
labelField: 'title',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: ['sort', 'title', 'interface'],
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['sort'],
},
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: [
'title',
'interface',
'dataSource',
'precision',
'dateFormat',
'showTime',
'timeFormat',
'maxLevel',
'incompletely',
'target',
'labelField',
'children',
'multiple',
// 'required',
],
},
],
} as TableOptions;

View File

@ -1,97 +0,0 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'scopes',
title: '表操作范围',
developerMode: true,
internal: true,
fields: [
{
comment: '范围名称',
type: 'string',
name: 'title',
title: '名称',
component: {
type: 'string',
showInTable: true,
showInForm: true,
},
},
{
interface: 'json',
type: 'jsonb',
name: 'filter',
title: '条件',
developerMode: false,
mode: 'replace',
defaultValue: {},
component: {
type: 'filter',
showInForm: true,
"x-linkages": [
{
type: "value:schema",
target: "filter",
schema: {
"x-component-props": {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
],
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'locked',
title: '锁定',
defaultValue: false,
component: {
showInTable: true,
}
},
{
type: 'belongsTo',
name: 'collection',
targetKey: 'name',
onDelete: 'CASCADE'
}
],
views_v2: [
{
developerMode: true,
type: 'table',
name: 'table',
title: '全部数据',
labelField: 'title',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: ['title'],
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['id'],
},
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: [
'title',
'filter',
],
},
],
} as TableOptions;

View File

@ -1,399 +0,0 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'tabs',
title: '标签配置',
internal: true,
sortable: true,
model: 'TabModel',
developerMode: true,
fields: [
{
interface: 'sort',
type: 'sort',
name: 'sort',
scope: ['collection'],
title: '排序',
component: {
type: 'sort',
className: 'drag-visible',
width: 60,
showInTable: true,
},
},
{
interface: 'string',
type: 'string',
name: 'title',
title: '名称',
required: true,
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: '类型',
required: true,
dataSource: [
{ label: '详情数据', value: 'details' },
{ label: '相关数据', value: 'association' },
{ label: '模块组合', value: 'module', disabled: true },
],
component: {
type: 'radio',
showInTable: true,
showInDetail: true,
showInForm: true,
"x-linkages": [
// {
// "type": "value:visible",
// "target": "association",
// "condition": "{{ $self.value === 'association' }}"
// },
{
type: "value:visible",
target: "associationField",
condition: "{{ $self.value === 'association' }}"
},
{
type: "value:visible",
target: "displayFields",
condition: "{{ $self.value === 'details' }}",
},
{
type: "value:visible",
target: "displayFormFields",
condition: "{{ $self.value === 'details' }}",
},
// {
// type: "value:schema",
// target: "association",
// condition: "{{ $self.value === 'association' }}",
// schema: {
// "x-component-props": {
// "associatedKey": "{{ $form.values && $form.values.associatedKey }}"
// },
// },
// },
{
type: "value:schema",
target: "displayFields",
condition: "{{ $self.value === 'details' }}",
schema: {
"x-component-props": {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
{
type: "value:schema",
target: "displayFormFields",
condition: "{{ $self.value === 'details' }}",
schema: {
"x-component-props": {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
{
type: "value:schema",
target: "associationField",
condition: "{{ $self.value === 'association' }}",
schema: {
"x-component-props": {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
],
},
},
// {
// interface: 'string',
// type: 'string',
// name: 'association',
// title: '相关数据',
// component: {
// type: 'remoteSelect',
// showInDetail: true,
// showInForm: true,
// 'x-component-props': {
// resourceName: 'collections.fields',
// labelField: 'title',
// valueField: 'name',
// filter: {
// interface: 'linkTo',
// },
// },
// },
// },
{
interface: 'linkTo',
type: 'belongsTo',
name: 'associationField',
target: 'fields',
title: '相关数据表',
labelField: 'title',
required: true,
// valueField: 'name',
component: {
type: 'remoteSelect',
showInDetail: true,
showInForm: true,
'x-component-props': {
resourceName: 'collections.fields',
labelField: 'title',
// valueField: 'name',
objectValue: true,
filter: {
interface: 'linkTo',
},
},
"x-linkages": [
{
type: "value:visible",
target: "viewName",
condition: "{{ !!$self.value }}"
},
{
type: "value:schema",
target: "viewName",
condition: "{{ !!$self.value }}",
schema: {
"x-component-props": {
associatedKey: "{{ $self.value.target }}"
},
},
},
],
},
},
{
interface: 'json',
type: 'json',
name: 'displayFields',
title: '显示在详情中的字段',
labelField: 'title',
// valueField: 'name',
component: {
type: 'draggableTable',
showInDetail: true,
showInForm: true,
'x-component-props': {
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
mode: 'showInDetail',
fields: [
// {
// interface: 'sort',
// name: 'sort',
// title: '排序',
// type: 'sort',
// dataIndex: ['sort'],
// className: 'drag-visible',
// },
{
interface: 'string',
name: 'title',
title: '字段名称',
type: 'string',
className: 'drag-visible',
dataIndex: ['title'],
}
],
},
},
},
{
interface: 'json',
type: 'json',
name: 'displayFormFields',
title: '当前标签页可编辑字段',
labelField: 'title',
// valueField: 'name',
component: {
type: 'draggableTable',
showInDetail: true,
showInForm: true,
'x-component-props': {
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
mode: 'showInForm',
fields: [
// {
// interface: 'sort',
// name: 'sort',
// title: '排序',
// type: 'sort',
// dataIndex: ['sort'],
// className: 'drag-visible',
// },
{
interface: 'string',
name: 'title',
title: '字段名称',
type: 'string',
className: 'drag-visible',
dataIndex: ['title'],
}
],
},
},
},
{
interface: 'string',
type: 'string',
name: 'viewName',
title: '视图',
labelField: 'title',
required: true,
// valueField: 'name',
component: {
type: 'remoteSelect',
showInDetail: true,
showInForm: true,
'x-component-props': {
resourceName: 'collections.views',
labelField: 'title',
valueField: 'name',
},
},
},
{
interface: 'boolean',
type: 'radio',
name: 'default',
title: '作为默认标签页',
defaultValue: false,
scope: ['collection'],
component: {
type: 'checkbox',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'enabled',
title: '启用',
defaultValue: true,
component: {
type: 'checkbox',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'developerMode',
title: '开发者模式',
defaultValue: false,
component: {
type: 'boolean',
},
},
{
interface: 'linkTo',
type: 'belongsTo',
name: 'collection',
title: '所属数据表',
target: 'collections',
targetKey: 'name',
component: {
type: 'drawerSelect',
},
},
{
interface: 'json',
type: 'json',
name: 'options',
title: '配置信息',
defaultValue: {},
component: {
type: 'hidden',
},
},
],
actions: [
{
type: 'list',
name: 'list',
title: '查看',
},
{
type: 'destroy',
name: 'destroy',
title: '删除',
filter: {
default: false
}
},
{
type: 'create',
name: 'create',
title: '新增',
viewName: 'form',
},
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
},
],
views: [
{
type: 'form',
name: 'form',
title: '表单',
template: 'DrawerForm',
developerMode: true,
},
{
type: 'details',
name: 'details',
title: '详情',
template: 'Details',
actionNames: ['update'],
developerMode: true,
},
{
type: 'table',
name: 'simple',
title: '简易模式',
template: 'Table',
mode: 'simple',
default: true,
actionNames: ['destroy', 'create'],
detailsViewName: 'details',
updateViewName: 'form',
paginated: false,
draggable: true,
},
],
} as TableOptions;

View File

@ -1,403 +0,0 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'views',
title: '视图配置',
internal: true,
sortable: true,
model: 'ViewModel',
developerMode: true,
fields: [
{
interface: 'sort',
type: 'sort',
name: 'sort',
scope: ['collection'],
title: '排序',
component: {
type: 'sort',
className: 'drag-visible',
width: 60,
showInTable: true,
},
},
{
interface: 'string',
type: 'string',
name: 'title',
title: '视图名称',
required: true,
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: '视图类型',
required: true,
dataSource: [
{ label: '表格', value: 'table' },
{ label: '日历', value: 'calendar' },
// { label: '表单', value: 'form' },
{ label: '看板', value: 'kanban', disabled: true },
{ label: '地图', value: 'map', disabled: true },
],
component: {
type: 'radio',
showInTable: true,
showInDetail: true,
showInForm: true,
default: 'table',
"x-linkages": [
{
"type": "value:visible",
"target": "filter",
"condition": "{{ $self.value !== 'form' }}"
},
{
type: "value:schema",
target: "labelField",
"condition": "{{ $self.value === 'calendar' }}",
schema: {
"x-component-props": {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
{
type: "value:schema",
target: "startDateField",
"condition": "{{ $self.value === 'calendar' }}",
schema: {
"x-component-props": {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
{
type: "value:schema",
target: "endDateField",
"condition": "{{ $self.value === 'calendar' }}",
schema: {
"x-component-props": {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
{
"type": "value:visible",
"target": "labelField",
"condition": "{{ $self.value === 'calendar' }}",
},
{
"type": "value:visible",
"target": "startDateField",
"condition": "{{ $self.value === 'calendar' }}",
},
{
"type": "value:visible",
"target": "endDateField",
"condition": "{{ $self.value === 'calendar' }}",
},
],
},
},
{
interface: 'select',
type: 'virtual',
title: '标题字段',
name: 'labelField',
required: true,
component: {
type: 'remoteSelect',
showInDetail: true,
showInForm: true,
'x-component-props': {
mode: 'simple',
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
filter: {
type: 'string',
},
},
},
},
{
interface: 'select',
type: 'virtual',
title: '开始日期字段',
name: 'startDateField',
// required: true,
component: {
type: 'remoteSelect',
showInDetail: true,
showInForm: true,
'x-component-props': {
placeholder: '默认为创建时间字段',
mode: 'simple',
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
filter: {
type: 'date',
},
},
},
},
{
interface: 'select',
type: 'virtual',
title: '结束日期字段',
name: 'endDateField',
// required: true,
component: {
type: 'remoteSelect',
showInDetail: true,
showInForm: true,
'x-component-props': {
placeholder: '默认为创建时间字段',
mode: 'simple',
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
filter: {
type: 'date',
},
},
},
},
{
interface: 'json',
type: 'json',
name: 'filter',
title: '筛选数据',
developerMode: false,
mode: 'replace',
defaultValue: {},
component: {
type: 'filter',
showInForm: true,
},
},
{
interface: 'radio',
type: 'string',
name: 'mode',
title: '查看和编辑模式',
required: true,
dataSource: [
{ label: '常规模式', value: 'default' },
{ label: '快捷模式', value: 'simple' },
],
component: {
tooltip: "常规模式:点击数据进入查看界面,再次点击进入编辑界面<br/>快捷模式:点击数据直接打开编辑界面",
type: 'radio',
default: 'default',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'select',
type: 'string',
name: 'template',
title: '模板',
required: true,
developerMode: true,
dataSource: [
{ label: '表单', value: 'DrawerForm' },
{ label: '常规表格', value: 'Table' },
{ label: '简易表格', value: 'SimpleTable' },
{ label: '日历模板', value: 'Calendar' },
],
component: {
type: 'select',
default: 'Table',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'radio',
type: 'virtual',
name: 'defaultPerPage',
title: '默认每页显示几行数据',
defaultValue: 50,
dataSource: [
{ label: '10', value: 10 },
{ label: '20', value: 20 },
{ label: '50', value: 50 },
{ label: '100', value: 100 },
],
component: {
type: 'radio',
showInForm: true,
showInDetail: true,
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'draggable',
title: '支持拖拽数据排序',
showInForm: true,
showInDetail: true,
component: {
type: 'checkbox',
showInForm: true,
showInDetail: true,
},
},
{
interface: 'boolean',
type: 'radio',
name: 'default',
title: '作为默认视图',
defaultValue: false,
scope: ['collection'],
component: {
type: 'checkbox',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'showInDataMenu',
title: '作为数据表子菜单',
defaultValue: false,
component: {
type: 'checkbox',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'developerMode',
title: '开发者模式',
defaultValue: false,
component: {
type: 'boolean',
},
},
{
interface: 'linkTo',
type: 'belongsTo',
name: 'collection',
title: '所属数据表',
target: 'collections',
targetKey: 'name',
component: {
type: 'drawerSelect',
},
},
{
interface: 'json',
type: 'json',
name: 'options',
title: '配置信息',
defaultValue: {},
component: {
type: 'hidden',
},
},
// 以下暂不考虑
// {
// type: 'belongsToMany',
// name: 'fields',
// component: {
// type: 'drawerSelect',
// },
// },
// {
// type: 'belongsToMany',
// name: 'actions',
// component: {
// type: 'drawerSelect',
// },
// },
],
actions: [
{
type: 'list',
name: 'list',
title: '查看',
},
{
type: 'destroy',
name: 'destroy',
title: '删除',
filter: {
default: false
}
},
{
type: 'create',
name: 'create',
title: '新增',
viewName: 'form',
},
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
},
],
views: [
{
type: 'form',
name: 'form',
title: '表单',
template: 'DrawerForm',
developerMode: true,
},
{
type: 'details',
name: 'details',
title: '详情',
template: 'Details',
actionNames: ['update'],
developerMode: true,
},
{
type: 'table',
name: 'simple',
title: '简易模式',
template: 'SimpleTable',
mode: 'simple',
default: true,
actionNames: ['destroy', 'create'],
detailsViewName: 'details',
updateViewName: 'form',
paginated: false,
draggable: true,
},
],
} as TableOptions;

View File

@ -1,42 +0,0 @@
import CollectionModel from '../models/collection';
const defaultValues = {
actions: [
{
type: 'filter',
name: 'filter',
title: '筛选',
},
{
type: 'list',
name: 'list',
title: '查看',
},
{
type: 'destroy',
name: 'destroy',
title: '删除',
},
{
type: 'create',
name: 'create',
title: '新增',
viewName: 'form',
},
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
},
],
};
export default async function (model: CollectionModel, options: any = {}) {
const { migrate = true } = options;
console.log('plugin-collections hook', { migrate })
if (migrate) {
await model.migrate({ ...options, isNewRecord: true });
}
await model.updateAssociations(defaultValues, options);
}

View File

@ -1,8 +0,0 @@
import CollectionModel from '../models/collection';
export default async function (model: CollectionModel, options: any = {}) {
const { migrate = true } = options;
if (migrate) {
await model.migrate(options);
}
}

View File

@ -1,5 +0,0 @@
import CollectionModel from '../models/collection';
export default async function (model: CollectionModel) {
model.generateNameIfNull();
}

View File

@ -1,20 +0,0 @@
import { Model, ModelCtor } from '@nocobase/database';
/**
* create bulk update fk
*
* @param options
*/
export default async function (options: any = {}) {
const { migrate = true, where = {}, attributes: { collection_name }, transaction } = options;
if (migrate && collection_name) {
const Field = this.database.getModel('fields') as ModelCtor<Model>;
const fields = await Field.findAll({
where,
transaction,
});
for (const field of fields) {
await field.migrate(options);
}
}
}

View File

@ -1,21 +0,0 @@
import FieldModel from '../models/field';
import { BELONGSTO, BELONGSTOMANY, HASMANY } from '@nocobase/database';
export default async function (model: FieldModel, options: any = {}) {
const { migrate = true } = options;
const Collection = model.database.getModel('collections');
if (model.get('interface') === 'subTable') {
const target = model.get('target');
if (target && !model.database.isDefined(target)) {
await Collection.import({
name: target,
internal: true,
developerMode: true,
}, options);
}
}
if (migrate) {
await model.migrate(options);
}
await model.generatePairField(options);
}

View File

@ -1,9 +0,0 @@
import FieldModel from '../models/field';
export default async function (model: FieldModel, options: any = {}) {
const { migrate = true } = options;
if (migrate) {
await model.migrate(options);
}
await model.generatePairField(options);
}

View File

@ -1,27 +0,0 @@
import FieldModel, { generateValueName } from '../models/field';
import _ from 'lodash';
export default async function (model: FieldModel, options) {
// 生成随机 name 要放最后
// model.generateNameIfNull();
// 如果 collection_name 不存在
if (!model.get('collection_name') && model.get('parent_id')) {
const parent = await model.getParent({
...options,
});
const target = parent.get('target');
if (target) {
model.set('collection_name', target);
}
}
const dataSource = model.get('dataSource');
if (Array.isArray(dataSource)) {
model.set('dataSource', dataSource.map(item => {
if (item.value === null || typeof item.value === 'undefined') {
item.value = generateValueName();
}
return { ...item };
}));
}
}

View File

@ -1,5 +0,0 @@
import { BaseModel } from '../models';
export default async function (model: BaseModel) {
model.generateNameIfNull();
}

View File

@ -1,33 +0,0 @@
import collectionsBeforeValidate from './collections-before-validate';
import collectionsAfterCreate from './collections-after-create';
import collectionsAfterUpdate from './collections-after-update';
import fieldsBeforeValidate from './fields-before-validate';
import fieldsAfterCreate from './fields-after-create';
import fieldsAfterBulkUpdate from './fields-after-bulk-update';
import fieldsAfterUpdate from './fields-after-update';
import generateName from './generateName';
export default {
collections: {
beforeValidate: collectionsBeforeValidate,
afterCreate: collectionsAfterCreate,
afterUpdate: collectionsAfterUpdate,
},
fields: {
beforeValidate: fieldsBeforeValidate,
afterCreate: fieldsAfterCreate,
afterUpdate: fieldsAfterUpdate,
afterBulkUpdate: fieldsAfterBulkUpdate,
},
actions: {
beforeValidate: generateName
},
views: {
beforeValidate: generateName
},
tabs: {
beforeValidate: generateName
},
};

View File

@ -1,47 +0,0 @@
/**
* Interface 便
*/
import * as types from './types';
export * as types from './types';
export const groupLabelMap = {
basic: '基本类型',
media: '多媒体类型',
choices: '选择类型',
datetime: '日期和时间',
relation: '关系类型',
systemInfo: '关系类型',
developerMode: '开发者模式',
others: '其他'
};
export function getOptions() {
return Object.keys(groupLabelMap).map(key => ({
key,
label: groupLabelMap[key],
children: Object.values(types)
.filter(type => type['group'] === key)
.map(type => ({
label: type.title,
value: type.options.interface,
// TODO(draft): 配置信息一并存到数据库方便字段配置时取出参与联动计算
// properties: type.properties,
disabled: type['disabled'],
}))
}));
}
export type interfaceType = {
title: string,
group?: string,
options: {
[key: string]: any
},
disabled?: boolean
};
// TODO(draft)
// 目前仅在内存中注册,应用启动时需要解决扩展字段读取并注册到内存
export function register(type: interfaceType) {
types[type.options.interface] = type;
}

View File

@ -1,638 +0,0 @@
// mergeinterface 模板,旧数据,用户数据
// TODO: 删除的情况怎么处理
// 联动的原则:尽量减少干预,尤其是尽量少改动 typetype 兼容
// 参数的优先级:
// 1、interfacetype 尽量只随 interface 变动,而不受别的字段影响(特殊情况除外)
// 2、
// TODO: interface 的修改
export const string = {
title: '单行文本',
group: 'basic',
options: {
interface: 'string',
type: 'string',
filterable: true,
component: {
type: 'string',
},
},
};
export const textarea = {
title: '多行文本',
group: 'basic',
options: {
interface: 'textarea',
type: 'text',
filterable: true,
component: {
type: 'textarea',
},
}
};
export const phone = {
title: '手机号码',
group: 'basic',
options: {
interface: 'phone',
type: 'string',
filterable: true,
format: 'phone', // 验证的问题
component: {
type: 'string',
'x-rules': 'phone',
},
},
};
export const email = {
title: '邮箱',
group: 'basic',
options: {
interface: 'email',
type: 'string',
filterable: true,
format: 'email',
component: {
type: 'string',
'x-rules': 'email',
},
},
};
/**
* precision
*/
export const number = {
title: '数字',
group: 'basic',
options: {
interface: 'number',
type: 'float',
filterable: true,
sortable: true,
precision: 0, // 需要考虑
component: {
type: 'number',
},
}
};
/**
* precision
*
*/
export const percent = {
title: '百分比',
group: 'basic',
options: {
interface: 'percent',
type: 'float',
filterable: true,
sortable: true,
precision: 0,
component: {
type: 'percent',
},
},
};
export const markdown = {
title: 'Markdown',
group: 'media',
options: {
interface: 'markdown',
type: 'json',
component: {
type: 'markdown',
},
},
};
export const wysiwyg = {
title: '可视化编辑器',
group: 'media',
disabled: true,
options: {
interface: 'wysiwyg',
type: 'json',
component: {
type: 'wysiwyg',
},
},
};
/**
*
*/
export const attachment = {
title: '附件',
group: 'media',
// disabled: true,
options: {
interface: 'attachment',
type: 'belongsToMany',
filterable: false,
target: 'attachments',
// storage: {
// name: 'local',
// },
component: {
type: 'upload',
},
},
};
/**
*
*/
export const select = {
title: '下拉选择(单选)',
group: 'choices',
options: {
interface: 'select',
type: 'string',
filterable: true,
dataSource: [],
component: {
type: 'select',
},
},
};
/**
* type
* json
* type=array
* array
* filter
* json hasMany
*
* 🤔 select合并成一个 interfacemultiple type
*/
export const multipleSelect = {
title: '下拉选择(多选)',
group: 'choices',
options: {
interface: 'multipleSelect',
type: 'json', // json 过滤
filterable: true,
dataSource: [],
defaultValue: [],
multiple: true, // 需要重点考虑
component: {
type: 'select',
},
},
};
export const radio = {
title: '单选框',
group: 'choices',
options: {
interface: 'radio',
type: 'string',
filterable: true,
dataSource: [],
component: {
type: 'radio',
},
},
};
export const checkboxes = {
title: '多选框',
group: 'choices',
options: {
interface: 'checkboxes',
type: 'json',
filterable: true,
dataSource: [],
defaultValue: [],
component: {
type: 'checkboxes',
},
},
};
export const boolean = {
title: '是/否',
group: 'choices',
options: {
interface: 'boolean',
type: 'boolean',
filterable: true,
component: {
type: 'checkbox', // switch
},
},
};
/**
* dateonly type
* dateonly
*/
export const datetime = {
title: '日期',
group: 'datetime',
options: {
interface: 'datetime',
type: 'date',
showTime: false,
filterable: true,
sortable: true,
dateFormat: 'YYYY-MM-DD',
timeFormat: 'HH:mm:ss',
component: {
type: 'date',
},
},
// TODO(draft): 配置参数的一种描述方式
// properties: [
// {
// name: 'showTime',
// title: '显示时间',
// interface: 'boolean',
// component: {
// 'x-linkages': [
// {
// type: 'value:visible',
// target: 'timeFormat',
// condition: '{{ $value }}'
// }
// ]
// }
// },
// {
// interface: 'string',
// name: 'timeFormat',
// title: '时间格式',
// filterable: false
// }
// ]
};
export const time = {
title: '时间',
group: 'datetime',
options: {
interface: 'time',
type: 'time',
filterable: true,
sortable: true,
timeFormat: 'HH:mm:ss',
component: {
type: 'time',
},
},
};
/**
*
*
* hasMany
* fields
*
* - virtual
* - hasMany
* - targetsource
*/
// 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: '子表格',
group: 'relation',
// disabled: true,
options: {
interface: 'subTable',
type: 'hasMany',
// fields: [],
component: {
type: 'subTable',
},
},
};
/**
* multiple
*
*
* name target addField target
* name targetname
* 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: '关联数据',
group: 'relation',
// disabled: true,
options: {
interface: 'linkTo',
multiple: true, // 可能影响 type
paired: false,
type: 'belongsToMany',
// name,
// target: '关联表', // 用户会输入
filterable: false,
component: {
type: 'drawerSelect',
},
},
};
export const createdAt = {
title: '创建时间',
group: 'systemInfo',
options: {
interface: 'createdAt',
type: 'date',
// name: 'created_at',
field: 'created_at',
showTime: false,
dateFormat: 'YYYY-MM-DD',
timeFormat: 'HH:mm:ss',
required: true,
filterable: true,
sortable: true,
component: {
type: 'date',
},
},
};
export const updatedAt = {
title: '修改时间',
group: 'systemInfo',
options: {
interface: 'updatedAt',
type: 'date',
// name: 'updated_at',
field: 'updated_at',
showTime: false,
dateFormat: 'YYYY-MM-DD',
timeFormat: 'HH:mm:ss',
required: true,
filterable: true,
sortable: true,
component: {
type: 'date',
},
},
};
export const createdBy = {
title: '创建人',
group: 'systemInfo',
// disabled: true,
options: {
interface: 'createdBy',
type: 'createdBy',
// name: 'createdBy',
// filterable: true,
target: 'users',
labelField: 'nickname',
foreignKey: 'created_by_id',
appends: true,
component: {
type: 'drawerSelect',
},
},
};
export const updatedBy = {
title: '修改人',
group: 'systemInfo',
// disabled: true,
options: {
interface: 'updatedBy',
// name: 'updatedBy',
type: 'updatedBy',
// filterable: true,
target: 'users',
labelField: 'nickname',
foreignKey: 'updated_by_id',
appends: true,
component: {
type: 'drawerSelect',
},
},
};
/**
*
*
*
*/
export const group = {
title: '字段组',
disabled: true,
options: {
interface: 'group',
// name: 'id',
type: 'virtual',
component: {
type: 'hidden',
},
},
};
export const status = {
title: '状态',
group: 'others',
options: {
interface: 'status',
name: 'status',
type: 'string',
filterable: true,
// index: true,
dataSource: [
{
label: '已发布',
value: 'publish',
},
{
label: '草稿',
value: 'draft',
}
],
component: {
type: 'select',
},
},
}
export const description = {
title: '说明文字',
group: 'others',
options: {
interface: 'description',
// name: 'id',
type: 'virtual',
component: {
type: 'description',
},
},
}
/**
*
*/
export const primaryKey = {
title: '主键',
group: 'developerMode',
options: {
interface: 'primaryKey',
name: 'id',
type: 'integer',
required: true,
autoIncrement: true,
primaryKey: true,
filterable: true,
developerMode: true,
component: {
type: 'number',
},
},
};
/**
*
* scope
*/
export const sort = {
title: '排序',
group: 'developerMode',
options: {
interface: 'sort',
type: 'integer',
required: true,
component: {
type: 'sort',
showInTable: true,
},
},
};
export const password = {
title: '密码',
group: 'developerMode',
options: {
interface: 'password',
type: 'password',
hidden: true, // hidden 用来控制 api 不输出这个字段,但是可能这个字段显示在表单里 showInForm
component: {
type: 'password',
},
},
};
export const json = {
title: 'JSON',
group: 'developerMode',
options: {
interface: 'json',
type: 'json',
mode: 'replace',
// developerMode: true,
component: {
type: 'hidden',
},
},
};
export const icon = {
title: '图标',
group: 'developerMode',
options: {
interface: 'icon',
type: 'string',
component: {
type: 'icon',
},
},
};
export const chinaRegion = {
title: '中国行政区划',
group: 'choices',
options: {
interface: 'chinaRegion',
type: 'belongsToMany',
// 数据来源的数据表,与 dataSource 不同,需要从表数据加载后转化成 dataSource
target: 'china_regions',
targetKey: 'code',
// 值字段
// valueField: 'code',
// 名称字段
labelField: 'name',
// TODO(refactor): 等 toWhere 重构完成后要改成 parent
// 上级字段名
parentField: 'parent_code',
// 深度限制,默认:-1代表不控制即如果是数据表则无限加载
// limit: -1,
// 可选层级,默认:-1代表可选的最深层级
// maxLevel: null,
// 是否可以不选择到最深一级
// 'x-component-props': { changeOnSelect: true }
incompletely: false,
component: {
type: 'cascader',
}
}
};

View File

@ -1,6 +0,0 @@
import _ from 'lodash';
import BaseModel from './base';
export class ActionModel extends BaseModel {
}

View File

@ -1,142 +0,0 @@
import _ from 'lodash';
import { getDataTypeKey, Model } from '@nocobase/database';
import { merge } from '../utils';
export function generateName(title?: string): string {
return `${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
}
export class BaseModel extends Model {
generateName() {
this.set('name', generateName());
}
generateNameIfNull() {
if (!this.get('name')) {
this.generateName();
}
}
get additionalAttribute() {
const tableOptions = this.database.getTable(this.constructor.name).getOptions();
return _.get(tableOptions, 'additionalAttribute') || 'options';
}
hasGetAttribute(key: string) {
const attribute = this.rawAttributes[key];
// virtual 如果有 get 方法就直接走 get
if (attribute && attribute.type && getDataTypeKey(attribute.type) === 'VIRTUAL') {
return !!attribute.get;
}
return !!attribute;
}
hasSetAttribute(key: string) {
const attribute = this.rawAttributes[key];
// virtual 如果有 set 方法就直接走 set
if (attribute && attribute.type && getDataTypeKey(attribute.type) === 'VIRTUAL') {
return !!attribute.set;
}
return !!attribute;
}
get(key?: any, options?: any) {
if (typeof key === 'string') {
const [column, ...path] = key.split('.');
if (this.hasGetAttribute(column)) {
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('.');
if (this.hasGetAttribute(column)) {
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 super.set(key, value, options);
}
// 如果是 object 数据merge 处理
if (_.isPlainObject(value)) {
// TODO 需要改进 JSON 字段的内部处理逻辑,暂时这里跳过了特殊的 filter 字段
if (key !== 'filter') {
// console.log(key, value);
// @ts-ignore
value = merge(this.get(key) || {}, value);
}
}
const [column, ...path] = key.split('.');
if (!options.raw) {
this.changed(column, true);
}
if (this.hasSetAttribute(column)) {
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);
if (!options.raw) {
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 super.setDataValue(key, value);
}
if (_.isPlainObject(value)) {
// @ts-ignore
value = Utils.merge(this.get(key) || {}, value);
}
const [column, ...path] = key.split('.');
this.changed(column, true);
if (this.hasSetAttribute(column)) {
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;

View File

@ -1,241 +0,0 @@
import _ from 'lodash';
import BaseModel from './base';
import Field from './field';
import { TableOptions } from '@nocobase/database';
import { SaveOptions, Op } from 'sequelize';
/**
*
*
* 使 3+2
* 1. id
* 2.
* 3.
* 4.
* 5.
*
* @param title
*/
export function generateCollectionName(title?: string): string {
return `t_${Date.now().toString(36)}_${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
}
export interface LoadOptions {
reset?: boolean;
where?: any;
skipExisting?: boolean;
[key: string]: any;
}
export interface MigrateOptions {
[key: string]: any;
}
export class CollectionModel extends BaseModel {
generateName() {
this.set('name', generateCollectionName());
}
/**
* name collection
*
* @param name
*/
static async findByName(name: string) {
return this.findOne({ where: { name } });
}
/**
* DOTO
* - database.table
* -
*
* @param opts
*/
async loadTableOptions(opts: any = {}) {
const options = await this.getOptions();
// const prevTable = this.database.getTable(this.get('name'));
// const prevOptions = prevTable ? prevTable.getOptions() : {};
// table 是初始化和重新初始化
const table = this.database.table(options);
// console.log({options, actions: table.getOptions()['actions']})
// 如果关系表未加载,一起处理
// const associationTableNames = [];
// for (const [key, association] of table.getAssociations()) {
// // TODO是否需要考虑重载的情况暂时是跳过处理
// if (!this.database.isDefined(association.options.target)) {
// continue;
// }
// associationTableNames.push(association.options.target);
// }
// console.log({associationTableNames});
// if (associationTableNames.length) {
// await CollectionModel.load({
// ...opts,
// where: {
// name: {
// [Op.in]: associationTableNames,
// }
// }
// });
// }
return table;
}
/**
*
*/
async migrate(options: MigrateOptions = {}) {
const { isNewRecord } = options;
const table = await this.loadTableOptions(options);
// 如果不是新增数据force 必须为 false
if (!isNewRecord) {
return await table.sync({
force: false,
alter: {
drop: false,
}
});
}
// TODO: 暂时加了个 collectionSync 解决 collection.create 的数据不清空问题
// @ts-ignore
const sync = this.sequelize.options.collectionSync;
return await table.sync(sync || {
force: false,
alter: {
drop: false,
}
});
}
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> {
const options: any = {
...this.get(),
actions: await this.getActions(),
fields: await this.getFieldsOptions(),
}
// @ts-ignore
// console.log(this.constructor.associations);
// @ts-ignore
if (this.constructor.hasAlias('views_v2')) {
options.views_v2 = await this.getViews_v2();
}
return options;
}
/**
* TODO
*
* @param options
*/
static async load(options: LoadOptions = {}) {
const { skipExisting = false, reset = false, where = {}, transaction } = options;
const collections = await this.findAll({
transaction,
where,
});
for (const collection of collections) {
if (skipExisting && this.database.isDefined(collection.get('name'))) {
continue;
}
await collection.loadTableOptions({
transaction,
reset,
});
}
}
static async import(data: TableOptions, options: SaveOptions = {}): Promise<CollectionModel> {
data = _.cloneDeep(data);
// @ts-ignore
const { update } = options;
let collection: CollectionModel;
if (data.name) {
collection = await this.findOne({
...options,
where: {
name: data.name,
},
});
}
if (collection) {
// @ts-ignore
await collection.update(data, options);
}
if (!collection) {
// @ts-ignore
collection = await this.create(data, options);
}
const associations = ['fields', 'actions', 'views_v2'];
for (const key of associations) {
if (!Array.isArray(data[key])) {
continue;
}
const Model = this.database.getModel(key);
if (!Model) {
continue;
}
let ids = [];
for (const index in data[key]) {
if (key === 'fields') {
ids = await Model.import(data[key], {
...options,
collectionName: collection.name,
});
continue;
}
let model;
const item = data[key][index];
if (item.name) {
model = await Model.findOne({
...options,
where: {
collection_name: collection.name,
name: item.name,
},
});
}
if (model) {
await model.update({
...item,
// sort: index+1
}, options);
}
if (!model) {
model = await Model.create(
{
...item,
// sort: index+1,
collection_name: collection.name,
},
// @ts-ignore
options
);
}
if (model) {
ids.push(model.id);
}
}
if (ids.length && collection.get('internal')) {
await collection.updateAssociations({
[key]: ids,
});
}
}
return collection;
}
}
export default CollectionModel;

View File

@ -1,263 +0,0 @@
import _ from 'lodash';
import BaseModel from './base';
import { FieldOptions, BELONGSTO, BELONGSTOMANY, HASMANY } from '@nocobase/database';
import * as types from '../interfaces/types';
import { merge } from '../utils';
import { BuildOptions } from 'sequelize';
import { SaveOptions, Utils } from 'sequelize';
import { generateCollectionName } from './collection';
interface FieldImportOptions extends SaveOptions {
parentId?: number;
collectionName?: string;
}
export function generateValueName(title?: string): string {
return `${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
}
export function generateFieldName(title?: string): string {
return `f_${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
}
export class FieldModel extends BaseModel {
constructor(values: any = {}, options: any = {}) {
let data = {
...(values.options || {}),
...values,
// ..._.omit(values, 'options'),
};
const interfaceType = data.interface;
if (interfaceType) {
const { options } = types[interfaceType];
let args = [options, data];
// @ts-ignore
data = merge(...args);
if (['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(data.type)) {
// 关系字段如果没有 name相关参数都随机生成
if (!data.name) {
data.name = generateFieldName();
data.paired = true;
// 通用,关系表
if (!data.target) {
data.target = generateCollectionName();
}
// 通用,外键
if (!data.foreignKey) {
data.foreignKey = generateFieldName();
}
if (data.type !== 'belongsTo' && !data.sourceKey) {
data.sourceKey = 'id';
}
if (['belongsTo', 'belongsToMany'].includes(data.type) && !data.targetKey) {
data.targetKey = 'id';
}
// 多对多关联
if (data.type === 'belongsToMany') {
if (!data.through) {
data.through = generateCollectionName();
}
if (!data.otherKey) {
data.otherKey = generateFieldName();
}
}
}
// 有 name但是没有 target
if (!data.target) {
data.target = ['hasOne', 'belongsTo'].includes(data.type) ? Utils.pluralize(data.name) : data.name;
}
}
if (!data.name) {
data.name = generateFieldName();
}
}
// @ts-ignore
super(data, options);
}
generateName() {
this.set('name', generateFieldName());
}
async generatePairField(options) {
const { interface: control, paired, type, target, sourceKey, targetKey, foreignKey, otherKey, through, collection_name } = this.get();
if (control !== 'linkTo' || type !== 'belongsToMany' || !collection_name || !paired) {
return;
}
if (!this.database.isDefined(target)) {
return;
}
const targetTable = this.database.getTable(target);
const Field = FieldModel;
let labelField = 'id';
const targetField = await Field.findOne({
...options,
where: {
type: 'string',
collection_name: target,
},
order: [['sort', 'asc']],
});
if (targetField) {
labelField = targetField.get('name');
}
const collection = await this.getCollection(options);
let targetOptions: any = {
...types.linkTo.options,
interface: 'linkTo',
title: collection.get('title'),
collection_name: target,
options: {
paired: true,
target: collection_name,
labelField,
},
component: {
showInTable: true,
showInForm: true,
showInDetail: true,
},
};
// 暂时不处理 hasone
switch (type) {
case 'hasMany':
targetOptions.type = 'belongsTo';
targetOptions.options.targetKey = sourceKey;
targetOptions.options.foreignKey = foreignKey;
break;
case 'belongsTo':
targetOptions.type = 'hasMany';
targetOptions.options.sourceKey = targetKey;
targetOptions.options.foreignKey = foreignKey;
break;
case 'belongsToMany':
targetOptions.type = 'belongsToMany';
targetOptions.options.sourceKey = targetKey;
targetOptions.options.foreignKey = otherKey;
targetOptions.options.targetKey = sourceKey;
targetOptions.options.otherKey = foreignKey;
targetOptions.options.through = through;
break;
}
const associations = targetTable.getAssociations();
// console.log(associations);
for (const association of associations.values()) {
if (association instanceof BELONGSTOMANY) {
if (
association.options.foreignKey === otherKey
&& association.options.sourceKey === targetKey
&& association.options.otherKey === foreignKey
&& association.options.targetKey === sourceKey
&& association.options.through === through
) {
return;
}
}
// if (association instanceof BELONGSTO) {
// continue;
// }
// if (association instanceof HASMANY) {
// continue;
// }
}
const f = await Field.create(targetOptions, options);
// console.log({targetOptions}, f.get('options'));
}
setInterface(value) {
const { options } = types[value];
let args = [];
// 如果是新数据或 interface 不相等interface options 放后
if (this.isNewRecord || this.get('interface') !== value) {
args = [this.get(), options];
} else {
// 已存在的数据更新不相等interface options 放前面
args = [options, this.get()];
}
// @ts-ignore
const values = merge(...args);
this.set(values);
}
async getOptions(): Promise<FieldOptions> {
return this.get();
}
async migrate(options: any = {}) {
const collectionName = this.get('collection_name');
if (!collectionName) {
return false;
}
if (!this.database.isDefined(collectionName)) {
throw new Error(`${collectionName} is not defined`);
}
const table = this.database.getTable(collectionName);
table.addField(await this.getOptions());
await table.sync({
force: false,
alter: {
drop: false,
}
});
}
static async import(items: any, options: FieldImportOptions = {}): Promise<any> {
const { parentId, collectionName } = options;
if (!Array.isArray(items)) {
items = [items];
}
const ids = [];
for (const index in items) {
const item = items[index];
let model;
const where: any = {};
if (parentId) {
where.parent_id = parentId
} else {
where.collection_name = collectionName;
}
if (item.name) {
model = await this.findOne({
...options,
where: {
...where,
name: item.name,
},
});
}
if (!model) {
const tmp: any = {};
if (parentId) {
tmp.parent_id = parentId
} else {
tmp.collection_name = collectionName;
}
model = await this.create(
{
...item,
...tmp,
},
//@ts-ignore
options
);
} else {
//@ts-ignore
await model.update(item, options);
}
if (Array.isArray(item.children)) {
const childrenIds = await this.import(item.children, {
...options,
parentId: model.id,
collectionName,
});
await model.updateAssociations({
children: childrenIds,
}, options);
}
}
return ids;
}
}
export default FieldModel;

View File

@ -1,7 +0,0 @@
export * from './base';
export * from './action';
export * from './collection';
export * from './field';
export * from './tab';
export * from './view';
export * from './page';

View File

@ -1,43 +0,0 @@
import _ from 'lodash';
import BaseModel from './base';
import { SaveOptions, Op } from 'sequelize';
interface PageImportOptions extends SaveOptions {
parentId?: number;
}
/**
*
*/
export class PageModel extends BaseModel {
static async import(items: any, options: PageImportOptions = {}): Promise<any> {
const { parentId } = options;
if (!Array.isArray(items)) {
items = [items];
}
for (const item of items) {
let page = await this.findOne({
...options,
where: {
path: item.path,
},
});
if (!page) {
page = await this.create(
{
...item,
parent_id: parentId,
},
// @ts-ignore
options
);
}
if (Array.isArray(item.children)) {
await this.import(item.children, {
...options,
parentId: page.id,
});
}
}
}
}

View File

@ -1,6 +0,0 @@
import _ from 'lodash';
import BaseModel from './base';
export class TabModel extends BaseModel {
}

View File

@ -1,6 +0,0 @@
import _ from 'lodash';
import BaseModel from './base';
export class ViewModel extends BaseModel {
}

View File

@ -1,77 +0,0 @@
import path from 'path';
import { Application } from '@nocobase/server';
import hooks from './hooks';
import { registerModels, Table } from '@nocobase/database';
import * as models from './models';
export default async function (this: Application, options = {}) {
const database = this.database;
const resourcer = this.resourcer;
// 提供全局的 models 注册机制
registerModels(models);
database.import({
directory: path.resolve(__dirname, 'collections'),
});
database.addHook('afterUpdateAssociations', async function (model, options) {
if (model instanceof models.FieldModel) {
if (model.get('interface') === 'subTable') {
const { migrate = true } = options;
const Collection = model.database.getModel('collections');
await Collection.load({ ...options, where: { name: model.get('collection_name') } });
migrate && await model.migrate(options);
}
}
});
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]);
});
});
const Collection = database.getModel('collections');
Collection.addHook('afterCreate', async (model: any, options) => {
if (model.get('developerMode')) {
return;
}
if (model.get('statusable') === false) {
return;
}
console.log("model.get('developerMode')", model.get('name'));
const { transaction = await model.sequelize.transaction() } = options;
await model.createField({
interface: 'radio',
name: 'status',
type: 'string',
filterable: true,
title: '状态',
// index: true,
dataSource: [
{
label: '已发布',
value: 'publish',
},
{
label: '草稿',
value: 'draft',
}
],
component: {
type: 'radio',
},
}, { transaction });
if (!options.transaction) {
await transaction.commit();
}
});
}

View File

@ -7,16 +7,33 @@ import { create } from './actions/fields';
export default async function (this: Application, options = {}) {
const database = this.database;
registerModels(models);
database.import({
directory: path.resolve(__dirname, 'collections'),
});
this.on('plugins.afterLoad', async () => {
console.log('plugins.afterLoad');
this.on('pluginsLoaded', async () => {
console.log('pluginsLoaded');
await database.getModel('collections').load();
});
this.on('collections.init', async () => {
const userTable = database.getTable('users');
const config = userTable.getOptions();
const Collection = database.getModel('collections');
const collection = await Collection.create(config);
await collection.updateAssociations({
generalFields: config.fields.filter((field) => field.state !== 0),
systemFields: config.fields.filter((field) => field.state === 0),
});
await collection.migrate();
});
const [Collection, Field] = database.getModels(['collections', 'fields']);
Field.beforeCreate(async (model) => {
database.on('fields.beforeCreate', async (model) => {
if (!model.get('name')) {
model.set('name', model.get('key'));
}
@ -30,7 +47,8 @@ export default async function (this: Application, options = {}) {
}
}
});
Field.beforeUpdate(async (model) => {
database.on('fields.beforeUpdate', async (model) => {
console.log('beforeUpdate', model.key);
if (!model.get('collection_name') && model.get('parentKey')) {
const field = await Field.findByPk(model.get('parentKey'));
@ -42,7 +60,8 @@ export default async function (this: Application, options = {}) {
}
}
});
Field.afterCreate(async (model, options) => {
database.on('fields.afterCreate', async (model) => {
console.log('afterCreate', model.key, model.get('collection_name'));
if (model.get('interface') !== 'subTable') {
return;
@ -71,7 +90,8 @@ export default async function (this: Application, options = {}) {
throw error;
}
});
Field.afterUpdate(async (model) => {
database.on('fields.afterUpdate', async (model) => {
console.log('afterUpdate');
if (model.get('interface') !== 'subTable') {
return;
@ -97,6 +117,7 @@ export default async function (this: Application, options = {}) {
throw error;
}
});
this.resourcer.registerActionHandler('collections.fields:create', create);
this.resourcer.registerActionHandler('collections:findAll', findAll);
this.resourcer.registerActionHandler('collections:createOrUpdate', createOrUpdate);

View File

@ -1,8 +1,8 @@
import xlsx from 'node-xlsx';
import { actions } from '@nocobase/actions';
import { actions, Context, Next } from '@nocobase/actions';
import render from '../renders';
async function _export(ctx: actions.Context, next: actions.Next) {
async function _export(ctx: Context, next: Next) {
let { columns } = ctx.action.params;
if (typeof columns === 'string') {
columns = JSON.parse(columns);
@ -13,7 +13,7 @@ async function _export(ctx: actions.Context, next: actions.Next) {
payload: 'replace',
});
console.log({ columns });
await actions.common.list(ctx, async () => {
await actions.list(ctx, async () => {
const {
db,
action: {

View File

@ -8,21 +8,21 @@ export default async function (options = {}) {
resourcer.registerActionHandler(ACTION_NAME_EXPORT, _export);
// TODO(temp): 继承 list 权限的临时写法
resourcer.use(async (ctx, next) => {
if (ctx.action.params.actionName === ACTION_NAME_EXPORT) {
ctx.action.mergeParams({
actionName: 'list'
});
// // TODO(temp): 继承 list 权限的临时写法
// resourcer.use(async (ctx, next) => {
// if (ctx.action.params.actionName === ACTION_NAME_EXPORT) {
// ctx.action.mergeParams({
// actionName: 'list'
// });
console.log('action name in export has been rewritten to:', ctx.action.params.actionName);
// console.log('action name in export has been rewritten to:', ctx.action.params.actionName);
const permissionPlugin = ctx.app.getPluginInstance('@nocobase/plugin-permissions');
if (permissionPlugin) {
return permissionPlugin.middleware(ctx, next);
}
}
// const permissionPlugin = ctx.app.getPluginInstance('@nocobase/plugin-permissions');
// if (permissionPlugin) {
// return permissionPlugin.middleware(ctx, next);
// }
// }
await next();
});
// await next();
// });
}

View File

@ -1,11 +1,11 @@
import path from 'path';
import multer from '@koa/multer';
import actions from '@nocobase/actions';
import { Context, Next } from '@nocobase/actions';
import storageMakers from '../storages';
import * as Rules from '../rules';
import { FILE_FIELD_NAME, LIMIT_FILES, LIMIT_MAX_FILE_SIZE } from '../constants';
function getRules(ctx: actions.Context) {
function getRules(ctx: Context) {
const { resourceField } = ctx.action.params;
if (!resourceField) {
return ctx.storage.rules;
@ -15,7 +15,7 @@ function getRules(ctx: actions.Context) {
}
// TODO(optimize): 需要优化错误处理,计算失败后需要抛出对应错误,以便程序处理
function getFileFilter(ctx: actions.Context) {
function getFileFilter(ctx: Context) {
return (req, file, cb) => {
// size 交给 limits 处理
const { size, ...rules } = getRules(ctx);
@ -27,7 +27,7 @@ function getFileFilter(ctx: actions.Context) {
}
}
export async function middleware(ctx: actions.Context, next: actions.Next) {
export async function middleware(ctx: Context, next: Next) {
const { resourceName, actionName, resourceField } = ctx.action.params;
if (actionName !== 'upload') {
return next();
@ -79,7 +79,7 @@ export async function middleware(ctx: actions.Context, next: actions.Next) {
return upload.single(FILE_FIELD_NAME)(ctx, next);
};
export async function action(ctx: actions.Context, next: actions.Next) {
export async function action(ctx: Context, next: Next) {
const { [FILE_FIELD_NAME]: file, storage } = ctx;
if (!file) {
return ctx.throw(400, 'file validation failed');

View File

@ -22,4 +22,27 @@ export default async function () {
resourcer.use(uploadMiddleware);
resourcer.registerActionHandler('upload', uploadAction);
localMiddleware(this);
this.on('file-manager.init', async () => {
const Storage = database.getModel('storages');
await Storage.create({
title: '本地存储',
name: `local`,
type: 'local',
baseUrl: process.env.LOCAL_STORAGE_BASE_URL,
default: process.env.STORAGE_TYPE === 'local',
});
await Storage.create({
name: `ali-oss`,
type: 'ali-oss',
baseUrl: process.env.ALI_OSS_STORAGE_BASE_URL,
options: {
region: process.env.ALI_OSS_REGION,
accessKeyId: process.env.ALI_OSS_ACCESS_KEY_ID,
accessKeySecret: process.env.ALI_OSS_ACCESS_KEY_SECRET,
bucket: process.env.ALI_OSS_BUCKET,
},
default: process.env.STORAGE_TYPE === 'ali-oss',
});
});
}

View File

@ -1,7 +0,0 @@
node_modules
*.log
docs
__tests__
tsconfig.json
src
.fatherrc.ts

View File

@ -1,18 +0,0 @@
{
"name": "@nocobase/plugin-full-collections",
"version": "0.4.0-alpha.7",
"main": "lib/index.js",
"license": "MIT",
"private": true,
"dependencies": {
"@nocobase/server": "^0.4.0-alpha.7",
"crypto-random-string": "^3.3.1",
"deepmerge": "^4.2.2",
"just-has": "^1.0.0"
},
"devDependencies": {
"@nocobase/actions": "^0.4.0-alpha.7",
"@nocobase/database": "^0.4.0-alpha.7",
"@nocobase/resourcer": "^0.4.0-alpha.7"
}
}

View File

@ -1,67 +0,0 @@
import { Agent, getAgent, getApp } from '.';
import { Application } from '@nocobase/server';
import Database, { ModelCtor, Model } from '@nocobase/database';
describe('collections', () => {
let app: Application;
let agent: Agent;
let db: Database;
let Collection: ModelCtor<Model>;
beforeEach(async () => {
app = await getApp();
agent = getAgent(app);
db = app.database;
Collection = db.getModel('collections');
});
afterEach(() => app.database.close());
it('create', async () => {
const collection = await Collection.create({
name: 'test1',
});
await collection.updateAssociations({
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'string',
name: 'title',
},
],
});
});
it('create2', async () => {
const collection = await Collection.create({});
await collection.updateAssociations({
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'string',
},
],
});
});
it('api', async () => {
await agent.resource('collections').create({
values: {
name: 'test2',
},
});
await agent.resource('collections.fields').create({
associatedKey: 'test2',
values: {
type: 'string',
name: 'name',
},
});
});
});

View File

@ -1,81 +0,0 @@
import { Agent, getAgent, getApp } from '.';
import { Application } from '@nocobase/server';
import Database, { ModelCtor, Model, FieldOptions } from '@nocobase/database';
import { FieldModel } from '../models';
type Options = { name?: string } & FieldOptions;
describe('fields', () => {
let app: Application;
let agent: Agent;
let db: Database;
let Collection: ModelCtor<Model>;
let collection: Model;
beforeEach(async () => {
app = await getApp();
agent = getAgent(app);
db = app.database;
Collection = db.getModel('collections');
collection = await Collection.create({ name: 'foos' });
await Collection.create({ name: 'bars' });
const tables = db.getTables();
for (const table of tables.values()) {
await Collection.import(table.getOptions(), { migrate: false });
}
});
afterEach(() => app.database.close());
async function createField(options: any) {
return await collection.createField(options);
}
describe('basic', () => {
it.only('string', async () => {
// await createField({
// interface: 'string',
// });
});
it('number', async () => {
await createField({
interface: 'number',
});
});
});
describe('relation', () => {
it('linkTo', async () => {
const field = await createField({
interface: 'linkTo',
'x-linkTo-props': {
multiple: false,
target: 'bars',
},
});
const data = field.get();
const keys = ['target', 'multiple', 'foreignKey', 'otherKey', 'sourceKey', 'targetKey'];
for (const key of keys) {
expect(data[key]).toEqual(field.get(key));
}
expect(data['x-linkTo-props']).toEqual({ target: 'bars', multiple: false });
});
it('linkTo', async () => {
const field = await createField({
interface: 'linkTo',
type: 'hasMany',
'x-linkTo-props': {
// multiple: false,
target: 'bars',
},
});
const data = field.get();
const keys = ['target', 'multiple', 'foreignKey', 'otherKey', 'sourceKey', 'targetKey'];
for (const key of keys) {
expect(data[key]).toEqual(field.get(key));
}
expect(data['x-linkTo-props']).toEqual({ target: 'bars', multiple: true });
});
});
});

View File

@ -1,144 +0,0 @@
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(/src\/__tests__/g, '')
.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.registerActionHandlers({ ...actions.associate, ...actions.common });
app.registerPlugin('full-collections', [plugin]);
await app.loadPlugins();
await app.database.sync();
// 表配置信息存到数据库里
// const tables = app.database.getTables([]);
// for (const table of tables) {
// const Collection = app.database.getModel('collections');
// await Collection.import(table.getOptions(), { migrate: 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}`;
}
}
});
};

View File

@ -1,81 +0,0 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'collections',
title: '数据表配置',
internal: true,
model: 'CollectionModel',
developerMode: true,
createdAt: false,
updatedAt: false,
fields: [
{
interface: 'string',
type: 'string',
name: 'title',
title: '数据表名称',
required: true,
},
{
interface: 'string',
type: 'randomString',
name: 'name',
randomString: {
length: 10,
template: 't_%r',
characters: 'abcdefghijklmnopqrstuvwxyz0123456789',
},
createOnly: true,
title: '标识',
unique: true,
required: true,
developerMode: true,
},
{
interface: 'boolean',
type: 'boolean',
name: 'developerMode',
title: '开发者模式',
developerMode: true,
defaultValue: false,
component: {
type: 'boolean',
},
},
{
interface: 'json',
type: 'json',
name: 'options',
title: '配置信息',
defaultValue: {},
component: {
type: 'hidden',
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'internal',
title: '系统内置',
defaultValue: false,
developerMode: true,
component: {
type: 'boolean',
},
},
{
interface: 'linkTo',
type: 'hasMany',
name: 'fields',
title: '字段',
sourceKey: 'name',
},
{
interface: 'linkTo',
type: 'hasMany',
name: 'actions',
title: '操作方法',
sourceKey: 'name',
},
],
} as TableOptions;

View File

@ -1,100 +0,0 @@
import { TableOptions } from '@nocobase/database';
import { getInterfaceFields } from '../interfaces';
export default {
name: 'fields',
title: '字段配置',
internal: true,
model: 'FieldModel',
developerMode: true,
createdAt: false,
updatedAt: false,
fields: [
{
interface: 'string',
type: 'string',
name: 'title',
title: '字段名称',
required: true,
},
{
interface: 'string',
type: 'randomString',
name: 'name',
title: '标识',
required: true,
createOnly: true,
randomString: {
length: 5,
template: 'f_%r',
characters: 'abcdefghijklmnopqrstuvwxyz0123456789',
},
developerMode: true,
},
...getInterfaceFields(),
{
interface: 'string',
type: 'string',
name: 'type',
title: '数据类型',
developerMode: true,
},
{
interface: 'number',
type: 'integer',
name: 'parent_id',
title: '所属分组',
},
{
interface: 'linkTo',
multiple: false,
type: 'belongsTo',
name: 'parent',
title: '所属分组',
target: 'fields',
foreignKey: 'parent_id',
targetKey: 'id',
component: {
type: 'drawerSelect',
},
},
{
interface: 'textarea',
type: 'virtual',
name: 'component.tooltip',
title: '提示信息',
},
{
interface: 'boolean',
type: 'boolean',
name: 'required',
title: '必填项',
defaultValue: false,
},
{
interface: 'linkTo',
type: 'belongsTo',
name: 'collection',
title: '所属数据表',
target: 'collections',
targetKey: 'name',
labelField: 'title',
},
{
interface: 'boolean',
type: 'boolean',
name: 'developerMode',
title: '开发者模式',
developerMode: true,
defaultValue: false,
},
{
interface: 'json',
type: 'json',
name: 'options',
title: '配置信息',
defaultValue: {},
developerMode: true,
},
],
} as TableOptions;

View File

@ -1,84 +0,0 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'menus',
title: '菜单配置',
internal: true,
// model: 'CollectionModel',
developerMode: true,
createdAt: false,
updatedAt: false,
fields: [
{
interface: 'string',
type: 'string',
name: 'title',
title: '菜单名称',
required: true,
},
{
interface: 'icon',
type: 'string',
name: 'icon',
title: '图标',
component: {
type: 'icon',
},
},
{
interface: 'radio',
type: 'string',
name: 'type',
title: '菜单类型',
required: true,
dataSource: [
{ value: 'group', label: '菜单组' },
{ value: 'link', label: '自定义链接' },
{ value: 'page', label: '页面' },
],
linkages: [
{
"type": "value:visible",
"target": "page",
"condition": "{{ $self.value === 'page' }}"
},
{
"type": "value:visible",
"target": "url",
"condition": "{{ $self.value === 'link' }}"
},
],
},
{
interface: 'linkTo',
type: 'belongsTo',
name: 'page',
title: '页面',
target: 'pages',
// targetKey: 'name',
},
{
interface: 'string',
type: 'string',
name: 'url',
title: '链接地址',
required: true,
},
{
interface: 'boolean',
type: 'boolean',
name: 'developerMode',
title: '开发者模式',
developerMode: true,
defaultValue: false,
},
{
interface: 'json',
type: 'json',
name: 'options',
title: '配置信息',
defaultValue: {},
developerMode: true,
},
],
} as TableOptions;

View File

@ -1,88 +0,0 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'pages',
title: '页面配置',
internal: true,
// model: 'CollectionModel',
developerMode: true,
createdAt: false,
updatedAt: false,
fields: [
{
interface: 'string',
type: 'string',
name: 'title',
title: '页面名称',
required: true,
},
{
interface: 'string',
type: 'randomString',
name: 'name',
title: '缩略名',
required: true,
createOnly: true,
randomString: {
length: 6,
characters: 'abcdefghijklmnopqrstuvwxyz0123456789',
},
developerMode: true,
},
{
interface: 'radio',
type: 'string',
name: 'type',
title: '类型',
required: true,
dataSource: [
{ value: 'default', label: '页面' },
{ value: 'collection', label: '数据集' },
],
},
{
interface: 'boolean',
type: 'boolean',
name: 'dynamic',
title: '单条数据子页面',
},
// {
// interface: 'subTable',
// type: 'hasMany',
// name: 'views',
// title: '视图',
// target: 'pages_views',
// children: [
// {
// interface: 'linkTo',
// type: 'belongsToMany',
// name: 'view',
// title: '视图',
// target: 'views',
// },
// {
// interface: 'percent',
// type: 'float',
// name: 'width',
// title: '宽度',
// },
// ],
// },
{
interface: 'linkTo',
type: 'belongsTo',
name: 'collection',
title: '所属数据表',
target: 'collections',
targetKey: 'name',
},
{
interface: 'json',
type: 'json',
name: 'options',
title: '配置信息',
defaultValue: {},
developerMode: true,
},
],
} as TableOptions;

View File

@ -1,63 +0,0 @@
import { TableOptions } from '@nocobase/database';
import { getViewFields } from '../views';
export default {
name: 'views',
title: '视图配置',
internal: true,
model: 'ViewModel',
developerMode: true,
fields: [
{
interface: 'string',
type: 'string',
name: 'title',
title: '视图名称',
required: true,
component: {
type: 'string',
},
},
{
interface: 'string',
type: 'string',
name: 'name',
title: '标识',
component: {
type: 'string',
},
},
...getViewFields(),
{
interface: 'boolean',
type: 'boolean',
name: 'developerMode',
title: '开发者模式',
defaultValue: false,
component: {
type: 'boolean',
},
},
{
interface: 'linkTo',
type: 'belongsTo',
name: 'collection',
title: '所属数据表',
target: 'collections',
targetKey: 'name',
component: {
type: 'drawerSelect',
},
},
{
interface: 'json',
type: 'json',
name: 'options',
title: '配置信息',
defaultValue: {},
component: {
type: 'hidden',
},
},
],
} as TableOptions;

View File

@ -1,27 +0,0 @@
import cryptoRandomString from 'crypto-random-string';
import { STRING, FieldContext } from '@nocobase/database';
import {
DataTypes
} from 'sequelize';
export class RANDOMSTRING extends STRING {
constructor(options: any, context: FieldContext) {
super(options, context);
const Model = context.sourceTable.getModel();
const { name, randomString } = options;
randomString && Model.addHook('beforeValidate', (model) => {
const { template, ...opts } = randomString;
let value = cryptoRandomString(opts);
if (template && template.includes('%r')) {
value = template.replace('%r', value);
}
if (!model.get(name)) {
model.set(name, value);
}
});
}
getDataType() {
return DataTypes.STRING;
}
}

View File

@ -1,6 +0,0 @@
export default async function afterCreate(model: any, options: any = {}) {
const { migrate = true } = options;
if (migrate) {
await model.migrate();
}
}

View File

@ -1,3 +0,0 @@
export default async function beforeValidate(model: any, options = {}) {
}

View File

@ -1,6 +0,0 @@
export default async function afterCreate(model: any, options: any = {}) {
const { migrate = true } = options;
if (migrate) {
await model.migrate();
}
}

View File

@ -1,36 +0,0 @@
import _ from 'lodash';
import { has, merge, generateRandomString } from '../utils';
import { interfaces } from '../interfaces';
import { Model } from '@nocobase/database';
export default async function beforeValidate(model: Model, opts = {}) {
let data = model.get();
const { interface: interfaceType } = data;
if (!interfaceType || !interfaces.has(interfaceType)) {
return;
}
const defaults = {};
Object.keys(data).forEach(name => {
const match = /options\.x-(\w+)-props\.(\w+)/.exec(name);
if (match) {
if (match[1] !== interfaceType) {
delete data[name];
delete model.dataValues[name];
} else {
_.set(defaults, match[2], data[name]);
}
}
});
const { options, properties = {}, initialize } = interfaces.get(interfaceType);
Object.keys(properties).forEach(name => {
if (has(data, `x-${interfaceType}-props.${name}`)) {
const value = _.get(data, `x-${interfaceType}-props.${name}`);
_.set(data, name, value);
}
});
data = merge(merge(defaults, options), data);
initialize && await initialize(data, model);
model.set(data);
// 清除掉 interfaceType 下的临时数据
model.set(`x-${interfaceType}-props`, undefined);
}

View File

@ -1,118 +0,0 @@
import * as types from './types';
export const groups= new Map(Object.entries({
basic: '基本类型',
media: '多媒体类型',
choices: '选择类型',
datetime: '日期和时间',
relation: '关系类型',
systemInfo: '系统信息',
developerMode: '开发者模式',
others: '其他'
}));
export const interfaces = new Map<string, any>();
export function registerInterface(name: string, options: any) {
interfaces.set(name, options);
}
export function registerInterfaces(values: any) {
Object.keys(values).forEach(name => {
registerInterface(name, {
...values[name],
interface: name,
});
});
}
export function getOptions() {
const options = [];
const map = new Map();
for (const [key, item] of interfaces) {
const { title, group } = item;
if (!map.has(group)) {
map.set(group, []);
}
map.get(group).push({
key: key,
value: key,
label: title,
});
}
for (const [key, label] of groups) {
options.push({
key,
label,
children: map.get(key) || [],
});
}
return options;
}
export function getInterfaceLinkages() {
let xlinkages = [];
for (const [key, item] of interfaces) {
const { linkages = {}, properties = {} } = item;
xlinkages.push({
"type": "value:visible",
"target": `x-${key}-props.*`,
"condition": `{{ $self.value === '${key}' }}`,
});
if (linkages.interface) {
xlinkages.push(linkages.interface);
}
if (linkages.interface) {
xlinkages = xlinkages.concat(linkages.interface.map(linkage => {
if (properties[linkage.target]) {
linkage.condition = `{{ $self.value === '${key}' }}`;
linkage.target = `x-${key}-props.${linkage.target}`;
}
return linkage;
}));
}
}
return xlinkages;
}
export function getInterfaceFields() {
const fields = new Map();
fields.set('interface', {
interface: 'select',
type: 'string',
name: 'interface',
title: '字段类型',
required: true,
dataSource: getOptions(),
createOnly: true,
component: {
type: 'select',
},
linkages: getInterfaceLinkages(),
});
for (const [key, item] of interfaces) {
const { properties = {}, linkages = {} } = item;
Object.keys(properties).forEach(name => {
const property = {
...properties[name],
name,
};
if (!property.type) {
property.type = 'virtual';
}
if (property.type === 'virtual') {
property.name = `x-${key}-props.${name}`;
}
if (linkages[name]) {
property.linkages = linkages[name].map((linkage: any) => {
linkage.target = `x-${key}-props.${linkage.target}`;
return linkage;
});
}
fields.set(`x-${key}-props.${name}`, property);
});
}
return [...fields.values()];
}
registerInterfaces(types);

View File

@ -1,738 +0,0 @@
import { generateRandomString } from '../utils';
export const string = {
title: '单行文本',
group: 'basic',
options: {
type: 'string',
filterable: true,
component: {
type: 'string',
},
},
};
export const textarea = {
title: '多行文本',
group: 'basic',
options: {
type: 'text',
filterable: true,
component: {
type: 'textarea',
},
}
};
export const phone = {
title: '手机号码',
group: 'basic',
options: {
type: 'string',
filterable: true,
format: 'phone', // 验证的问题
component: {
type: 'string',
'x-rules': 'phone',
},
},
};
export const email = {
title: '邮箱',
group: 'basic',
options: {
type: 'string',
filterable: true,
format: 'email',
component: {
type: 'string',
'x-rules': 'email',
},
},
};
export const number = {
title: '数字',
group: 'basic',
options: {
type: 'float',
filterable: true,
sortable: true,
precision: 0,
component: {
type: 'number',
},
},
properties: {
precision: {
interface: 'select',
type: 'virtual',
title: '精度',
dataSource: [
{ value: 0, label: '1' },
{ value: 1, label: '1.0' },
{ value: 2, label: '1.00' },
{ value: 3, label: '1.000' },
{ value: 4, label: '1.0000' },
],
component: {
type: 'number',
default: 0,
},
},
},
};
export const percent = {
title: '百分比',
group: 'basic',
options: {
type: 'float',
filterable: true,
sortable: true,
precision: 0,
component: {
type: 'percent',
},
},
properties: {
precision: {
interface: 'select',
type: 'virtual',
title: '精度',
dataSource: [
{ value: 0, label: '1' },
{ value: 1, label: '1.0' },
{ value: 2, label: '1.00' },
{ value: 3, label: '1.000' },
{ value: 4, label: '1.0000' },
],
component: {
type: 'number',
default: 0,
},
},
},
};
export const wysiwyg = {
title: '可视化编辑器',
group: 'media',
disabled: true,
options: {
type: 'text',
component: {
type: 'wysiwyg',
},
},
};
export const boolean = {
title: '是/否',
group: 'choices',
options: {
type: 'boolean',
filterable: true,
component: {
type: 'checkbox', // switch
},
},
};
export const select = {
title: '下拉选择(单选)',
group: 'choices',
options: {
type: 'string',
filterable: true,
dataSource: [],
component: {
type: 'select',
},
},
properties: {
dataSource: {
interface: 'subTable',
type: 'virtual',
title: '可选项',
component: {
type: 'table',
default: [{}],
items: {
type: 'object',
properties: {
value: {
type: "string",
title: "值",
required: true
},
label: {
type: "string",
title: "选项",
required: true
},
},
},
},
},
},
initialize: (data: any) => {
if (Array.isArray(data.dataSource)) {
data.dataSource = data.dataSource.map((item: any) => {
if (item.value === null || typeof item.value === 'undefined') {
item.value = generateRandomString();
}
return { ...item };
});
}
},
};
export const multipleSelect = {
title: '下拉选择(多选)',
group: 'choices',
options: {
type: 'json', // TODO: json 不是个通用的方案
filterable: true,
dataSource: [],
defaultValue: [],
multiple: true,
component: {
type: 'select',
},
},
properties: {
dataSource: select.properties.dataSource,
},
initialize: select.initialize,
};
export const radio = {
title: '单选框',
group: 'choices',
options: {
type: 'string',
filterable: true,
dataSource: [],
component: {
type: 'radio',
},
},
properties: {
dataSource: select.properties.dataSource,
},
initialize: select.initialize,
};
export const checkboxes = {
title: '多选框',
group: 'choices',
options: {
type: 'json', // TODO: json 不是个通用的方案
filterable: true,
dataSource: [],
defaultValue: [],
component: {
type: 'checkboxes',
},
},
properties: {
dataSource: select.properties.dataSource,
},
initialize: select.initialize,
};
export const datetime = {
title: '日期',
group: 'datetime',
options: {
type: 'date',
showTime: false,
filterable: true,
sortable: true,
dateFormat: 'YYYY-MM-DD',
timeFormat: 'HH:mm:ss',
component: {
type: 'date',
},
},
properties: {
dateFormat: {
interface: 'select',
type: 'virtual',
title: '日期格式',
dataSource: [
{ value: 'YYYY/MM/DD', label: '年/月/日' },
{ value: 'YYYY-MM-DD', label: '年-月-日' },
{ value: 'DD/MM/YYYY', label: '日/月/年' },
],
defaultValue: 'YYYY-MM-DD',
component: {
type: 'string',
default: 'YYYY-MM-DD',
},
},
showTime: {
interface: 'boolean',
type: 'virtual',
title: '显示时间',
component: {
type: 'boolean',
default: false,
},
},
timeFormat: {
interface: 'select',
type: 'virtual',
title: '时间格式',
dataSource: [
{ value: 'HH:mm:ss', label: '24小时制' },
{ value: 'hh:mm:ss a', label: '12小时制' },
],
defaultValue: 'HH:mm:ss',
component: {
type: 'string',
default: 'HH:mm:ss',
},
},
},
linkages: {
showTime: [
{
"type": "value:visible",
"target": "timeFormat",
"condition": "{{ ($form.values && $form.values.control === 'time') || $self.value === true }}"
},
],
},
};
export const time = {
title: '时间',
group: 'datetime',
options: {
type: 'time',
filterable: true,
sortable: true,
timeFormat: 'HH:mm:ss',
component: {
type: 'time',
},
},
};
export const subTable = {
title: '子表格',
group: 'relation',
// disabled: true,
options: {
type: 'hasMany',
// target,
// children: [],
component: {
type: 'subTable',
},
},
properties: {
children: {
interface: 'subTable',
type: 'hasMany',
target: 'fields',
sourceKey: 'id',
foreignKey: 'parent_id',
title: '子表格字段',
component: {
type: 'subTable',
default: [],
},
},
},
initialize(data: any) {
if (!data.target) {
data.target = generateRandomString({ prefix: 't_', length: 12 });
}
},
};
export const linkTo = {
title: '关联数据',
group: 'relation',
// disabled: true,
options: {
type: 'belongsToMany',
// name,
// target: '关联表',
// targetKey,
// sourceKey,
// otherKey,
// foreignKey,
// labelField,
// valueField,
filterable: false,
// multiple: true,
component: {
type: 'drawerSelect',
// labelField,
// valueField,
},
},
properties: {
target: {
interface: 'string',
type: 'virtual',
name: 'target',
title: '要关联的数据表',
required: true,
createOnly: true,
component: {
type: 'remoteSelect',
labelField: 'title',
valueField: 'name',
resourceName: 'collections',
'x-component-props': {
mode: 'simple',
},
},
},
'component.labelField': {
interface: 'string',
type: 'virtual',
title: '要显示的字段',
required: true,
component: {
type: 'remoteSelect',
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
'x-component-props': {
mode: 'simple',
},
},
},
'component.filter': {
interface: 'json',
type: 'virtual',
title: '数据范围',
component: {
type: 'filter',
resourceName: 'collections.fields',
},
},
multiple: {
interface: 'boolean',
type: 'virtual',
name: 'multiple',
title: '允许添加多条记录',
defaultValue: true,
component: {
type: 'checkbox',
},
},
},
linkages: {
target: [
{
type: "value:state",
target: "component.labelField",
condition: "{{ $self.inputed }}",
state: {
value: null,
}
},
{
"type": "value:visible",
"target": "component.labelField",
"condition": "{{ !!$self.value }}"
},
{
type: "value:schema",
target: "component.labelField",
// condition: "{{ $self.value }}",
schema: {
"x-component-props": {
"associatedKey": "{{ $self.value }}"
},
},
},
{
type: 'value:visible',
target: 'component.filter',
condition: '{{ !!$self.value }}'
},
{
type: "value:schema",
target: "component.filter",
schema: {
"x-component-props": {
"associatedKey": "{{ $self.value }}"
},
},
},
],
},
initialize(data: any, model: any) {
if (!['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(data.type)) {
return;
}
if (!data.foreignKey) {
data.foreignKey = generateRandomString({ prefix: 'f_', length: 6 });
}
if (data.type === 'belongsToMany') {
if (!data.through) {
data.through = generateRandomString({ prefix: 't_', length: 12 });
}
if (!data.otherKey) {
data.otherKey = generateRandomString({ prefix: 'f_', length: 6 });
}
}
if (data.type !== 'belongsTo' && !data.sourceKey) {
data.sourceKey = model.constructor.primaryKeyAttribute;
}
if (['belongsTo', 'belongsToMany'].includes(data.type) && !data.targetKey) {
const TargetModel = model.database.getModel(data.target);
data.targetKey = TargetModel.primaryKeyAttribute;
}
}
};
export const createdAt = {
title: '创建时间',
group: 'systemInfo',
options: {
interface: 'createdAt',
type: 'date',
field: 'created_at',
showTime: false,
dateFormat: 'YYYY-MM-DD',
timeFormat: 'HH:mm:ss',
required: true,
filterable: true,
sortable: true,
component: {
type: 'date',
},
},
properties: {
...datetime.properties,
},
linkages: {
...datetime.linkages,
},
};
export const updatedAt = {
title: '修改时间',
group: 'systemInfo',
options: {
interface: 'updatedAt',
type: 'date',
field: 'updated_at',
showTime: false,
dateFormat: 'YYYY-MM-DD',
timeFormat: 'HH:mm:ss',
required: true,
filterable: true,
sortable: true,
component: {
type: 'date',
},
},
properties: {
...datetime.properties,
},
linkages: {
...datetime.linkages,
},
};
export const group = {
title: '字段组',
disabled: true,
options: {
type: 'virtual',
component: {
type: 'hidden',
},
},
};
export const description = {
title: '说明文字',
group: 'others',
options: {
type: 'virtual',
component: {
type: 'description',
},
},
};
export const primaryKey = {
title: '主键',
group: 'developerMode',
options: {
name: 'id',
type: 'integer',
required: true,
autoIncrement: true,
primaryKey: true,
filterable: true,
developerMode: true,
component: {
type: 'number',
},
},
};
export const sort = {
title: '排序',
group: 'developerMode',
options: {
type: 'integer',
required: true,
// scope: [],
component: {
type: 'sort',
},
},
};
export const password = {
title: '密码',
group: 'developerMode',
options: {
type: 'password',
hidden: true, // hidden 用来控制 api 不输出这个字段
component: {
type: 'password',
},
},
};
export const json = {
title: 'JSON',
group: 'developerMode',
options: {
type: 'json',
mode: 'replace',
// developerMode: true,
component: {
type: 'hidden',
},
},
};
export const icon = {
title: '图标',
group: 'developerMode',
options: {
type: 'string',
component: {
type: 'icon',
},
},
};
export const createdBy = {
title: '创建人',
group: 'systemInfo',
options: {
type: 'createdBy',
// filterable: true,
target: 'users',
foreignKey: 'created_by_id',
component: {
type: 'drawerSelect',
labelField: 'nickname',
},
},
};
export const updatedBy = {
title: '修改人',
group: 'systemInfo',
options: {
type: 'updatedBy',
// filterable: true,
target: 'users',
foreignKey: 'updated_by_id',
component: {
type: 'drawerSelect',
labelField: 'nickname',
},
},
};
export const attachment = {
title: '附件',
group: 'media',
// disabled: true,
options: {
type: 'belongsToMany',
filterable: false,
target: 'attachments',
// storage: {
// name: 'local',
// },
component: {
type: 'upload',
},
},
initialize(data: any, model: any) {
if (data.type === 'belongsToMany' && !data.through) {
data.through = generateRandomString({ prefix: 't_', length: 12 });
}
},
};
export const chinaRegion = {
title: '中国行政区划',
group: 'choices',
options: {
type: 'belongsToMany',
// 数据来源的数据表,与 dataSource 不同,需要从表数据加载后转化成 dataSource
target: 'china_regions',
targetKey: 'code',
// 可选层级,最大层级
maxLevel: 3,
// 可以选到任意一级结束
incompletely: false,
component: {
type: 'cascader',
// 值字段
valueField: 'code',
// 名称字段
labelField: 'name',
// TODO(refactor): 等 toWhere 重构完成后要改成 parent
// 上级字段名
parentField: 'parent_code',
},
},
properties: {
maxLevel: {
interface: 'radio',
type: 'virtual',
title: '可选层级',
defaultValue: 3,
dataSource: [
{ value: 1, label: '省' },
{ value: 2, label: '市' },
{ value: 3, label: '区/县' },
{ value: 4, label: '乡镇/街道' },
{ value: 5, label: '村/居委会' },
],
},
incompletely: {
interface: 'boolean',
type: 'virtual',
title: '可以选到任意一级结束',
defaultValue: false,
}
},
initialize(data: any, model: any) {
if (data.type === 'belongsToMany' && !data.through) {
data.through = generateRandomString({ prefix: 't_', length: 12 });
}
},
};

View File

@ -1,77 +0,0 @@
import _ from 'lodash';
import { getDataTypeKey, Model } from '@nocobase/database';
import Dottie from 'dottie';
export class BaseModel extends Model {
get additionalAttribute() {
const tableOptions = this.database.getTable(this.constructor.name).getOptions();
return _.get(tableOptions, 'additionalAttribute') || 'options';
}
hasGetAttribute(key: string) {
const attribute = this.rawAttributes[key];
// virtual 如果有 get 方法就直接走 get
if (attribute && attribute.type && getDataTypeKey(attribute.type) === 'VIRTUAL') {
return !!attribute.get;
}
return !!attribute;
}
hasSetAttribute(key: string) {
// @ts-ignore
if (this.constructor.hasAlias(key)) {
return false;
}
const attribute = this.rawAttributes[key];
// virtual 如果有 set 方法就直接走 set
if (attribute && attribute.type && getDataTypeKey(attribute.type) === 'VIRTUAL') {
return !!attribute.set;
}
return !!attribute;
}
get(key?: any, options?: any) {
if (typeof key !== 'string') {
const data = super.get(key);
return {
..._.omit(data, [this.additionalAttribute]),
...(data[this.additionalAttribute] || {}),
};
}
const [column, ...path] = key.split('.');
if (this.hasGetAttribute(column)) {
const value = super.get(column, options);
if (path.length) {
return _.get(value, path);
}
return value;
}
return _.get(super.get(this.additionalAttribute, options) || {}, key);
}
set(key?: any, value?: any, options: any = {}) {
if (typeof key !== 'string') {
return super.set(key, value, options);
}
// @ts-ignore
if (this.constructor.hasAlias(key)) {
return this;
}
const [column] = key.split('.');
if (this.hasSetAttribute(column)) {
return super.set(key, value, options);
}
return super.set(`${this.additionalAttribute}.${key}`, value, options);
}
// getDataValue(key: any) {
// return super.getDataValue(key);
// }
// setDataValue(key: any, value: any) {
// return super.setDataValue(key, value);
// }
}
export default BaseModel;

View File

@ -1,100 +0,0 @@
import _ from 'lodash';
import { Model, Table, TableOptions } from '@nocobase/database';
import { SaveOptions, Op } from 'sequelize';
import BaseModel from './base';
export interface LoadOptions {
reset?: boolean;
where?: any;
skipExisting?: boolean;
[key: string]: any;
}
export class CollectionModel extends BaseModel {
async migrate() {
const table = await this.loadTableOptions();
return await table.sync({
force: false,
alter: {
drop: false,
}
});
}
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(),
fields: await this.getFieldsOptions(),
};
}
async loadTableOptions(opts: any = {}): Promise<Table> {
const options = await this.getOptions();
const table = this.database.table(options);
return table;
}
/**
* TODO
*
* @param options
*/
static async load(options: LoadOptions = {}) {
const { skipExisting = false, reset = false, where = {}, transaction } = options;
const collections = await this.findAll({
transaction,
where,
});
for (const collection of collections) {
if (skipExisting && this.database.isDefined(collection.get('name'))) {
continue;
}
await collection.loadTableOptions({
transaction,
reset,
});
}
}
static async import(data: TableOptions, options: SaveOptions = {}): Promise<CollectionModel> {
let collection: CollectionModel;
if (data.name) {
collection = await this.findOne({
...options,
where: {
name: data.name,
},
});
} else if (data.title) {
collection = await this.findOne({
...options,
where: {
title: data.title,
},
});
}
if (collection) {
await collection.update(data, options);
} else {
collection = await this.create(data, options);
}
await collection.updateAssociations(data, options);
return collection;
}
}
export default CollectionModel;

View File

@ -1,60 +0,0 @@
import _ from 'lodash';
import { Model, FieldOptions } from '@nocobase/database';
import BaseModel from './base';
import { interfaces } from '../interfaces';
import has from 'just-has';
export class FieldModel extends BaseModel {
get(key?: any, options?: any) {
if (typeof key !== 'string') {
const data = super.get(key);
const { interface: interfaceType } = data;
if (interfaceType && interfaces.has(interfaceType)) {
const { properties = {} } = interfaces.get(interfaceType);
Object.keys(properties).forEach(name => {
if (has(data, name)) {
const value = _.get(data, name);
_.set(data, `x-${interfaceType}-props.${name}`, value);
}
});
}
return data;
}
return super.get(key, options);
}
async getOptions(): Promise<FieldOptions> {
return {
...this.get(),
};
}
async migrate(options: any = {}) {
const collectionName = this.get('collection_name');
if (!collectionName) {
return false;
}
if (!this.database.isDefined(collectionName)) {
throw new Error(`${collectionName} is not defined`);
}
const table = this.database.getTable(collectionName);
const fieldOptions = await this.getOptions();
console.log({ fieldOptions });
table.addField(fieldOptions);
return await table.sync({
force: false,
alter: {
drop: false,
}
});
}
}
export default FieldModel;

View File

@ -1,3 +0,0 @@
export * from './collection';
export * from './field';
export * from './view';

View File

@ -1,33 +0,0 @@
import _ from 'lodash';
import BaseModel from './base';
import { interfaces } from '../interfaces';
import has from 'just-has';
export class ViewModel extends BaseModel {
get(key?: any, options?: any) {
if (typeof key !== 'string') {
const data = super.get(key);
const { type } = data;
if (type && interfaces.has(type)) {
const { properties = {} } = interfaces.get(type);
Object.keys(properties).forEach(name => {
if (has(data, name)) {
const value = _.get(data, name);
_.set(data, `x-${type}-props.${name}`, value);
}
});
}
return data;
}
return super.get(key, options);
}
async getOptions(): Promise<any> {
return {
...this.get(),
};
}
}
export default ViewModel;

View File

@ -1,40 +0,0 @@
import path from 'path';
import { Application } from '@nocobase/server';
import { registerModels, registerFields } from '@nocobase/database';
import * as models from './models';
import collectionsBeforeValidate from './hooks/collections.beforeValidate';
import collectionsAfterCreate from './hooks/collections.afterCreate';
import fieldsBeforeValidate from './hooks/fields.beforeValidate';
import fieldsAfterCreate from './hooks/fields.afterCreate';
import { RANDOMSTRING } from './fields/randomString';
import { interfaces } from './interfaces';
import { merge } from './utils';
export default async function (this: Application, options = {}) {
const database = this.database;
const resourcer = this.resourcer;
// 提供全局的 models 注册机制
registerModels(models);
registerFields({
RANDOMSTRING,
});
database.addHook('beforeAddField', (options: any) => {
const { interface: interfaceType } = options;
if (interfaceType && interfaces.has(interfaceType)) {
const defaults = interfaces.get(interfaceType).options;
Object.assign(options, merge(defaults, options))
}
// console.log({options});
});
database.import({
directory: path.resolve(__dirname, 'collections'),
});
database.getModel('collections').addHook('beforeValidate', collectionsBeforeValidate);
database.getModel('fields').addHook('beforeValidate', fieldsBeforeValidate);
database.getModel('collections').addHook('afterCreate', collectionsAfterCreate);
database.getModel('fields').addHook('afterCreate', fieldsAfterCreate);
}

View File

@ -1,23 +0,0 @@
import deepmerge from 'deepmerge';
import cryptoRandomString from 'crypto-random-string';
import justHas from 'just-has';
const overwriteMerge = (destinationArray, sourceArray, options) => sourceArray
export function merge(obj1: any, obj2: any) {
return deepmerge(obj1, obj2, {
arrayMerge: overwriteMerge,
});
}
export function generateRandomString(options: any = {}) {
const { prefix = '' } = options;
// @ts-ignore
return prefix + cryptoRandomString({
length: 6,
characters: 'abcdefghijklmnopqrstuvwxyz0123456789',
...options,
});
}
export const has = justHas;

View File

@ -1,92 +0,0 @@
import * as types from './types';
export const views = new Map();
export function registerView(type: string, value: any) {
views.set(type, value);
}
export function registerViews(values: any) {
Object.keys(values).forEach(type => {
registerView(type, {
...values[type],
type,
});
});
}
registerViews(types);
export function getOptions() {
const options = [];
for (const [type, view] of views) {
options.push({
key: type,
value: type,
label: view.title,
});
}
return options;
}
export function getViewTypeLinkages() {
let xlinkages = [];
for (const [key, item] of views) {
const { linkages = {}, properties = {} } = item;
xlinkages.push({
"type": "value:visible",
"target": `x-${key}-props.*`,
"condition": `{{ $self.value === '${key}' }}`,
});
if (linkages.type) {
xlinkages = xlinkages.concat(linkages.type.map(linkage => {
if (properties[linkage.target]) {
linkage.condition = `{{ $self.value === '${key}' }}`;
linkage.target = `x-${key}-props.${linkage.target}`;
}
return linkage;
}));
}
}
return xlinkages;
}
export function getViewFields() {
const fields = new Map();
fields.set('type', {
interface: 'select',
type: 'string',
name: 'type',
title: '视图类型',
required: true,
dataSource: getOptions(),
createOnly: true,
component: {
type: 'select',
},
linkages: getViewTypeLinkages(),
});
for (const [key, item] of views) {
const { properties = {}, linkages = {} } = item;
Object.keys(properties).forEach(name => {
const property = {
...properties[name],
name,
};
if (!property.type) {
property.type = 'virtual';
}
if (property.type === 'virtual') {
property.name = `x-${key}-props.${name}`;
}
if (linkages[name]) {
property.linkages = linkages[name].map((linkage: any) => {
linkage.target = `x-${key}-props.${linkage.target}`;
return linkage;
});
}
fields.set(`x-${key}-props.${name}`, property);
});
}
return [...fields.values()];
}

View File

@ -1,403 +0,0 @@
const fields = {
interface: 'json',
title: '要显示的字段',
// target: 'views_fields',
fields: [
{
interface: 'string',
type: 'string',
name: 'name',
title: '字段',
},
{
interface: 'string',
type: 'string',
name: 'title',
title: '字段标题',
},
],
};
const actions = {
interface: 'json',
title: '可进行的操作',
fields: [
{
interface: 'string',
type: 'string',
name: 'name',
title: '操作',
},
],
};
const pages = {
interface: 'json',
title: '详情页要显示的单条数据子页面',
fields: [
{
interface: 'string',
type: 'string',
name: 'name',
title: '页面',
},
],
};
const openMode = {
interface: 'radio',
// type: 'string',
title: '单条数据详情页的打开方式',
required: true,
dataSource: [
{ label: '常规页面', value: 'default' },
{ label: '快捷抽屉', value: 'simple' },
],
component: {
type: 'radio',
default: 'default',
},
};
export const form = {
// fields,
title: '表单',
options: {
// fields,
},
properties: {
fields,
},
linkages: {
type: [
{
type: 'value:schema',
target: "fields",
schema: {
'x-component-props': {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
],
},
};
export const detail = {
title: '详情',
options: {
// actions,
// fields,
},
properties: {
actions,
fields,
},
linkages: {
type: [
{
type: 'value:schema',
target: "fields",
schema: {
'x-component-props': {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
],
},
};
export const table = {
title: '表格',
options: {
defaultPerPage: 20,
draggable: false,
filter: {},
sort: {},
openMode: 'default',
// actions,
// fields,
// pages,
// labelField,
},
properties: {
// 数据配置
filter: {
interface: 'json',
type: 'virtual',
title: '筛选数据',
mode: 'replace',
defaultValue: {},
component: {
type: 'filter',
},
},
sort: {
interface: 'json',
type: 'virtual',
title: '默认排序',
mode: 'replace',
defaultValue: {},
component: {
type: 'string',
},
},
// 表格配置
labelField: {
interface: 'select',
type: 'virtual',
title: '标题字段',
name: 'labelField',
required: true,
component: {
type: 'remoteSelect',
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
filter: {
type: 'string',
},
},
},
fields,
defaultPerPage: {
interface: 'radio',
type: 'virtual',
name: 'defaultPerPage',
title: '默认每页显示几行数据',
defaultValue: 50,
dataSource: [
{ label: '10', value: 10 },
{ label: '20', value: 20 },
{ label: '50', value: 50 },
{ label: '100', value: 100 },
],
},
draggable: {
interface: 'boolean',
type: 'virtual',
title: '支持拖拽数据排序',
},
// 操作配置
actions,
// 详情配置
openMode,
pages,
},
linkages: {
type: [
{
type: 'value:schema',
target: "filter",
schema: {
'x-component-props': {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
{
type: 'value:schema',
target: "sort",
schema: {
'x-component-props': {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
{
type: 'value:schema',
target: "labelField",
schema: {
'x-component-props': {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
{
type: 'value:schema',
target: "fields",
schema: {
'x-component-props': {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
{
type: 'value:schema',
target: "pages",
schema: {
'x-component-props': {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
}
],
},
};
export const calendar = {
title: '日历',
options: {
// filter,
// labelField,
// startDateField,
// endDateField,
// openMode,
// pages,
},
properties: {
// 数据配置
filter: {
interface: 'json',
type: 'virtual',
title: '筛选数据',
mode: 'replace',
defaultValue: {},
component: {
type: 'filter',
},
},
// 日历配置
labelField: {
interface: 'select',
type: 'virtual',
title: '标题字段',
name: 'labelField',
required: true,
component: {
type: 'remoteSelect',
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
filter: {
type: 'string',
},
},
},
startDateField: {
interface: 'select',
type: 'virtual',
title: '开始日期字段',
// required: true,
component: {
type: 'remoteSelect',
placeholder: '默认为创建时间字段',
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
filter: {
type: 'date',
},
},
},
endDateField: {
interface: 'select',
type: 'virtual',
title: '结束日期字段',
// required: true,
component: {
type: 'remoteSelect',
placeholder: '默认为创建时间字段',
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
filter: {
type: 'date',
},
},
},
// 详情配置
openMode,
pages,
},
linkages: {
type: [
{
type: 'value:schema',
target: "filter",
schema: {
'x-component-props': {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
{
type: 'value:schema',
target: "labelField",
schema: {
'x-component-props': {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
{
type: 'value:schema',
target: "startDateField",
schema: {
'x-component-props': {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
{
type: 'value:schema',
target: "endDateField",
schema: {
'x-component-props': {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
{
type: 'value:schema',
target: "pages",
schema: {
'x-component-props': {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
],
},
};
export const association = {
title: '相关数据视图',
options: {
// tableName,
// viewName,
// actions,
},
properties: {
tableName: {
interface: 'select',
type: 'virtual',
title: '相关数据',
required: true,
component: {
type: 'remoteSelect',
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
},
},
viewName: {
interface: 'select',
type: 'virtual',
title: '相关数据表的视图',
required: true,
component: {
type: 'remoteSelect',
resourceName: 'collections.views',
labelField: 'title',
valueField: 'name',
},
},
actions,
},
linkages: {
tableName: [],
viewName: [],
},
};

View File

@ -1,5 +0,0 @@
export default {
target: 'node',
cjs: { type: 'babel', lazy: true },
disableTypeCheck: true,
};

View File

@ -1,7 +0,0 @@
node_modules
*.log
docs
__tests__
tsconfig.json
src
.fatherrc.ts

View File

@ -1,14 +0,0 @@
{
"name": "@nocobase/plugin-pages",
"version": "0.4.0-alpha.7",
"main": "lib/index.js",
"license": "MIT",
"dependencies": {
"@nocobase/actions": "^0.4.0-alpha.7",
"@nocobase/database": "^0.4.0-alpha.7",
"@nocobase/resourcer": "^0.4.0-alpha.7",
"@nocobase/server": "^0.4.0-alpha.7",
"crypto-random-string": "^3.3.1"
},
"gitHead": "f0b335ac30f29f25c95d7d137655fa64d8d67f1e"
}

View File

@ -1,61 +0,0 @@
import { Model, ModelCtor } from '@nocobase/database';
export default async (ctx, next) => {
const { resourceName, resourceKey } = ctx.action.params;
const [Collection, Field, Tab, View] = ctx.db.getModels(['collections', 'fields', 'tabs', 'views']) as ModelCtor<Model>[];
const collection = await Collection.findOne(Collection.parseApiJson({
filter: {
name: resourceName,
},
}));
const permissions = (await ctx.ac.isRoot() || collection.developerMode || collection.internal)
? await ctx.ac.getRootPermissions()
: await ctx.ac.can(resourceName).permissions();
const defaultView = await collection.getViews({
where: {
default: true,
},
limit: 1,
plain: true,
});
collection.setDataValue('defaultViewName', defaultView.get('name'));
const options = Tab.parseApiJson({
filter: ctx.state.developerMode ? {
'id.in': permissions.tabs,
enabled: true,
} : {
'id.in': permissions.tabs,
enabled: true,
developerMode: { '$isFalsy': true },
},
fields: {
appends: ['associationField'],
},
sort: ['sort'],
});
const tabs = await collection.getTabs(options) as Model[];
const tabItems = [];
for (const tab of tabs) {
const itemTab = {
...tab.get(),
};
if (itemTab.type === 'details' && !itemTab.viewName) {
itemTab.viewName = 'details';
}
// if (itemTab.type == 'association') {
// itemTab.field = await collection.getFields({
// where: {
// name: itemTab.association,
// },
// limit: 1,
// plain: true,
// });
// }
tabItems.push(itemTab);
}
ctx.body = {
...collection.toJSON(),
tabs: tabItems,
};
await next();
}

View File

@ -1,108 +0,0 @@
import { Model, ModelCtor } from '@nocobase/database';
import _ from 'lodash';
async function getPageInfo(ctx, { resourceName, resourceKey }) {
// const { resourceName, resourceKey } = ctx.action.params;
const M = ctx.db.getModel(resourceName) as ModelCtor<Model>;
const model = await M.findByPk(resourceKey);
const Field = ctx.db.getModel('fields') as ModelCtor<Model>;
const field = await Field.findOne({
where: {
collection_name: resourceName,
type: 'string',
},
order: [['sort', 'asc']],
});
return {
pageTitle: field ? (model.get(field.get('name')) || `#${model.get(M.primaryKeyAttribute)} 无标题`) : model.get(M.primaryKeyAttribute),
...model.toJSON(),
};
};
async function getCollection(ctx, resourceName) {
const [Collection, Field, Tab, View] = ctx.db.getModels(['collections', 'fields', 'tabs', 'views']) as ModelCtor<Model>[];
const collection = await Collection.findOne(Collection.parseApiJson({
filter: {
name: resourceName,
},
}));
const permissions = (await ctx.ac.isRoot() || collection.developerMode || collection.internal)
? await ctx.ac.can(resourceName).getRootPermissions()
: await ctx.ac.can(resourceName).permissions();
const defaultView = await collection.getViews({
where: {
default: true,
},
limit: 1,
plain: true,
});
collection.setDataValue('defaultViewName', defaultView.get('name'));
const options = Tab.parseApiJson({
filter: ctx.state.developerMode ? {
'id.in': permissions.tabs,
enabled: true,
} : {
'id.in': permissions.tabs,
enabled: true,
developerMode: { '$isFalsy': true },
},
fields: {
appends: ['associationField'],
},
sort: ['sort'],
});
const tabs = await collection.getTabs(options) as Model[];
const tabItems = [];
for (const tab of tabs) {
const itemTab = {
...tab.get(),
};
if (itemTab.type === 'details' && !itemTab.viewName) {
itemTab.viewName = 'details';
}
// if (itemTab.type == 'association') {
// itemTab.field = await collection.getFields({
// where: {
// name: itemTab.association,
// },
// limit: 1,
// plain: true,
// });
// }
tabItems.push(itemTab);
}
return {
...collection.toJSON(),
tabs: tabItems,
};
}
export default async (ctx, next) => {
const { resourceName, values = {} } = ctx.action.params;
const { tabs: items = [] } = values;
// console.log({items})
const collection = await getCollection(ctx, resourceName);
ctx.body = [
collection,
];
const lastItem = items.pop();
for (const item of items) {
const lastCollection = ctx.body[ctx.body.length - 1];
lastCollection.pageInfo = await getPageInfo(ctx, { resourceName: lastCollection.name, resourceKey: item.itemId });
const activeTab = _.find(lastCollection.tabs, tab => tab.name == item.tabName) || {};
if (activeTab && activeTab.type === 'association') {
// console.log(activeTab.associationField.target);
const nextCollection = await getCollection(ctx, activeTab.associationField.target);
ctx.body.push(nextCollection);
}
}
const lastCollection = ctx.body[ctx.body.length - 1];
lastCollection.pageInfo = await getPageInfo(ctx, { resourceName: lastCollection.name, resourceKey: lastItem.itemId });
await next();
}

View File

@ -1,59 +0,0 @@
import { ResourceOptions } from '@nocobase/resourcer';
import { Model, ModelCtor } from '@nocobase/database';
import _ from 'lodash';
import { Op } from 'sequelize';
export default async (ctx, next) => {
const { resourceKey } = ctx.action.params;
let primaryKey: any;
let pageName: any;
let collectionName: any;
const roles = ctx.ac ? await ctx.ac.getRoles() : [];
const isRoot = ctx.ac.constructor.isRoot(roles);
const MenuPermission = ctx.db.getModel('menus_permissions');
const menu_permissions = await MenuPermission.findAll({
where: {
role_id: {
[Op.in]: roles.map(role => role.id),
}
}
});
const menuIds = menu_permissions.map(item => item.menu_id);
const Menu = ctx.db.getModel('menus') as ModelCtor<Model>;
const menu = await Menu.findOne({
where: isRoot ? {
name: resourceKey,
} : {
id: {
[Op.in]: menuIds,
},
name: resourceKey,
}
});
if (!menu) {
ctx.throw(404, 'Not Found');
}
const body: any = {
...menu.toJSON(),
};
if (body.views) {
const views = [];
for (const item of body.views) {
let name: string;
if (typeof item === 'object') {
if (item.view) {
item.name = item.view.collection_name ? `${item.view.collection_name}.${item.view.name}` : item.view.name;
}
views.push(item);
} else if (typeof item === 'string') {
views.push({
name: item,
width: '100%',
});
}
}
body.views = views;
}
ctx.body = body;
await next();
};

View File

@ -1,21 +0,0 @@
import { ResourceOptions } from '@nocobase/resourcer';
import { Model, ModelCtor } from '@nocobase/database';
export default async (ctx, next) => {
const { resourceName, resourceKey } = ctx.action.params;
const M = ctx.db.getModel(resourceName) as ModelCtor<Model>;
const model = await M.findByPk(resourceKey);
const Field = ctx.db.getModel('fields') as ModelCtor<Model>;
const field = await Field.findOne({
where: {
collection_name: resourceName,
type: 'string',
},
order: [['sort', 'asc']],
});
ctx.body = {
pageTitle: field ? (model.get(field.get('name')) || `#${model.get(M.primaryKeyAttribute)} 无标题`) : model.get(M.primaryKeyAttribute),
...model.toJSON(),
};
await next();
};

View File

@ -1,181 +0,0 @@
import Database from '@nocobase/database';
import { ResourceOptions } from '@nocobase/resourcer';
import { flatToTree } from '../utils';
import { get } from 'lodash';
import { Op } from 'sequelize';
function pages2routes(pages: Array<any>) {
let routes: any = {};
pages.forEach(page => {
const { children = [], ...restProps } = page;
const route: any = {
...restProps,
};
// page.type === 'layout' &&
if (!page.redirect && children.length) {
const items = children.filter(item => item.showInMenu).sort((a, b) => a.sort - b.sort);
const redirect = get(items, [0, 'path']);
if (redirect) {
route.redirect = redirect;
}
}
if (page.type === 'layout' && children.length) {
const items = children.filter(item => item.showInMenu).sort((a, b) => a.sort - b.sort);
route.menu = items.map(child => ({
...child,
title: child.title,
path: child.path,
sort: child.sort,
}));
}
if (page.children) {
routes = { ...routes, ...pages2routes(page.children) };
}
routes[page.path] = route;
});
return routes;
}
export default async function getRoutes(ctx, next) {
const database: Database = ctx.database;
const Page = database.getModel('pages');
const View = database.getModel('views');
const Collection = database.getModel('collections');
const RoutePermission = database.getModel('routes_permissions');
const roles = ctx.ac ? await ctx.ac.getRoles() : [];
// TODO(optimize): isRoot 的判断需要在内部完成,尽量不要交给调用者
const isRoot = true; // ctx.ac ? ctx.ac.constructor.isRoot(roles) : true;
const routesPermissionsMap = new Map();
if (!isRoot) {
const routesPermissions = await RoutePermission.findAll({
where: {
role_id: roles.map(({ id }) => id)
}
});
routesPermissions.forEach(permission => {
routesPermissionsMap.set(`${permission.routable_type}:${permission.routable_id}`, permission);
});
}
let pages = await Page.findAll(Page.parseApiJson(ctx.state.developerMode ? {
filter: {
},
sort: ['sort'],
} : {
filter: {
developerMode: { '$isFalsy': true },
},
sort: ['sort'],
}));
const items = [];
for (const page of pages) {
if (!isRoot
&& !routesPermissionsMap.has(`pages:${page.id}`)
// 以下路径先临时处理
&& page.get('path') !== '/'
&& page.get('path') !== '/admin'
&& page.get('path') !== '/register'
&& page.get('path') !== '/login'
) {
continue;
}
items.push(page.toJSON());
if (page.get('path') === '/collections') {
const collections = await Collection.findAll(Collection.parseApiJson(ctx.state.developerMode ? {
filter: {
showInDataMenu: true,
},
sort: ['sort'],
} : {
filter: {
developerMode: { '$isFalsy': true },
showInDataMenu: true,
},
sort: ['sort'],
}));
for (const collection of collections) {
if (!isRoot && !routesPermissionsMap.has(`collections:${collection.id}`)) {
continue;
}
const pageId = `collection-${collection.id}`;
items.push({
id: pageId,
type: 'collection',
collection: collection.get('name'),
title: collection.get('title'),
icon: collection.get('icon'),
path: `/collections/${collection.name}`,
parent_id: page.id,
showInMenu: true,
sort: collection.get('sort'),
});
const views = await collection.getViews({
where: {
[Op.or]: [
{ showInDataMenu: true },
{ default: true }
]
},
order: [['sort', 'asc']]
});
if (views.length > 1) {
for (const view of views) {
if (!isRoot && !routesPermissionsMap.has(`views:${view.id}`)) {
continue;
}
items.push({
id: `view-${view.get('id')}`,
type: 'collection',
collection: collection.get('name'),
title: view.title,
viewName: view.name,
path: `/collections/${collection.name}/views/${view.name}`,
parent_id: pageId,
showInMenu: true,
sort: view.get('sort'),
});
}
}
}
} else if (page.get('path') === '/users/users') {
const userViews = await View.findAll(View.parseApiJson(ctx.state.developerMode ? {
filter: {
collection_name: 'users',
showInDataMenu: true,
},
sort: ['sort'],
} : {
filter: {
collection_name: 'users',
developerMode: { '$isFalsy': true },
showInDataMenu: true,
},
sort: ['sort'],
}));
if (userViews.length > 1) {
for (const view of userViews) {
if (!isRoot && !routesPermissionsMap.has(`views:${view.id}`)) {
continue;
}
items.push({
id: `view-${view.get('id')}`,
type: 'collection',
collection: 'users',
title: view.title,
viewName: view.name,
path: `${page.get('path')}/views/${view.name}`,
parent_id: page.id,
showInMenu: true,
sort: view.get('sort'),
});
}
}
}
}
const data = flatToTree(items, {
id: 'id',
parentId: 'parent_id',
children: 'children',
});
ctx.body = pages2routes(data);
await next();
}

View File

@ -1,76 +0,0 @@
import { Model, ModelCtor } from '@nocobase/database';
import { flatToTree } from '../utils';
import { Op } from 'sequelize';
export function generateName(): string {
return `${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
}
function toPaths(item) {
if (!Array.isArray(item.children)) {
return [];
}
if (item.children.length === 0) {
return [];
}
let paths = [];
for (const child of item.children) {
if (child.path && !child.children) {
paths.push(child.path);
}
if (child.children) {
child.paths = toPaths(child);
paths = paths.concat(child.paths);
}
}
return paths;
}
export default async (ctx, next) => {
const { resourceName, resourceKey } = ctx.action.params;
const [Menu] = ctx.db.getModels(['menus']) as ModelCtor<Model>[];
const roles = ctx.ac ? await ctx.ac.getRoles() : [];
const isRoot = ctx.ac.constructor.isRoot(roles);
const MenuPermission = ctx.db.getModel('menus_permissions');
const menu_permissions = await MenuPermission.findAll({
where: {
role_id: {
[Op.in]: roles.map(role => role.id),
}
}
});
const menuIds = menu_permissions.map(item => item.menu_id);
const menus = await Menu.findAll(Menu.parseApiJson({
filter: isRoot ? {
} : {
'id.in': menuIds,
},
sort: 'sort',
}));
const data = flatToTree(menus.map(item => {
const json: any = item.toJSON();
if (item.url) {
json.path = item.url;
} else {
json.path = item.name;
}
return json;
}), {
id: 'id',
parentId: 'parent_id',
children: 'children',
});
const items = [];
for (const item of data) {
if (item.parent_id) {
continue;
}
item.paths = toPaths(item);
if (item.paths[0]) {
item.path = item.paths[0];
}
items.push(item);
}
ctx.body = items;
await next();
}

View File

@ -1,589 +0,0 @@
import { Model, ModelCtor, BELONGSTOMANY } from '@nocobase/database';
import { get, set, isString } from 'lodash';
import { Op } from 'sequelize';
const transforms = {
table: async (fields: Model[], context?: any) => {
const arr = [];
for (const field of fields) {
if (!field.get('component.showInTable')) {
continue;
}
if (!context.listFields.includes(field.id)) {
continue;
}
arr.push({
...field.get(),
sorter: field.get('sortable'),
dataIndex: field.name.split('.'),
});
}
return arr;
},
form: async (fields: Model[], ctx?: any) => {
const [Field] = ctx.db.getModels(['fields']) as ModelCtor<Model>[];
const mode = get(ctx.action.params, ['values', 'mode'], ctx.action.params.mode);
const schema = {};
for (const field of fields) {
if (field.get('component.type') === 'hidden') {
continue;
}
if (!field.get('component.showInForm')) {
continue;
}
if (!ctx.listFields.includes(field.id)) {
continue;
}
const interfaceType = field.get('interface');
const type = field.get('component.type') || 'string';
const prop: any = {
type,
title: field.title || field.name,
...(field.component || {}),
}
if (field.interface === 'description') {
field.title && set(prop, 'x-component-props.title', field.title);
field.get('component.tooltip') && set(prop, 'x-component-props.children', field.get('component.tooltip'));
}
if (ctx.formMode === 'update') {
if (!ctx.updateFields.includes(field.id)) {
set(prop, 'x-component-props.disabled', true);
}
} else if (!ctx.createFields.includes(field.id)) {
set(prop, 'x-component-props.disabled', true);
}
if (field.get('name') === 'interface' && ctx.state.developerMode === false) {
const dataSource = field.get('dataSource').filter(item => item.key !== 'developerMode');
field.set('dataSource', dataSource);
}
const { values } = ctx.action.params;
if (field.get('component.type') === 'filter' && get(values, 'associatedKey') && isString(get(values, 'associatedKey'))) {
const options = Field.parseApiJson(ctx.state.developerMode ? {
filter: {
collection_name: get(values, 'associatedKey'),
},
sort: 'sort',
} : {
filter: {
collection_name: get(values, 'associatedKey'),
developerMode: { '$isFalsy': true },
},
sort: 'sort',
});
const all = await Field.findAll(options);
set(prop, 'x-component-props.fields', all.filter(f => f.get('filterable')));
}
if (type === 'select') {
prop.type = 'string'
}
if (field.get('component.tooltip')) {
// prop.description = `{{html('${encodeURIComponent(field.get('component.tooltip'))}')}}`;
}
if (field.get('name') === 'dataSource') {
set(prop, 'x-component-props.operationsWidth', 'auto');
set(prop, 'x-component-props.bordered', true);
set(prop, 'x-component-props.className', 'data-source-table');
const properties = {};
if (ctx.state.developerMode) {
Object.assign(properties, {
value: {
type: "string",
title: "值",
// required: true,
'x-component-props': {
bordered: false,
},
},
});
}
Object.assign(properties, {
label: {
type: "string",
title: "选项",
required: true,
'x-component-props': {
bordered: false,
},
},
color: {
type: "colorSelect",
title: "颜色",
'x-component-props': {
bordered: false,
},
},
});
set(prop, 'items.properties', properties);
}
if (['number', 'percent'].includes(interfaceType) && field.get('precision')) {
set(prop, 'x-component-props.step', field.get('precision'));
}
if (field.get('required')) {
prop.required = true;
}
if (mode === 'update' && field.get('createOnly')) {
set(prop, 'x-component-props.disabled', true);
}
if (typeof field.get('showTime') === 'boolean') {
set(prop, 'x-component-props.showTime', field.get('showTime'));
}
const defaultValue = get(field.options, 'defaultValue');
if (typeof defaultValue !== 'undefined') {
prop.default = defaultValue;
}
if (interfaceType === 'boolean') {
set(prop, 'x-component-props.children', prop.title);
delete prop.title;
}
if (interfaceType === 'linkTo') {
set(prop, 'x-component-props.associatedName', field.get('collection_name'));
set(prop, 'x-component-props.target', field.get('target'));
set(prop, 'x-component-props.multiple', field.get('multiple'));
set(prop, 'x-component-props.labelField', field.get('labelField'));
}
if (interfaceType === 'multipleSelect') {
set(prop, 'x-component-props.mode', 'multiple');
}
if (interfaceType === 'subTable' && field.get('target')) {
set(prop, 'x-component-props.target', field.get('target'));
// resourceName
}
if (['radio', 'select', 'multipleSelect', 'checkboxes'].includes(interfaceType)) {
prop.enum = field.get('dataSource');
}
if (interfaceType === 'chinaRegion') {
set(prop, 'x-component-props.target', field.get('target'));
set(prop, 'x-component-props.labelField', field.get('labelField'));
set(prop, 'x-component-props.valueField', field.get('targetKey'));
set(prop, 'x-component-props.parentField', field.get('parentField'));
set(prop, 'x-component-props.maxLevel', field.get('maxLevel'));
set(prop, 'x-component-props.changeOnSelect', field.get('incompletely'));
}
schema[field.name] = {
id: field.id,
...prop,
};
}
return schema;
},
details: async (fields: Model[], context?: any) => {
const [Field] = context.db.getModels(['fields']) as ModelCtor<Model>[];
const arr = [];
for (const field of fields) {
if (!get(field.component, 'showInDetail')) {
continue;
}
if (!context.listFields.includes(field.id)) {
continue;
}
const props = {};
if (field.get('interface') === 'subTable') {
const children = await Field.findAll(Field.parseApiJson({
filter: {
collection_name: field.get('target'),
},
perPage: -1,
sort: ['sort'],
}));
// const children = await field.getChildren({
// order: [['sort', 'asc']],
// });
props['children'] = children.filter(item => item.get('component.showInTable')).map(child => ({ ...child.toJSON(), dataIndex: child.name.split('.') }))
}
arr.push({
...field.toJSON(),
...props,
});
}
return arr;
},
filter: async (fields: Model[], ctx?: any) => {
const properties = {
filter: {
type: 'filter',
'x-component-props': {
fields: fields.filter(field => ctx.listFields.includes(field.id) && field.get('filterable')),
},
},
}
return properties;
},
};
export default async (ctx, next) => {
const { resourceName, resourceKey, values = {} } = ctx.action.params;
const [View, Collection, Field, Action] = ctx.db.getModels(['views', 'collections', 'fields', 'actions']) as ModelCtor<Model>[];
const collection = await Collection.findOne({
where: {
name: resourceName,
},
});
let view = await View.findOne(View.parseApiJson({
filter: {
collection_name: resourceName,
name: resourceKey,
},
// fields: {
// appends: ['actions', 'fields'],
// },
}));
let throughName;
const { resourceKey: resourceKey2, associatedName, resourceFieldName, associatedKey } = values;
// TODO: 暂时不处理 developerMode 和 internal 的情况
const permissions = (await ctx.ac.isRoot() || collection.developerMode || collection.internal)
? await ctx.ac.getRootPermissions()
: await ctx.ac.can(resourceName).permissions();
ctx.listFields = [];
ctx.createFields = [];
ctx.updateFields = [];
ctx.allowedActions = [];
for (const action of permissions.actions) {
ctx.allowedActions.push(action.name);
}
// console.log(ctx.allowedActions);
for (const permissionField of permissions.fields) {
const pfc = permissionField.actions;
if (pfc.includes(`${resourceName}:list`)) {
ctx.listFields.push(permissionField.field_id);
}
if (pfc.includes(`${resourceName}:create`)) {
ctx.createFields.push(permissionField.field_id);
}
if (pfc.includes(`${resourceName}:update`)) {
ctx.updateFields.push(permissionField.field_id);
}
}
// console.log({
// a: (await ctx.ac.isRoot() || collection.developerMode || collection.internal),
// listFields: ctx.listFields,
// createFields: ctx.createFields,
// updateFields: ctx.updateFields,
// })
if (associatedName) {
const table = ctx.db.getTable(associatedName);
const resourceField = table.getField(resourceFieldName);
if (resourceField instanceof BELONGSTOMANY) {
// console.log({associatedName, resourceField});
throughName = resourceField.options.through;
}
}
if (!view) {
// 如果不存在 view新建一个
view = new View({ type: resourceKey, template: 'FilterForm' });
}
// const where: any = {
// developerMode: ctx.state.developerMode,
// }
const filter: any = {}
if (!ctx.state.developerMode) {
filter.developerMode = { '$isFalsy': true }
}
if (!view.get('draggable')) {
filter.type = {
not: 'sort',
};
}
const fieldOptions = Field.parseApiJson({
filter,
sort: 'sort',
});
let fields = await collection.getFields(fieldOptions);
fields = fields.filter(field => {
if (field.get('interface') === 'linkTo') {
if (throughName && throughName === field.get('through')) {
return false;
}
}
return true;
})
const options = Action.parseApiJson(ctx.state.developerMode ? {
filter: {},
sort: 'sort',
} : {
filter: {
developerMode: { '$isFalsy': true },
},
sort: 'sort',
});
const actions = await collection.getActions(options);
let actionNames = view.get('actionNames') || [];
if (actionNames.length === 0 && resourceKey !== 'permissionTable') {
actionNames = ['filter', 'create', 'destroy'];
}
const defaultTabs = await collection.getTabs({
where: {
id: { [Op.in]: permissions.tabs },
},
order: [['sort', 'asc']],
});
const defaultTab = defaultTabs.find(tab => tab.default) || get(defaultTabs, [0]);
view.setDataValue('defaultTabName', get(defaultTab, ['name']));
if (view.get('type') === 'table') {
view.setDataValue('rowViewName', 'form');
}
if (view.get('type') === 'calendar') {
view.setDataValue('template', 'Calendar');
view.setDataValue('rowViewName', 'form');
}
if (view.get('updateViewName')) {
view.setDataValue('rowViewName', view.get('updateViewName'));
}
if (!view.get('template')) {
if (view.get('type') === 'table') {
view.setDataValue('template', 'Table');
} else if (view.get('type') === 'calendar') {
view.setDataValue('template', 'Calendar');
}
}
// view.setDataValue('viewCollectionName', view.collection_name);
let title = collection.get('title');
const mode = get(ctx.action.params, ['values', 'mode'], ctx.action.params.mode);
ctx.formMode = mode;
if (mode === 'update') {
title = `编辑${title}`;
} else {
title = `新增${title}`;
}
const viewType = view.get('type');
const actionDefaultParams: any = {};
if (resourceName === 'collections') {
actionDefaultParams.sort = ['sort'];
}
if (view.filter) {
actionDefaultParams.filter = view.filter;
}
const appends = [];
const others: any = {};
if (viewType === 'form') {
if (associatedName === 'automations' && resourceFieldName === 'jobs' && mode !== 'update') {
const Automation = ctx.db.getModel('automations');
others['initialValues'] = {
automation: await Automation.findByPk(associatedKey),
};
}
}
for (const field of fields) {
if (!['subTable', 'linkTo', 'attachment', 'createdBy', 'updatedBy', 'chinaRegion'].includes(field.get('interface'))) {
continue;
}
let showInKey;
switch (viewType) {
case 'table':
showInKey = 'showInTable';
break;
case 'form':
showInKey = 'showInForm';
break;
case 'details':
showInKey = 'showInDetail';
break;
}
if (showInKey && field.get(`component.${showInKey}`)) {
appends.push(field.name);
if (field.get('interface') === 'attachment') {
appends.push(`${field.name}.storage`);
}
if (field.get('interface') === 'subTable') {
const children = await field.getChildren();
// console.log(children);
for (const child of children) {
if (!['subTable', 'linkTo', 'attachment', 'updatedBy', 'createdBy'].includes(child.get('interface'))) {
continue;
}
appends.push(`${field.name}.${child.name}`);
}
}
}
}
actionDefaultParams['fields[appends]'] = appends.join(',');
if (resourceFieldName === 'pages' && resourceKey === 'permissionTable') {
ctx.body = {
...view.get(),
title,
actionDefaultParams,
original: fields,
disableRowClick: true,
fields: [
{
"title": "页面",
"name": "title",
"interface": "string",
"type": "string",
"parent_id": null,
"required": true,
"developerMode": false,
"component": {
"type": "string",
"className": "drag-visible",
"showInForm": true,
"showInTable": true,
"showInDetail": true
},
"dataIndex": ["title"]
},
{
"title": "访问权限",
"name": "accessible",
"interface": "boolean",
"type": "boolean",
"parent_id": null,
"required": true,
"editable": true,
"resource": 'roles.pages',
"developerMode": false,
"component": {
"type": "boolean",
"showInTable": true,
},
"dataIndex": ["accessible"]
}
],
};
} else if (resourceFieldName === 'collections' && resourceKey === 'permissionTable') {
ctx.body = {
...view.get(),
title,
actionDefaultParams,
original: fields,
rowKey: 'name',
fields: [
{
"title": "数据表名称",
"name": "title",
"interface": "string",
"type": "string",
"parent_id": null,
"required": true,
"developerMode": false,
"component": {
"type": "string",
"className": "drag-visible",
"showInForm": true,
"showInTable": true,
"showInDetail": true
},
"dataIndex": ["title"]
},
{
"title": "描述",
"name": "permissions[0].description",
"interface": "string",
"type": "string",
"parent_id": null,
"required": true,
"developerMode": false,
"component": {
"type": "string",
"className": "drag-visible",
"showInForm": true,
"showInTable": true,
"showInDetail": true
},
"dataIndex": ["permissions", 0, 'description']
}
],
};
} else if (
(resourceFieldName === 'collections' && resourceKey === 'permissionForm')
||
(resourceFieldName === 'roles' && resourceKey === 'permissionForm')
) {
ctx.body = {
...view.get(),
title,
actionDefaultParams,
original: fields,
fields: {
actions: {
type: 'permissions.actions',
title: '数据操作权限',
'x-linkages': [
{
type: "value:schema",
target: "actions",
schema: {
"x-component-props": {
resourceKey: resourceFieldName === 'roles' ? associatedKey : "{{ $form.values && $form.values.resourceKey }}"
},
},
},
],
'x-component-props': {
dataSource: [],
},
},
fields: {
type: 'permissions.fields',
title: '字段权限',
'x-linkages': [
{
type: "value:schema",
target: "fields",
schema: {
"x-component-props": {
resourceKey: resourceFieldName === 'roles' ? associatedKey : "{{ $form.values && $form.values.resourceKey }}"
},
},
},
],
'x-component-props': {
dataSource: [],
}
},
tabs: {
type: 'permissions.tabs',
title: '标签页权限',
'x-linkages': [
{
type: "value:schema",
target: "tabs",
schema: {
"x-component-props": {
resourceKey: resourceFieldName === 'roles' ? associatedKey : "{{ $form.values && $form.values.resourceKey }}"
},
},
},
],
'x-component-props': {
dataSource: [],
}
},
description: {
type: 'textarea',
title: '权限描述',
},
},
};
} else {
let allowedUpdate = false;
if (view.type === 'details' && await ctx.ac.can(resourceName).act('update').one(resourceKey2)) {
allowedUpdate = true;
}
ctx.body = {
...view.get(),
...others,
title,
actionDefaultParams,
original: fields,
fields: await (transforms[view.type] || transforms.table)(fields, ctx),
actions: actions.filter(action => {
if (view.type === 'details' && action.name === 'update') {
return allowedUpdate;
}
return actionNames.includes(action.name) && ctx.allowedActions.includes(`${resourceName}:${action.name}`);
}).map(action => ({
...action.toJSON(),
...action.options,
// viewCollectionName: action.collection_name,
})),
};
}
await next();
};

View File

@ -1,15 +0,0 @@
import { Model, ModelCtor } from '@nocobase/database';
import { actions } from '@nocobase/actions';
import { flatToTree } from '../utils';
export const list = async (ctx, next) => {
await actions.common.list(ctx, async () => {
const items = ctx.body.rows as any;
ctx.body.rows = flatToTree(items.map(item => item.toJSON()), {
id: 'id',
parentId: 'parent_id',
children: 'children',
});
});
await next();
}

View File

@ -1,32 +0,0 @@
import { Op } from 'sequelize';
import { actions } from '@nocobase/actions';
import _ from 'lodash';
import { flatToTree } from '../utils';
export async function list(ctx: any, next: actions.Next) {
const { associated, associatedKey } = ctx.action.params;
// TODO: 暂时 action 中间件就这么写了
ctx.action.mergeParams({
associated: null
});
await actions.common.list(ctx, async () => {
});
const MenuPermission = ctx.db.getModel('menus_permissions');
const rows = ctx.body.rows as any;
for (const row of rows) {
row.setDataValue('associatedKey', associatedKey);
const mp = await MenuPermission.findOne({
where: {
role_id: associatedKey,
menu_id: row.id,
},
});
row.setDataValue('accessible', !!mp);
}
ctx.body.rows = flatToTree(rows.map(item => item.toJSON()), {
id: 'id',
parentId: 'parent_id',
children: 'children',
});
await next();
}

View File

@ -1,277 +0,0 @@
import { actions } from '@nocobase/actions';
import Database from '@nocobase/database';
import { flatToTree } from '../utils';
import { Op } from 'sequelize';
async function getRoutes(ctx) {
const database: Database = ctx.db;
const Page = database.getModel('pages');
const View = database.getModel('views');
const Collection = database.getModel('collections');
let pages = await Page.findAll(Page.parseApiJson(ctx.state.developerMode ? {
filter: {
'parent_id.$notNull': true,
},
sort: ['sort'],
} : {
filter: {
'parent_id.$notNull': true,
developerMode: { '$isFalsy': true },
},
sort: ['sort'],
}));
const items = [];
for (const page of pages) {
items.push({
routable_type: 'pages',
routable_id: page.id,
});
if (page.get('path') === '/collections') {
const collections = await Collection.findAll(Collection.parseApiJson(ctx.state.developerMode ? {
filter: {
showInDataMenu: true,
},
sort: ['sort'],
} : {
filter: {
developerMode: { '$isFalsy': true },
showInDataMenu: true,
},
sort: ['sort'],
}));
for (const collection of collections) {
items.push({
routable_type: 'collections',
routable_id: collection.id,
});
const views = await collection.getViews({
where: {
[Op.or]: [
{ showInDataMenu: true },
{ default: true }
]
},
order: [['sort', 'asc']]
});
if (views.length > 1) {
for (const view of views) {
items.push({
routable_id: view.id,
routable_type: 'views',
});
}
}
}
} else if (page.get('path') === '/users/users') {
const userViews = await View.findAll(View.parseApiJson(ctx.state.developerMode ? {
filter: {
showInDataMenu: true,
collection_name: 'users',
},
sort: ['sort'],
} : {
filter: {
developerMode: { '$isFalsy': true },
showInDataMenu: true,
collection_name: 'users',
},
sort: ['sort'],
}));
if (userViews.length > 1) {
for (const view of userViews) {
items.push({
routable_id: view.id,
routable_type: 'views',
});
}
}
}
}
return items;
}
export async function list(ctx: actions.Context, next: actions.Next) {
const database: Database = ctx.db;
const { associatedKey, associated } = ctx.action.params;
const Page = database.getModel('pages');
const View = database.getModel('views');
const Collection = database.getModel('collections');
// TODO(optimize): isRoot 的判断需要在内部完成,尽量不要交给调用者
const isRoot = ctx.ac.constructor.isRoot(associated);
const routesPermissionsMap = new Map();
if (!isRoot) {
const routesPermissions = await associated.getRoutes();
routesPermissions.forEach(permission => {
routesPermissionsMap.set(`${permission.routable_type}:${permission.routable_id}`, permission);
});
}
let pages = await Page.findAll(Page.parseApiJson(ctx.state.developerMode ? {
filter: {
'parent_id.$notNull': true,
},
sort: ['sort'],
} : {
filter: {
'parent_id.$notNull': true,
developerMode: { '$isFalsy': true },
},
sort: ['sort'],
}));
const items = [];
for (const page of pages) {
items.push({
id: page.id,
key: `page-${page.id}`,
title: page.title,
tableName: 'pages',
parent_id: `page-${page.parent_id}`,
associatedKey,
accessible: isRoot || routesPermissionsMap.has(`pages:${page.id}`), // TODO 对接权限
});
if (page.get('path') === '/collections') {
const collections = await Collection.findAll(Collection.parseApiJson(ctx.state.developerMode ? {
filter: {
showInDataMenu: true,
},
sort: ['sort'],
} : {
filter: {
developerMode: { '$isFalsy': true },
showInDataMenu: true,
},
sort: ['sort'],
}));
for (const collection of collections) {
items.push({
associatedKey,
id: collection.id,
key: `collection-${collection.id}`,
tableName: 'collections',
title: collection.get('title'),
parent_id: `page-${page.id}`,
accessible: isRoot || routesPermissionsMap.has(`collections:${collection.id}`), // TODO 对接权限
});
const views = await collection.getViews({
where: {
[Op.or]: [
{ showInDataMenu: true },
{ default: true }
]
},
order: [['sort', 'asc']]
});
if (views.length > 1) {
for (const view of views) {
items.push({
associatedKey,
id: view.id,
tableName: 'views',
title: view.title,
key: `view-${view.id}`,
parent_id: `collection-${collection.id}`,
accessible: isRoot || routesPermissionsMap.has(`views:${view.id}`), // TODO 对接权限
});
}
}
}
} else if (page.get('path') === '/users/users') {
const userViews = await View.findAll(View.parseApiJson(ctx.state.developerMode ? {
filter: {
showInDataMenu: true,
collection_name: 'users',
},
sort: ['sort'],
} : {
filter: {
collection_name: 'users',
developerMode: { '$isFalsy': true },
showInDataMenu: true,
},
sort: ['sort'],
}));
if (userViews.length > 1) {
for (const view of userViews) {
items.push({
associatedKey,
id: view.id,
tableName: 'views',
title: view.title,
key: `view-${view.id}`,
parent_id: `page-${page.id}`,
accessible: isRoot || routesPermissionsMap.has(`views:${view.id}`), // TODO 对接权限
});
}
}
}
}
const data = flatToTree(items, {
id: 'key',
parentId: 'parent_id',
children: 'children',
});
ctx.body = data;
// TODO: 暂时 action 中间件就这么写了
// ctx.action.mergeParams({associated: null});
// const { associatedKey } = ctx.action.params;
// ctx.action.mergeParams({
// filter: {
// 'parent_id.$notNull': true,
// }
// })
// const done = async () => {
// ctx.body.rows = ctx.body.rows.map(row => {
// row.setDataValue('tableName', 'pages');
// row.setDataValue('associatedKey', parseInt(associatedKey));
// return row.get();
// });
// console.log(ctx.body.rows);
// await next();
// }
// return actions.common.list(ctx, done);
}
export async function update(ctx: actions.Context, next: actions.Next) {
const {
associated,
resourceKey,
values: {
tableName,
accessible
}
} = ctx.action.params;
if (!resourceKey) {
if (accessible === false) {
await associated.updateAssociations({
routes: [],
});
} else if (accessible === true) {
const routes = await getRoutes(ctx);
// console.log(routes);
await associated.updateAssociations({
routes,
});
}
ctx.body = {};
return next();
}
// console.log(ctx.action.params, { routable_type: tableName, routable_id: resourceKey });
let [route] = await associated.getRoutes({
where: { routable_type: tableName, routable_id: resourceKey },
limit: 1
});
if (accessible) {
if (!route) {
route = await associated.createRoute({ routable_type: tableName, routable_id: resourceKey });
}
ctx.body = route;
} else {
if (route) {
await route.destroy();
}
}
await next();
}

View File

@ -1,398 +0,0 @@
import { ResourceOptions } from '@nocobase/resourcer';
import { Model, ModelCtor } from '@nocobase/database';
import actions from '@nocobase/actions';
import { merge } from '../utils';
import _ from 'lodash';
import { getViewTypeLinkages } from '../views';
export const getInfo = async (ctx: actions.Context, next) => {
const { resourceKey } = ctx.action.params;
const View = ctx.db.getModel('views_v2') as ModelCtor<Model>;
const Field = ctx.db.getModel('fields') as ModelCtor<Model>;
let primaryKey: string;
let viewName: string;
let collectionName: string;
let associatedName: string;
let resourceName: string;
let associationField: any;
if (!resourceKey.includes('.')) {
const view = await View.findOne({
where: {
name: resourceKey,
},
});
const viewData: any = view.toJSON();
for (const [key, value] of Object.entries(viewData[`x-${viewData.type}-props`] || {})) {
if (_.get(viewData, key) === null || _.get(viewData, key) === undefined) {
_.set(viewData, key, value);
}
}
ctx.body = viewData;
return next();
}
if (resourceKey.includes('.')) {
const keys = resourceKey.split('.');
viewName = keys.pop();
const key1 = keys.shift();
const key2 = keys.join('.');
// const [key1, key2] = keys;
if (key2) {
const field = ctx.db.getTable(key1).getField(key2);
associationField = await Field.findOne({
where: {
collection_name: key1,
name: key2,
},
});
collectionName = field.options.target;
associatedName = key1;
resourceName = key2;
} else {
collectionName = key1;
}
}
// console.log({viewName, collectionName, associatedName})
let view = await View.findOne({
where: {
name: viewName,
collection_name: collectionName
},
});
if (view && view.type === 'form') {
const statusable = !!ctx.db.getTable(view.collection_name).getField('status');
if (statusable) {
view.setDataValue('statusable', statusable);
}
}
const Collection = ctx.db.getModel('collections') as ModelCtor<Model>;
const M = ctx.db.getModel(collectionName) as ModelCtor<Model>;
// if (!view && viewName === 'descriptions') {
// view = await View.findOne({
// where: {
// name: 'form',
// collection_name: collectionName
// },
// });
// view.type = 'descriptions';
// }
let viewData: any = view ? view.toJSON() : {};
if (view) {
viewData.fields = view.get(`x-${view.type}-props.fields`) || view.get('fields') || [];
viewData.actions = view.get(`x-${view.type}-props.actions`) || view.get('actions') || [];
viewData.details = view.get(`x-${view.type}-props.details`) || view.get('details') || [];
}
if (!view) {
const collection = await Collection.findOne({
where: {
name: collectionName,
}
});
const fields = await collection.getFields({
order: [['sort', 'asc']],
});
if (viewName === 'table') {
viewData = {
collection_name: collectionName,
type: 'table',
name: 'table',
title: '全部数据',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
// labelField: 'title',
fields: fields.filter(field => {
return field.name !== 'action_logs';
}).map(field => field.name),
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['id'],
};
} else if (viewName === 'form') {
viewData = {
collection_name: collectionName,
type: 'form',
name: 'form',
title: '表单',
actions: [
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
}
],
// labelField: 'title',
fields: fields.filter(field => {
return field.name !== 'action_logs';
}).map(field => field.name),
};
} else if (viewName === 'descriptions') {
viewData = {
collection_name: collectionName,
type: 'descriptions',
name: 'descriptions',
title: '详情',
// labelField: 'title',
actions: [],
fields: fields.filter(field => {
return field.name !== 'action_logs';
}).map(field => field.name),
};
}
}
// ctx.body = {};
// return next();
const fields = [];
for (const field of viewData.fields || []) {
let fieldName: any;
let json: any;
if (typeof field === 'string') {
fieldName = field;
} else if (typeof field === 'object') {
// console.log({field});
if (field.field) {
const { field: f, ...others } = field;
fieldName = f.name;
json = { ...others };
} else if (field.name) {
fieldName = field.name;
json = field;
}
}
const model = await Field.findOne({
where: {
collection_name: viewData.collection_name,
name: fieldName,
},
});
if (model) {
const target = model.get('target');
if (target && model.get('interface') === 'subTable') {
const children = await Field.findAll(Field.parseApiJson({
filter: {
collection_name: target,
'name.ne': 'action_logs',
},
sort: 'sort',
}));
if (children.length) {
model.setDataValue('children', children);
}
}
model.setDataValue('dataIndex', model.name.split('.'));
if (typeof field === 'object') {
json = merge(model.toJSON(), json);
} else {
json = model.toJSON();
}
}
if (!json.name) {
continue;
}
// console.log({field, json})
if (json.name === 'type' && json.collection_name === 'views_v2') {
json.linkages = getViewTypeLinkages();
}
json && fields.push(json);
}
const actions = [];
const toFields = async (values = []) => {
const fields = [];
for (const value of values) {
if (typeof value === 'string') {
const model = await Field.findOne({
where: {
collection_name: viewData.collection_name,
name: value,
},
});
if (model) {
fields.push(model);
}
}
}
return fields;
}
if (viewData.actions) {
const parts = resourceKey.split('.');
parts.pop();
for (const action of viewData.actions || []) {
if (action.viewName) {
if (!action.viewName.includes('.')) {
action.viewName = `${parts.join('.')}.${action.viewName}`;
}
}
if (action.view && action.view.name) {
action.viewName = `${parts.join('.')}.${action.view.name}`;
}
if (action.fields) {
action.fields = await toFields(action.fields);
}
actions.push({
...action,
})
}
}
const details = [];
for (const item of viewData.details || []) {
let vName: string;
if (typeof item === 'string') {
vName = item;
const sView = await View.findOne({
where: {
collection_name: viewData.collection_name,
name: vName,
}
});
if (sView) {
details.push({
title: sView.title,
views: [sView],
});
} else {
details.push({
title: '表单',
views: [{
name: item,
width: '50%',
}],
});
}
} else if (typeof item === 'object') {
if (item.views) {
// TODO 标签页多视图支持
} else if (item.view) {
const { view: v, ...others } = item;
vName = v.name;
const sView = await View.findOne({
where: {
collection_name: viewData.collection_name,
name: vName,
}
});
sView && details.push({
...others,
views: [sView],
});
}
}
}
const data: any = {
...viewData,
details,
actions,
fields,
};
if (data.target_field_id) {
const targetField = await Field.findByPk(data.target_field_id);
data.targetFieldName = targetField.name;
}
if (data.target_view_id) {
const targetView = await View.findByPk(data.target_view_id);
data.targetViewName = targetView.name;
}
if (associationField) {
data.associationField = associationField;
}
if (data.dataSourceType === 'association') {
const field = await Field.findOne({
where: {
name: data.targetFieldName,
collection_name: collectionName,
}
});
// console.log(field, data.targetFieldName, collectionName)
const targetViewName = `${field.get('target')}.${data.targetViewName}`;
const resourceName = `${collectionName}.${data.targetFieldName}`;
ctx.action.mergeParams({
resourceKey: targetViewName,
});
await getInfo(ctx, async () => { });
const body = ctx.body as any;
const actions = body.actions.map(action => {
if (action.viewName) {
const names = action.viewName.split('.');
action.viewName = `${resourceName}.${names.pop()}`;
}
return action;
});
const item = {
...body,
associationField: field,
resourceName,
actions,
rowKey: resourceKey === 'roles.collections' ? 'name' : body.rowKey,
}
ctx.body = item;
return next();
} else {
data.rowKey = data.rowKey || M.primaryKeyAttribute;
if (associatedName) {
data.resourceName = `${associatedName}.${resourceName}`;
} else {
data.resourceName = collectionName;
}
data.appends = data.fields.filter(field => {
return ['hasMany', 'hasOne', 'belongsToMany', 'belongsTo'].includes(field.type);
}).map(field => field.name);
for (const field of data.fields) {
if (field.interface === 'attachment') {
data.appends.push(`${field.name}.storage`);
}
}
}
delete data['resourceKey'];
delete data['associatedKey'];
for (const [key, value] of Object.entries(viewData[`x-${viewData.type}-props`] || {})) {
if (_.get(data, key) === null || _.get(data, key) === undefined) {
_.set(data, key, value);
}
}
if (_.isEmpty(data.sort)) {
data.sort = [];
}
if (data.type === 'kanban' && data.groupField) {
const groupField = await Field.findOne({
where: {
name: data.groupField,
collection_name: data.collection_name,
}
});
data.groupField = groupField;
}
data.actions = data.actions.map(action => {
if (action.type === 'filter') {
if (!action.fields) {
action.fields = data.fields.filter(({ filterable }) => filterable);
}
}
return action;
});
ctx.body = data;
await next();
};

View File

@ -1,334 +0,0 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'menus',
title: '菜单和页面配置',
internal: true,
// model: 'CollectionModel',
developerMode: true,
createdAt: false,
updatedAt: false,
fields: [
{
interface: 'sort',
type: 'sort',
name: 'sort',
scope: ['parent_id'],
title: '排序',
component: {
type: 'sort',
className: 'drag-visible',
width: 60,
showInTable: true,
},
},
{
interface: 'string',
type: 'randomString',
name: 'name',
title: '缩略名',
required: true,
createOnly: true,
randomString: {
length: 6,
characters: 'abcdefghijklmnopqrstuvwxyz0123456789',
},
developerMode: true,
},
{
interface: 'linkTo',
multiple: false,
type: 'belongsTo',
name: 'parent',
title: '父级菜单',
target: 'menus',
foreignKey: 'parent_id',
targetKey: 'id',
labelField: 'title',
valueField: 'id',
component: {
type: 'drawerSelect',
'x-component-props': {
placeholder: '请选择菜单组',
labelField: 'title',
valueField: 'id',
filter: {
type: 'group',
},
},
},
},
{
interface: 'string',
type: 'string',
name: 'title',
title: '菜单/页面名称',
required: true,
},
{
interface: 'icon',
type: 'string',
name: 'icon',
title: '图标',
component: {
type: 'icon',
},
},
{
interface: 'radio',
type: 'string',
name: 'type',
title: '菜单类型',
required: true,
dataSource: [
{ value: 'group', label: '菜单组', color: 'red' },
{ value: 'page', label: '页面', color: 'green' },
{ value: 'link', label: '自定义链接', color: 'orange' },
],
component: {
'x-linkages': [
{
"type": "value:visible",
"target": "views",
"condition": "{{ $self.value === 'page' }}"
},
{
"type": "value:visible",
"target": "url",
"condition": "{{ $self.value === 'link' }}"
},
],
},
},
{
interface: 'string',
type: 'string',
name: 'url',
title: '链接地址',
required: true,
},
{
interface: 'boolean',
type: 'boolean',
name: 'developerMode',
title: '开发者模式',
developerMode: true,
defaultValue: false,
},
{
interface: 'json',
type: 'json',
name: 'views',
title: '显示在页面里的视图',
target: 'menus_views_v2',
component: {
type: 'subTable',
'x-component-props': {
viewName: 'menus.views',
},
'x-linkages': [
{
type: 'value:schema',
target: 'views',
schema: {
'x-component-props': {
__parent: '{{ $form.values && $form.values.associatedKey }}',
associatedKey: "{{ $form.values && $form.values.id }}"
},
},
},
],
},
},
// {
// interface: 'subTable',
// type: 'hasMany',
// name: 'menus_views',
// title: '显示在页面里的视图',
// target: 'menus_views_v2',
// sourceKey: 'id',
// foreignKey: 'menu_id',
// component: {
// type: 'subTable',
// 'x-component-props': {
// viewName: 'menus.menus_views',
// filter: {
// or: [
// {
// 'type.neq': 'descriptions',
// },
// {
// 'data_source_type.neq': 'association',
// }
// ],
// },
// },
// 'x-linkages': [
// {
// type: 'value:schema',
// target: 'menus_views',
// schema: {
// 'x-component-props': {
// __parent: '{{ $form.values && $form.values.associatedKey }}',
// associatedKey: "{{ $form.values && $form.values.id }}"
// },
// },
// },
// ],
// },
// },
{
interface: 'json',
type: 'json',
name: 'options',
title: '配置信息',
defaultValue: {},
developerMode: true,
},
{
type: 'hasMany',
name: 'children',
target: 'menus',
foreignKey: 'parent_id',
},
{
interface: 'boolean',
type: 'boolean',
name: 'developerMode',
title: '开发者模式',
defaultValue: false,
component: {
type: 'boolean',
},
},
],
actions: [
{
type: 'list',
name: 'list',
title: '查看',
},
{
type: 'create',
name: 'create',
title: '新增',
viewName: 'form',
},
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
},
{
type: 'destroy',
name: 'destroy',
title: '删除',
},
],
views: [
{
type: 'form',
name: 'form',
title: '表单',
template: 'DrawerForm',
},
{
type: 'details',
name: 'details',
title: '详情',
template: 'Details',
actionNames: ['update'],
},
{
type: 'table',
name: 'simple',
title: '简易模式',
template: 'SimpleTable',
default: true,
mode: 'simple',
actionNames: ['create', 'destroy'],
detailsViewName: 'details',
updateViewName: 'form',
paginated: false,
},
{
type: 'table',
name: 'table',
title: '列表',
template: 'Table',
actionNames: ['create', 'destroy'],
},
],
views_v2: [
{
developerMode: true,
type: 'table',
name: 'table',
title: '全部数据',
labelField: 'title',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
expandable: {
expandIconColumnIndex: 3,
},
paginated: false,
fields: ['sort', 'title', 'icon', 'type'],
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['sort'],
},
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: ['type', 'parent', 'title', 'icon', 'url', 'views'],
},
{
developerMode: true,
type: 'table',
name: 'permissions_table',
title: '权限表格',
labelField: 'title',
actions: [],
expandable: {},
fields: [
'title',
{
interface: 'boolean',
name: 'accessible',
type: 'boolean',
title: '允许访问',
dataIndex: ['accessible'],
editable: true,
resourceName: 'roles.menus',
component: {
type: 'checkbox',
},
},
],
// detailsOpenMode: 'drawer', // window
details: [],
sort: ['sort'],
},
{
developerMode: true,
type: 'form',
name: 'permissions_form',
title: '权限表单',
fields: ['type', 'title'],
},
],
} as TableOptions;

View File

@ -1,296 +0,0 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'menus_views_v2',
title: '页面视图',
internal: true,
// model: 'BaseModelV2',
developerMode: true,
createdAt: false,
updatedAt: false,
fields: [
{
interface: 'linkTo',
type: 'belongsTo',
name: 'view',
target: 'views_v2',
title: '视图',
labelField: 'title',
valueField: 'id',
multiple: false,
viewName: 'views_v2.form',
component: {
type: 'drawerSelect',
'x-component-props': {
viewName: 'views_v2.table',
resourceName: 'views_v2',
labelField: 'title',
valueField: 'id',
},
'x-linkages': [
{
"type": "value:visible",
"target": "returnType",
"condition": "{{ $self.value && $self.value.type === 'form' }}"
},
{
"type": "value:visible",
"target": "redirect",
"condition": "{{ $self.value && $self.value.type === 'form' }}"
},
{
"type": "value:visible",
"target": "message",
"condition": "{{ $self.value && $self.value.type === 'form' }}"
},
{
"type": "value:visible",
"target": "draft.returnType",
"condition": "{{ $self.value && $self.value.type === 'form' }}"
},
],
},
},
{
interface: 'string',
type: 'virtual',
name: 'view.collection.title',
title: '所属数据表',
},
{
interface: 'radio',
type: 'string',
name: 'width',
title: '宽度',
dataSource: [
{ label: '50%', value: '50%' },
{ label: '100%', value: '100%' },
],
component: {
type: 'radio',
'x-linkages': [
{
"type": "value:visible",
"target": "float",
"condition": "{{ $self.value && $self.value === '50%' }}"
},
]
},
},
{
interface: 'radio',
type: 'string',
name: 'float',
title: '位置',
dataSource: [
{ label: '左边', value: 'left' },
{ label: '右边', value: 'right' },
],
component: {
type: 'radio',
},
},
{
interface: 'radio',
type: 'string',
name: 'returnType',
title: '表单提交成功后',
dataSource: [
{ label: '显示文字信息', value: 'message' },
{ label: '跳转到页面', value: 'redirect' },
],
component: {
type: 'radio',
'x-linkages': [
{
"type": "value:visible",
"target": "message",
"condition": "{{ $self.value === 'message' }}"
},
{
"type": "value:visible",
"target": "redirect",
"condition": "{{ $self.value === 'redirect' }}"
},
],
},
},
{
interface: 'linkTo',
type: 'belongsTo',
name: 'redirect',
target: 'menus',
title: '跳转到页面',
labelField: 'title',
valueField: 'id',
multiple: false,
component: {
type: 'drawerSelect',
'x-component-props': {
viewName: 'menus.table',
resourceName: 'menus',
labelField: 'title',
valueField: 'id',
},
},
},
{
interface: 'markdown',
type: 'json',
title: '显示文字信息',
name: 'message',
component: {
type: 'markdown',
},
},
{
interface: 'radio',
type: 'virtual',
name: 'draft.returnType',
title: '草稿提交成功后',
dataSource: [
{ label: '显示文字信息', value: 'message' },
{ label: '跳转到页面', value: 'redirect' },
],
component: {
type: 'radio',
'x-linkages': [
{
"type": "value:visible",
"target": "draft.message",
"condition": "{{ $self.value === 'message' }}"
},
{
"type": "value:visible",
"target": "draft.redirect",
"condition": "{{ $self.value === 'redirect' }}"
},
],
},
},
{
interface: 'linkTo',
type: 'virtual',
name: 'draft.redirect',
target: 'menus',
title: '跳转到页面',
labelField: 'title',
valueField: 'id',
multiple: false,
component: {
type: 'drawerSelect',
'x-component-props': {
viewName: 'menus.table',
resourceName: 'menus',
labelField: 'title',
valueField: 'id',
},
},
},
{
interface: 'markdown',
type: 'virtual',
title: '显示文字信息',
name: 'draft.message',
component: {
type: 'markdown',
},
}
],
views_v2: [
{
developerMode: true,
type: 'table',
name: 'table',
title: '全部数据',
labelField: 'title',
draggable: true,
actions: [
{
name: 'add',
type: 'add',
title: '选择',
transform: {
'data': 'view',
'data.title': 'title',
},
viewName: 'views_v2.table',
componentProps: {
type: 'primary',
},
filter: {
and: [
{ 'type.ne': 'descriptions' },
{ 'data_source_type.ne': 'association' },
]
},
},
{
name: 'create',
type: 'create',
title: '新增',
transform: {
'data': 'view',
'data.title': 'title',
},
componentProps: {
type: 'default',
},
viewName: 'views_v2.form',
},
{
name: 'destroy',
type: 'destroy',
title: '移除',
},
],
fields: [
'view',
'view.collection.title',
'width',
'float'
],
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['id'],
},
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: [
'view',
'width',
'float',
'returnType',
'redirect',
'message',
'draft.returnType',
'draft.redirect',
'draft.message',
],
},
{
developerMode: true,
type: 'descriptions',
name: 'descriptions',
title: '详情',
actions: [
{
name: 'update',
type: 'update',
title: '编辑',
},
],
fields: [
'view',
'width',
'float',
'returnType',
'redirect',
'message',
],
},
],
} as TableOptions;

View File

@ -1,238 +0,0 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'pages',
title: '页面配置',
model: 'PageModel',
internal: true,
developerMode: true,
fields: [
{
interface: 'sort',
type: 'sort',
name: 'sort',
scope: ['parent_id'],
title: '排序',
component: {
type: 'sort',
className: 'drag-visible',
width: 60,
showInTable: true,
},
},
{
interface: 'string',
type: 'string',
name: 'title',
title: '名称',
component: {
type: 'string',
className: 'drag-visible',
showInTable: true,
showInForm: true,
showInDetail: true,
},
},
{
interface: 'number',
type: 'integer',
name: 'parent_id',
title: '父级页面',
component: {
type: 'number',
showInForm: true,
showInDetail: true,
},
},
{
interface: 'string',
type: 'string',
name: 'path',
title: '路径',
unique: true,
component: {
type: 'string',
showInTable: true,
showInForm: true,
showInDetail: true,
},
},
{
interface: 'icon',
type: 'string',
name: 'icon',
title: '图标',
component: {
type: 'icon',
showInTable: true,
showInForm: true,
showInDetail: true,
},
},
{
interface: 'select',
type: 'string',
name: 'type',
title: '类型',
dataSource: [
{
label: '页面',
value: 'page',
},
{
label: '布局',
value: 'layout',
},
{
label: '数据集',
value: 'collection',
},
],
component: {
type: 'string',
showInTable: true,
showInForm: true,
showInDetail: true,
'x-linkages': [
{
"type": "value:visible",
"target": "collection",
"condition": "{{ ['collection'].indexOf($self.value) !== -1 }}"
},
]
},
},
{
interface: 'select',
type: 'string',
name: 'collection',
title: '属于哪种数据集?',
component: {
type: 'select',
showInForm: true,
showInDetail: true,
},
},
{
interface: 'select',
type: 'string',
name: 'template',
title: '模板',
dataSource: [
{
label: '顶部菜单布局',
value: 'TopMenuLayout',
},
{
label: '左侧菜单布局',
value: 'SideMenuLayout',
},
],
component: {
type: 'select',
showInTable: true,
showInForm: true,
showInDetail: true,
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'showInMenu',
title: '在菜单里显示',
defaultValue: false,
component: {
type: 'checkbox',
showInTable: true,
showInForm: true,
showInDetail: true,
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'inherit',
title: '继承父级页面内容',
defaultValue: true,
component: {
type: 'checkbox',
showInTable: true,
showInForm: true,
showInDetail: true,
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'developerMode',
title: '开发者模式',
defaultValue: false,
component: {
type: 'boolean',
},
},
{
interface: 'linkTo',
type: 'hasMany',
name: 'children',
title: '子页面',
target: 'pages',
foreignKey: 'parent_id',
sourceKey: 'id',
component: {
type: 'drawerSelect',
},
},
{
interface: 'number',
type: 'integer',
name: 'viewId',
title: '视图ID',
component: {
type: 'number',
},
},
// {
// type: 'belongsToMany',
// name: 'roles',
// through: 'routes_permissions',
// foreignKey: 'routable_id',
// otherKey: 'role_id',
// morphType: 'routable',
// constraints: false,
// },
{
interface: 'json',
type: 'json',
name: 'options',
title: '元数据',
component: {
type: 'hidden',
},
},
],
actions: [
{
type: 'list',
name: 'list',
title: '查看',
},
{
type: 'create',
name: 'create',
title: '新增',
viewName: 'form',
},
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
},
{
type: 'destroy',
name: 'destroy',
title: '删除',
},
],
} as TableOptions;

View File

@ -1,267 +0,0 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'pages_v2',
title: '页面配置',
internal: true,
model: 'BaseModel',
developerMode: true,
createdAt: false,
updatedAt: false,
fields: [
{
interface: 'string',
type: 'string',
name: 'title',
title: '页面名称',
required: true,
},
{
interface: 'string',
type: 'string',
name: 'path',
title: '路径',
required: true,
unique: true,
createOnly: true,
developerMode: true,
},
{
interface: 'string',
type: 'randomString',
name: 'name',
title: '缩略名',
required: true,
createOnly: true,
randomString: {
length: 6,
characters: 'abcdefghijklmnopqrstuvwxyz0123456789',
},
developerMode: true,
},
{
interface: 'radio',
type: 'string',
name: 'type',
title: '类型',
required: true,
defaultValue: 'static',
dataSource: [
{ value: 'static', label: '多条数据页面' },
{ value: 'dynamic', label: '单条数据子页面' },
],
},
{
interface: 'linkTo',
type: 'belongsTo',
name: 'collection',
target: 'collections',
targetKey: 'name',
title: '所属数据表',
labelField: 'title',
valueField: 'name',
multiple: false,
component: {
type: 'remoteSelect',
showInDetail: true,
showInForm: true,
'x-component-props': {
resourceName: 'collections',
labelField: 'title',
valueField: 'name',
},
},
},
{
interface: 'json',
type: 'virtual',
name: 'views',
title: '显示在页面里的视图',
target: 'pages_views_v2',
component: {
type: 'subTable',
'x-linkages': [
{
type: 'value:schema',
target: 'views',
schema: {
'x-component-props': {
__parent: '{{ $form.values && $form.values.associatedKey }}',
associatedKey: "{{ $form.values && $form.values.id }}"
},
},
},
],
},
},
// {
// interface: 'subTable',
// type: 'hasMany',
// name: 'pages_views',
// target: 'pages_views_v2',
// // sourceKey: 'path',
// title: '显示在页面里的视图(pages_views)',
// component: {
// type: 'subTable',
// 'x-linkages': [
// {
// type: 'value:schema',
// target: 'pages_views',
// schema: {
// 'x-component-props': {
// __parent: '{{ $form.values && $form.values.associatedKey }}',
// associatedKey: "{{ $form.values && $form.values.id }}"
// },
// },
// },
// ],
// },
// },
{
interface: 'json',
type: 'json',
name: 'options',
title: '配置信息',
defaultValue: {},
developerMode: true,
},
{
interface: 'boolean',
type: 'boolean',
name: 'developerMode',
title: '开发者模式',
defaultValue: false,
component: {
type: 'boolean',
},
},
],
actions: [
{
type: 'list',
name: 'list',
title: '查看',
},
{
type: 'create',
name: 'create',
title: '新增',
viewName: 'form',
},
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
},
{
type: 'destroy',
name: 'destroy',
title: '删除',
},
],
views_v2: [
{
developerMode: true,
type: 'table',
name: 'all_pages',
title: '全部页面',
rowKey: 'path',
labelField: 'title',
fields: ['title', 'collection'],
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['id'],
filter: {
type: 'static',
},
actions: [
{
name: 'filter',
type: 'filter',
title: '过滤',
fields: [
'title',
],
}
],
},
{
developerMode: true,
type: 'table',
name: 'collection_pages',
title: '数据表页面',
labelField: 'title',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: ['title'],
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['id'],
},
{
developerMode: true,
type: 'table',
name: 'global_pages',
title: '独立页面',
labelField: 'title',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'global_form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
filter: {
collection_name: null,
},
fields: ['title'],
detailsOpenMode: 'drawer', // window
details: ['global_form'],
sort: ['id'],
},
{
developerMode: true,
type: 'table',
name: 'permissions_table',
title: '全部数据',
labelField: 'title',
actions: [],
fields: ['title'],
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['id'],
},
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: ['title', 'type', 'views', 'pages_views'],
},
{
developerMode: true,
type: 'form',
name: 'global_form',
title: '独立页面表单',
fields: ['title', 'views', 'pages_views'],
},
],
} as TableOptions;

View File

@ -1,84 +0,0 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'pages_views_v2',
title: '页面视图',
internal: true,
// model: 'BaseModelV2',
developerMode: true,
createdAt: false,
updatedAt: false,
fields: [
{
interface: 'linkTo',
type: 'belongsTo',
name: 'view',
target: 'views_v2',
title: '视图',
labelField: 'title',
valueField: 'id',
multiple: false,
component: {
type: 'drawerSelect',
'x-component-props': {
viewName: 'views_v2.table',
resourceName: 'views_v2',
labelField: 'title',
valueField: 'id',
},
},
},
{
interface: 'radio',
type: 'string',
name: 'width',
title: '宽度',
dataSource: [
{ label: '50%', value: '50%' },
{ label: '100%', value: '100%' },
],
component: {
type: 'radio',
},
}
],
views_v2: [
{
developerMode: true,
type: 'table',
name: 'table',
title: '全部数据',
labelField: 'title',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: [
'view',
'width'
],
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['id'],
},
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: [
'view',
'width'
],
},
],
} as TableOptions;

View File

@ -1,23 +0,0 @@
import { extend } from '@nocobase/database';
export default extend({
name: 'roles',
fields: [
{
type: 'hasMany',
name: 'routes',
target: 'routes_permissions',
},
{
interface: 'linkTo',
type: 'belongsToMany',
name: 'pages',
title: '可访问的页面',
through: 'routes_permissions',
foreignKey: 'role_id',
otherKey: 'routable_id',
morphType: 'routable', // 现在没有多态关联的设置,暂时先这么写了
constraints: false, // 多态关联建立外键约束会有问题
}
],
});

View File

@ -1,30 +0,0 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'routes_permissions',
title: '页面权限',
developerMode: true,
internal: true,
fields: [
{
type: 'integer',
name: 'id',
primaryKey: true,
autoIncrement: true,
},
{
type: 'string',
name: 'routable_type',
title: '关联的表', // 仅 pages 和 collections
},
{
type: 'integer',
name: 'routable_id',
title: '关联的对象'
},
{
type: 'belongsTo',
name: 'role'
},
],
} as TableOptions;

View File

@ -1,80 +0,0 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'system_settings',
title: '系统配置',
internal: true,
// model: 'CollectionModel',
developerMode: true,
createdAt: false,
updatedAt: false,
fields: [
{
interface: 'string',
type: 'string',
name: 'title',
title: '系统名称',
},
{
interface: 'attachment',
type: 'belongsTo',
name: 'logo',
filterable: false,
target: 'attachments',
title: 'LOGO',
component: {
'x-component-props': {
multiple: false,
},
},
},
],
actions: [
{
type: 'list',
name: 'list',
title: '查看',
},
{
type: 'create',
name: 'create',
title: '新增',
viewName: 'form',
},
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
},
{
type: 'destroy',
name: 'destroy',
title: '删除',
},
],
views_v2: [
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: ['title', 'logo'],
},
{
developerMode: true,
type: 'descriptions',
name: 'descriptions',
title: '详情',
fields: ['title', 'logo'],
actions: [
{
name: 'update',
type: 'update',
title: '编辑',
viewName: 'form',
},
],
},
],
} as TableOptions;

View File

@ -1,121 +0,0 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'views_actions_v2',
title: '视图操作配置',
internal: true,
// model: 'BaseModelV2',
developerMode: true,
createdAt: false,
updatedAt: false,
fields: [
{
interface: 'string',
type: 'string',
name: 'title',
title: '按钮名称',
component: {
type: 'string',
},
},
{
interface: 'radio',
type: 'string',
name: 'type',
title: '操作类型',
dataSource: [
{ label: '筛选', value: 'filter' },
{ label: '打印', value: 'print' },
{ label: '导出', value: 'export' },
{ label: '新增', value: 'create' },
{ label: '编辑', value: 'update' },
{ label: '删除', value: 'destroy' },
],
component: {
type: 'radio',
'x-linkages': [
{
"type": "value:visible",
"target": "view",
"condition": "{{ $self.value === 'create' || $self.value === 'update' }}"
},
// {
// "type": "value:visible",
// "target": "fields",
// "condition": "{{ $self.value === 'filter' }}"
// },
],
},
},
{
interface: 'string',
type: 'string',
name: 'name',
title: '操作ID',
component: {
type: 'string',
},
},
{
interface: 'linkTo',
type: 'belongsTo',
name: 'view',
target: 'views_v2',
title: '操作绑定的视图',
labelField: 'title',
valueField: 'id',
multiple: false,
required: true,
component: {
type: 'drawerSelect',
'x-component-props': {
viewName: 'views_v2.table',
resourceName: 'views_v2',
labelField: 'title',
valueField: 'id',
},
},
},
],
views_v2: [
{
developerMode: true,
type: 'table',
name: 'table',
title: '全部数据',
labelField: 'title',
draggable: true,
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: [
'title',
'type',
],
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['id'],
},
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: [
'title',
'type',
'view'
],
},
],
} as TableOptions;

View File

@ -1,110 +0,0 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'views_details_v2',
title: '详情子视图',
internal: true,
// model: 'BaseModelV2',
developerMode: true,
createdAt: false,
updatedAt: false,
fields: [
{
interface: 'string',
type: 'string',
name: 'title',
title: '标签页名称',
component: {
type: 'string',
},
},
{
interface: 'linkTo',
type: 'belongsTo',
name: 'view',
target: 'views_v2',
title: '标签页显示的视图',
labelField: 'title',
valueField: 'id',
multiple: false,
required: true,
viewName: 'views_v2.form',
component: {
type: 'drawerSelect',
'x-component-props': {
viewName: 'views_v2.table',
resourceName: 'views_v2',
labelField: 'title',
valueField: 'id',
},
},
},
],
views_v2: [
{
developerMode: true,
type: 'table',
name: 'table',
title: '全部数据',
labelField: 'title',
draggable: true,
actions: [
{
name: 'add',
type: 'add',
title: '选择',
transform: {
'data': 'view',
'data.title': 'title',
},
viewName: 'collections.views_v2.table',
componentProps: {
type: 'primary',
},
filter: {
or: [
{ 'type': 'form' },
{ 'type': 'descriptions' },
{ 'data_source_type': 'association' },
]
},
},
{
name: 'create',
type: 'create',
title: '新增',
transform: {
'data': 'view',
'data.title': 'title',
},
viewName: 'collections.views_v2.form',
componentProps: {
type: 'default',
},
},
{
name: 'destroy',
type: 'destroy',
title: '移除',
},
],
fields: [
'title',
'view',
],
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['id'],
},
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: [
'title',
'view',
],
},
],
} as TableOptions;

View File

@ -1,169 +0,0 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'views_fields_v2',
title: '视图字段配置',
internal: true,
// model: 'BaseModelV2',
developerMode: true,
createdAt: false,
updatedAt: false,
fields: [
{
interface: 'string',
type: 'string',
name: 'title',
title: '字段名称',
component: {
type: 'string',
},
},
{
interface: 'linkTo',
type: 'belongsTo',
name: 'field',
target: 'fields',
title: '字段',
labelField: 'title',
valueField: 'id',
multiple: false,
viewName: 'fields.form',
component: {
type: 'drawerSelect',
'x-component-props': {
viewName: 'fields.table',
resourceName: 'fields',
labelField: 'title',
valueField: 'id',
},
"x-linkages": [
{
"type": "value:schema",
"target": "field",
"schema": {
"x-component-props": {
"filter": {
"collection_name": "{{ $self.value && $self.value.collection_name }}"
}
}
}
}
]
},
},
{
interface: 'textarea',
type: 'text',
name: 'tooltip',
title: '提示信息',
component: {
type: 'textarea',
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'required',
title: '必填项',
component: {
type: 'checkbox',
},
},
],
views_v2: [
{
developerMode: true,
type: 'table',
name: 'table',
title: '全部数据',
labelField: 'title',
draggable: true,
actions: [
{
name: 'add',
type: 'add',
title: '选择',
transform: {
'data': 'field',
'data.title': 'title',
},
viewName: 'collections.fields.table',
componentProps: {
type: 'primary',
},
},
{
name: 'destroy',
type: 'destroy',
title: '移除',
},
],
fields: [
'title',
'field',
],
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['id'],
},
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: [
'title',
'field',
'tooltip',
],
},
{
developerMode: true,
type: 'table',
name: 'tableForForm',
title: '全部数据',
labelField: 'title',
draggable: true,
actions: [
{
name: 'add',
type: 'add',
title: '选择',
transform: {
'data': 'field',
'data.title': 'title',
},
viewName: 'collections.fields.table',
componentProps: {
type: 'primary',
},
},
{
name: 'destroy',
type: 'destroy',
title: '移除',
},
],
fields: [
'title',
'field',
'required',
],
detailsOpenMode: 'drawer', // window
details: ['formForForm'],
sort: ['id'],
},
{
developerMode: true,
type: 'form',
name: 'formForForm',
title: '表单',
fields: [
'title',
'field',
'tooltip',
'required',
],
},
],
} as TableOptions;

View File

@ -1,259 +0,0 @@
import { TableOptions } from '@nocobase/database';
import { getTypeFieldOptions, getViewFields } from '../views';
const fields = getViewFields();
const associatedKeyValue = "{{ $form.values && $form.values.collection && $form.values.collection.name }}";
export default {
name: 'views_v2',
title: '视图配置',
internal: true,
model: 'BaseModelV2',
developerMode: true,
createdAt: false,
updatedAt: false,
fields: [
{
interface: 'string',
type: 'string',
name: 'title',
title: '视图名称',
required: true,
},
getTypeFieldOptions(),
{
interface: 'string',
type: 'randomString',
name: 'name',
title: '缩略名',
required: true,
createOnly: true,
randomString: {
length: 6,
characters: 'abcdefghijklmnopqrstuvwxyz0123456789',
},
developerMode: true,
},
{
interface: 'radio',
type: 'string',
name: 'dataSourceType',
title: '数据来源',
defaultValue: 'collection',
dataSource: [
{ label: '所属数据表', value: 'collection' },
{ label: '所属数据表的相关数据', value: 'association' },
],
linkages: [
{
"type": "value:visible",
"target": "targetField",
"condition": "{{ $self.value === 'association' }}"
},
...['form', 'descriptions', 'table', 'kanban', 'calendar'].map(type => {
return {
"type": "value:visible",
"target": `x-${type}-props.*`,
"condition": `{{ $form.values.type === '${type}' && $self.value === 'collection' }}`
}
}),
],
},
{
interface: 'linkTo',
type: 'belongsTo',
title: '相关数据',
name: 'targetField',
target: 'fields',
required: true,
multiple: false,
component: {
type: 'remoteSelect',
'x-component-props': {
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'id',
objectValue: true,
filter: {
interface: 'linkTo',
},
multiple: false,
},
'x-linkages': [
{
"type": "value:visible",
"target": "targetView",
"condition": "{{ !!$self.value }}"
},
{
type: 'value:schema',
target: 'targetView',
"condition": "{{ !!$self.value }}",
schema: {
'x-component-props': {
associatedKey: "{{ $self.value && $self.value.target }}"
},
},
},
],
},
},
{
interface: 'linkTo',
type: 'belongsTo',
title: '相关数据表视图',
name: 'targetView',
target: 'views_v2',
required: true,
multiple: false,
component: {
type: 'remoteSelect',
'x-component-props': {
resourceName: 'collections.views_v2',
labelField: 'title',
valueField: 'id',
multiple: false,
},
},
},
// {
// interface: 'select',
// type: 'virtual',
// title: '相关数据表的视图',
// name: 'targetViewName',
// required: true,
// component: {
// type: 'remoteSelect',
// resourceName: 'collections.views',
// labelField: 'title',
// valueField: 'name',
// 'x-component-props': {
// resourceName: 'collections.views',
// labelField: 'title',
// valueField: 'name',
// multiple: false,
// },
// },
// },
...fields,
{
interface: 'linkTo',
type: 'belongsTo',
name: 'collection',
target: 'collections',
targetKey: 'name',
title: '所属数据表',
labelField: 'title',
valueField: 'name',
multiple: false,
component: {
type: 'drawerSelect',
showInDetail: true,
showInForm: true,
'x-component-props': {
resourceName: 'collections',
labelField: 'title',
valueField: 'name',
},
'x-linkages': [
{
type: 'value:schema',
target: '*',
schema: {
'x-component-props': {
associatedKey: associatedKeyValue,
},
},
},
],
},
},
{
interface: 'json',
type: 'json',
name: 'options',
title: '配置信息',
defaultValue: {},
developerMode: true,
},
{
interface: 'boolean',
type: 'boolean',
name: 'developerMode',
title: '开发者模式',
defaultValue: false,
component: {
type: 'boolean',
},
},
],
actions: [
{
type: 'list',
name: 'list',
title: '查看',
},
{
type: 'create',
name: 'create',
title: '新增',
viewName: 'form',
},
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
},
{
type: 'destroy',
name: 'destroy',
title: '删除',
},
],
views_v2: [
{
developerMode: true,
type: 'table',
name: 'table',
title: '全部数据',
labelField: 'title',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: [
'title',
'type',
'collection',
],
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['id'],
},
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: [
'title',
'type',
'collection',
'dataSourceType',
'targetField',
'targetView',
...fields.map(field => field.name),
],
},
],
} as TableOptions;

View File

@ -1,27 +0,0 @@
import cryptoRandomString from 'crypto-random-string';
import { STRING, FieldContext } from '@nocobase/database';
import {
DataTypes
} from 'sequelize';
export class RANDOMSTRING extends STRING {
constructor(options: any, context: FieldContext) {
super(options, context);
const Model = context.sourceTable.getModel();
const { name, randomString } = options;
randomString && Model.addHook('beforeValidate', (model) => {
const { template, ...opts } = randomString;
let value = cryptoRandomString(opts);
if (template && template.includes('%r')) {
value = template.replace('%r', value);
}
if (!model.get(name)) {
model.set(name, value);
}
});
}
getDataType() {
return DataTypes.STRING;
}
}

View File

@ -1,214 +0,0 @@
import _ from 'lodash';
import { getDataTypeKey, Model } from '@nocobase/database';
import { merge } from '../utils';
export function generateName(title?: string): string {
return `${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
}
export class BaseModel extends Model {
generateName() {
this.set('name', generateName());
}
generateNameIfNull() {
if (!this.get('name')) {
this.generateName();
}
}
get additionalAttribute() {
const tableOptions = this.database.getTable(this.constructor.name).getOptions();
return _.get(tableOptions, 'additionalAttribute') || 'options';
}
hasGetAttribute(key: string) {
const attribute = this.rawAttributes[key];
// virtual 如果有 get 方法就直接走 get
if (attribute && attribute.type && getDataTypeKey(attribute.type) === 'VIRTUAL') {
return !!attribute.get;
}
return !!attribute;
}
hasSetAttribute(key: string) {
const attribute = this.rawAttributes[key];
// virtual 如果有 set 方法就直接走 set
if (attribute && attribute.type && getDataTypeKey(attribute.type) === 'VIRTUAL') {
return !!attribute.set;
}
return !!attribute;
}
get(key?: any, options?: any) {
if (typeof key === 'string') {
const [column, ...path] = key.split('.');
if (this.hasGetAttribute(column)) {
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('.');
if (this.hasGetAttribute(column)) {
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 super.set(key, value, options);
}
// 如果是 object 数据merge 处理
if (_.isPlainObject(value)) {
// TODO 需要改进 JSON 字段的内部处理逻辑,暂时这里跳过了特殊的 filter 字段
if (key !== 'filter') {
// console.log(key, value);
// @ts-ignore
value = merge(this.get(key) || {}, value);
}
}
const [column, ...path] = key.split('.');
if (!options.raw) {
this.changed(column, true);
}
if (this.hasSetAttribute(column)) {
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);
if (!options.raw) {
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 super.setDataValue(key, value);
}
if (_.isPlainObject(value)) {
// @ts-ignore
value = Utils.merge(this.get(key) || {}, value);
}
const [column, ...path] = key.split('.');
this.changed(column, true);
if (this.hasSetAttribute(column)) {
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 class BaseModel2 extends Model {
get additionalAttribute() {
const tableOptions = this.database.getTable(this.constructor.name).getOptions();
return _.get(tableOptions, 'additionalAttribute') || 'options';
}
hasGetAttribute(key: string) {
const attribute = this.rawAttributes[key];
// virtual 如果有 get 方法就直接走 get
if (attribute && attribute.type && getDataTypeKey(attribute.type) === 'VIRTUAL') {
return !!attribute.get;
}
return !!attribute;
}
hasSetAttribute(key: string) {
// @ts-ignore
if (this.constructor.hasAlias(key)) {
return false;
}
const attribute = this.rawAttributes[key];
// virtual 如果有 set 方法就直接走 set
if (attribute && attribute.type && getDataTypeKey(attribute.type) === 'VIRTUAL') {
return !!attribute.set;
}
return !!attribute;
}
get(key?: any, options?: any) {
if (typeof key !== 'string') {
const data = super.get(key);
return {
..._.omit(data, [this.additionalAttribute]),
...(data[this.additionalAttribute] || {}),
};
}
const [column, ...path] = key.split('.');
if (this.hasGetAttribute(column)) {
const value = super.get(column, options);
if (path.length) {
return _.get(value, path);
}
return value;
}
return _.get(super.get(this.additionalAttribute, options) || {}, key);
}
set(key?: any, value?: any, options: any = {}) {
if (typeof key !== 'string') {
return super.set(key, value, options);
}
// @ts-ignore
if (this.constructor.hasAlias(key)) {
return this;
}
const [column] = key.split('.');
if (this.hasSetAttribute(column)) {
return super.set(key, value, options);
}
return super.set(`${this.additionalAttribute}.${key}`, value, options);
}
// getDataValue(key: any) {
// return super.getDataValue(key);
// }
// setDataValue(key: any, value: any) {
// return super.setDataValue(key, value);
// }
}
export default BaseModel;

View File

@ -1,256 +0,0 @@
import path from 'path';
import Database from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import getCollection from './actions/getCollection';
import getView from './actions/getView';
import getRoutes from './actions/getRoutes';
import getPageInfo from './actions/getPageInfo';
import * as rolesPagesActions from './actions/roles.pages';
import getCollections from './actions/getCollections';
import { list as menusList } from './actions/menus';
import getTree from './actions/getTree';
import getInfo from './actions/getInfo';
import { getInfo as viewGetInfo } from './actions/views_v2';
import { RANDOMSTRING } from './fields/randomString';
import { registerFields, registerModels } from '@nocobase/database';
import { BaseModel } from './models/BaseModel'
import * as rolesMenusActions from './actions/roles.menus';
import _ from 'lodash';
export default async function (options = {}) {
const database: Database = this.database;
const resourcer: Resourcer = this.resourcer;
registerFields({
RANDOMSTRING,
});
registerModels({
BaseModelV2: BaseModel,
});
database.import({
directory: path.resolve(__dirname, 'collections'),
});
resourcer.use(async (ctx, next) => {
const { actionName, resourceName, resourceKey } = ctx.action.params;
if (resourceName === 'system_settings' && actionName === 'get') {
const SystemSetting = database.getModel('system_settings');
let model = await SystemSetting.findOne();
if (!model) {
model = await SystemSetting.create();
}
ctx.action.mergeParams({
resourceKey: model.id,
});
}
await next();
});
resourcer.use(async (ctx, next) => {
const { actionName, resourceName, values } = ctx.action.params;
if (resourceName === 'menus' && ['create', 'update'].includes(actionName)) {
if (values.parent) {
delete values.parent.children;
ctx.action.mergeParams({
values: {...values},
}, {
payload: 'replace',
});
}
}
await next();
});
resourcer.use(async (ctx, next) => {
await next();
const { actionName, resourceName } = ctx.action.params;
if (resourceName === 'menus' && actionName === 'get') {
const menu = ctx.body;
const items = menu.get('views') || [];
const View = database.getModel('views_v2');
for (const item of items) {
if (!(item.view && item.view.id)) {
continue;
}
const view = await View.findOne(View.parseApiJson({
filter: {
id: item.view.id,
},
fields: {
appends: ['collection', 'targetField', 'targetView'],
},
}));
if (!view) {
continue;
}
const details = view.get(`options.x-${view.type}-props.details`);
if (!Array.isArray(details)) {
item.view = view;
continue;
}
for (const detail of details) {
if (!(detail.view && detail.view.id)) {
continue;
}
const detailView = await View.findOne(View.parseApiJson({
filter: {
id: detail.view.id,
},
fields: {
appends: ['collection', 'targetField', 'targetView'],
},
}));
if (!detailView) {
continue;
}
detail.view = detailView;
}
view.set(`options.x-${view.type}-props.details`, details);
item.view = view;
}
menu.set('views', items);
}
});
resourcer.registerActionHandler('getCollection', getCollection);
resourcer.registerActionHandler('getView', getView);
resourcer.registerActionHandler('getPageInfo', getPageInfo);
resourcer.registerActionHandler('getCollections', getCollections);
resourcer.registerActionHandler('pages:getRoutes', getRoutes);
resourcer.registerActionHandler('menus:getTree', getTree);
resourcer.registerActionHandler('menus:getInfo', getInfo);
resourcer.registerActionHandler('views_v2:getInfo', viewGetInfo);
resourcer.registerActionHandler('menus:list', menusList);
Object.keys(rolesPagesActions).forEach(actionName => {
resourcer.registerActionHandler(`roles.pages:${actionName}`, rolesPagesActions[actionName]);
});
Object.keys(rolesMenusActions).forEach(actionName => {
resourcer.registerActionHandler(`roles.menus:${actionName}`, rolesMenusActions[actionName]);
});
const createDetailsViews = async (model, options) => {
const data = model.get();
const View = database.getModel('views_v2');
const types = ['table', 'calendar', 'kanban'];
for (const type of types) {
const items = _.get(data, `x-${type}-props.details`) || [];
if (items.length) {
const details = [];
for (const item of items) {
if (item.view) {
if (!item.view.id) {
const view = await View.create(item.view);
await view.updateAssociations(item.view);
item.view.id = view.id;
} else {
const view = await View.findByPk(item.view.id);
if (view) {
await view.update(item.view);
await view.updateAssociations(item.view);
}
}
const view = await View.findOne(View.parseApiJson({
filter: {
id: item.view.id,
},
fields: {
appends: ['collection', 'targetField', 'targetView'],
},
}));
if (view) {
console.log({view});
item.view = view.toJSON();
}
}
details.push(item);
}
model.set(`options.x-${type}-props.details`, details);
}
}
};
database.getModel('views_v2').addHook('beforeCreate', createDetailsViews);
database.getModel('views_v2').addHook('beforeUpdate', createDetailsViews);
database.getModel('views_v2').addHook('beforeSave', async (model, options) => {
const data = model.get();
if (data.type !== 'kanban') {
return;
}
let groupField = _.get(data, `x-kanban-props.groupField`);
if (!groupField) {
return;
}
if (typeof groupField === 'object' && groupField.name) {
groupField = groupField.name;
}
const Field = database.getModel('fields');
let field = await Field.findOne({
where: {
name: `${groupField}_sort`,
collection_name: data.collection_name,
},
});
if (field) {
return;
}
await Field.create({
interface: 'sort',
type: 'sort',
name: `${groupField}_sort`,
// TODO: 不支持相关数据
collection_name: data.collection_name,
scope: [groupField],
title: '看板分组排序',
developerMode: true,
component: {
type: 'sort',
},
});
});
database.getModel('menus').addHook('beforeSave', async (model, options) => {
const { transaction } = options;
// console.log('beforeSave', model.get('views'));
const items = model.get('views');
if (!Array.isArray(items)) {
return;
}
const View = database.getModel('views_v2');
const views = [];
for (const item of items) {
if (item.view) {
if (!item.view.id) {
const view = await View.create(item.view);
await view.updateAssociations(item.view);
item.view.id = view.id;
} else {
const view = await View.findByPk(item.view.id);
await view.update(item.view);
await view.updateAssociations(item.view);
}
const view = await View.findOne(View.parseApiJson({
filter: {
id: item.view.id,
},
fields: {
appends: ['collection', 'targetField', 'targetView'],
},
}));
if (view) {
console.log({view});
item.view = view.toJSON();
}
}
views.push(item);
}
model.set('views', views);
// @ts-ignore
model.changed('views', true);
});
}

View File

@ -1,46 +0,0 @@
import deepmerge from 'deepmerge';
export const flatToTree = (flatArray, options) => {
options = {
id: "id",
parentId: "parentId",
children: "children",
...options
};
const dictionary = {}; // a hash table mapping to the specific array objects with their ids as key
const tree = [];
const children = options.children;
flatArray.forEach(node => {
const nodeId = node[options.id];
const nodeParentId = node[options.parentId];
// set up current node data in dictionary
dictionary[nodeId] = {
[children]: [], // init a children property
...node, // add other propertys
...dictionary[nodeId] // children will be replaced if this node already has children property which was set below
};
dictionary[nodeParentId] = dictionary[nodeParentId] || { [children]: [] }; // if it's not exist in dictionary, init an object with children property
dictionary[nodeParentId][children].push(dictionary[nodeId]); // add reference to current node object in parent node object
});
// find root nodes
Object.values(dictionary).forEach(obj => {
if (typeof obj[options.id] === "undefined") {
tree.push(...obj[children]);
}
});
return treeData(tree);
};
function treeData(pages: Array<any>) {
return pages.map(data => {
return { ...data, children: data.children && data.children.length ? treeData(data.children) : undefined }
});
}
const overwriteMerge = (destinationArray, sourceArray, options) => sourceArray
export function merge(obj1: any, obj2: any) {
return deepmerge(obj1, obj2, {
arrayMerge: overwriteMerge,
});
}

View File

@ -1,99 +0,0 @@
import * as types from './types';
import _ from 'lodash';
export const views = new Map();
export function registerView(type: string, value: any) {
views.set(type, value);
}
export function registerViews(values: any) {
Object.keys(values).forEach(type => {
registerView(type, {
...values[type],
type,
});
});
}
registerViews(types);
export function getOptions() {
const options = [];
for (const [type, view] of views) {
options.push({
key: type,
value: type,
label: view.title,
disabled: !!view.disabled,
});
}
return options;
}
export function getViewTypeLinkages() {
let xlinkages = [];
for (const [key, item] of views) {
const { linkages = {}, properties = {} } = _.cloneDeep(item);
if (key === 'markdown') {
xlinkages.push({
"type": "value:visible",
"target": `x-${key}-props.*`,
"condition": `{{ $self.value === '${key}' }}`,
});
} else {
xlinkages.push({
"type": "value:visible",
"target": `x-${key}-props.*`,
"condition": `{{ $self.value === '${key}' && $form.values.dataSourceType === 'collection' }}`,
});
}
if (linkages.type) {
xlinkages.push(...linkages.type);
}
}
return xlinkages;
}
export function getTypeFieldOptions() {
return {
interface: 'select',
type: 'string',
name: 'type',
title: '视图类型',
required: true,
dataSource: getOptions(),
createOnly: false,
component: {
type: 'select',
},
linkages: getViewTypeLinkages(),
};
}
export function getViewFields() {
const fields = new Map();
for (const [key, item] of views) {
const { properties = {}, linkages = {} } = _.cloneDeep(item);
Object.keys(properties).forEach(name => {
const property = {
...properties[name],
name,
};
if (!property.type) {
property.type = 'virtual';
}
if (property.type === 'virtual') {
property.name = `x-${key}-props.${name}`;
}
if (linkages[name]) {
property.linkages = linkages[name].map((linkage: any) => {
linkage.target = `x-${key}-props.${linkage.target}`;
return linkage;
});
}
fields.set(`x-${key}-props.${name}`, property);
});
}
return [...fields.values()];
}

View File

@ -1,492 +0,0 @@
const fields = {
interface: 'json',
type: 'virtual',
title: '要显示的字段',
target: 'views_fields_v2',
component: {
type: 'subTable',
},
};
const actions = {
interface: 'json',
type: 'virtual',
title: '操作按钮配置',
target: 'views_actions_v2',
component: {
type: 'subTable',
},
};
const details = {
interface: 'json',
type: 'virtual',
title: '单条数据页面的标签页和视图',
target: 'views_details_v2',
component: {
type: 'subTable',
},
};
const detailsOpenMode = {
interface: 'radio',
// type: 'string',
title: '单条数据页面的打开方式',
required: true,
dataSource: [
{
label: '{{ markdown(\'<span>常规页面 <span style="color: #999;">点击数据进入独立的页面</i></span>\') }}',
value: 'window',
style: {
display: 'block',
lineHeight: '32px',
},
},
{
label: '{{ markdown(\'<span>快捷抽屉 <span style="color: #999;">点击数据不离开当前页面,在右侧抽屉里打开操作界面</i></span>\') }}',
value: 'drawer',
style: {
display: 'block',
lineHeight: '32px',
},
},
],
defaultValue: 'drawer',
component: {
type: 'radio',
default: 'default',
},
};
export const form = {
// fields,
title: '表单',
options: {
// fields,
},
properties: {
info1: {
interface: 'description',
type: 'virtual',
title: '表单配置',
component: {
type: 'description',
},
},
fields: {
...fields,
viewName: 'tableForForm',
title: '显示在表单里的字段'
},
},
linkages: {
},
};
export const descriptions = {
title: '详情',
options: {
// actions,
// fields,
},
properties: {
info1: {
interface: 'description',
type: 'virtual',
title: '详情配置',
component: {
type: 'description',
},
},
fields: {
...fields,
title: '显示在详情里的字段'
},
info2: {
interface: 'description',
type: 'virtual',
title: '操作按钮配置',
component: {
type: 'description',
},
},
actions,
},
linkages: {
},
};
export const table = {
title: '表格',
options: {
defaultPerPage: 20,
draggable: false,
filter: {},
sort: [],
detailsOpenMode: 'drawer',
// actions,
// fields,
// details,
// labelField,
},
properties: {
info1: {
interface: 'description',
type: 'virtual',
title: '表格配置',
component: {
type: 'description',
},
},
// 表格配置
labelField: {
interface: 'select',
type: 'virtual',
title: '作为单条数据标题的字段',
name: 'labelField',
required: true,
component: {
type: 'remoteSelect',
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
filter: {
type: 'string',
},
},
},
fields: {
...fields,
title: '显示在表格里的字段'
},
defaultPerPage: {
interface: 'radio',
type: 'virtual',
name: 'defaultPerPage',
title: '每页默认显示几条数据',
defaultValue: 50,
dataSource: [
{ label: '10', value: 10 },
{ label: '20', value: 20 },
{ label: '50', value: 50 },
{ label: '100', value: 100 },
],
},
draggable: {
interface: 'boolean',
type: 'virtual',
title: '表格里支持拖拽数据排序',
},
info2: {
interface: 'description',
type: 'virtual',
title: '数据配置',
component: {
type: 'description',
},
},
filter: {
interface: 'json',
type: 'virtual',
title: '只显示符合以下条件的数据',
mode: 'replace',
defaultValue: {},
component: {
type: 'filter',
},
},
// sort: {
// interface: 'json',
// type: 'virtual',
// title: '默认排序',
// mode: 'replace',
// defaultValue: [],
// component: {
// type: 'string',
// },
// },
info3: {
interface: 'description',
type: 'virtual',
title: '操作按钮配置',
component: {
type: 'description',
},
},
actions,
info4: {
interface: 'description',
type: 'virtual',
title: '单条数据页面配置',
component: {
type: 'description',
},
},
detailsOpenMode,
details,
},
linkages: {
},
};
export const calendar = {
title: '日历',
options: {
// filter,
// labelField,
// startDateField,
// endDateField,
// openMode,
// details,
},
properties: {
info1: {
interface: 'description',
type: 'virtual',
title: '日历配置',
component: {
type: 'description',
},
},
// 日历配置
labelField: {
interface: 'select',
type: 'virtual',
title: '作为单条数据标题的字段',
name: 'labelField',
required: true,
component: {
type: 'remoteSelect',
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
filter: {
type: 'string',
},
},
},
startDateField: {
interface: 'select',
type: 'virtual',
title: '开始日期字段',
required: true,
component: {
type: 'remoteSelect',
placeholder: '默认为创建时间字段',
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
filter: {
type: 'date',
},
},
},
endDateField: {
interface: 'select',
type: 'virtual',
title: '结束日期字段',
// required: true,
component: {
type: 'remoteSelect',
placeholder: '默认为创建时间字段',
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
filter: {
type: 'date',
},
},
},
info2: {
interface: 'description',
type: 'virtual',
title: '数据配置',
component: {
type: 'description',
},
},
filter: {
interface: 'json',
type: 'virtual',
title: '只显示符合以下条件的数据',
mode: 'replace',
defaultValue: {},
component: {
type: 'filter',
},
},
info3: {
interface: 'description',
type: 'virtual',
title: '操作按钮配置',
component: {
type: 'description',
},
},
actions,
info4: {
interface: 'description',
type: 'virtual',
title: '单条数据页面配置',
component: {
type: 'description',
},
},
detailsOpenMode,
details,
},
linkages: {
},
};
export const kanban = {
title: '看板',
options: {
},
properties: {
info1: {
interface: 'description',
type: 'virtual',
title: '看板配置',
component: {
type: 'description',
},
},
labelField: {
interface: 'select',
type: 'virtual',
title: '作为单条数据标题的字段',
name: 'labelField',
required: true,
component: {
type: 'remoteSelect',
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
filter: {
type: 'string',
},
},
},
groupField: {
interface: 'select',
type: 'virtual',
title: '看板分组字段',
name: 'groupField',
required: true,
component: {
type: 'remoteSelect',
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
filter: {
type: 'string',
},
},
},
fields: {
...fields,
title: '显示在看板里的字段'
},
info2: {
interface: 'description',
type: 'virtual',
title: '数据配置',
component: {
type: 'description',
},
},
filter: {
interface: 'json',
type: 'virtual',
title: '只显示符合以下条件的数据',
mode: 'replace',
defaultValue: {},
component: {
type: 'filter',
},
},
info3: {
interface: 'description',
type: 'virtual',
title: '操作按钮配置',
component: {
type: 'description',
},
},
actions,
info4: {
interface: 'description',
type: 'virtual',
title: '单条数据页面配置',
component: {
type: 'description',
},
},
detailsOpenMode,
details,
},
};
export const markdown = {
title: 'Markdown',
options: {
// html,
},
properties: {
// 数据配置
html: {
interface: 'markdown',
type: 'virtual',
title: 'Markdown 内容',
component: {
type: 'markdown',
},
},
},
linkages: {
type: [
{
type: "value:visible",
target: 'collection',
condition: `{{ $self.value && $self.value !== 'markdown' }}`,
},
{
type: "value:visible",
target: 'dataSourceType',
condition: `{{ $self.value && $self.value !== 'markdown' }}`,
},
],
},
};
export const map = {
title: '地图',
disabled: true,
options: {},
properties: {},
};
export const chart = {
title: '图表',
disabled: true,
options: {},
properties: {},
};
export const report = {
title: '报表',
disabled: true,
options: {},
properties: {},
};
export const aggregate = {
title: '汇总指标',
disabled: true,
options: {},
properties: {},
};

Some files were not shown because too many files have changed in this diff Show More