mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-12-01 03:38:32 +08:00
feat: improve code
This commit is contained in:
parent
f300fd6ae9
commit
c6b68f2b10
@ -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': [
|
||||
|
@ -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'
|
||||
}
|
||||
]
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
@ -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' },
|
||||
],
|
||||
});
|
||||
})
|
||||
});
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
@ -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();
|
||||
}
|
@ -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();
|
||||
};
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import CollectionModel from '../models/collection';
|
||||
|
||||
export default async function (model: CollectionModel) {
|
||||
model.generateNameIfNull();
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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 };
|
||||
}));
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { BaseModel } from '../models';
|
||||
|
||||
export default async function (model: BaseModel) {
|
||||
model.generateNameIfNull();
|
||||
}
|
@ -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
|
||||
},
|
||||
};
|
@ -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;
|
||||
}
|
@ -1,638 +0,0 @@
|
||||
// merge:interface 模板,旧数据,用户数据
|
||||
// TODO: 删除的情况怎么处理
|
||||
// 联动的原则:尽量减少干预,尤其是尽量少改动 type,type 兼容
|
||||
// 参数的优先级:
|
||||
// 1、interface,type 尽量只随 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合并成一个 interface,multiple 会影响 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
|
||||
* - 子字段只属于子表格字段关联的表(target),不属于当前表(source)
|
||||
*/
|
||||
// database.table({
|
||||
// name: 'tablename',
|
||||
// fields: [
|
||||
// {
|
||||
// type: 'hasMany',
|
||||
// name: 'foos',
|
||||
// target: 'foos',
|
||||
// fields: [
|
||||
// {
|
||||
// type: 'string',
|
||||
// name: 'xxx',
|
||||
// }
|
||||
// ],
|
||||
// }
|
||||
// ],
|
||||
// });
|
||||
// database.table({
|
||||
// name: 'foos',
|
||||
// fields: [
|
||||
// {
|
||||
// type: 'string',
|
||||
// name: 'xxx',
|
||||
// }
|
||||
// ],
|
||||
// });
|
||||
export const subTable = {
|
||||
title: '子表格',
|
||||
group: 'relation',
|
||||
// disabled: true,
|
||||
options: {
|
||||
interface: 'subTable',
|
||||
type: 'hasMany',
|
||||
// fields: [],
|
||||
component: {
|
||||
type: 'subTable',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 尽量减少更新 multiple 造成的影响
|
||||
* 同步生成配对的关系字段
|
||||
*
|
||||
* 只传 name 没有 target,可以通过 addField 处理,找到 target
|
||||
* 没有 name 但是有 target,name 随机生成
|
||||
* 有 name 也有 target
|
||||
*/
|
||||
|
||||
// database.table({
|
||||
// name: 'foos',
|
||||
// fields: [
|
||||
// {
|
||||
// type: 'hasMany',
|
||||
// name: 'bars',
|
||||
// // target: 'bars',
|
||||
// // sourceKey: 'id',
|
||||
// // foreignKey: 'foo_id',
|
||||
// },
|
||||
// {
|
||||
// type: 'hasMany',
|
||||
// name: 'xxxxx', // 如果没有随机生成
|
||||
// target: 'bars',
|
||||
// // sourceKey: 'id',
|
||||
// // foreignKey: 'foo_id',
|
||||
// },
|
||||
// {
|
||||
// type: 'hasMany',
|
||||
// name: 'xxxxx', // 如果没有随机生成
|
||||
// target: 'bars',
|
||||
// sourceKey: 'id',
|
||||
// foreignKey: 'foo_id',
|
||||
// }
|
||||
// ],
|
||||
// });
|
||||
|
||||
// const field = table.addField({
|
||||
// type: 'hasMany',
|
||||
// name: 'xxx', // xxx
|
||||
// });
|
||||
|
||||
export const linkTo = {
|
||||
title: '关联数据',
|
||||
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',
|
||||
}
|
||||
}
|
||||
};
|
@ -1,6 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import BaseModel from './base';
|
||||
|
||||
export class ActionModel extends BaseModel {
|
||||
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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';
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import BaseModel from './base';
|
||||
|
||||
export class TabModel extends BaseModel {
|
||||
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import BaseModel from './base';
|
||||
|
||||
export class ViewModel extends BaseModel {
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
@ -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);
|
||||
|
@ -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: {
|
||||
|
@ -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();
|
||||
// });
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
node_modules
|
||||
*.log
|
||||
docs
|
||||
__tests__
|
||||
tsconfig.json
|
||||
src
|
||||
.fatherrc.ts
|
@ -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"
|
||||
}
|
||||
}
|
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
@ -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}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export default async function afterCreate(model: any, options: any = {}) {
|
||||
const { migrate = true } = options;
|
||||
if (migrate) {
|
||||
await model.migrate();
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export default async function beforeValidate(model: any, options = {}) {
|
||||
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export default async function afterCreate(model: any, options: any = {}) {
|
||||
const { migrate = true } = options;
|
||||
if (migrate) {
|
||||
await model.migrate();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
@ -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 });
|
||||
}
|
||||
},
|
||||
};
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -1,3 +0,0 @@
|
||||
export * from './collection';
|
||||
export * from './field';
|
||||
export * from './view';
|
@ -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;
|
@ -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);
|
||||
}
|
@ -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;
|
@ -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()];
|
||||
}
|
@ -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: [],
|
||||
},
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
export default {
|
||||
target: 'node',
|
||||
cjs: { type: 'babel', lazy: true },
|
||||
disableTypeCheck: true,
|
||||
};
|
@ -1,7 +0,0 @@
|
||||
node_modules
|
||||
*.log
|
||||
docs
|
||||
__tests__
|
||||
tsconfig.json
|
||||
src
|
||||
.fatherrc.ts
|
@ -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"
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
};
|
@ -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();
|
||||
};
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
};
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
};
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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, // 多态关联建立外键约束会有问题
|
||||
}
|
||||
],
|
||||
});
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
@ -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);
|
||||
});
|
||||
}
|
@ -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,
|
||||
});
|
||||
}
|
@ -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()];
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user