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([]);
for (let table of tables) {
await Collection.import(table.getOptions(), { hooks: false });
await Collection.import(table.getOptions(), { migrate: false });
}
const Page = database.getModel('pages');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import Database, { ModelCtor } from '@nocobase/database';
import { getDatabase } from '.';
import BaseModel from '../models/base';
import _ from 'lodash';
describe('base model', () => {
let database: Database;
@ -21,6 +22,11 @@ describe('base model', () => {
name: 'title',
type: 'virtual',
},
{
name: 'xyz',
type: 'virtual',
defaultValue: 'xyz1',
},
{
name: 'content',
type: 'virtual',
@ -92,6 +98,7 @@ describe('base model', () => {
bb: 'bb',
},
bcd: 'bbb',
xyz: "xyz1",
arr: [{a: 'a'}, {b: 'b'}],
});
});
@ -189,4 +196,64 @@ describe('base model', () => {
});
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([]);
for (const table of tables) {
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',
title: '提示信息',
component: {
type: 'string',
type: 'textarea',
showInDetail: true,
showInForm: true,
},

View File

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

View File

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

View File

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

View File

@ -1,28 +1,10 @@
import FieldModel from '../models/field';
import * as types from '../interfaces/types';
import _ from 'lodash';
export default async function (model: FieldModel) {
const values = model.get();
if (!values.name) {
values.name = this.generateName();
model.generateNameIfNull();
if (model.get('interface')) {
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: {
interface: 'json',
type: 'json',
dottie: true,
component: {
type: 'hidden',
},

View File

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

View File

@ -1,10 +1,36 @@
import _ from 'lodash';
import BaseModel from './base';
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 {
generateName() {
this.set('name', generateCollectionName());
}
generateNameIfNull() {
if (!this.get('name')) {
this.generateName();
}
}
/**
* name collection
*
@ -14,22 +40,6 @@ export class CollectionModel extends BaseModel {
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,
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;
}
}

View File

@ -1,10 +1,30 @@
import _ from 'lodash';
import BaseModel from './base';
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 {
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> {