feat: improve collection hooks/fields/actions/views... (#30)

* feat: add onFinish callback

* fix: update json attribute unsaved after query

* refactor: collection hooks

* feat: add migrate options
This commit is contained in:
chenos 2020-12-04 21:09:39 +08:00 committed by GitHub
parent 1980464f63
commit 3e3cb416b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 198 additions and 73 deletions

View File

@ -166,7 +166,7 @@ const data = {
const tables = database.getTables([]); const tables = database.getTables([]);
for (let table of tables) { for (let table of tables) {
await Collection.import(table.getOptions(), { hooks: false }); await Collection.import(table.getOptions(), { migrate: false });
} }
const Page = database.getModel('pages'); const Page = database.getModel('pages');

View File

@ -5,15 +5,15 @@ import ViewFactory from '@/components/views';
export function Create(props) { export function Create(props) {
console.log(props); console.log(props);
const { title, viewName, collection_name } = props.schema; const { title, viewName, collection_name } = props.schema;
const { activeTab = {}, item = {} } = props; const { activeTab = {}, item = {}, associatedName, associatedKey } = props;
const { association } = activeTab; const { association } = activeTab;
const params = {}; const params = {};
if (association) { if (association) {
params['resourceName'] = association; params['resourceName'] = association;
params['associatedName'] = activeTab.collection_name; params['associatedName'] = associatedName;
params['associatedKey'] = item.itemId; params['associatedKey'] = associatedKey;
} else { } else {
params['resourceName'] = collection_name; params['resourceName'] = collection_name;
params['resourceKey'] = item.itemId; params['resourceKey'] = item.itemId;

View File

@ -26,6 +26,7 @@ export function Update(props) {
{...props} {...props}
reference={drawerRef} reference={drawerRef}
viewName={viewName} viewName={viewName}
mode={'update'}
{...params} {...params}
/> />
<Button type={'primary'} onClick={() => { <Button type={'primary'} onClick={() => {

View File

@ -15,7 +15,7 @@ export function Details(props: any) {
associatedKey, associatedKey,
resourceKey, resourceKey,
} = props; } = props;
const { data = {}, loading } = useRequest(() => { const { data = {}, loading, refresh } = useRequest(() => {
const name = associatedName ? `${associatedName}.${resourceName}` : resourceName; const name = associatedName ? `${associatedName}.${resourceName}` : resourceName;
return api.resource(name).get({ return api.resource(name).get({
resourceKey, resourceKey,
@ -26,7 +26,9 @@ export function Details(props: any) {
const { actions = [], fields = [] } = props.schema; const { actions = [], fields = [] } = props.schema;
return ( return (
<Card bordered={false}> <Card bordered={false}>
<Actions {...props} style={{ marginBottom: 14 }} actions={actions}/> <Actions {...props} onFinish={() => {
refresh();
}} style={{ marginBottom: 14 }} actions={actions}/>
{loading ? <Spin/> : ( {loading ? <Spin/> : (
<Descriptions bordered column={1}> <Descriptions bordered column={1}>
{fields.map((field: any) => { {fields.map((field: any) => {

View File

@ -27,10 +27,14 @@ export const DrawerForm = forwardRef((props: any, ref) => {
resourceName, resourceName,
associatedName, associatedName,
associatedKey, associatedKey,
onFinish,
} = props; } = props;
console.log(associatedKey);
const [resourceKey, setResourceKey] = useState(props.resourceKey);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const name = associatedName ? `${associatedName}.${resourceName}` : resourceName;
const { data, run, loading } = useRequest((resourceKey) => { const { data, run, loading } = useRequest((resourceKey) => {
const name = associatedName ? `${associatedName}.${resourceName}` : resourceName; setResourceKey(resourceKey);
return api.resource(name).get({ return api.resource(name).get({
resourceKey, resourceKey,
associatedKey, associatedKey,
@ -44,7 +48,7 @@ export const DrawerForm = forwardRef((props: any, ref) => {
})); }));
const actions = createAsyncFormActions(); const actions = createAsyncFormActions();
const { title, fields: properties ={} } = props.schema||{}; const { title, fields: properties ={} } = props.schema||{};
console.log({properties}); console.log({onFinish});
return ( return (
<Drawer <Drawer
{...props} {...props}
@ -57,8 +61,22 @@ export const DrawerForm = forwardRef((props: any, ref) => {
title={title} title={title}
footer={[ footer={[
<Button type={'primary'} onClick={async () => { <Button type={'primary'} onClick={async () => {
const values = await actions.submit(); const { values = {} } = await actions.submit();
console.log(values); console.log(values);
if (resourceKey) {
await api.resource(name).update({
resourceKey,
associatedKey,
...values,
});
} else {
await api.resource(name).create({
associatedKey,
...values,
});
}
setVisible(false);
onFinish && onFinish(values);
}}></Button> }}></Button>
]} ]}
> >

View File

@ -64,9 +64,17 @@ export function SimpleTable(props: SimpleTableProps) {
console.log('rowViewName', {rowViewName}) console.log('rowViewName', {rowViewName})
return ( return (
<Card bordered={false}> <Card bordered={false}>
<Actions {...props} style={{ marginBottom: 14 }} actions={actions}/> <Actions
{...props}
style={{ marginBottom: 14 }}
actions={actions}
onFinish={() => {
refresh();
}}
/>
<ViewFactory <ViewFactory
{...props} {...props}
mode={'update'}
viewName={rowViewName} viewName={rowViewName}
reference={drawerRef} reference={drawerRef}
/> />

View File

@ -80,7 +80,14 @@ export function Table(props: TableProps) {
} }
return ( return (
<Card bordered={false}> <Card bordered={false}>
<Actions {...props} style={{ marginBottom: 14 }} actions={actions}/> <Actions
{...props}
style={{ marginBottom: 14 }}
actions={actions}
onFinish={() => {
refresh();
}}
/>
<AntdTable <AntdTable
rowKey={rowKey} rowKey={rowKey}
columns={fields2columns(fields)} columns={fields2columns(fields)}

View File

@ -45,12 +45,14 @@ export default function ViewFactory(props: ViewProps) {
associatedKey, associatedKey,
resourceName, resourceName,
viewName, viewName,
mode,
reference, reference,
} = props; } = props;
const { data = {}, loading } = useRequest(() => { const { data = {}, loading } = useRequest(() => {
const params = { const params = {
resourceKey: viewName, resourceKey: viewName,
associatedName: associatedName, associatedName: associatedName,
mode,
}; };
return api.resource(resourceName).getView(params); return api.resource(resourceName).getView(params);
}, { }, {

View File

@ -1,6 +1,7 @@
import Database, { ModelCtor } from '@nocobase/database'; import Database, { ModelCtor } from '@nocobase/database';
import { getDatabase } from '.'; import { getDatabase } from '.';
import BaseModel from '../models/base'; import BaseModel from '../models/base';
import _ from 'lodash';
describe('base model', () => { describe('base model', () => {
let database: Database; let database: Database;
@ -21,6 +22,11 @@ describe('base model', () => {
name: 'title', name: 'title',
type: 'virtual', type: 'virtual',
}, },
{
name: 'xyz',
type: 'virtual',
defaultValue: 'xyz1',
},
{ {
name: 'content', name: 'content',
type: 'virtual', type: 'virtual',
@ -92,6 +98,7 @@ describe('base model', () => {
bb: 'bb', bb: 'bb',
}, },
bcd: 'bbb', bcd: 'bbb',
xyz: "xyz1",
arr: [{a: 'a'}, {b: 'b'}], arr: [{a: 'a'}, {b: 'b'}],
}); });
}); });
@ -189,4 +196,64 @@ describe('base model', () => {
}); });
expect(test2.get('content')).toBeUndefined(); 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',
});
});
}); });

View File

@ -17,7 +17,7 @@ describe('collection hooks', () => {
const tables = app.database.getTables([]); const tables = app.database.getTables([]);
for (const table of tables) { for (const table of tables) {
const Collection = app.database.getModel('collections'); const Collection = app.database.getModel('collections');
await Collection.import(table.getOptions(), { hooks: false }); await Collection.import(table.getOptions(), { migrate: false });
} }
}); });

View File

@ -139,7 +139,7 @@ export default {
name: 'component.tooltip', name: 'component.tooltip',
title: '提示信息', title: '提示信息',
component: { component: {
type: 'string', type: 'textarea',
showInDetail: true, showInDetail: true,
showInForm: true, showInForm: true,
}, },

View File

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

View File

@ -1,7 +1,5 @@
import CollectionModel from '../models/collection'; import CollectionModel from '../models/collection';
export default async function (model: CollectionModel) { export default async function (model: CollectionModel) {
if (!model.get('name')) { model.generateNameIfNull();
model.setDataValue('name', this.generateName());
}
} }

View File

@ -1,6 +1,8 @@
import FieldModel from '../models/field'; import FieldModel from '../models/field';
export default async function (model: FieldModel) { export default async function (model: FieldModel, options: any = {}) {
// console.log('afterCreate', model.toJSON()); const { migrate = true } = options;
await model.migrate(); if (migrate) {
await model.migrate();
}
} }

View File

@ -1,28 +1,10 @@
import FieldModel from '../models/field'; import FieldModel from '../models/field';
import * as types from '../interfaces/types'; import * as types from '../interfaces/types';
import _ from 'lodash';
export default async function (model: FieldModel) { export default async function (model: FieldModel) {
const values = model.get(); model.generateNameIfNull();
if (!values.name) { if (model.get('interface')) {
values.name = this.generateName(); model.setInterface(model.get('interface'));
} }
if (values.interface) {
const { options } = types[values.interface];
Object.keys(options).forEach(key => {
switch (typeof values[key]) {
case 'undefined':
values[key] = options[key];
break;
case 'object':
values[key] = {
...options[key],
...values[key]
};
break;
}
});
}
model.set(values, { raw: true });
} }

View File

@ -453,6 +453,7 @@ export const json = {
options: { options: {
interface: 'json', interface: 'json',
type: 'json', type: 'json',
dottie: true,
component: { component: {
type: 'hidden', type: 'hidden',
}, },

View File

@ -1,7 +1,9 @@
import _ from 'lodash'; import _ from 'lodash';
import { getDataTypeKey, Model } from '@nocobase/database'; import { getDataTypeKey, Model } from '@nocobase/database';
import { Utils } from 'sequelize';
export class BaseModel extends Model { export class BaseModel extends Model {
get additionalAttribute() { get additionalAttribute() {
const tableOptions = this.database.getTable(this.constructor.name).getOptions(); const tableOptions = this.database.getTable(this.constructor.name).getOptions();
return _.get(tableOptions, 'additionalAttribute') || 'options'; return _.get(tableOptions, 'additionalAttribute') || 'options';
@ -57,19 +59,22 @@ export class BaseModel extends Model {
return _.get(options, key); return _.get(options, key);
} }
set(key?: any, value?: any, options?: any) { set(key?: any, value?: any, options: any = {}) {
if (typeof key === 'string') { if (typeof key === 'string') {
// 不处理关系数据 // 不处理关系数据
// @ts-ignore // @ts-ignore
if (_.get(this.constructor.associations, key)) { if (_.get(this.constructor.associations, key)) {
return this; return this;
} }
// 如果是 object 数据merge 处理 // 如果是 object 数据merge 处理
if (_.isPlainObject(value)) { if (_.isPlainObject(value)) {
value = _.merge(this.get(key)||{}, value); // @ts-ignore
value = Utils.merge(this.get(key)||{}, value);
} }
const [column, ...path] = key.split('.'); const [column, ...path] = key.split('.');
this.changed(column, true); if (!options.raw) {
this.changed(column, true);
}
if (this.hasSetAttribute(column)) { if (this.hasSetAttribute(column)) {
if (!path.length) { if (!path.length) {
return super.set(key, value, options); return super.set(key, value, options);
@ -81,7 +86,9 @@ export class BaseModel extends Model {
// 如果未设置 attribute存到 additionalAttribute 里 // 如果未设置 attribute存到 additionalAttribute 里
const opts = this.get(this.additionalAttribute, options) || {}; const opts = this.get(this.additionalAttribute, options) || {};
_.set(opts, key, value); _.set(opts, key, value);
this.changed(this.additionalAttribute, true); if (!options.raw) {
this.changed(this.additionalAttribute, true);
}
return super.set(this.additionalAttribute, opts, options); return super.set(this.additionalAttribute, opts, options);
} }
return super.set(key, value, options); return super.set(key, value, options);
@ -94,7 +101,8 @@ export class BaseModel extends Model {
return this; return this;
} }
if (_.isPlainObject(value)) { if (_.isPlainObject(value)) {
value = _.merge(this.get(key)||{}, value); // @ts-ignore
value = Utils.merge(this.get(key)||{}, value);
} }
const [column, ...path] = key.split('.'); const [column, ...path] = key.split('.');
this.changed(column, true); this.changed(column, true);

View File

@ -1,10 +1,36 @@
import _ from 'lodash'; import _ from 'lodash';
import BaseModel from './base'; import BaseModel from './base';
import { TableOptions } from '@nocobase/database'; import { TableOptions } from '@nocobase/database';
import { SaveOptions, Utils } from 'sequelize'; import { SaveOptions } 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 class CollectionModel extends BaseModel { export class CollectionModel extends BaseModel {
generateName() {
this.set('name', generateCollectionName());
}
generateNameIfNull() {
if (!this.get('name')) {
this.generateName();
}
}
/** /**
* name collection * name collection
* *
@ -14,22 +40,6 @@ export class CollectionModel extends BaseModel {
return this.findOne({ where: { name } }); return this.findOne({ where: { name } });
} }
/**
*
*
* 使 3+2
* 1. id
* 2.
* 3.
* 4.
* 5.
*
* @param title
*/
static generateName(title?: string): string {
return `t_${Date.now().toString(36)}_${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
}
/** /**
* *
*/ */
@ -80,12 +90,8 @@ export class CollectionModel extends BaseModel {
...item, ...item,
sort, sort,
})); }));
for (const item of items[key]) {
await collection[`create${_.upperFirst(Utils.singularize(key))}`](item);
}
} }
// updateAssociations 有 BUG await collection.updateAssociations(items, options);
// await collection.updateAssociations(items, options);
return collection; return collection;
} }
} }

View File

@ -1,10 +1,30 @@
import _ from 'lodash'; import _ from 'lodash';
import BaseModel from './base'; import BaseModel from './base';
import { FieldOptions } from '@nocobase/database'; import { FieldOptions } from '@nocobase/database';
import * as types from '../interfaces/types';
import { Utils } from 'sequelize';
export function generateFieldName(title?: string): string {
return `f_${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
}
export class FieldModel extends BaseModel { export class FieldModel extends BaseModel {
static generateName(title?: string): string {
return `f_${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`; generateName() {
this.set('name', generateFieldName());
}
generateNameIfNull() {
if (!this.get('name')) {
this.generateName();
}
}
setInterface(value) {
const { options } = types[value];
// @ts-ignore
const values = Utils.merge(options, this.get());
this.set(values);
} }
async getOptions(): Promise<FieldOptions> { async getOptions(): Promise<FieldOptions> {