feat: improve code (#978)

* feat: 图形化管理数据表

* feat: 图形化管理数据表

* feat: 图形化管理数据表

* feat: 图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 完善图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat: 样式优化图形化管理数据表

* feat(collection-manager): add foreignKey Field and support relate field record foreignKey info

through collection record into collections and foreignKey field record info fields

* fix(collection-manager): if has through collection then don't create through collections record

* fix(client/route-switch): skip sub routes

* feat: 添加graphpostion

* feat: 图形化collection新增表时刷新数据

* fix(collection-manager): refactor afterCreateForRelateField

* feat: 图形化collection存储位置

* feat: 图形化collection存储位置

* feat: 图形化collection存储位置

* feat: 图形化collection存储位置

* feat: 图形化collection存储位置

* feat: 图形化collection存储位置

* feat: 图形化collection存储位置

* feat: 图形化collection存储位置

* feat: 图形化collection存储位置

* feat: 图形化collection存储位置

* feat: 图形化样式优化

* feat: styling

* feat: 图形化样式优化

* feat: 图形化样式优化

* feat: 图形化数据表多语言完善

* feat: 图形化数据表多语言完善

* feat: improve code

* feat: 图形化数据表连线样式修改

* feat: 图形化数据表样式修改

* feat: 图形化数据表样式修改

* feat: 图形化数据表样式修改

* feat: 图形化数据表样式修改

* fix(collection-manager): fix afterCreateForRelateField

* feat: 样式优化

* feat: 样式优化

* feat: afterCreateForForeignKeyField

* fix: timestamps: false

* feat: 连线锚点优化

* fix(collection-manager): when del foreign key field, relate fields will be del too

* fix: update package.json

* fix: update package.json

* feat: 文件名大小写

* feat: 连线锚点优化

* feat: 连线锚点通过计算得到样式优化

* feat: 连线锚点通过计算得到样式优化

* fix: fk

* fix: remove index

* feat: 连线hover时高亮

* fix: test error

* feat: 初始化计算位置

* feat: 初始化时计算坐标位置

* feat: 初始化时计算坐标位置

* feat: improve code (#933)

* fix: built in

* feat: 没有关系字段时也要连线

* feat: 自关联也要连线

* fix: styling

* feat: 滚动条问题

* feat: 拖拽优化

* feat: 画布paddig优化

* feat: 编辑时支持反向关联字段配置

* feat: 画布拖拽滚动优化

* feat: 画布拖拽滚动优化

* fix: reload

* feat: 修复数据表新建重叠

* fix: refreshCM & refreshGM

* feat: 修复表达式输入框显示异常

* feat: 渲染性能优化(增量渲染)

* feat: 渲染性能优化(增量渲染)

* feat: 渲染性能优化(增量渲染)

* fix: 消除代码警告

* fix: 消除代码警告

* feat: 渲染性能优化(增量渲染)

* feat: 渲染性能优化(增量渲染)

* feat: 渲染性能优化(增量渲染)

* feat: 渲染性能优化(增量渲染)

* feat: 渲染性能优化(增量渲染)

* feat: 渲染性能优化(增量渲染)

* feat: 渲染性能优化(增量渲染)

* feat: 渲染性能优化

* feat: 渲染性能优化

* feat: 外键生成在位置在前面

* feat: 限制表最多显示10个字段其余滚动

* feat: 移动表位置的连线重新计算最优位置

* fix: error

* feat: 布局自动换行

* fix: test error

* fix: xpipe.eq

* fix: upgrade error

* fix: upgrade error

* feat: 选中表时只显示和目标表关联的表和连线

* fix: maxListenersExceededWarning

* feat: remove graph-collection-manager

* fix: remove graph-collection-manager

* fix: update yarn.lock

Co-authored-by: 唐小爱 <tangxiaoai@192.168.0.103>
Co-authored-by: lyf-coder <lyf-coder@foxmail.com>
Co-authored-by: katherinehhh <katherine_15995@163.com>
This commit is contained in:
chenos 2022-10-28 15:09:14 +08:00 committed by GitHub
parent 274d80d501
commit f67afba964
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 545 additions and 144 deletions

View File

@ -27,7 +27,7 @@ export async function move(ctx: Context, next) {
await sortAbleCollection.sticky(sourceId); await sortAbleCollection.sticky(sourceId);
} }
} }
ctx.body = 'ok';
await next(); await next();
} }

View File

@ -14,11 +14,7 @@ export const CollectionManagerProvider: React.FC<CollectionManagerOptions> = (pr
service, service,
interfaces: { ...defaultInterfaces, ...interfaces }, interfaces: { ...defaultInterfaces, ...interfaces },
collections, collections,
refreshCM: async () => { refreshCM,
if (refreshCM) {
await refreshCM();
}
},
}} }}
> >
<CollectionManagerSchemaComponentProvider>{props.children}</CollectionManagerSchemaComponentProvider> <CollectionManagerSchemaComponentProvider>{props.children}</CollectionManagerSchemaComponentProvider>
@ -47,13 +43,18 @@ export const RemoteCollectionManagerProvider = (props: any) => {
} }
return ( return (
<CollectionManagerProvider <CollectionManagerProvider
service={{...service, contentLoading, setContentLoading}} service={{ ...service, contentLoading, setContentLoading }}
collections={service?.data?.data} collections={service?.data?.data}
refreshCM={async () => { refreshCM={async (opts) => {
setContentLoading(true); if (opts?.reload) {
setContentLoading(true);
}
const { data } = await api.request(options); const { data } = await api.request(options);
service.mutate(data); service.mutate(data);
setContentLoading(false); if (opts?.reload) {
setContentLoading(false);
}
return data?.data || [];
}} }}
{...props} {...props}
/> />

View File

@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { PluginManager } from '../plugin-manager'; import { PluginManager } from '../plugin-manager';
import { ActionContext, SchemaComponent } from '../schema-component'; import { ActionContext, SchemaComponent } from '../schema-component';
import { AddFieldAction, ConfigurationTable, EditFieldAction } from './Configuration'; import { AddCollectionField, AddFieldAction, ConfigurationTable, EditFieldAction,EditCollectionField } from './Configuration';
const schema: ISchema = { const schema: ISchema = {
type: 'object', type: 'object',
@ -37,7 +37,7 @@ const schema2: ISchema = {
export const CollectionManagerPane = () => { export const CollectionManagerPane = () => {
return ( return (
<Card bordered={false}> <Card bordered={false}>
<SchemaComponent schema={schema2} components={{ ConfigurationTable, AddFieldAction, EditFieldAction }} /> <SchemaComponent schema={schema2} components={{ ConfigurationTable, AddFieldAction, AddCollectionField, EditFieldAction,EditCollectionField }} />
</Card> </Card>
); );
}; };

View File

@ -7,9 +7,9 @@ import { cloneDeep } from 'lodash';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useRequest } from '../../api-client'; import { useRequest } from '../../api-client';
import { useRecord } from '../../record-provider'; import { RecordProvider, useRecord } from '../../record-provider';
import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useCreateAction } from '../action-hooks'; import { useCancelAction, useCreateAction } from '../action-hooks';
import { useCollectionManager } from '../hooks'; import { useCollectionManager } from '../hooks';
import { IField } from '../interfaces/types'; import { IField } from '../interfaces/types';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
@ -44,6 +44,9 @@ const getSchema = (schema: IField, record: any, compile): ISchema => {
[uid()]: { [uid()]: {
type: 'void', type: 'void',
'x-component': 'Action.Drawer', 'x-component': 'Action.Drawer',
'x-component-props': {
getContainer: '{{ getContainer }}',
},
'x-decorator': 'Form', 'x-decorator': 'Form',
'x-decorator-props': { 'x-decorator-props': {
useValues(options) { useValues(options) {
@ -75,7 +78,7 @@ const getSchema = (schema: IField, record: any, compile): ISchema => {
title: '{{ t("Cancel") }}', title: '{{ t("Cancel") }}',
'x-component': 'Action', 'x-component': 'Action',
'x-component-props': { 'x-component-props': {
useAction: '{{ cm.useCancelAction }}', useAction: '{{ useCancelAction }}',
}, },
}, },
action2: { action2: {
@ -94,11 +97,25 @@ const getSchema = (schema: IField, record: any, compile): ISchema => {
}; };
}; };
export const useCollectionFieldFormValues = () => {
const form = useForm();
return {
getValues() {
const values = cloneDeep(form.values);
if (values.autoCreateReverseField) {
} else {
delete values.reverseField;
}
delete values.autoCreateReverseField;
return values;
}
}
}
const useCreateCollectionField = () => { const useCreateCollectionField = () => {
const form = useForm(); const form = useForm();
const { run } = useCreateAction(); const { run } = useCreateAction();
const { refreshCM } = useCollectionManager(); const { refreshCM } = useCollectionManager();
const { title } = useRecord();
const ctx = useActionContext(); const ctx = useActionContext();
const { refresh } = useResourceActionContext(); const { refresh } = useResourceActionContext();
const { resource } = useResourceContext(); const { resource } = useResourceContext();
@ -120,53 +137,71 @@ const useCreateCollectionField = () => {
}; };
}; };
export const AddFieldAction = () => { export const AddCollectionField = (props) => {
const record = useRecord();
return <AddFieldAction item={record} {...props} />;
};
export const AddFieldAction = (props) => {
const { scope, getContainer, item: record, children } = props;
const { getInterface } = useCollectionManager(); const { getInterface } = useCollectionManager();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [schema, setSchema] = useState({}); const [schema, setSchema] = useState({});
const compile = useCompile(); const compile = useCompile();
const { t } = useTranslation(); const { t } = useTranslation();
const record = useRecord();
return ( return (
<ActionContext.Provider value={{ visible, setVisible }}> <RecordProvider record={record}>
<Dropdown <ActionContext.Provider value={{ visible, setVisible }}>
overlay={ <Dropdown
<Menu getPopupContainer={getContainer}
style={{ overlay={
maxHeight: '60vh', <Menu
overflow: 'auto', style={{
}} maxHeight: '60vh',
onClick={(info) => { overflow: 'auto',
const schema = getSchema(getInterface(info.key), record, compile); }}
setSchema(schema); onClick={(info) => {
setVisible(true); const schema = getSchema(getInterface(info.key), record, compile);
}} setSchema(schema);
> setVisible(true);
{options.map((option) => { }}
return ( >
option.children.length > 0 && ( {options.map((option) => {
<Menu.ItemGroup key={option.label} title={compile(option.label)}> return (
{option.children option.children.length > 0 && (
.filter((child) => !['o2o', 'subTable'].includes(child.name)) <Menu.ItemGroup key={option.label} title={compile(option.label)}>
.map((child) => { {option.children
return <Menu.Item key={child.name}>{compile(child.title)}</Menu.Item>; .filter((child) => !['o2o', 'subTable'].includes(child.name))
})} .map((child) => {
</Menu.ItemGroup> return <Menu.Item key={child.name}>{compile(child.title)}</Menu.Item>;
) })}
); </Menu.ItemGroup>
})} )
</Menu> );
} })}
> </Menu>
<Button icon={<PlusOutlined />} type={'primary'}> }
{t('Add field')} >
</Button> {children || (
</Dropdown> <Button icon={<PlusOutlined />} type={'primary'}>
<SchemaComponent {t('Add field')}
schema={schema} </Button>
components={{ ...components, ArrayTable }} )}
scope={{ createOnly: true, useCreateCollectionField, record, showReverseFieldConfig: true }} </Dropdown>
/> <SchemaComponent
</ActionContext.Provider> schema={schema}
components={{ ...components, ArrayTable }}
scope={{
getContainer,
useCancelAction,
createOnly: true,
useCreateCollectionField,
record,
showReverseFieldConfig: true,
...scope,
}}
/>
</ActionContext.Provider>
</RecordProvider>
); );
}; };

View File

@ -6,15 +6,15 @@ import set from 'lodash/set';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAPIClient, useRequest } from '../../api-client'; import { useAPIClient, useRequest } from '../../api-client';
import { useRecord } from '../../record-provider'; import { useRecord, RecordProvider } from '../../record-provider';
import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useUpdateAction } from '../action-hooks'; import { useCancelAction, useUpdateAction } from '../action-hooks';
import { useCollectionManager } from '../hooks'; import { useCollectionManager } from '../hooks';
import { IField } from '../interfaces/types'; import { IField } from '../interfaces/types';
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
import * as components from './components'; import * as components from './components';
const getSchema = (schema: IField, record: any, compile): ISchema => { const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => {
if (!schema) { if (!schema) {
return; return;
} }
@ -64,7 +64,7 @@ const getSchema = (schema: IField, record: any, compile): ISchema => {
title: '{{ t("Cancel") }}', title: '{{ t("Cancel") }}',
'x-component': 'Action', 'x-component': 'Action',
'x-component-props': { 'x-component-props': {
useAction: '{{ cm.useCancelAction }}', useAction: '{{ useCancelAction }}',
}, },
}, },
action2: { action2: {
@ -109,8 +109,13 @@ const useUpdateCollectionField = () => {
}; };
}; };
export const EditFieldAction = (props) => { export const EditCollectionField = (props) => {
const record = useRecord(); const record = useRecord();
return <EditFieldAction item={record} {...props} />;
};
export const EditFieldAction = (props) => {
const { scope, getContainer, item: record,children } = props;
const { getInterface } = useCollectionManager(); const { getInterface } = useCollectionManager();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [schema, setSchema] = useState({}); const [schema, setSchema] = useState({});
@ -118,42 +123,52 @@ export const EditFieldAction = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const compile = useCompile(); const compile = useCompile();
const [data, setData] = useState<any>({}); const [data, setData] = useState<any>({});
return ( return (
<ActionContext.Provider value={{ visible, setVisible }}> <RecordProvider record={record}>
<a <ActionContext.Provider value={{ visible, setVisible }}>
onClick={async () => { <a
const { data } = await api.resource('collections.fields', record.collectionName).get({ onClick={async () => {
filterByTk: record.name, const { data } = await api.resource('collections.fields', record.collectionName).get({
appends: ['uiSchema', 'reverseField'], filterByTk: record.name,
}); appends: ['uiSchema', 'reverseField'],
setData(data?.data); });
const interfaceConf = getInterface(record.interface); setData(data?.data);
const defaultValues: any = cloneDeep(data?.data) || {}; const interfaceConf = getInterface(record.interface);
if (!defaultValues?.reverseField) { const defaultValues: any = cloneDeep(data?.data) || {};
defaultValues.autoCreateReverseField = false; if (!defaultValues?.reverseField) {
defaultValues.reverseField = interfaceConf.default?.reverseField; defaultValues.autoCreateReverseField = false;
set(defaultValues.reverseField, 'name', `f_${uid()}`); defaultValues.reverseField = interfaceConf.default?.reverseField;
set(defaultValues.reverseField, 'uiSchema.title', record.__parent.title); set(defaultValues.reverseField, 'name', `f_${uid()}`);
} set(defaultValues.reverseField, 'uiSchema.title', record.__parent.title);
const schema = getSchema( }
{ const schema = getSchema(
...interfaceConf, {
default: defaultValues, ...interfaceConf,
}, default: defaultValues,
record, },
compile, record,
); compile,
setSchema(schema); getContainer,
setVisible(true); );
}} setSchema(schema);
> setVisible(true);
{t('Edit')} }}
</a> >
<SchemaComponent {children||t('Edit')}
schema={schema} </a>
components={{ ...components, ArrayTable }} <SchemaComponent
scope={{ useUpdateCollectionField, showReverseFieldConfig: !data?.reverseField }} schema={schema}
/> components={{ ...components, ArrayTable }}
</ActionContext.Provider> scope={{
getContainer,
useUpdateCollectionField,
useCancelAction,
showReverseFieldConfig: !data?.reverseField,
...scope,
}}
/>
</ActionContext.Provider>
</RecordProvider>
); );
}; };

View File

@ -1,8 +1,11 @@
import { registerValidateFormats } from '@formily/core'; import { registerValidateFormats } from '@formily/core';
import { exp } from 'mathjs';
export * from './AddFieldAction'; export * from './AddFieldAction';
export * from './ConfigurationTable'; export * from './ConfigurationTable';
export * from './EditFieldAction'; export * from './EditFieldAction';
export * from './interfaces';
export * from './components';
registerValidateFormats({ registerValidateFormats({
uid: /^[A-Za-z0-9][A-Za-z0-9_-]*$/, uid: /^[A-Za-z0-9][A-Za-z0-9_-]*$/,

View File

@ -113,7 +113,7 @@ export const collectionFieldSchema: ISchema = {
create: { create: {
type: 'void', type: 'void',
title: '{{ t("Add new") }}', title: '{{ t("Add new") }}',
'x-component': 'AddFieldAction', 'x-component': 'AddCollectionField',
'x-component-props': { 'x-component-props': {
type: 'primary', type: 'primary',
}, },
@ -181,7 +181,7 @@ export const collectionFieldSchema: ISchema = {
update: { update: {
type: 'void', type: 'void',
title: '{{ t("Edit") }}', title: '{{ t("Edit") }}',
'x-component': 'EditFieldAction', 'x-component': 'EditCollectionField',
'x-component-props': { 'x-component-props': {
type: 'primary', type: 'primary',
}, },

View File

@ -9,4 +9,6 @@ export * from './context';
export * from './hooks'; export * from './hooks';
export * from './ResourceActionProvider'; export * from './ResourceActionProvider';
export * from './types'; export * from './types';
export * from './Configuration';

View File

@ -66,6 +66,10 @@ export const SettingsCenterDropdown = () => {
title: t('Workflow'), title: t('Workflow'),
path: 'workflow/workflows', path: 'workflow/workflows',
}, },
{
title: t('Graph Collections'),
path: 'graph/collections',
},
]; ];
return ( return (
<ActionContext.Provider value={{ visible, setVisible }}> <ActionContext.Provider value={{ visible, setVisible }}>
@ -79,6 +83,7 @@ export const SettingsCenterDropdown = () => {
onClick={() => { onClick={() => {
history.push('/admin/settings/' + item.path); history.push('/admin/settings/' + item.path);
}} }}
key={item.path}
> >
{item.title} {item.title}
</Menu.Item> </Menu.Item>

View File

@ -178,10 +178,10 @@ const InternalAdminLayout = (props: any) => {
className={css` className={css`
min-height: calc(100vh - 46px); min-height: calc(100vh - 46px);
position: relative; position: relative;
padding-bottom: 70px; // padding-bottom: 70px;
> div { > div {
position: relative; position: relative;
z-index: 1; // z-index: 1;
} }
.ant-layout-footer { .ant-layout-footer {
position: absolute; position: absolute;
@ -189,6 +189,7 @@ const InternalAdminLayout = (props: any) => {
text-align: center; text-align: center;
width: 100%; width: 100%;
z-index: 0; z-index: 0;
padding: 10px 50px;
} }
`} `}
> >

View File

@ -155,6 +155,8 @@ export class Database extends EventEmitter implements AsyncEmitter {
constructor(options: DatabaseOptions) { constructor(options: DatabaseOptions) {
super(); super();
// this.setMaxListeners(100);
this.version = new DatabaseVersion(this); this.version = new DatabaseVersion(this);
const opts = { const opts = {

View File

@ -17,6 +17,10 @@ export class BelongsToManyField extends RelationField {
); );
} }
get otherKey() {
return this.options.otherKey;
}
bind() { bind() {
const { database, collection } = this.context; const { database, collection } = this.context;
const Target = this.TargetModel; const Target = this.TargetModel;
@ -34,6 +38,7 @@ export class BelongsToManyField extends RelationField {
} else { } else {
Through = database.collection({ Through = database.collection({
name: through, name: through,
// timestamps: false,
}); });
Object.defineProperty(Through.model, 'isThrough', { value: true }); Object.defineProperty(Through.model, 'isThrough', { value: true });
@ -80,6 +85,7 @@ export class BelongsToManyField extends RelationField {
unbind() { unbind() {
const { database, collection } = this.context; const { database, collection } = this.context;
const Through = database.getCollection(this.through);
// 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段 // 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段
database.removePendingField(this); database.removePendingField(this);
// 删掉 model 的关联字段 // 删掉 model 的关联字段

View File

@ -5,12 +5,11 @@ import {
ModelIndexesOptions, ModelIndexesOptions,
QueryInterfaceOptions, QueryInterfaceOptions,
SyncOptions, SyncOptions,
Transactionable, Transactionable
} from 'sequelize'; } from 'sequelize';
import { Collection } from '../collection'; import { Collection } from '../collection';
import { Database } from '../database'; import { Database } from '../database';
import { ModelEventTypes } from '../types'; import { ModelEventTypes } from '../types';
import { checkIdentifier } from '../utils';
export interface FieldContext { export interface FieldContext {
database: Database; database: Database;
@ -84,6 +83,7 @@ export abstract class Field {
} }
remove() { remove() {
this.collection.removeIndex([this.name]);
return this.collection.removeField(this.name); return this.collection.removeField(this.name);
} }

View File

@ -26,7 +26,8 @@
"koa-bodyparser": "^4.3.0", "koa-bodyparser": "^4.3.0",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"semver": "^7.3.7" "semver": "^7.3.7",
"xpipe": "^1.0.5"
}, },
"devDependencies": { "devDependencies": {
"@types/semver": "^7.3.9" "@types/semver": "^7.3.9"

View File

@ -207,10 +207,15 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
this._events = []; this._events = [];
// @ts-ignore // @ts-ignore
this._eventsCount = []; this._eventsCount = [];
this.removeAllListeners();
this.middleware = new Toposort<any>(); this.middleware = new Toposort<any>();
// this.context = Object.create(context); // this.context = Object.create(context);
this.plugins = new Map<string, Plugin>(); this.plugins = new Map<string, Plugin>();
this._acl = createACL(); this._acl = createACL();
if (this._db) {
// MaxListenersExceededWarning
this._db.removeAllListeners();
}
this._db = this.createDatabase(options); this._db = this.createDatabase(options);
this._resourcer = createResourcer(options); this._resourcer = createResourcer(options);
this._cli = new Command('nocobase').usage('[command] [options]'); this._cli = new Command('nocobase').usage('[command] [options]');
@ -220,9 +225,14 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
this.context.resourcer = this._resourcer; this.context.resourcer = this._resourcer;
this.context.cache = this._cache; this.context.cache = this._cache;
this._pm = new PluginManager({ if (this._pm) {
app: this, this._pm = this._pm.clone();
}); } else {
this._pm = new PluginManager({
app: this,
plugins: options.plugins,
});
}
this._appManager = new AppManager(this); this._appManager = new AppManager(this);
@ -236,8 +246,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
registerActions(this); registerActions(this);
} }
this.loadPluginConfig(options.plugins || []);
registerCli(this); registerCli(this);
this._version = new ApplicationVersion(this); this._version = new ApplicationVersion(this);
@ -264,16 +272,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
return this.pm.addStatic(pluginClass, options); return this.pm.addStatic(pluginClass, options);
} }
loadPluginConfig(pluginsConfigurations: PluginConfiguration[]) {
for (let pluginConfiguration of pluginsConfigurations) {
if (typeof pluginConfiguration == 'string') {
this.plugin(pluginConfiguration);
} else {
this.plugin(...pluginConfiguration);
}
}
}
// @ts-ignore // @ts-ignore
use<NewStateT = {}, NewContextT = {}>( use<NewStateT = {}, NewContextT = {}>(
middleware: Koa.Middleware<StateT & NewStateT, ContextT & NewContextT>, middleware: Koa.Middleware<StateT & NewStateT, ContextT & NewContextT>,

View File

@ -4,6 +4,7 @@ import execa from 'execa';
import fs from 'fs'; import fs from 'fs';
import net from 'net'; import net from 'net';
import { resolve } from 'path'; import { resolve } from 'path';
import xpipe from 'xpipe';
import Application from '../application'; import Application from '../application';
import { Plugin } from '../plugin'; import { Plugin } from '../plugin';
import collectionOptions from './options/collection'; import collectionOptions from './options/collection';
@ -12,6 +13,7 @@ import { PluginManagerRepository } from './PluginManagerRepository';
export interface PluginManagerOptions { export interface PluginManagerOptions {
app: Application; app: Application;
plugins?: any[];
} }
export interface InstallOptions { export interface InstallOptions {
@ -27,11 +29,12 @@ export class PluginManager {
plugins = new Map<string, Plugin>(); plugins = new Map<string, Plugin>();
server: net.Server; server: net.Server;
pmSock: string; pmSock: string;
_tmpPluginArgs = [];
constructor(options: PluginManagerOptions) { constructor(options: PluginManagerOptions) {
this.app = options.app; this.app = options.app;
const f = resolve(process.cwd(), 'storage', 'pm.sock'); const f = resolve(process.cwd(), 'storage', 'pm.sock');
this.pmSock = this.app.options.pmSock || f; this.pmSock = xpipe.eq(this.app.options.pmSock || f);
this.app.db.registerRepositories({ this.app.db.registerRepositories({
PluginManagerRepository, PluginManagerRepository,
}); });
@ -71,6 +74,17 @@ export class PluginManager {
await this.repository.load(); await this.repository.load();
} }
}); });
this.addStaticMultiple(options.plugins);
}
addStaticMultiple(plugins: any) {
for (let plugin of plugins || []) {
if (typeof plugin == 'string') {
this.addStatic(plugin);
} else {
this.addStatic(...plugin);
}
}
} }
getPlugins() { getPlugins() {
@ -123,7 +137,20 @@ export class PluginManager {
await run('yarn', ['install']); await run('yarn', ['install']);
} }
clone() {
const pm = new PluginManager({
app: this.app,
});
for (const arg of this._tmpPluginArgs) {
pm.addStatic(...arg);
}
return pm;
}
addStatic(plugin?: any, options?: any) { addStatic(plugin?: any, options?: any) {
if (!options?.async) {
this._tmpPluginArgs.push([plugin, options]);
}
let name: string; let name: string;
if (typeof plugin === 'string') { if (typeof plugin === 'string') {
name = plugin; name = plugin;
@ -154,7 +181,10 @@ export class PluginManager {
// console.log(`adding ${plugin} plugin`); // console.log(`adding ${plugin} plugin`);
const packageName = await PluginManager.findPackage(plugin); const packageName = await PluginManager.findPackage(plugin);
const packageJson = require(`${packageName}/package.json`); const packageJson = require(`${packageName}/package.json`);
const instance = this.addStatic(plugin, options); const instance = this.addStatic(plugin, {
...options,
async: true,
});
let model = await this.repository.findOne({ let model = await this.repository.findOne({
filter: { name: plugin }, filter: { name: plugin },
}); });

View File

@ -56,11 +56,11 @@ interface Resource {
export class MockServer extends Application { export class MockServer extends Application {
async loadAndInstall(options: any = {}) { async loadAndInstall(options: any = {}) {
await this.load(); await this.load({ method: 'install' });
await this.install({ await this.install({
...options, ...options,
sync: { sync: {
force: true, force: false,
alter: { alter: {
drop: false, drop: false,
}, },

View File

@ -507,7 +507,7 @@ describe('acl', () => {
expect(response.statusCode).toEqual(200); expect(response.statusCode).toEqual(200);
}); });
it('should sync data to acl before app start', async () => { it('should sync data to acl after app reload', async () => {
const role = await db.getRepository('roles').create({ const role = await db.getRepository('roles').create({
values: { values: {
name: 'new', name: 'new',
@ -527,14 +527,14 @@ describe('acl', () => {
hooks: false, hooks: false,
}); });
expect(acl.getRole('new')).toBeUndefined(); expect(app.acl.getRole('new')).toBeUndefined();
await app.start(); await app.reload();
expect(acl.getRole('new')).toBeDefined(); expect(app.acl.getRole('new')).toBeDefined();
expect( expect(
acl.can({ app.acl.can({
role: 'new', role: 'new',
resource: 'posts', resource: 'posts',
action: 'view', action: 'view',

View File

@ -8,12 +8,11 @@ export async function prepareApp() {
plugins: ['error-handler', 'users', 'ui-schema-storage', 'collection-manager'], plugins: ['error-handler', 'users', 'ui-schema-storage', 'collection-manager'],
}); });
await app.cleanDb();
app.plugin(PluginACL, { app.plugin(PluginACL, {
name: 'acl', name: 'acl',
}); });
await app.loadAndInstall();
await app.loadAndInstall({ clean: true });
await app.db.sync(); await app.db.sync();

View File

@ -269,7 +269,7 @@ export class PluginACL extends Plugin {
// sync database role data to acl // sync database role data to acl
this.app.on('afterLoad', async (app, options) => { this.app.on('afterLoad', async (app, options) => {
if (options.method === 'install') { if (options?.method === 'install') {
return; return;
} }
const exists = await this.app.db.collectionExistsInDb('roles'); const exists = await this.app.db.collectionExistsInDb('roles');
@ -278,6 +278,13 @@ export class PluginACL extends Plugin {
} }
}); });
this.app.on('afterInstall', async (app, options) => {
const exists = await this.app.db.collectionExistsInDb('roles');
if (exists) {
await this.writeRolesToACL();
}
});
this.app.on('beforeInstallPlugin', async (plugin) => { this.app.on('beforeInstallPlugin', async (plugin) => {
if (plugin.constructor.name !== 'UsersPlugin') { if (plugin.constructor.name !== 'UsersPlugin') {
return; return;

View File

@ -48,7 +48,7 @@ describe('field indexes', () => {
console.log(response3.body); console.log(response3.body);
expect(response3.status).toBe(500); expect(response3.status).toBe(400);
const response4 = await agent.resource(tableName).create({ const response4 = await agent.resource(tableName).create({
values: { title: 't1' }, values: { title: 't1' },
@ -84,7 +84,7 @@ describe('field indexes', () => {
title: 't1', title: 't1',
}, },
}); });
expect(response2.status).toBe(500); expect(response2.status).toBe(400);
// update field to remove unique constraint // update field to remove unique constraint
await agent.resource('fields').update({ await agent.resource('fields').update({
@ -111,7 +111,7 @@ describe('field indexes', () => {
}, },
}); });
expect(response4.status).toBe(500); expect(response4.status).toBe(400);
// remove a duplicated record // remove a duplicated record
await agent.resource(tableName).destroy({ await agent.resource(tableName).destroy({

View File

@ -1,7 +1,6 @@
import PluginErrorHandler from '@nocobase/plugin-error-handler'; import PluginErrorHandler from '@nocobase/plugin-error-handler';
import PluginUiSchema from '@nocobase/plugin-ui-schema-storage'; import PluginUiSchema from '@nocobase/plugin-ui-schema-storage';
import { mockServer } from '@nocobase/test'; import { mockServer } from '@nocobase/test';
import lodash from 'lodash';
import Plugin from '../'; import Plugin from '../';
export async function createApp(options = {}) { export async function createApp(options = {}) {
@ -9,10 +8,6 @@ export async function createApp(options = {}) {
acl: false, acl: false,
}); });
if (lodash.get(options, 'cleanDB', true)) {
await app.cleanDb();
}
app.plugin(PluginErrorHandler, { name: 'error-handler' }); app.plugin(PluginErrorHandler, { name: 'error-handler' });
app.plugin(Plugin, { name: 'collection-manager' }); app.plugin(Plugin, { name: 'collection-manager' });
app.plugin(PluginUiSchema, { name: 'ui-schema-storage' }); app.plugin(PluginUiSchema, { name: 'ui-schema-storage' });

View File

@ -0,0 +1,140 @@
import Database from '@nocobase/database';
export function afterCreateForForeignKeyField(db: Database) {
function generateFkOptions(collectionName: string, foreignKey: string) {
const M = db.getModel(collectionName);
const attr = M.rawAttributes[foreignKey];
if (!attr) {
throw new Error(`${collectionName}.${foreignKey} does not exists`);
}
return attribute2field(attr);
}
// Foreign key types are only integer and string
function attribute2field(attribute: any) {
const type = attribute.type.constructor.name === 'INTEGER' ? 'integer' : 'string';
const name = attribute.fieldName;
const data = {
interface: 'integer',
name,
type,
uiSchema: {
type: 'number',
title: name,
'x-component': 'InputNumber',
'x-read-pretty': true,
},
};
if (type === 'string') {
data['interface'] = 'input';
data['uiSchema'] = {
type: 'string',
title: name,
'x-component': 'Input',
'x-read-pretty': true,
};
}
return data;
}
async function createFieldIfNotExists({ values, transaction }) {
const { collectionName, name } = values;
if (!collectionName || !name) {
throw new Error(`field options invalid`);
}
const r = db.getRepository('fields');
const instance = await r.findOne({
filter: {
collectionName,
name,
},
transaction,
});
if (instance) {
if (instance.type !== values.type) {
throw new Error(`fk type invalid`);
}
instance.set('sort',1);
instance.set('isForeignKey', true);
await instance.save({ transaction });
} else {
await r.create({
values: {
isForeignKey: true,
sort:1,
...values,
},
transaction,
});
}
}
return async (model, { transaction, context }) => {
// skip if no app context
if (!context) {
return;
}
const { type, interface: interfaceType, collectionName, target, through, foreignKey, otherKey } = model.get();
// foreign key in target collection
if (['oho', 'o2m'].includes(interfaceType)) {
const values = generateFkOptions(target, foreignKey);
await createFieldIfNotExists({
values: {
collectionName: target,
...values,
},
transaction,
});
}
// foreign key in source collection
else if (['obo', 'm2o'].includes(interfaceType)) {
const values = generateFkOptions(collectionName, foreignKey);
await createFieldIfNotExists({
values: { collectionName, ...values },
transaction,
});
}
// foreign key in through collection
else if (['linkTo', 'm2m'].includes(interfaceType)) {
if (type !== 'belongsToMany') {
return;
}
const r = db.getRepository('collections');
const instance = await r.findOne({
filter: {
name: through,
},
transaction,
});
if (!instance) {
await r.create({
values: {
name: through,
title: through,
timestamps: false,
autoGenId: false,
autoCreate: true,
isThrough: true,
},
transaction,
});
}
const opts1 = generateFkOptions(through, foreignKey);
const opts2 = generateFkOptions(through, otherKey);
await createFieldIfNotExists({
values: {
collectionName: through,
...opts1,
},
transaction,
});
await createFieldIfNotExists({
values: {
collectionName: through,
...opts2,
},
transaction,
});
}
};
}

View File

@ -0,0 +1,71 @@
import Database, { FindOneOptions, FindOptions, Model } from '@nocobase/database';
import { Transaction } from 'sequelize';
async function destroyFields(db: Database, transaction: Transaction, fieldRecords: Model[]) {
const fieldsRepo = db.getRepository('fields');
for (const fieldRecord of fieldRecords) {
await fieldsRepo.destroy({
filter: {
name: fieldRecord.get('name'),
collectionName: fieldRecord.get('collectionName'),
},
transaction,
});
}
}
export function afterDestroyForForeignKeyField(db: Database) {
return async (model, opts) => {
const { transaction } = opts;
const options = model.get('options');
if (!options?.isForeignKey) {
return;
}
const collectionRepo = db.getRepository('collections');
const foreignKey = model.get('name');
const foreignKeyCollectionName = model.get('collectionName');
const collectionRecord = await collectionRepo.findOne({
filter: {
name: foreignKeyCollectionName,
},
transaction,
} as FindOneOptions);
const collectionOptions = collectionRecord.get('options');
const fieldsRepo = db.getRepository('fields');
if (collectionOptions?.isThrough) {
// through collection
const fieldRecords = await fieldsRepo.find({
filter: {
options: { through: foreignKeyCollectionName, foreignKey: foreignKey },
},
transaction,
} as FindOptions);
await destroyFields(db, transaction, fieldRecords);
} else {
await destroyFields(
db,
transaction,
await fieldsRepo.find({
filter: {
collectionName: foreignKeyCollectionName,
options: { foreignKey: foreignKey },
},
transaction,
} as FindOptions),
);
await destroyFields(
db,
transaction,
await fieldsRepo.find({
filter: {
options: { foreignKey: foreignKey, target: foreignKeyCollectionName },
},
transaction,
} as FindOptions),
);
}
};
}

View File

@ -0,0 +1,51 @@
import Database from '@nocobase/database';
export function beforeDestroyForeignKey(db: Database) {
return async (model, opts) => {
const { transaction } = opts;
const { isForeignKey, collectionName: fkCollectionName, name: fkName } = model.get();
if (!isForeignKey) {
return;
}
const fieldKeys = [];
for (const [sourceName, collection] of db.collections) {
for (const [, field] of collection.fields) {
const fieldKey = field.options?.key;
if (!fieldKey) {
continue;
}
// fk in source collection
if (field.type === 'belongsTo') {
if (sourceName === fkCollectionName && field.foreignKey === fkName) {
fieldKeys.push(fieldKey);
}
}
// fk in target collection
else if (field.type === 'hasOne' || field.type === 'hasMany') {
if (fkCollectionName === field.target && field.foreignKey === fkName) {
fieldKeys.push(fieldKey);
}
}
// fk in through collection
else if (field.type === 'belongsToMany' && field.through === fkCollectionName) {
console.log(field.foreignKey, field.otherKey);
if (field.foreignKey === fkName || field.otherKey === fkName) {
fieldKeys.push(fieldKey);
}
}
}
}
const r = db.getRepository('fields');
await r.destroy({
filter: {
'key.$in': fieldKeys,
},
transaction,
});
};
}

View File

@ -1,5 +1,7 @@
export * from './afterCreateForForeignKeyField';
export * from './afterCreateForReverseField'; export * from './afterCreateForReverseField';
export * from './beforeCreateForChildrenCollection'; export * from './beforeCreateForChildrenCollection';
export * from './beforeCreateForReverseField'; export * from './beforeCreateForReverseField';
export * from './beforeDestroyForeignKey';
export * from './beforeInitOptions'; export * from './beforeInitOptions';

View File

@ -8,9 +8,11 @@ import { Plugin } from '@nocobase/server';
import { CollectionRepository } from '.'; import { CollectionRepository } from '.';
import { import {
afterCreateForForeignKeyField,
afterCreateForReverseField, afterCreateForReverseField,
beforeCreateForChildrenCollection, beforeCreateForChildrenCollection,
beforeCreateForReverseField, beforeCreateForReverseField,
beforeDestroyForeignKey,
beforeInitOptions beforeInitOptions
} from './hooks'; } from './hooks';
import { CollectionModel, FieldModel } from './models'; import { CollectionModel, FieldModel } from './models';
@ -81,6 +83,8 @@ export class CollectionManagerPlugin extends Plugin {
}); });
} }
}); });
// after migrate
this.app.db.on('fields.afterCreate', afterCreateForForeignKeyField(this.app.db));
this.app.db.on('fields.afterUpdate', async (model: FieldModel, { context, transaction }) => { this.app.db.on('fields.afterUpdate', async (model: FieldModel, { context, transaction }) => {
const prevOptions = model.previous('options'); const prevOptions = model.previous('options');
@ -109,6 +113,8 @@ export class CollectionManagerPlugin extends Plugin {
} }
}); });
// before field remove
this.app.db.on('fields.beforeDestroy', beforeDestroyForeignKey(this.app.db));
this.app.db.on('fields.beforeDestroy', async (model, options) => { this.app.db.on('fields.beforeDestroy', async (model, options) => {
await model.remove(options); await model.remove(options);
}); });

View File

@ -27,6 +27,19 @@ export class PresetNocoBase extends Plugin {
} }
afterAdd() { afterAdd() {
this.app.on('beforeLoad', async (app, options) => {
if (options?.method !== 'upgrade') {
return;
}
const result = await this.app.version.satisfies('<0.8.0-alpha.1');
if (result) {
const r = await this.db.collectionExistsInDb('applicationPlugins');
if (r) {
await this.db.getRepository('applicationPlugins').destroy({ truncate: true });
await this.app.reload();
}
}
});
this.app.on('beforeUpgrade', async () => { this.app.on('beforeUpgrade', async () => {
const result = await this.app.version.satisfies('<0.8.0-alpha.1'); const result = await this.app.version.satisfies('<0.8.0-alpha.1');
if (result) { if (result) {

View File

@ -18537,7 +18537,7 @@ rc-cascader@~3.2.1:
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
array-tree-filter "^2.1.0" array-tree-filter "^2.1.0"
classnames "^2.3.1" classnames "^2.3.1"
rc-select "~14.0.2" rc-select "~14.0.0-alpha.23"
rc-tree "~5.4.3" rc-tree "~5.4.3"
rc-util "^5.6.1" rc-util "^5.6.1"
@ -18772,6 +18772,19 @@ rc-resize-observer@^1.2.0:
rc-util "^5.15.0" rc-util "^5.15.0"
resize-observer-polyfill "^1.5.1" resize-observer-polyfill "^1.5.1"
rc-select@~14.0.0-alpha.23, rc-select@~14.0.0-alpha.8:
version "14.0.6"
resolved "https://registry.npmjs.org/rc-select/-/rc-select-14.0.6.tgz#93be0b185a9d66dc84795e079121f0f65310d8bf"
integrity sha512-HMb2BwfTvBxMmIWTR/afP4bcRJLbVKFSBW/VFfL5Z+kdV2XlrYdlliK2uHY7pRRvW16PPGwmOwGfV+eoulPINw==
dependencies:
"@babel/runtime" "^7.10.1"
classnames "2.x"
rc-motion "^2.0.1"
rc-overflow "^1.0.0"
rc-trigger "^5.0.4"
rc-util "^5.16.1"
rc-virtual-list "^3.2.0"
rc-select@~14.0.2: rc-select@~14.0.2:
version "14.0.5" version "14.0.5"
resolved "https://registry.npmjs.org/rc-select/-/rc-select-14.0.5.tgz#145c42e7fd66a7fc6c5c56f6b0cf35d8b50f9e23" resolved "https://registry.npmjs.org/rc-select/-/rc-select-14.0.5.tgz#145c42e7fd66a7fc6c5c56f6b0cf35d8b50f9e23"
@ -18874,7 +18887,7 @@ rc-tree-select@~5.1.1:
dependencies: dependencies:
"@babel/runtime" "^7.10.1" "@babel/runtime" "^7.10.1"
classnames "2.x" classnames "2.x"
rc-select "~14.0.2" rc-select "~14.0.0-alpha.8"
rc-tree "~5.4.3" rc-tree "~5.4.3"
rc-util "^5.16.1" rc-util "^5.16.1"
@ -23296,6 +23309,11 @@ xmldom-sre@^0.1.31:
resolved "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz#10860d5bab2c603144597d04bf2c4980e98067f4" resolved "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz#10860d5bab2c603144597d04bf2c4980e98067f4"
integrity sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw== integrity sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==
xpipe@^1.0.5:
version "1.0.5"
resolved "https://registry.npmjs.org/xpipe/-/xpipe-1.0.5.tgz#8dd8bf45fc3f7f55f0e054b878f43a62614dafdf"
integrity sha512-tuqoLk8xPl0o+7ny9iPlEZuzjfy1zC5ZJtAGjDDZWmVTVBK5PJP0arMGVu3Y53zSyeYK+YonMVSUv0DJgGN/ig==
xregexp@2.0.0: xregexp@2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" resolved "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"