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:
parent
274d80d501
commit
f67afba964
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
@ -49,11 +45,16 @@ export const RemoteCollectionManagerProvider = (props: any) => {
|
|||||||
<CollectionManagerProvider
|
<CollectionManagerProvider
|
||||||
service={{ ...service, contentLoading, setContentLoading }}
|
service={{ ...service, contentLoading, setContentLoading }}
|
||||||
collections={service?.data?.data}
|
collections={service?.data?.data}
|
||||||
refreshCM={async () => {
|
refreshCM={async (opts) => {
|
||||||
|
if (opts?.reload) {
|
||||||
setContentLoading(true);
|
setContentLoading(true);
|
||||||
|
}
|
||||||
const { data } = await api.request(options);
|
const { data } = await api.request(options);
|
||||||
service.mutate(data);
|
service.mutate(data);
|
||||||
|
if (opts?.reload) {
|
||||||
setContentLoading(false);
|
setContentLoading(false);
|
||||||
|
}
|
||||||
|
return data?.data || [];
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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,16 +137,23 @@ 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 (
|
||||||
|
<RecordProvider record={record}>
|
||||||
<ActionContext.Provider value={{ visible, setVisible }}>
|
<ActionContext.Provider value={{ visible, setVisible }}>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
getPopupContainer={getContainer}
|
||||||
overlay={
|
overlay={
|
||||||
<Menu
|
<Menu
|
||||||
style={{
|
style={{
|
||||||
@ -158,15 +182,26 @@ export const AddFieldAction = () => {
|
|||||||
</Menu>
|
</Menu>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{children || (
|
||||||
<Button icon={<PlusOutlined />} type={'primary'}>
|
<Button icon={<PlusOutlined />} type={'primary'}>
|
||||||
{t('Add field')}
|
{t('Add field')}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<SchemaComponent
|
<SchemaComponent
|
||||||
schema={schema}
|
schema={schema}
|
||||||
components={{ ...components, ArrayTable }}
|
components={{ ...components, ArrayTable }}
|
||||||
scope={{ createOnly: true, useCreateCollectionField, record, showReverseFieldConfig: true }}
|
scope={{
|
||||||
|
getContainer,
|
||||||
|
useCancelAction,
|
||||||
|
createOnly: true,
|
||||||
|
useCreateCollectionField,
|
||||||
|
record,
|
||||||
|
showReverseFieldConfig: true,
|
||||||
|
...scope,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</ActionContext.Provider>
|
</ActionContext.Provider>
|
||||||
|
</RecordProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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,7 +123,9 @@ 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 (
|
||||||
|
<RecordProvider record={record}>
|
||||||
<ActionContext.Provider value={{ visible, setVisible }}>
|
<ActionContext.Provider value={{ visible, setVisible }}>
|
||||||
<a
|
<a
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@ -142,18 +149,26 @@ export const EditFieldAction = (props) => {
|
|||||||
},
|
},
|
||||||
record,
|
record,
|
||||||
compile,
|
compile,
|
||||||
|
getContainer,
|
||||||
);
|
);
|
||||||
setSchema(schema);
|
setSchema(schema);
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('Edit')}
|
{children||t('Edit')}
|
||||||
</a>
|
</a>
|
||||||
<SchemaComponent
|
<SchemaComponent
|
||||||
schema={schema}
|
schema={schema}
|
||||||
components={{ ...components, ArrayTable }}
|
components={{ ...components, ArrayTable }}
|
||||||
scope={{ useUpdateCollectionField, showReverseFieldConfig: !data?.reverseField }}
|
scope={{
|
||||||
|
getContainer,
|
||||||
|
useUpdateCollectionField,
|
||||||
|
useCancelAction,
|
||||||
|
showReverseFieldConfig: !data?.reverseField,
|
||||||
|
...scope,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</ActionContext.Provider>
|
</ActionContext.Provider>
|
||||||
|
</RecordProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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_-]*$/,
|
||||||
|
@ -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',
|
||||||
},
|
},
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
@ -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 = {
|
||||||
|
@ -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 的关联字段
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
if (this._pm) {
|
||||||
|
this._pm = this._pm.clone();
|
||||||
|
} else {
|
||||||
this._pm = new PluginManager({
|
this._pm = new PluginManager({
|
||||||
app: this,
|
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>,
|
||||||
|
@ -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 },
|
||||||
});
|
});
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -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',
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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({
|
||||||
|
@ -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' });
|
||||||
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
@ -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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
@ -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';
|
||||||
|
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -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) {
|
||||||
|
22
yarn.lock
22
yarn.lock
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user