mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-12-01 19:58:15 +08:00
refactor: plugin manager (#775)
* feat: dynamic import plugin client * refactor: pm * chore: improve cli * feat: improve code * feat: update dependences * feat: hello plugin * fix: plugin.enabled * fix: test error * feat: improve code * feat: pm command * feat: add samples * fix: redirect * feat: transitions * feat: bookmark * feat: add pm script
This commit is contained in:
parent
12c3915a57
commit
f9f8dc78f4
@ -4,12 +4,13 @@
|
||||
# 步骤
|
||||
|
||||
Step 1: Start app
|
||||
yarn run:example plugins/custom-plugin start
|
||||
yarn run:example app/custom-plugin start
|
||||
|
||||
Step 2: View test list
|
||||
http://localhost:13000/api/test:list
|
||||
*/
|
||||
import { Application, Plugin } from '@nocobase/server';
|
||||
import { uid } from '@nocobase/utils';
|
||||
|
||||
const app = new Application({
|
||||
database: {
|
||||
@ -22,7 +23,7 @@ const app = new Application({
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT as any,
|
||||
timezone: process.env.DB_TIMEZONE,
|
||||
tablePrefix: process.env.DB_TABLE_PREFIX,
|
||||
tablePrefix: `t_${uid()}_`,
|
||||
},
|
||||
resourcer: {
|
||||
prefix: '/api',
|
||||
|
@ -10,6 +10,7 @@ Step 2:
|
||||
curl http://localhost:13000/api/test:list
|
||||
*/
|
||||
import { Application } from '@nocobase/server';
|
||||
import { uid } from '@nocobase/utils';
|
||||
|
||||
const app = new Application({
|
||||
database: {
|
||||
@ -22,7 +23,7 @@ const app = new Application({
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT as any,
|
||||
timezone: process.env.DB_TIMEZONE,
|
||||
tablePrefix: process.env.DB_TABLE_PREFIX,
|
||||
tablePrefix: `t_${uid()}_`,
|
||||
},
|
||||
resourcer: {
|
||||
prefix: '/api',
|
||||
@ -37,6 +38,7 @@ app.resource({
|
||||
async list(ctx, next) {
|
||||
ctx.body = 'test list';
|
||||
await next();
|
||||
process.stdout.write('rs');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -13,6 +13,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"nocobase": "nocobase",
|
||||
"pm": "nocobase pm",
|
||||
"dev": "nocobase dev",
|
||||
"start": "nocobase start",
|
||||
"build": "nocobase build",
|
||||
|
@ -10,6 +10,7 @@ export default defineConfig({
|
||||
define: {
|
||||
...umiConfig.define,
|
||||
},
|
||||
dynamicImportSyntax: {},
|
||||
// only proxy when using `umi dev`
|
||||
// if the assets are built, will not proxy
|
||||
proxy: {
|
||||
|
@ -4,12 +4,9 @@ export const app = new Application({
|
||||
apiClient: {
|
||||
baseURL: process.env.API_BASE_URL,
|
||||
},
|
||||
plugins: [
|
||||
require('@nocobase/plugin-china-region/client').default,
|
||||
require('@nocobase/plugin-export/client').default,
|
||||
require('@nocobase/plugin-audit-logs/client').default,
|
||||
require('@nocobase/plugin-workflow/client').default,
|
||||
],
|
||||
dynamicImport: (name: string) => {
|
||||
return import(`../plugins/${name}`);
|
||||
},
|
||||
});
|
||||
|
||||
export default app.render();
|
||||
|
1
packages/app/client/src/plugins/audit-logs.ts
Normal file
1
packages/app/client/src/plugins/audit-logs.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from '@nocobase/plugin-audit-logs/client';
|
1
packages/app/client/src/plugins/china-region.ts
Normal file
1
packages/app/client/src/plugins/china-region.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from '@nocobase/plugin-china-region/client';
|
1
packages/app/client/src/plugins/export.ts
Normal file
1
packages/app/client/src/plugins/export.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from '@nocobase/plugin-export/client';
|
1
packages/app/client/src/plugins/hello-sample.ts
Normal file
1
packages/app/client/src/plugins/hello-sample.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from '@nocobase/plugin-hello-sample/client';
|
1
packages/app/client/src/plugins/workflow.ts
Normal file
1
packages/app/client/src/plugins/workflow.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from '@nocobase/plugin-workflow/client';
|
@ -11,6 +11,7 @@
|
||||
"baseUrl": "./",
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"@nocobase/plugin-*-sample/client": ["../../samples/*/src/client"],
|
||||
"@nocobase/plugin-*/client": ["../../plugins/*/src/client"],
|
||||
"@nocobase/utils/client": ["../../core/utils/src/client"],
|
||||
"@nocobase/*": ["../../core/*/src/"],
|
||||
|
@ -1,5 +1,5 @@
|
||||
const { Command } = require('commander');
|
||||
const { run, isDev, promptForTs } = require('../util');
|
||||
const { run, isDev, isProd, promptForTs } = require('../util');
|
||||
|
||||
/**
|
||||
*
|
||||
@ -23,7 +23,7 @@ module.exports = (cli) => {
|
||||
`./packages/${APP_PACKAGE_ROOT}/server/src/index.ts`,
|
||||
...process.argv.slice(2),
|
||||
]);
|
||||
} else {
|
||||
} else if (isProd()) {
|
||||
run('node', [`./packages/${APP_PACKAGE_ROOT}/server/lib/index.js`, ...process.argv.slice(2)]);
|
||||
}
|
||||
});
|
||||
|
@ -30,11 +30,26 @@ exports.isDev = function isDev() {
|
||||
return exports.hasTsNode();
|
||||
};
|
||||
|
||||
const isProd = () => {
|
||||
const { APP_PACKAGE_ROOT } = process.env;
|
||||
const file = `./packages/${APP_PACKAGE_ROOT}/server/lib/index.js`;
|
||||
if (!existsSync(resolve(process.cwd(), file))) {
|
||||
console.log('For production environment, please build the code first.');
|
||||
console.log();
|
||||
console.log(chalk.yellow('$ yarn build'));
|
||||
console.log();
|
||||
process.exit(1);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
exports.isProd = isProd;
|
||||
|
||||
exports.nodeCheck = () => {
|
||||
if (!exports.hasTsNode()) {
|
||||
console.log('Please install all dependencies');
|
||||
console.log(chalk.yellow('$ yarn install'));
|
||||
process.exit(0);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
@ -100,8 +115,9 @@ exports.runInstall = async () => {
|
||||
'-s',
|
||||
];
|
||||
await exports.run('ts-node', argv);
|
||||
} else {
|
||||
const argv = [`./packages/${APP_PACKAGE_ROOT}/server/lib/index.js`, 'install', '-s'];
|
||||
} else if (isProd()) {
|
||||
const file = `./packages/${APP_PACKAGE_ROOT}/server/lib/index.js`;
|
||||
const argv = [file, 'install', '-s'];
|
||||
await exports.run('node', argv);
|
||||
}
|
||||
};
|
||||
@ -120,7 +136,7 @@ exports.runAppCommand = async (command, args = []) => {
|
||||
...args,
|
||||
];
|
||||
await exports.run('ts-node', argv);
|
||||
} else {
|
||||
} else if (isProd()) {
|
||||
const argv = [`./packages/${APP_PACKAGE_ROOT}/server/lib/index.js`, command, ...args];
|
||||
await exports.run('node', argv);
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { LockOutlined } from '@ant-design/icons';
|
||||
import { ISchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Card } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { PluginManager } from '../plugin-manager';
|
||||
import { ActionContext, SchemaComponent } from '../schema-component';
|
||||
import * as components from './Configuration';
|
||||
@ -24,7 +26,38 @@ const schema: ISchema = {
|
||||
},
|
||||
};
|
||||
|
||||
const schema2: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
'x-component': 'RoleTable',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ACLPane = () => {
|
||||
return (
|
||||
<Card bordered={false}>
|
||||
<SchemaComponent components={components} schema={schema2} />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const ACLShortcut = () => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
return (
|
||||
<PluginManager.Toolbar.Item
|
||||
icon={<LockOutlined />}
|
||||
title={t('Roles & Permissions')}
|
||||
onClick={() => {
|
||||
history.push('/admin/settings/acl/roles');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ACLShortcut2 = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Spin } from 'antd';
|
||||
import { i18n as i18next } from 'i18next';
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
import { ACLProvider, ACLShortcut } from '../acl';
|
||||
@ -11,6 +12,7 @@ import { RemoteDocumentTitleProvider } from '../document-title';
|
||||
import { FileStorageShortcut } from '../file-manager';
|
||||
import { i18n } from '../i18n';
|
||||
import { PluginManagerProvider } from '../plugin-manager';
|
||||
import PMProvider, { PluginManagerLink, SettingsCenterDropdown } from '../pm';
|
||||
import {
|
||||
AdminLayout,
|
||||
AuthLayout,
|
||||
@ -35,6 +37,7 @@ export interface ApplicationOptions {
|
||||
apiClient?: any;
|
||||
i18n?: any;
|
||||
plugins?: any[];
|
||||
dynamicImport?: any;
|
||||
}
|
||||
|
||||
export const getCurrentTimezone = () => {
|
||||
@ -45,14 +48,28 @@ export const getCurrentTimezone = () => {
|
||||
|
||||
export type PluginCallback = () => Promise<any>;
|
||||
|
||||
const App = React.memo((props: any) => {
|
||||
const C = compose(...props.providers)(() => {
|
||||
const routes = useRoutes();
|
||||
return (
|
||||
<div>
|
||||
<RouteSwitch routes={routes} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return <C />;
|
||||
});
|
||||
|
||||
export class Application {
|
||||
providers = [];
|
||||
mainComponent = null;
|
||||
apiClient: APIClient;
|
||||
i18n: i18next;
|
||||
plugins: PluginCallback[] = [];
|
||||
options: ApplicationOptions;
|
||||
|
||||
constructor(options: ApplicationOptions) {
|
||||
this.options = options;
|
||||
this.apiClient = new APIClient({
|
||||
baseURL: process.env.API_BASE_URL,
|
||||
headers: {
|
||||
@ -85,6 +102,8 @@ export class Application {
|
||||
SystemSettingsShortcut,
|
||||
SchemaTemplateShortcut,
|
||||
FileStorageShortcut,
|
||||
PluginManagerLink,
|
||||
SettingsCenterDropdown,
|
||||
},
|
||||
});
|
||||
this.use(SchemaComponentProvider, { components: { Link, NavLink } });
|
||||
@ -97,11 +116,7 @@ export class Application {
|
||||
this.use(AntdSchemaComponentProvider);
|
||||
this.use(ACLProvider);
|
||||
this.use(RemoteDocumentTitleProvider);
|
||||
|
||||
for (const plugin of options.plugins) {
|
||||
const [component, props] = Array.isArray(plugin) ? plugin : [plugin];
|
||||
this.use(component, props);
|
||||
}
|
||||
this.use(PMProvider);
|
||||
}
|
||||
|
||||
use(component, props?: any) {
|
||||
@ -120,16 +135,27 @@ export class Application {
|
||||
}
|
||||
|
||||
render() {
|
||||
return compose(...this.providers)(
|
||||
this.mainComponent ||
|
||||
(() => {
|
||||
const routes = useRoutes();
|
||||
return (
|
||||
<div>
|
||||
<RouteSwitch routes={routes} />
|
||||
</div>
|
||||
);
|
||||
}),
|
||||
);
|
||||
return (props: any) => {
|
||||
const { plugins = [], dynamicImport } = this.options;
|
||||
const [loading, setLoading] = useState(false);
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
(async () => {
|
||||
const res = await this.apiClient.request({ url: 'app:getPlugins' });
|
||||
if (Array.isArray(res.data?.data)) {
|
||||
plugins.push(...res.data.data);
|
||||
}
|
||||
for (const plugin of plugins) {
|
||||
const pluginModule = await dynamicImport(plugin);
|
||||
this.use(pluginModule.default);
|
||||
}
|
||||
setLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
return <App providers={this.providers} />;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { DatabaseOutlined } from '@ant-design/icons';
|
||||
import { ISchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Card } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { PluginManager } from '../plugin-manager';
|
||||
import { ActionContext, SchemaComponent } from '../schema-component';
|
||||
import { AddFieldAction, ConfigurationTable, EditFieldAction } from './Configuration';
|
||||
@ -23,7 +25,38 @@ const schema: ISchema = {
|
||||
},
|
||||
};
|
||||
|
||||
const schema2: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
'x-component': 'ConfigurationTable',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CollectionManagerPane = () => {
|
||||
return (
|
||||
<Card bordered={false}>
|
||||
<SchemaComponent schema={schema2} components={{ ConfigurationTable, AddFieldAction, EditFieldAction }} />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const CollectionManagerShortcut = () => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
return (
|
||||
<PluginManager.Toolbar.Item
|
||||
icon={<DatabaseOutlined />}
|
||||
title={t('Collections & Fields')}
|
||||
onClick={() => {
|
||||
history.push('/admin/settings/collection-manager/collections');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CollectionManagerShortcut2 = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
|
26
packages/core/client/src/file-manager/FileStorage.tsx
Normal file
26
packages/core/client/src/file-manager/FileStorage.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { uid } from '@formily/shared';
|
||||
import { Card } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SchemaComponent } from '../schema-component';
|
||||
import { storageSchema } from './schemas/storage';
|
||||
import { StorageOptions } from './StorageOptions';
|
||||
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
[uid()]: storageSchema,
|
||||
},
|
||||
};
|
||||
|
||||
export const FileStoragePane = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Card bordered={false}>
|
||||
<SchemaComponent components={{ StorageOptions }} schema={schema} />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// WZvC&6cR8@aAJu!
|
@ -2,6 +2,7 @@ import { FileOutlined } from '@ant-design/icons';
|
||||
import { uid } from '@formily/shared';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { PluginManager } from '..';
|
||||
import { ActionContext, SchemaComponent } from '../schema-component';
|
||||
import { storageSchema } from './schemas/storage';
|
||||
@ -22,6 +23,20 @@ const schema = {
|
||||
};
|
||||
|
||||
export const FileStorageShortcut = () => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
return (
|
||||
<PluginManager.Toolbar.Item
|
||||
icon={<FileOutlined />}
|
||||
title={t('File storages')}
|
||||
onClick={() => {
|
||||
history.push('/admin/settings/file-manager/storages');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const FileStorageShortcut2 = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
|
@ -1 +1,3 @@
|
||||
export * from './FileStorage';
|
||||
export * from './FileStorageShortcut';
|
||||
|
||||
|
@ -15,6 +15,7 @@ export * from './file-manager';
|
||||
export * from './i18n';
|
||||
export * from './icon';
|
||||
export * from './plugin-manager';
|
||||
export * from './pm';
|
||||
export * from './powered-by';
|
||||
export * from './record-provider';
|
||||
export * from './route-switch';
|
||||
@ -25,3 +26,4 @@ export * from './schema-templates';
|
||||
export * from './settings-form';
|
||||
export * from './system-settings';
|
||||
export * from './user';
|
||||
|
||||
|
@ -660,4 +660,16 @@ export default {
|
||||
"View all plugins": "查看所有插件",
|
||||
"Print": "打印",
|
||||
'Sign up successfully, and automatically jump to the sign in page': '注册成功,即将跳转到登录页面',
|
||||
'File manager': '文件管理器',
|
||||
'ACL': '访问控制',
|
||||
'Collection manager': '数据表管理',
|
||||
'Plugin manager': '插件管理器',
|
||||
'Local': '本地',
|
||||
'Built-in': '内置',
|
||||
'Marketplace': '插件市场',
|
||||
'Coming soon...': '敬请期待...',
|
||||
'Settings center': '配置中心',
|
||||
|
||||
'Bookmark': '书签',
|
||||
'Manage all settings': '管理所有配置',
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { AppstoreOutlined, EllipsisOutlined } from '@ant-design/icons';
|
||||
import { SettingOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { ConfigProvider, Menu, MenuItemProps, Spin, Tooltip } from 'antd';
|
||||
import { ConfigProvider, Menu, MenuItemProps, Tooltip } from 'antd';
|
||||
import cls from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAPIClient, useRequest } from '../api-client';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { PluginManagerContext } from './context';
|
||||
|
||||
export const usePrefixCls = (
|
||||
@ -20,7 +20,7 @@ export const usePrefixCls = (
|
||||
|
||||
type PluginManagerType = {
|
||||
Toolbar?: React.FC<ToolbarProps> & {
|
||||
Item?: React.FC<MenuItemProps & { selected?: boolean, subtitle?: string }>;
|
||||
Item?: React.FC<MenuItemProps & { selected?: boolean; subtitle?: string }>;
|
||||
};
|
||||
};
|
||||
|
||||
@ -55,6 +55,7 @@ PluginManager.Toolbar = (props: ToolbarProps) => {
|
||||
const { items = [] } = props;
|
||||
const [pinned, unpinned] = splitItems(items);
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
return (
|
||||
<div style={{ display: 'inline-block' }}>
|
||||
<Menu style={{ width: '100%' }} selectable={false} mode={'horizontal'} theme={'dark'}>
|
||||
@ -69,7 +70,7 @@ PluginManager.Toolbar = (props: ToolbarProps) => {
|
||||
);
|
||||
})}
|
||||
{unpinned.length > 0 && (
|
||||
<Menu.SubMenu popupClassName={'pm-sub-menu'} key={'more'} title={<EllipsisOutlined />}>
|
||||
<Menu.SubMenu popupClassName={'pm-sub-menu'} key={'more'} title={<SettingOutlined />}>
|
||||
{unpinned.map((item, index) => {
|
||||
const Action = get(components, item.component);
|
||||
return (
|
||||
@ -81,8 +82,14 @@ PluginManager.Toolbar = (props: ToolbarProps) => {
|
||||
);
|
||||
})}
|
||||
{unpinned.length > 0 && <Menu.Divider key={'divider'}></Menu.Divider>}
|
||||
<Menu.Item key={'plugins'} disabled icon={<AppstoreOutlined />}>
|
||||
{t('View all plugins')}
|
||||
<Menu.Item
|
||||
key={'plugins'}
|
||||
onClick={() => {
|
||||
history.push('/admin/settings');
|
||||
}}
|
||||
icon={<SettingOutlined />}
|
||||
>
|
||||
{t('Settings center')}
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
)}
|
||||
@ -97,29 +104,34 @@ PluginManager.Toolbar.Item = (props) => {
|
||||
const prefix = usePrefixCls();
|
||||
const className = cls({ [`${prefix}-menu-item-selected`]: selected });
|
||||
if (item.pin) {
|
||||
|
||||
const subtitleComponent = subtitle && (
|
||||
<div
|
||||
className={css`
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
`}
|
||||
>{subtitle}</div>
|
||||
)
|
||||
>
|
||||
{subtitle}
|
||||
</div>
|
||||
);
|
||||
|
||||
const titleComponent = (
|
||||
<div>
|
||||
<div>{title}</div>
|
||||
{subtitleComponent}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
return title ? (
|
||||
<Tooltip title={titleComponent}>
|
||||
<Menu.Item {...others} className={className} eventKey={item.component}>
|
||||
{icon}
|
||||
</Menu.Item>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Menu.Item {...others} className={className} eventKey={item.component}>
|
||||
{icon}
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
@ -130,13 +142,19 @@ PluginManager.Toolbar.Item = (props) => {
|
||||
};
|
||||
|
||||
export const RemotePluginManagerToolbar = () => {
|
||||
const api = useAPIClient();
|
||||
const { data, loading } = useRequest({
|
||||
resource: 'plugins',
|
||||
action: 'getPinned',
|
||||
});
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
return <PluginManager.Toolbar items={data?.data} />;
|
||||
// const api = useAPIClient();
|
||||
// const { data, loading } = useRequest({
|
||||
// resource: 'plugins',
|
||||
// action: 'getPinned',
|
||||
// });
|
||||
// if (loading) {
|
||||
// return <Spin />;
|
||||
// }
|
||||
const items = [
|
||||
{ component: 'DesignableSwitch', pin: true },
|
||||
{ component: 'PluginManagerLink', pin: true },
|
||||
{ component: 'SettingsCenterDropdown', pin: true },
|
||||
// ...data?.data,
|
||||
];
|
||||
return <PluginManager.Toolbar items={items} />;
|
||||
};
|
||||
|
106
packages/core/client/src/pm/PluginManagerLink.tsx
Normal file
106
packages/core/client/src/pm/PluginManagerLink.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import { AppstoreAddOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { ISchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Dropdown, Menu } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { PluginManager } from '../plugin-manager';
|
||||
import { ActionContext } from '../schema-component';
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
'x-component': 'Action.Drawer',
|
||||
type: 'void',
|
||||
title: '{{t("Collections & Fields")}}',
|
||||
properties: {
|
||||
configuration: {
|
||||
'x-component': 'ConfigurationTable',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const PluginManagerLink = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
return (
|
||||
<ActionContext.Provider value={{ visible, setVisible }}>
|
||||
<PluginManager.Toolbar.Item
|
||||
icon={<AppstoreAddOutlined />}
|
||||
title={t('Plugin manager')}
|
||||
onClick={() => {
|
||||
history.push('/admin/plugins');
|
||||
}}
|
||||
/>
|
||||
</ActionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const SettingsCenterDropdown = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const items = [
|
||||
{
|
||||
title: t('Collections & Fields'),
|
||||
path: 'collection-manager/collections',
|
||||
},
|
||||
{
|
||||
title: t('Roles & Permissions'),
|
||||
path: 'acl/roles',
|
||||
},
|
||||
{
|
||||
title: t('File storages'),
|
||||
path: 'file-manager/storages',
|
||||
},
|
||||
{
|
||||
title: t('System settings'),
|
||||
path: 'system-settings/system-settings',
|
||||
},
|
||||
{
|
||||
title: t('Workflow'),
|
||||
path: 'workflow/workflows',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<ActionContext.Provider value={{ visible, setVisible }}>
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.ItemGroup title={t('Bookmark')}>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
history.push('/admin/settings/' + item.path);
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu.ItemGroup>
|
||||
<Menu.Divider></Menu.Divider>
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
history.push('/admin/settings');
|
||||
}}
|
||||
>
|
||||
{t('Settings center')}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<PluginManager.Toolbar.Item
|
||||
icon={<SettingOutlined />}
|
||||
// title={t('Settings center')}
|
||||
></PluginManager.Toolbar.Item>
|
||||
</Dropdown>
|
||||
</ActionContext.Provider>
|
||||
);
|
||||
};
|
384
packages/core/client/src/pm/index.tsx
Normal file
384
packages/core/client/src/pm/index.tsx
Normal file
@ -0,0 +1,384 @@
|
||||
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { Avatar, Card, Layout, Menu, message, PageHeader, Popconfirm, Spin, Switch, Tabs } from 'antd';
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Redirect, useHistory, useRouteMatch } from 'react-router-dom';
|
||||
import { ACLPane } from '../acl';
|
||||
import { useAPIClient, useRequest } from '../api-client';
|
||||
import { CollectionManagerPane } from '../collection-manager';
|
||||
import { useDocumentTitle } from '../document-title';
|
||||
import { FileStoragePane } from '../file-manager';
|
||||
import { Icon } from '../icon';
|
||||
import { RouteSwitchContext } from '../route-switch';
|
||||
import { useCompile } from '../schema-component';
|
||||
import { BlockTemplatesPane } from '../schema-templates';
|
||||
import { SystemSettingsPane } from '../system-settings';
|
||||
|
||||
const SettingsCenterContext = createContext<any>({});
|
||||
|
||||
const PluginCard = (props) => {
|
||||
const history = useHistory<any>();
|
||||
const { data = {} } = props;
|
||||
const api = useAPIClient();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{ width: 'calc(20% - 24px)', marginRight: 24, marginBottom: 24 }}
|
||||
actions={[
|
||||
data.enabled ? (
|
||||
<SettingOutlined
|
||||
onClick={() => {
|
||||
history.push(`/admin/settings/${data.name}`);
|
||||
}}
|
||||
/>
|
||||
) : null,
|
||||
<Popconfirm
|
||||
title={t('Are you sure to delete this plugin?')}
|
||||
onConfirm={async () => {
|
||||
await api.request({
|
||||
url: `pm:remove/${data.name}`,
|
||||
});
|
||||
message.success(t('插件删除成功'));
|
||||
window.location.reload();
|
||||
}}
|
||||
onCancel={() => {}}
|
||||
okText={t('Yes')}
|
||||
cancelText={t('No')}
|
||||
>
|
||||
<DeleteOutlined />
|
||||
</Popconfirm>,
|
||||
<Switch
|
||||
size={'small'}
|
||||
onChange={async (checked) => {
|
||||
await api.request({
|
||||
url: `pm:${checked ? 'enable' : 'disable'}/${data.name}`,
|
||||
});
|
||||
message.success(checked ? t('插件激活成功') : t('插件禁用成功'));
|
||||
window.location.reload();
|
||||
}}
|
||||
defaultChecked={data.enabled}
|
||||
></Switch>,
|
||||
].filter(Boolean)}
|
||||
>
|
||||
<Card.Meta
|
||||
className={css`
|
||||
.ant-card-meta-avatar {
|
||||
margin-top: 8px;
|
||||
.ant-avatar {
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
`}
|
||||
avatar={<Avatar />}
|
||||
description={data.description}
|
||||
title={
|
||||
<span>
|
||||
{data.name}
|
||||
<span
|
||||
className={css`
|
||||
display: block;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
// margin-left: 8px;
|
||||
`}
|
||||
>
|
||||
{data.version}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const BuiltInPluginCard = (props) => {
|
||||
const { data } = props;
|
||||
return (
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{ width: 'calc(20% - 24px)', marginRight: 24, marginBottom: 24 }}
|
||||
// actions={[<a>Settings</a>, <a>Remove</a>, <Switch size={'small'} defaultChecked={true}></Switch>]}
|
||||
>
|
||||
<Card.Meta
|
||||
className={css`
|
||||
.ant-card-meta-avatar {
|
||||
margin-top: 8px;
|
||||
.ant-avatar {
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
`}
|
||||
avatar={<Avatar />}
|
||||
description={data.description}
|
||||
title={
|
||||
<span>
|
||||
{data.name}
|
||||
<span
|
||||
className={css`
|
||||
display: block;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
// margin-left: 8px;
|
||||
`}
|
||||
>
|
||||
{data.version}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const LocalPlugins = () => {
|
||||
const { data, loading } = useRequest({
|
||||
url: 'applicationPlugins:list',
|
||||
params: {
|
||||
filter: {
|
||||
'builtIn.$isFalsy': true,
|
||||
},
|
||||
sort: 'name',
|
||||
},
|
||||
});
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{data?.data?.map((item) => {
|
||||
return <PluginCard data={item} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const BuiltinPlugins = () => {
|
||||
const { data, loading } = useRequest({
|
||||
url: 'applicationPlugins:list',
|
||||
params: {
|
||||
filter: {
|
||||
'builtIn.$isTruly': true,
|
||||
},
|
||||
sort: 'name',
|
||||
},
|
||||
});
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{data?.data?.map((item) => {
|
||||
return <BuiltInPluginCard data={item} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const MarketplacePlugins = () => {
|
||||
const { t } = useTranslation();
|
||||
return <div style={{ fontSize: 18 }}>{t('Coming soon...')}</div>;
|
||||
};
|
||||
|
||||
const PluginList = (props) => {
|
||||
const match = useRouteMatch<any>();
|
||||
const history = useHistory<any>();
|
||||
const { tabName = 'local' } = match.params || {};
|
||||
const { setTitle } = useDocumentTitle();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
ghost={false}
|
||||
title={t('Plugin manager')}
|
||||
footer={
|
||||
<Tabs
|
||||
activeKey={tabName}
|
||||
onChange={(activeKey) => {
|
||||
history.push(`/admin/plugins/${activeKey}`);
|
||||
}}
|
||||
>
|
||||
<Tabs.TabPane tab={t('Local')} key={'local'} />
|
||||
<Tabs.TabPane tab={t('Built-in')} key={'built-in'} />
|
||||
<Tabs.TabPane tab={t('Marketplace')} key={'marketplace'} />
|
||||
</Tabs>
|
||||
}
|
||||
/>
|
||||
<div style={{ margin: 24, display: 'flex', flexFlow: 'row wrap' }}>
|
||||
{React.createElement(
|
||||
{
|
||||
local: LocalPlugins,
|
||||
'built-in': BuiltinPlugins,
|
||||
marketplace: MarketplacePlugins,
|
||||
}[tabName],
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const settings = {
|
||||
acl: {
|
||||
title: '{{t("ACL")}}',
|
||||
icon: 'LockOutlined',
|
||||
tabs: {
|
||||
roles: {
|
||||
title: '{{t("Roles & Permissions")}}',
|
||||
component: ACLPane,
|
||||
},
|
||||
},
|
||||
},
|
||||
'block-templates': {
|
||||
title: '{{t("Block templates")}}',
|
||||
icon: 'LayoutOutlined',
|
||||
tabs: {
|
||||
list: {
|
||||
title: '{{t("Block templates")}}',
|
||||
component: BlockTemplatesPane,
|
||||
},
|
||||
},
|
||||
},
|
||||
'collection-manager': {
|
||||
icon: 'DatabaseOutlined',
|
||||
title: '{{t("Collection manager")}}',
|
||||
tabs: {
|
||||
collections: {
|
||||
title: '{{t("Collections & Fields")}}',
|
||||
component: CollectionManagerPane,
|
||||
},
|
||||
},
|
||||
},
|
||||
'file-manager': {
|
||||
title: '{{t("File manager")}}',
|
||||
icon: 'FileOutlined',
|
||||
tabs: {
|
||||
storages: {
|
||||
title: '{{t("File storages")}}',
|
||||
component: FileStoragePane,
|
||||
},
|
||||
// test: {
|
||||
// title: 'Test',
|
||||
// component: FileStoragePane,
|
||||
// },
|
||||
},
|
||||
},
|
||||
'system-settings': {
|
||||
icon: 'SettingOutlined',
|
||||
title: '{{t("System settings")}}',
|
||||
tabs: {
|
||||
'system-settings': {
|
||||
title: '{{t("System settings")}}',
|
||||
component: SystemSettingsPane,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const SettingsCenter = (props) => {
|
||||
const match = useRouteMatch<any>();
|
||||
const history = useHistory<any>();
|
||||
const items = useContext(SettingsCenterContext);
|
||||
const compile = useCompile();
|
||||
const firstUri = useMemo(() => {
|
||||
const keys = Object.keys(items).sort();
|
||||
const pluginName = keys.shift();
|
||||
const tabName = Object.keys(items?.[pluginName]?.tabs || {}).shift();
|
||||
return `/admin/settings/${pluginName}/${tabName}`;
|
||||
}, [items]);
|
||||
const { pluginName, tabName } = match.params || {};
|
||||
if (!pluginName) {
|
||||
return <Redirect to={firstUri} />;
|
||||
}
|
||||
if (!items[pluginName]) {
|
||||
return <Redirect to={firstUri} />;
|
||||
}
|
||||
if (!tabName) {
|
||||
const firstTabName = Object.keys(items[pluginName]?.tabs).shift();
|
||||
return <Redirect to={`/admin/settings/${pluginName}/${firstTabName}`} />;
|
||||
}
|
||||
const component = items[pluginName]?.tabs?.[tabName]?.component;
|
||||
return (
|
||||
<div>
|
||||
<Layout>
|
||||
<Layout.Sider theme={'light'}>
|
||||
<Menu selectedKeys={[pluginName]} style={{ height: 'calc(100vh - 46px)' }}>
|
||||
{Object.keys(items)
|
||||
.sort()
|
||||
.map((key) => {
|
||||
const item = items[key];
|
||||
const tabKey = Object.keys(item.tabs).shift();
|
||||
return (
|
||||
<Menu.Item
|
||||
key={key}
|
||||
icon={item.icon ? <Icon type={item.icon} /> : null}
|
||||
onClick={() => {
|
||||
history.push(`/admin/settings/${key}/${tabKey}`);
|
||||
}}
|
||||
>
|
||||
{compile(item.title)}
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
</Layout.Sider>
|
||||
<Layout.Content>
|
||||
<PageHeader
|
||||
ghost={false}
|
||||
title={compile(items[pluginName]?.title)}
|
||||
footer={
|
||||
<Tabs
|
||||
activeKey={tabName}
|
||||
onChange={(activeKey) => {
|
||||
history.push(`/admin/settings/${pluginName}/${activeKey}`);
|
||||
}}
|
||||
>
|
||||
{Object.keys(items[pluginName]?.tabs).map((tabKey) => {
|
||||
const tab = items[pluginName].tabs?.[tabKey];
|
||||
return <Tabs.TabPane tab={compile(tab?.title)} key={tabKey} />;
|
||||
})}
|
||||
</Tabs>
|
||||
}
|
||||
/>
|
||||
<div style={{ margin: 24 }}>{component && React.createElement(component)}</div>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SettingsCenterProvider = (props) => {
|
||||
const { settings = {} } = props;
|
||||
const items = useContext(SettingsCenterContext);
|
||||
return (
|
||||
<SettingsCenterContext.Provider value={{ ...items, ...settings }}>{props.children}</SettingsCenterContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const PMProvider = (props) => {
|
||||
const { routes, ...others } = useContext(RouteSwitchContext);
|
||||
routes[1].routes.unshift(
|
||||
{
|
||||
type: 'route',
|
||||
path: '/admin/plugins/:tabName?',
|
||||
component: PluginList,
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/admin/settings/:pluginName?/:tabName?',
|
||||
component: SettingsCenter,
|
||||
},
|
||||
);
|
||||
return (
|
||||
<SettingsCenterProvider settings={settings}>
|
||||
<RouteSwitchContext.Provider value={{ ...others, routes }}>{props.children}</RouteSwitchContext.Provider>
|
||||
</SettingsCenterProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default PMProvider;
|
||||
|
||||
export * from './PluginManagerLink';
|
@ -19,3 +19,11 @@ export const BlockTemplatePage = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const BlockTemplatesPane = () => {
|
||||
return (
|
||||
<CollectionManagerProvider collections={[uiSchemaTemplatesCollection]}>
|
||||
<SchemaComponent schema={uiSchemaTemplatesSchema} />
|
||||
</CollectionManagerProvider>
|
||||
);
|
||||
};
|
||||
|
@ -12,7 +12,7 @@ export const SchemaTemplateShortcut = () => {
|
||||
icon={<LayoutOutlined />}
|
||||
title={t('Block templates')}
|
||||
onClick={() => {
|
||||
history.push('/admin/plugins/block-templates');
|
||||
history.push('/admin/settings/block-templates/list');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { SettingOutlined } from '@ant-design/icons';
|
||||
import { ISchema, useForm } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Card, message } from 'antd';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useSystemSettings } from '.';
|
||||
import { i18n, PluginManager, useAPIClient, useRequest } from '..';
|
||||
import locale from '../locale';
|
||||
@ -39,6 +41,7 @@ const useSaveSystemSettingsValues = () => {
|
||||
const form = useForm();
|
||||
const { mutate, data } = useSystemSettings();
|
||||
const api = useAPIClient();
|
||||
const { t } = useTranslation();
|
||||
return {
|
||||
async run() {
|
||||
await form.submit();
|
||||
@ -54,6 +57,7 @@ const useSaveSystemSettingsValues = () => {
|
||||
method: 'post',
|
||||
data: values,
|
||||
});
|
||||
message.success(t('Saved successfully'));
|
||||
const lang = values.enabledLanguages?.[0] || 'en-US';
|
||||
if (values.enabledLanguages.length < 2 && api.auth.getLocale() !== lang) {
|
||||
api.auth.setLocale('');
|
||||
@ -135,13 +139,6 @@ const schema: ISchema = {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer.Footer',
|
||||
properties: {
|
||||
cancel: {
|
||||
title: 'Cancel',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ useCloseAction }}',
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
title: 'Submit',
|
||||
'x-component': 'Action',
|
||||
@ -151,6 +148,13 @@ const schema: ISchema = {
|
||||
useAction: '{{ useSaveSystemSettingsValues }}',
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
title: 'Cancel',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ useCloseAction }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -158,7 +162,128 @@ const schema: ISchema = {
|
||||
},
|
||||
};
|
||||
|
||||
const schema2: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
'x-decorator': 'Form',
|
||||
'x-decorator-props': {
|
||||
useValues: '{{ useSystemSettingsValues }}',
|
||||
},
|
||||
'x-component': 'div',
|
||||
type: 'void',
|
||||
title: '{{t("System settings")}}',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
title: "{{t('System title')}}",
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
},
|
||||
logo: {
|
||||
type: 'string',
|
||||
title: "{{t('Logo')}}",
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Upload.Attachment',
|
||||
'x-component-props': {
|
||||
action: 'attachments:upload',
|
||||
multiple: false,
|
||||
// accept: 'jpg,png'
|
||||
},
|
||||
},
|
||||
enabledLanguages: {
|
||||
type: 'array',
|
||||
title: '{{t("Enabled languages")}}',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
mode: 'multiple',
|
||||
},
|
||||
'x-decorator': 'FormItem',
|
||||
enum: langs,
|
||||
'x-reactions': (field) => {
|
||||
field.dataSource = langs.map((item) => {
|
||||
let label = item.label;
|
||||
if (field.value?.[0] === item.value) {
|
||||
label += `(${i18n.t('Default')})`;
|
||||
}
|
||||
return {
|
||||
label,
|
||||
value: item.value,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
allowSignUp: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
'x-content': '{{t("Allow sign up")}}',
|
||||
'x-component': 'Checkbox',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
smsAuthEnabled: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
'x-content': '{{t("Enable SMS authentication")}}',
|
||||
'x-component': 'Checkbox',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
footer1: {
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
layout: 'one-column',
|
||||
},
|
||||
properties: {
|
||||
submit: {
|
||||
title: 'Submit',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
htmlType: 'submit',
|
||||
useAction: '{{ useSaveSystemSettingsValues }}',
|
||||
},
|
||||
},
|
||||
// cancel: {
|
||||
// title: 'Cancel',
|
||||
// 'x-component': 'Action',
|
||||
// 'x-component-props': {
|
||||
// useAction: '{{ useCloseAction }}',
|
||||
// },
|
||||
// },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const SystemSettingsPane = () => {
|
||||
return (
|
||||
<Card bordered={false}>
|
||||
<SchemaComponent
|
||||
scope={{ useSaveSystemSettingsValues, useSystemSettingsValues, useCloseAction }}
|
||||
schema={schema2}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const SystemSettingsShortcut = () => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
return (
|
||||
<PluginManager.Toolbar.Item
|
||||
icon={<SettingOutlined />}
|
||||
title={t('System settings')}
|
||||
onClick={() => {
|
||||
history.push('/admin/settings/system-settings/system-settings');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SystemSettingsShortcut2 = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
|
@ -6,6 +6,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"nocobase": "nocobase",
|
||||
"pm": "nocobase pm",
|
||||
"dev": "nocobase dev",
|
||||
"start": "nocobase start",
|
||||
"clean": "nocobase clean",
|
||||
|
@ -65,6 +65,14 @@ function resolveNocobasePackagesAlias(config) {
|
||||
config.resolve.alias.set(`@nocobase/plugin-${package}`, packageSrc);
|
||||
}
|
||||
}
|
||||
const samples = fs.readdirSync(resolve(process.cwd(), './packages/samples'));
|
||||
for (const package of samples) {
|
||||
const packageSrc = resolve(process.cwd(), './packages/samples/', package, 'src');
|
||||
if (existsSync(packageSrc)) {
|
||||
config.module.rules.get('ts-in-node_modules').include.add(packageSrc);
|
||||
config.resolve.alias.set(`@nocobase/plugin-${package}-sample`, packageSrc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.getUmiConfig = getUmiConfig;
|
||||
|
@ -208,6 +208,10 @@ export class Resourcer {
|
||||
return this.resources.has(name);
|
||||
}
|
||||
|
||||
removeResource(name) {
|
||||
return this.resources.delete(name);
|
||||
}
|
||||
|
||||
registerAction(name: ActionName, handler: HandlerType) {
|
||||
this.registerActionHandler(name, handler);
|
||||
}
|
||||
|
@ -128,21 +128,21 @@ export class ApplicationVersion {
|
||||
}
|
||||
|
||||
export class Application<StateT = DefaultState, ContextT = DefaultContext> extends Koa implements AsyncEmitter {
|
||||
public readonly db: Database;
|
||||
protected _db: Database;
|
||||
|
||||
public readonly resourcer: Resourcer;
|
||||
protected _resourcer: Resourcer;
|
||||
|
||||
public readonly cli: Command;
|
||||
protected _cli: Command;
|
||||
|
||||
public readonly i18n: i18n;
|
||||
protected _i18n: i18n;
|
||||
|
||||
public readonly pm: PluginManager;
|
||||
protected _pm: PluginManager;
|
||||
|
||||
public readonly acl: ACL;
|
||||
protected _acl: ACL;
|
||||
|
||||
public readonly appManager: AppManager;
|
||||
protected _appManager: AppManager;
|
||||
|
||||
public readonly version: ApplicationVersion;
|
||||
protected _version: ApplicationVersion;
|
||||
|
||||
protected plugins = new Map<string, Plugin>();
|
||||
|
||||
@ -150,18 +150,61 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
|
||||
constructor(public options: ApplicationOptions) {
|
||||
super();
|
||||
this.init();
|
||||
}
|
||||
|
||||
this.acl = createACL();
|
||||
this.db = this.createDatabase(options);
|
||||
this.resourcer = createResourcer(options);
|
||||
this.cli = new Command('nocobase').usage('[command] [options]');
|
||||
this.i18n = createI18n(options);
|
||||
get db() {
|
||||
return this._db;
|
||||
}
|
||||
|
||||
this.pm = new PluginManager({
|
||||
get resourcer() {
|
||||
return this._resourcer;
|
||||
}
|
||||
|
||||
get cli() {
|
||||
return this._cli;
|
||||
}
|
||||
|
||||
get acl() {
|
||||
return this._acl;
|
||||
}
|
||||
|
||||
get i18n() {
|
||||
return this._i18n;
|
||||
}
|
||||
|
||||
get pm() {
|
||||
return this._pm;
|
||||
}
|
||||
|
||||
get version() {
|
||||
return this._version;
|
||||
}
|
||||
|
||||
get appManager() {
|
||||
return this._appManager;
|
||||
}
|
||||
|
||||
protected init() {
|
||||
const options = this.options;
|
||||
// @ts-ignore
|
||||
this._events = [];
|
||||
// @ts-ignore
|
||||
this._eventsCount = [];
|
||||
this.middleware = [];
|
||||
// this.context = Object.create(context);
|
||||
this.plugins = new Map<string, Plugin>();
|
||||
this._acl = createACL();
|
||||
this._db = this.createDatabase(options);
|
||||
this._resourcer = createResourcer(options);
|
||||
this._cli = new Command('nocobase').usage('[command] [options]');
|
||||
this._i18n = createI18n(options);
|
||||
|
||||
this._pm = new PluginManager({
|
||||
app: this,
|
||||
});
|
||||
|
||||
this.appManager = new AppManager(this);
|
||||
this._appManager = new AppManager(this);
|
||||
|
||||
registerMiddlewares(this, options);
|
||||
|
||||
@ -173,7 +216,7 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
|
||||
registerCli(this);
|
||||
|
||||
this.version = new ApplicationVersion(this);
|
||||
this._version = new ApplicationVersion(this);
|
||||
}
|
||||
|
||||
private createDatabase(options: ApplicationOptions) {
|
||||
@ -242,6 +285,11 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
await this.pm.load();
|
||||
}
|
||||
|
||||
async reload() {
|
||||
this.init();
|
||||
await this.pm.load();
|
||||
}
|
||||
|
||||
getPlugin<P extends Plugin>(name: string) {
|
||||
return this.pm.get(name) as P;
|
||||
}
|
||||
@ -250,8 +298,10 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
return this.runAsCLI(argv);
|
||||
}
|
||||
|
||||
async runAsCLI(argv?: readonly string[], options?: ParseOptions) {
|
||||
await this.load();
|
||||
async runAsCLI(argv = process.argv, options?: ParseOptions) {
|
||||
if (argv?.[2] !== 'install') {
|
||||
await this.load();
|
||||
}
|
||||
return this.cli.parseAsync(argv, options);
|
||||
}
|
||||
|
||||
@ -328,8 +378,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
}
|
||||
|
||||
async install(options: InstallOptions = {}) {
|
||||
await this.emitAsync('beforeInstall', this, options);
|
||||
|
||||
const r = await this.db.version.satisfies({
|
||||
mysql: '>=8.0.17',
|
||||
sqlite: '3.x',
|
||||
@ -345,6 +393,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
await this.db.clean(isBoolean(options.clean) ? { drop: options.clean } : options.clean);
|
||||
}
|
||||
|
||||
await this.emitAsync('beforeInstall', this, options);
|
||||
|
||||
await this.load();
|
||||
await this.db.sync(options?.sync);
|
||||
await this.pm.install(options);
|
||||
await this.version.update();
|
||||
|
@ -9,6 +9,7 @@ export function registerCli(app: Application) {
|
||||
require('./migrator').default(app);
|
||||
require('./start').default(app);
|
||||
require('./upgrade').default(app);
|
||||
require('./pm').default(app);
|
||||
|
||||
// development only with @nocobase/cli
|
||||
app.command('build').argument('[packages...]');
|
||||
|
89
packages/core/server/src/commands/pm.ts
Normal file
89
packages/core/server/src/commands/pm.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import axios from 'axios';
|
||||
import { resolve } from 'path';
|
||||
import Application from '../application';
|
||||
|
||||
export default (app: Application) => {
|
||||
app
|
||||
.command('pm')
|
||||
.argument('<method>')
|
||||
.arguments('<plugins...>')
|
||||
.action(async (method, plugins, options, ...args) => {
|
||||
const { APP_PORT, API_BASE_PATH = '/api/', API_BASE_URL } = process.env;
|
||||
const baseURL = API_BASE_URL || `http://localhost:${APP_PORT}${API_BASE_PATH}`;
|
||||
let started = true;
|
||||
try {
|
||||
await axios.get(`${baseURL}app:getLang`);
|
||||
} catch (error) {
|
||||
started = false;
|
||||
}
|
||||
const pm = {
|
||||
async create() {
|
||||
const name = plugins[0];
|
||||
const { PluginGenerator } = require('@nocobase/cli/src/plugin-generator');
|
||||
const generator = new PluginGenerator({
|
||||
cwd: resolve(process.cwd(), name),
|
||||
args: options,
|
||||
context: {
|
||||
name,
|
||||
},
|
||||
});
|
||||
await generator.run();
|
||||
},
|
||||
async add() {
|
||||
if (started) {
|
||||
const res = await axios.get(`${baseURL}pm:add/${plugins.join(',')}`);
|
||||
console.log(res.data);
|
||||
return;
|
||||
}
|
||||
await app.pm.add(plugins);
|
||||
},
|
||||
async enable() {
|
||||
if (started) {
|
||||
const res = await axios.get(`${baseURL}pm:enable/${plugins.join(',')}`);
|
||||
console.log(res.data);
|
||||
return;
|
||||
}
|
||||
const repository = app.db.getRepository('applicationPlugins');
|
||||
await repository.update({
|
||||
filter: {
|
||||
'name.$in': plugins,
|
||||
},
|
||||
values: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
async disable() {
|
||||
if (started) {
|
||||
const res = await axios.get(`${baseURL}pm:disable/${plugins.join(',')}`);
|
||||
console.log(res.data);
|
||||
return;
|
||||
}
|
||||
const repository = app.db.getRepository('applicationPlugins');
|
||||
await repository.update({
|
||||
filter: {
|
||||
'name.$in': plugins,
|
||||
},
|
||||
values: {
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
},
|
||||
async remove() {
|
||||
if (started) {
|
||||
const res = await axios.get(`${baseURL}pm:disable/${plugins.join(',')}`);
|
||||
console.log(res.data);
|
||||
return;
|
||||
}
|
||||
const repository = app.db.getRepository('applicationPlugins');
|
||||
await repository.destroy({
|
||||
filter: {
|
||||
'name.$in': plugins,
|
||||
},
|
||||
});
|
||||
plugins.map((name) => app.pm.remove(name));
|
||||
},
|
||||
};
|
||||
await pm[method]();
|
||||
});
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { CleanOptions, SyncOptions } from '@nocobase/database';
|
||||
import { CleanOptions, Collection, Model, Repository, SyncOptions } from '@nocobase/database';
|
||||
import Application from './application';
|
||||
import { Plugin } from './plugin';
|
||||
|
||||
@ -12,14 +12,158 @@ export interface InstallOptions {
|
||||
sync?: SyncOptions;
|
||||
}
|
||||
|
||||
type PluginConstructor<P, O = any> = { new(app: Application, options: O): P };
|
||||
class PluginManagerRepository extends Repository {
|
||||
getInstance(): Plugin {
|
||||
return;
|
||||
}
|
||||
async add(name: string | string[], options) {}
|
||||
async enable(name: string | string[], options) {}
|
||||
async disable(name: string | string[], options) {}
|
||||
async remove(name: string | string[], options) {}
|
||||
async upgrade(name: string | string[], options) {}
|
||||
}
|
||||
|
||||
export class PluginManager {
|
||||
app: Application;
|
||||
protected plugins = new Map<string, Plugin>();
|
||||
collection: Collection;
|
||||
repository: PluginManagerRepository;
|
||||
plugins = new Map<string, Plugin>();
|
||||
|
||||
constructor(options: PluginManagerOptions) {
|
||||
this.app = options.app;
|
||||
this.collection = this.app.db.collection({
|
||||
name: 'applicationPlugins',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name', unique: true },
|
||||
{ type: 'string', name: 'version' },
|
||||
{ type: 'boolean', name: 'enabled' },
|
||||
{ type: 'boolean', name: 'builtIn' },
|
||||
{ type: 'json', name: 'options' },
|
||||
],
|
||||
});
|
||||
const app = this.app;
|
||||
const pm = this;
|
||||
this.repository = this.collection.repository as PluginManagerRepository;
|
||||
this.app.resourcer.define({
|
||||
name: 'pm',
|
||||
actions: {
|
||||
async add(ctx, next) {
|
||||
const { filterByTk } = ctx.action.params;
|
||||
if (!filterByTk) {
|
||||
ctx.throw(400, 'null');
|
||||
}
|
||||
await pm.add(filterByTk);
|
||||
ctx.body = filterByTk;
|
||||
await next();
|
||||
},
|
||||
async enable(ctx, next) {
|
||||
const { filterByTk } = ctx.action.params;
|
||||
if (!filterByTk) {
|
||||
ctx.throw(400, 'filterByTk invalid');
|
||||
}
|
||||
const name = pm.getPackageName(filterByTk);
|
||||
const plugin = pm.get(name);
|
||||
if (plugin.model) {
|
||||
plugin.model.set('enabled', true);
|
||||
await plugin.model.save();
|
||||
}
|
||||
if (!plugin) {
|
||||
ctx.throw(400, 'plugin invalid');
|
||||
}
|
||||
await app.reload();
|
||||
await app.start();
|
||||
ctx.body = 'ok';
|
||||
await next();
|
||||
},
|
||||
async disable(ctx, next) {
|
||||
const { filterByTk } = ctx.action.params;
|
||||
if (!filterByTk) {
|
||||
ctx.throw(400, 'filterByTk invalid');
|
||||
}
|
||||
const name = pm.getPackageName(filterByTk);
|
||||
const plugin = pm.get(name);
|
||||
if (plugin.model) {
|
||||
plugin.model.set('enabled', false);
|
||||
await plugin.model.save();
|
||||
}
|
||||
if (!plugin) {
|
||||
ctx.throw(400, 'plugin invalid');
|
||||
}
|
||||
await app.reload();
|
||||
await app.start();
|
||||
ctx.body = 'ok';
|
||||
await next();
|
||||
},
|
||||
async upgrade(ctx, next) {
|
||||
ctx.body = 'ok';
|
||||
await next();
|
||||
},
|
||||
async remove(ctx, next) {
|
||||
const { filterByTk } = ctx.action.params;
|
||||
if (!filterByTk) {
|
||||
ctx.throw(400, 'filterByTk invalid');
|
||||
}
|
||||
const name = pm.getPackageName(filterByTk);
|
||||
const plugin = pm.get(name);
|
||||
if (plugin.model) {
|
||||
await plugin.model.destroy();
|
||||
}
|
||||
pm.remove(name);
|
||||
await app.reload();
|
||||
await app.start();
|
||||
ctx.body = 'ok';
|
||||
await next();
|
||||
},
|
||||
},
|
||||
});
|
||||
this.app.acl.use(async (ctx, next) => {
|
||||
if (ctx.action.resourceName === 'pm') {
|
||||
ctx.permission = {
|
||||
skip: true,
|
||||
};
|
||||
}
|
||||
await next();
|
||||
});
|
||||
this.app.on('beforeInstall', async () => {
|
||||
await this.collection.sync();
|
||||
});
|
||||
this.app.on('beforeLoadAll', async (options) => {
|
||||
const exists = await this.app.db.collectionExistsInDb('applicationPlugins');
|
||||
if (!exists) {
|
||||
return;
|
||||
}
|
||||
const items = await this.repository.find();
|
||||
for (const item of items) {
|
||||
await this.add(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getPackageName(name: string) {
|
||||
if (name.includes('@nocobase/plugin-')) {
|
||||
return name;
|
||||
}
|
||||
if (name.includes('/')) {
|
||||
return `@${name}`;
|
||||
}
|
||||
return `@nocobase/plugin-${name}`;
|
||||
}
|
||||
|
||||
private addByModel(model) {
|
||||
try {
|
||||
const packageName = this.getPackageName(model.get('name'));
|
||||
require.resolve(packageName);
|
||||
const cls = require(packageName).default;
|
||||
const instance = new cls(this.app, {
|
||||
...model.get('options'),
|
||||
name: model.get('name'),
|
||||
version: model.get('version'),
|
||||
enabled: model.get('enabled'),
|
||||
});
|
||||
instance.setModel(model);
|
||||
this.plugins.set(packageName, instance);
|
||||
return instance;
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
getPlugins() {
|
||||
@ -30,13 +174,60 @@ export class PluginManager {
|
||||
return this.plugins.get(name);
|
||||
}
|
||||
|
||||
add<P extends Plugin = Plugin, O = any>(pluginClass: PluginConstructor<P, O>, options?: O): P {
|
||||
const instance = new pluginClass(this.app, options);
|
||||
remove(name: string) {
|
||||
return this.plugins.delete(name);
|
||||
}
|
||||
|
||||
add<P = Plugin, O = any>(pluginClass: any, options?: O) {
|
||||
if (Array.isArray(pluginClass)) {
|
||||
const addMultiple = async () => {
|
||||
for (const plugin of pluginClass) {
|
||||
await this.add(plugin);
|
||||
}
|
||||
}
|
||||
return addMultiple();
|
||||
}
|
||||
if (typeof pluginClass === 'string') {
|
||||
const packageName = this.getPackageName(pluginClass);
|
||||
try {
|
||||
require.resolve(packageName);
|
||||
} catch (error) {
|
||||
throw new Error(`${pluginClass} plugin does not exist`);
|
||||
}
|
||||
const packageJson = require(`${packageName}/package.json`);
|
||||
const addNew = async () => {
|
||||
let model = await this.repository.findOne({
|
||||
filter: { name: pluginClass },
|
||||
});
|
||||
if (model) {
|
||||
throw new Error(`${pluginClass} plugin already exists`);
|
||||
}
|
||||
model = await this.repository.create({
|
||||
values: {
|
||||
name: pluginClass,
|
||||
version: packageJson.version,
|
||||
enabled: false,
|
||||
options: {},
|
||||
},
|
||||
});
|
||||
return this.addByModel(model);
|
||||
};
|
||||
return addNew();
|
||||
}
|
||||
|
||||
if (pluginClass instanceof Model) {
|
||||
return this.addByModel(pluginClass);
|
||||
}
|
||||
|
||||
const instance = new pluginClass(this.app, {
|
||||
...options,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const name = instance.getName();
|
||||
|
||||
if (this.plugins.has(name)) {
|
||||
throw new Error(`plugin name [${name}] `);
|
||||
throw new Error(`plugin name [${name}] exists`);
|
||||
}
|
||||
|
||||
this.plugins.set(name, instance);
|
||||
@ -48,10 +239,16 @@ export class PluginManager {
|
||||
await this.app.emitAsync('beforeLoadAll');
|
||||
|
||||
for (const [name, plugin] of this.plugins) {
|
||||
if (!plugin.enabled) {
|
||||
continue;
|
||||
}
|
||||
await plugin.beforeLoad();
|
||||
}
|
||||
|
||||
for (const [name, plugin] of this.plugins) {
|
||||
if (!plugin.enabled) {
|
||||
continue;
|
||||
}
|
||||
await this.app.emitAsync('beforeLoadPlugin', plugin);
|
||||
await plugin.load();
|
||||
await this.app.emitAsync('afterLoadPlugin', plugin);
|
||||
@ -62,6 +259,9 @@ export class PluginManager {
|
||||
|
||||
async install(options: InstallOptions = {}) {
|
||||
for (const [name, plugin] of this.plugins) {
|
||||
if (!plugin.enabled) {
|
||||
continue;
|
||||
}
|
||||
await this.app.emitAsync('beforeInstallPlugin', plugin, options);
|
||||
await plugin.install(options);
|
||||
await this.app.emitAsync('afterInstallPlugin', plugin, options);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Database } from '@nocobase/database';
|
||||
import { Database, Model } from '@nocobase/database';
|
||||
import finder from 'find-package-json';
|
||||
import { Application } from './application';
|
||||
import { InstallOptions } from './plugin-manager';
|
||||
@ -15,6 +15,7 @@ export interface PluginOptions {
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
enabled?: boolean;
|
||||
install?: (this: Plugin) => void;
|
||||
load?: (this: Plugin) => void;
|
||||
plugin?: typeof Plugin;
|
||||
@ -27,19 +28,31 @@ export abstract class Plugin<O = any> implements PluginInterface {
|
||||
options: O;
|
||||
app: Application;
|
||||
db: Database;
|
||||
model: Model;
|
||||
|
||||
constructor(app: Application, options?: O) {
|
||||
this.app = app;
|
||||
this.db = app.db;
|
||||
this.setOptions(options);
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
setOptions(options: O) {
|
||||
this.options = options || ({} as any);
|
||||
}
|
||||
|
||||
setModel(model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
get enabled() {
|
||||
return (this.options as any).enabled;
|
||||
}
|
||||
|
||||
public abstract getName(): string;
|
||||
|
||||
initialize() {}
|
||||
|
||||
beforeLoad() {}
|
||||
|
||||
async install(options?: InstallOptions) {}
|
||||
@ -53,6 +66,10 @@ export abstract class Plugin<O = any> implements PluginInterface {
|
||||
}
|
||||
}
|
||||
|
||||
async disable() {
|
||||
|
||||
}
|
||||
|
||||
collectionPath() {
|
||||
return null;
|
||||
}
|
||||
|
@ -56,7 +56,6 @@ interface Resource {
|
||||
|
||||
export class MockServer extends Application {
|
||||
async loadAndInstall(options: any = {}) {
|
||||
await this.load();
|
||||
await this.install({
|
||||
...options,
|
||||
sync: {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ACL } from '@nocobase/acl';
|
||||
import { Database } from '@nocobase/database';
|
||||
import { MockServer } from '@nocobase/test';
|
||||
import UsersPlugin from '@nocobase/plugin-users';
|
||||
import { UiSchemaRepository } from '@nocobase/plugin-ui-schema-storage';
|
||||
import UsersPlugin from '@nocobase/plugin-users';
|
||||
import { MockServer } from '@nocobase/test';
|
||||
import { prepareApp } from './prepare';
|
||||
|
||||
describe('acl', () => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import PluginUsers from '@nocobase/plugin-users';
|
||||
import PluginErrorHandler from '@nocobase/plugin-error-handler';
|
||||
import PluginCollectionManager from '@nocobase/plugin-collection-manager';
|
||||
import PluginErrorHandler from '@nocobase/plugin-error-handler';
|
||||
import PluginUiSchema from '@nocobase/plugin-ui-schema-storage';
|
||||
import PluginUsers from '@nocobase/plugin-users';
|
||||
import { mockServer } from '@nocobase/test';
|
||||
import PluginACL from '../server';
|
||||
|
||||
|
@ -20,6 +20,7 @@ export class ClientPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.acl.allow('app', 'getLang');
|
||||
this.app.acl.allow('app', 'getInfo');
|
||||
this.app.acl.allow('app', 'getPlugins');
|
||||
this.app.acl.allow('plugins', 'getPinned', 'loggedIn');
|
||||
this.app.resource({
|
||||
name: 'app',
|
||||
@ -53,6 +54,24 @@ export class ClientPlugin extends Plugin {
|
||||
};
|
||||
await next();
|
||||
},
|
||||
async getPlugins(ctx, next) {
|
||||
const pm = ctx.db.getRepository('applicationPlugins');
|
||||
const items = await pm.find({
|
||||
filter: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
ctx.body = items
|
||||
.filter((item) => {
|
||||
try {
|
||||
require.resolve(`@nocobase/plugin-${item.name}/client`);
|
||||
return true;
|
||||
} catch (error) {}
|
||||
return false;
|
||||
})
|
||||
.map((item) => item.name);
|
||||
await next();
|
||||
},
|
||||
},
|
||||
});
|
||||
this.app.resource({
|
||||
@ -61,8 +80,7 @@ export class ClientPlugin extends Plugin {
|
||||
// TODO: 临时
|
||||
async getPinned(ctx, next) {
|
||||
ctx.body = [
|
||||
{ component: 'DesignableSwitch', pin: true },
|
||||
{ component: 'CollectionManagerShortcut', pin: true },
|
||||
{ component: 'CollectionManagerShortcut' },
|
||||
{ component: 'ACLShortcut' },
|
||||
{ component: 'WorkflowShortcut' },
|
||||
{ component: 'SchemaTemplateShortcut' },
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { Database } from '@nocobase/database';
|
||||
import { mockServer, MockServer } from '@nocobase/test';
|
||||
import CollectionManagerPlugin from '@nocobase/plugin-collection-manager';
|
||||
import { UiSchemaStoragePlugin } from '@nocobase/plugin-ui-schema-storage';
|
||||
import { MockServer } from '@nocobase/test';
|
||||
import { createApp } from '.';
|
||||
|
||||
describe('action test', () => {
|
||||
@ -10,7 +8,6 @@ describe('action test', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.install({ clean: true });
|
||||
db = app.db;
|
||||
});
|
||||
|
||||
|
@ -10,7 +10,6 @@ describe('collections repository', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.db.sync();
|
||||
db = app.db;
|
||||
Collection = db.getCollection('collections');
|
||||
Field = db.getCollection('fields');
|
||||
|
@ -10,7 +10,6 @@ describe('collections repository', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.db.sync();
|
||||
db = app.db;
|
||||
Collection = db.getCollection('collections');
|
||||
Field = db.getCollection('fields');
|
||||
|
@ -6,8 +6,6 @@ describe('field defaultValue', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.install({ clean: true });
|
||||
await app.start();
|
||||
await app
|
||||
.agent()
|
||||
.resource('collections')
|
||||
|
@ -7,8 +7,6 @@ describe('field indexes', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.install({ clean: true });
|
||||
await app.start();
|
||||
agent = app.agent();
|
||||
await agent
|
||||
.resource('collections')
|
||||
|
@ -10,7 +10,6 @@ describe('collections repository', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.db.sync();
|
||||
db = app.db;
|
||||
Collection = db.getCollection('collections');
|
||||
Field = db.getCollection('fields');
|
||||
|
@ -10,7 +10,6 @@ describe('belongsTo', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.db.sync();
|
||||
db = app.db;
|
||||
Collection = db.getCollection('collections');
|
||||
Field = db.getCollection('fields');
|
||||
|
@ -10,7 +10,6 @@ describe('belongsToMany', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.db.sync();
|
||||
db = app.db;
|
||||
Collection = db.getCollection('collections');
|
||||
Field = db.getCollection('fields');
|
||||
|
@ -10,7 +10,6 @@ describe('children options', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.db.sync();
|
||||
db = app.db;
|
||||
Collection = db.getCollection('collections');
|
||||
Field = db.getCollection('fields');
|
||||
|
@ -10,7 +10,6 @@ describe('hasMany field options', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.db.sync();
|
||||
db = app.db;
|
||||
Collection = db.getCollection('collections');
|
||||
Field = db.getCollection('fields');
|
||||
|
@ -1,4 +1,4 @@
|
||||
import Database, { Collection as DBCollection, StringFieldOptions } from '@nocobase/database';
|
||||
import Database, { Collection as DBCollection } from '@nocobase/database';
|
||||
import Application from '@nocobase/server';
|
||||
import { createApp } from '..';
|
||||
|
||||
@ -10,7 +10,6 @@ describe('hasOne field options', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.db.sync();
|
||||
db = app.db;
|
||||
Collection = db.getCollection('collections');
|
||||
Field = db.getCollection('fields');
|
||||
|
@ -10,7 +10,6 @@ describe('reverseField options', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.db.sync();
|
||||
db = app.db;
|
||||
Collection = db.getCollection('collections');
|
||||
Field = db.getCollection('fields');
|
||||
|
@ -8,8 +8,6 @@ describe('collections repository', () => {
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
agent = app.agent();
|
||||
await app.install({ clean: true });
|
||||
await app.start();
|
||||
await agent
|
||||
.resource('collections')
|
||||
.create({
|
||||
|
@ -15,6 +15,8 @@ export async function createApp(options = {}) {
|
||||
app.plugin(Plugin);
|
||||
app.plugin(PluginUiSchema);
|
||||
|
||||
await app.load();
|
||||
// await app.load();
|
||||
await app.install({ clean: true });
|
||||
await app.start();
|
||||
return app;
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ describe('collections repository', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.db.sync();
|
||||
db = app.db;
|
||||
Collection = db.getCollection('collections');
|
||||
Field = db.getCollection('fields');
|
||||
|
@ -6,7 +6,6 @@ describe('collections.fields', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.install({ clean: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
@ -7,7 +7,6 @@ describe('collections', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.install({ clean: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
@ -12,7 +12,6 @@ describe('collections repository', () => {
|
||||
await app1.cleanDb();
|
||||
app1.plugin(PluginErrorHandler);
|
||||
app1.plugin(Plugin);
|
||||
await app1.load();
|
||||
await app1.install({ clean: true });
|
||||
await app1.start();
|
||||
await app1
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Plugin, PluginManager } from '@nocobase/server';
|
||||
import { mockServer } from '@nocobase/test';
|
||||
import { uid } from '@nocobase/utils';
|
||||
import { PluginMultiAppManager } from '../server';
|
||||
|
||||
describe('test with start', () => {
|
||||
@ -35,9 +36,11 @@ describe('test with start', () => {
|
||||
|
||||
const db = app.db;
|
||||
|
||||
const name = `d_${uid()}`;
|
||||
|
||||
await db.getRepository('applications').create({
|
||||
values: {
|
||||
name: 'sub1',
|
||||
name,
|
||||
options: {
|
||||
plugins: ['test-package'],
|
||||
},
|
||||
@ -47,6 +50,8 @@ describe('test with start', () => {
|
||||
expect(loadFn).toHaveBeenCalledTimes(1);
|
||||
expect(installFn).toHaveBeenCalledTimes(1);
|
||||
|
||||
const subApp = await app.appManager.getApplication(name);
|
||||
await subApp.destroy();
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
@ -60,14 +65,18 @@ describe('test with start', () => {
|
||||
|
||||
const db = app.db;
|
||||
|
||||
const name = `d_${uid()}`;
|
||||
|
||||
await db.getRepository('applications').create({
|
||||
values: {
|
||||
name: 'sub1',
|
||||
name,
|
||||
options: {
|
||||
plugins: ['@nocobase/plugin-ui-schema-storage'],
|
||||
},
|
||||
},
|
||||
});
|
||||
const subApp = await app.appManager.getApplication(name);
|
||||
await subApp.destroy();
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
@ -91,17 +100,21 @@ describe('test with start', () => {
|
||||
mockGetPluginByName.mockReturnValue(TestPlugin);
|
||||
PluginManager.resolvePlugin = mockGetPluginByName;
|
||||
|
||||
const name = `d_${uid()}`;
|
||||
console.log(name);
|
||||
|
||||
await db.getRepository('applications').create({
|
||||
values: {
|
||||
name: 'sub1',
|
||||
name,
|
||||
options: {
|
||||
plugins: ['test-package'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(app.appManager.applications.get('sub1')).toBeDefined();
|
||||
expect(app.appManager.applications.get(name)).toBeDefined();
|
||||
|
||||
await app.appManager.applications.get(name).destroy();
|
||||
await app.stop();
|
||||
|
||||
let newApp = mockServer({
|
||||
@ -115,14 +128,16 @@ describe('test with start', () => {
|
||||
await newApp.start();
|
||||
|
||||
expect(await newApp.db.getRepository('applications').count()).toEqual(1);
|
||||
expect(newApp.appManager.applications.get('sub1')).not.toBeDefined();
|
||||
expect(newApp.appManager.applications.get(name)).not.toBeDefined();
|
||||
|
||||
newApp.appManager.setAppSelector(() => {
|
||||
return 'sub1';
|
||||
return name;
|
||||
});
|
||||
|
||||
await newApp.agent().resource('test').test();
|
||||
expect(newApp.appManager.applications.get('sub1')).toBeDefined();
|
||||
expect(newApp.appManager.applications.get(name)).toBeDefined();
|
||||
|
||||
await newApp.appManager.applications.get(name).destroy();
|
||||
|
||||
await app.destroy();
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Database } from '@nocobase/database';
|
||||
import { mockServer, MockServer } from '@nocobase/test';
|
||||
import { uid } from '@nocobase/utils';
|
||||
import { ApplicationModel } from '..';
|
||||
import { PluginMultiAppManager } from '../server';
|
||||
|
||||
@ -21,84 +22,89 @@ describe('multiple apps create', () => {
|
||||
});
|
||||
|
||||
it('should create application', async () => {
|
||||
const name = `td_${uid()}`;
|
||||
const miniApp = await db.getRepository('applications').create({
|
||||
values: {
|
||||
name: 'miniApp',
|
||||
name,
|
||||
},
|
||||
});
|
||||
|
||||
expect(app.appManager.applications.get('miniApp')).toBeDefined();
|
||||
expect(app.appManager.applications.get(name)).toBeDefined();
|
||||
});
|
||||
|
||||
it('should remove application', async () => {
|
||||
const name = `td_${uid()}`;
|
||||
await db.getRepository('applications').create({
|
||||
values: {
|
||||
name: 'miniApp',
|
||||
name,
|
||||
},
|
||||
});
|
||||
|
||||
expect(app.appManager.applications.get('miniApp')).toBeDefined();
|
||||
expect(app.appManager.applications.get(name)).toBeDefined();
|
||||
|
||||
await db.getRepository('applications').destroy({
|
||||
filter: {
|
||||
name: 'miniApp',
|
||||
name,
|
||||
},
|
||||
});
|
||||
|
||||
expect(app.appManager.applications.get('miniApp')).toBeUndefined();
|
||||
expect(app.appManager.applications.get(name)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create with plugins', async () => {
|
||||
const name = `td_${uid()}`;
|
||||
await db.getRepository('applications').create({
|
||||
values: {
|
||||
name: 'miniApp',
|
||||
name,
|
||||
options: {
|
||||
plugins: [['@nocobase/plugin-ui-schema-storage', { test: 'B' }]],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const miniApp = app.appManager.applications.get('miniApp');
|
||||
const miniApp = app.appManager.applications.get(name);
|
||||
expect(miniApp).toBeDefined();
|
||||
|
||||
const plugin = miniApp.pm.get('@nocobase/plugin-ui-schema-storage');
|
||||
|
||||
expect(plugin).toBeDefined();
|
||||
expect(plugin.options).toEqual({
|
||||
expect(plugin.options).toMatchObject({
|
||||
test: 'B',
|
||||
});
|
||||
});
|
||||
|
||||
it('should lazy load applications', async () => {
|
||||
const name = `td_${uid()}`;
|
||||
await db.getRepository('applications').create({
|
||||
values: {
|
||||
name: 'miniApp',
|
||||
name,
|
||||
options: {
|
||||
plugins: ['@nocobase/plugin-ui-schema-storage'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await app.appManager.removeApplication('miniApp');
|
||||
await app.appManager.removeApplication(name);
|
||||
|
||||
app.appManager.setAppSelector(() => {
|
||||
return 'miniApp';
|
||||
return name;
|
||||
});
|
||||
|
||||
expect(app.appManager.applications.has('miniApp')).toBeFalsy();
|
||||
expect(app.appManager.applications.has(name)).toBeFalsy();
|
||||
|
||||
await app.agent().resource('test').test();
|
||||
|
||||
expect(app.appManager.applications.has('miniApp')).toBeTruthy();
|
||||
expect(app.appManager.applications.has(name)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should change handleAppStart', async () => {
|
||||
const customHandler = jest.fn();
|
||||
ApplicationModel.handleAppStart = customHandler;
|
||||
const name = `td_${uid()}`;
|
||||
|
||||
await db.getRepository('applications').create({
|
||||
values: {
|
||||
name: 'miniApp',
|
||||
name,
|
||||
options: {
|
||||
plugins: ['@nocobase/plugin-ui-schema-storage'],
|
||||
},
|
||||
|
@ -17,12 +17,11 @@ export class ApplicationModel extends Model {
|
||||
}
|
||||
|
||||
static async handleAppStart(app: Application, options: registerAppOptions) {
|
||||
await app.load();
|
||||
|
||||
if (!lodash.get(options, 'skipInstall', false)) {
|
||||
if (!options?.skipInstall) {
|
||||
await app.install();
|
||||
} else {
|
||||
await app.load();
|
||||
}
|
||||
|
||||
await app.start();
|
||||
}
|
||||
|
||||
@ -32,14 +31,9 @@ export class ApplicationModel extends Model {
|
||||
|
||||
const AppModel = this.constructor as typeof ApplicationModel;
|
||||
|
||||
const app = mainApp.appManager.createApplication(appName, {
|
||||
...AppModel.initOptions(appName, mainApp),
|
||||
...appOptions,
|
||||
});
|
||||
|
||||
// create database before installation if it not exists
|
||||
app.on('beforeInstall', async function createDatabase() {
|
||||
const { host, port, username, password, database, dialect } = AppModel.getDatabaseConfig(app);
|
||||
const createDatabase = async () => {
|
||||
const database = appName;
|
||||
const { host, port, username, password, dialect } = mainApp.db.options;
|
||||
|
||||
if (dialect === 'mysql') {
|
||||
const mysql = require('mysql2/promise');
|
||||
@ -56,7 +50,7 @@ export class ApplicationModel extends Model {
|
||||
port,
|
||||
user: username,
|
||||
password,
|
||||
database: 'postgres'
|
||||
database: 'postgres',
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
@ -67,6 +61,15 @@ export class ApplicationModel extends Model {
|
||||
|
||||
await client.end();
|
||||
}
|
||||
};
|
||||
|
||||
if (!options?.skipInstall) {
|
||||
await createDatabase();
|
||||
}
|
||||
|
||||
const app = mainApp.appManager.createApplication(appName, {
|
||||
...AppModel.initOptions(appName, mainApp),
|
||||
...appOptions,
|
||||
});
|
||||
|
||||
await AppModel.handleAppStart(app, options);
|
||||
|
@ -2,7 +2,7 @@ import { Migration } from '@nocobase/server';
|
||||
|
||||
export default class AlertSubTableMigration extends Migration {
|
||||
async up() {
|
||||
const match = await this.app.version.satisfies('<=0.7.4-alpha.8');
|
||||
const match = await this.app.version.satisfies('<=0.7.4-alpha.7');
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
@ -10,8 +10,8 @@ export default class AlertSubTableMigration extends Migration {
|
||||
const existed = await Field.count({
|
||||
filter: {
|
||||
name: 'phone',
|
||||
collectionName: 'users'
|
||||
}
|
||||
collectionName: 'users',
|
||||
},
|
||||
});
|
||||
if (!existed) {
|
||||
await Field.create({
|
||||
@ -30,12 +30,10 @@ export default class AlertSubTableMigration extends Migration {
|
||||
},
|
||||
},
|
||||
// NOTE: to trigger hook
|
||||
context: {}
|
||||
context: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async down() {
|
||||
|
||||
}
|
||||
async down() {}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { PluginManagerContext, RouteSwitchContext, SettingsCenterProvider } from '@nocobase/client';
|
||||
import React, { useContext } from 'react';
|
||||
import { PluginManagerContext, RouteSwitchContext } from '@nocobase/client';
|
||||
import { WorkflowPage } from './WorkflowPage';
|
||||
import { WorkflowShortcut } from './WorkflowShortcut';
|
||||
import { WorkflowPane, WorkflowShortcut } from './WorkflowShortcut';
|
||||
|
||||
export const WorkflowProvider = (props) => {
|
||||
const ctx = useContext(PluginManagerContext);
|
||||
@ -12,17 +12,32 @@ export const WorkflowProvider = (props) => {
|
||||
component: 'WorkflowPage',
|
||||
});
|
||||
return (
|
||||
<PluginManagerContext.Provider
|
||||
value={{
|
||||
components: {
|
||||
...ctx?.components,
|
||||
WorkflowShortcut,
|
||||
<SettingsCenterProvider
|
||||
settings={{
|
||||
workflow: {
|
||||
icon: 'PartitionOutlined',
|
||||
title: '{{t("Workflow")}}',
|
||||
tabs: {
|
||||
workflows: {
|
||||
title: '{{t("Workflow")}}',
|
||||
component: WorkflowPane,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RouteSwitchContext.Provider value={{ components: { ...components, WorkflowPage }, ...others, routes }}>
|
||||
{props.children}
|
||||
</RouteSwitchContext.Provider>
|
||||
</PluginManagerContext.Provider>
|
||||
<PluginManagerContext.Provider
|
||||
value={{
|
||||
components: {
|
||||
...ctx?.components,
|
||||
WorkflowShortcut,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RouteSwitchContext.Provider value={{ components: { ...components, WorkflowPage }, ...others, routes }}>
|
||||
{props.children}
|
||||
</RouteSwitchContext.Provider>
|
||||
</PluginManagerContext.Provider>
|
||||
</SettingsCenterProvider>
|
||||
);
|
||||
};
|
||||
|
@ -1,16 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { PartitionOutlined } from '@ant-design/icons';
|
||||
import { ISchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { ActionContext, PluginManager, SchemaComponent } from '@nocobase/client';
|
||||
import { Card } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { PluginManager, ActionContext, SchemaComponent } from '@nocobase/client';
|
||||
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { ExecutionResourceProvider } from './ExecutionResourceProvider';
|
||||
import { workflowSchema } from './schemas/workflows';
|
||||
import { WorkflowLink } from './WorkflowLink';
|
||||
import { ExecutionResourceProvider } from './ExecutionResourceProvider';
|
||||
|
||||
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
@ -26,7 +24,44 @@ const schema: ISchema = {
|
||||
},
|
||||
};
|
||||
|
||||
const schema2: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
[uid()]: workflowSchema,
|
||||
},
|
||||
};
|
||||
|
||||
export const WorkflowPane = () => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
return (
|
||||
<Card bordered={false}>
|
||||
<SchemaComponent
|
||||
schema={schema2}
|
||||
components={{
|
||||
WorkflowLink,
|
||||
ExecutionResourceProvider,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkflowShortcut = () => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
return (
|
||||
<PluginManager.Toolbar.Item
|
||||
icon={<PartitionOutlined />}
|
||||
title={t('Workflow')}
|
||||
onClick={() => {
|
||||
history.push('/admin/settings/workflow/workflows');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkflowShortcut2 = () => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
return (
|
||||
@ -42,7 +77,7 @@ export const WorkflowShortcut = () => {
|
||||
schema={schema}
|
||||
components={{
|
||||
WorkflowLink,
|
||||
ExecutionResourceProvider
|
||||
ExecutionResourceProvider,
|
||||
}}
|
||||
/>
|
||||
</ActionContext.Provider>
|
||||
|
@ -5,23 +5,33 @@ export class PresetNocoBase<O = any> extends Plugin {
|
||||
return this.getPackageName(__dirname);
|
||||
}
|
||||
|
||||
beforeLoad(): void {
|
||||
this.app.loadPluginConfig([
|
||||
'@nocobase/plugin-error-handler',
|
||||
'@nocobase/plugin-collection-manager',
|
||||
'@nocobase/plugin-ui-schema-storage',
|
||||
'@nocobase/plugin-ui-routes-storage',
|
||||
'@nocobase/plugin-file-manager',
|
||||
'@nocobase/plugin-system-settings',
|
||||
'@nocobase/plugin-verification',
|
||||
'@nocobase/plugin-users',
|
||||
'@nocobase/plugin-acl',
|
||||
'@nocobase/plugin-china-region',
|
||||
'@nocobase/plugin-workflow',
|
||||
'@nocobase/plugin-client',
|
||||
'@nocobase/plugin-export',
|
||||
'@nocobase/plugin-audit-logs',
|
||||
]);
|
||||
initialize() {
|
||||
this.app.on('beforeInstall', async () => {
|
||||
const plugins = [
|
||||
'error-handler',
|
||||
'collection-manager',
|
||||
'ui-schema-storage',
|
||||
'ui-routes-storage',
|
||||
'file-manager',
|
||||
'system-settings',
|
||||
'verification',
|
||||
'users',
|
||||
'acl',
|
||||
'china-region',
|
||||
'workflow',
|
||||
'client',
|
||||
'export',
|
||||
'audit-logs',
|
||||
];
|
||||
for (const plugin of plugins) {
|
||||
const instance = await this.app.pm.add(plugin);
|
||||
if (instance.model && plugin !== 'hello') {
|
||||
instance.model.enabled = true;
|
||||
instance.model.builtIn = true;
|
||||
await instance.model.save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
4
packages/samples/hello/client.d.ts
vendored
Executable file
4
packages/samples/hello/client.d.ts
vendored
Executable file
@ -0,0 +1,4 @@
|
||||
// @ts-nocheck
|
||||
export * from './lib/client';
|
||||
export { default } from './lib/client';
|
||||
|
30
packages/samples/hello/client.js
Executable file
30
packages/samples/hello/client.js
Executable file
@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
|
||||
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
||||
|
||||
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
||||
|
||||
var _index = _interopRequireWildcard(require("./lib/client"));
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
var _exportNames = {};
|
||||
Object.defineProperty(exports, "default", {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index.default;
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(_index).forEach(function (key) {
|
||||
if (key === "default" || key === "__esModule") return;
|
||||
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
||||
if (key in exports && exports[key] === _index[key]) return;
|
||||
Object.defineProperty(exports, key, {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index[key];
|
||||
}
|
||||
});
|
||||
});
|
16
packages/samples/hello/package.json
Normal file
16
packages/samples/hello/package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-hello-sample",
|
||||
"version": "0.7.4-alpha.7",
|
||||
"main": "lib/server/index.js",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@nocobase/client": "0.7.4-alpha.7",
|
||||
"@nocobase/server": "0.7.4-alpha.7",
|
||||
"@nocobase/test": "0.7.4-alpha.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nocobase/client": "*",
|
||||
"@nocobase/server": "*",
|
||||
"@nocobase/test": "*"
|
||||
}
|
||||
}
|
4
packages/samples/hello/server.d.ts
vendored
Executable file
4
packages/samples/hello/server.d.ts
vendored
Executable file
@ -0,0 +1,4 @@
|
||||
// @ts-nocheck
|
||||
export * from './lib/server';
|
||||
export { default } from './lib/server';
|
||||
|
30
packages/samples/hello/server.js
Executable file
30
packages/samples/hello/server.js
Executable file
@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
|
||||
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
||||
|
||||
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
||||
|
||||
var _index = _interopRequireWildcard(require("./lib/server"));
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
var _exportNames = {};
|
||||
Object.defineProperty(exports, "default", {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index.default;
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(_index).forEach(function (key) {
|
||||
if (key === "default" || key === "__esModule") return;
|
||||
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
||||
if (key in exports && exports[key] === _index[key]) return;
|
||||
Object.defineProperty(exports, key, {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index[key];
|
||||
}
|
||||
});
|
||||
});
|
22
packages/samples/hello/src/client/HelloDesigner.tsx
Normal file
22
packages/samples/hello/src/client/HelloDesigner.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import {
|
||||
GeneralSchemaDesigner,
|
||||
SchemaSettings,
|
||||
useCollection
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
|
||||
export const HelloDesigner = () => {
|
||||
const { name, title } = useCollection();
|
||||
const fieldSchema = useFieldSchema();
|
||||
return (
|
||||
<GeneralSchemaDesigner title={title || name}>
|
||||
<SchemaSettings.Remove
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={{
|
||||
'x-component': 'Grid',
|
||||
}}
|
||||
/>
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
};
|
68
packages/samples/hello/src/client/index.tsx
Normal file
68
packages/samples/hello/src/client/index.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import { TableOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
SchemaComponentOptions,
|
||||
SchemaInitializer,
|
||||
SchemaInitializerContext,
|
||||
SettingsCenterProvider
|
||||
} from '@nocobase/client';
|
||||
import { Card } from 'antd';
|
||||
import React, { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HelloDesigner } from './HelloDesigner';
|
||||
|
||||
export const HelloBlockInitializer = (props) => {
|
||||
const { insert } = props;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<SchemaInitializer.Item
|
||||
{...props}
|
||||
icon={<TableOutlined />}
|
||||
onClick={() => {
|
||||
insert({
|
||||
type: 'void',
|
||||
'x-component': 'CardItem',
|
||||
'x-designer': 'HelloDesigner',
|
||||
properties: {
|
||||
hello: {
|
||||
type: 'void',
|
||||
'x-component': 'div',
|
||||
'x-content': 'Hello World',
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
title={t('Hello block')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo((props) => {
|
||||
const items = useContext(SchemaInitializerContext);
|
||||
const children = items.BlockInitializers.items[2].children;
|
||||
children.push({
|
||||
key: 'hello',
|
||||
type: 'item',
|
||||
title: '{{t("Hello block")}}',
|
||||
component: 'HelloBlockInitializer',
|
||||
});
|
||||
return (
|
||||
<SettingsCenterProvider
|
||||
settings={{
|
||||
'hello-sample': {
|
||||
title: 'Hello',
|
||||
icon: 'ApiOutlined',
|
||||
tabs: {
|
||||
tab1: {
|
||||
title: 'Hello tab',
|
||||
component: () => <Card bordered={false}>Hello Settings</Card>,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SchemaComponentOptions components={{ HelloDesigner, HelloBlockInitializer }}>
|
||||
<SchemaInitializerContext.Provider value={items}>{props.children}</SchemaInitializerContext.Provider>
|
||||
</SchemaComponentOptions>
|
||||
</SettingsCenterProvider>
|
||||
);
|
||||
});
|
1
packages/samples/hello/src/index.ts
Normal file
1
packages/samples/hello/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './server';
|
0
packages/samples/hello/src/server/actions/.gitkeep
Normal file
0
packages/samples/hello/src/server/actions/.gitkeep
Normal file
36
packages/samples/hello/src/server/index.ts
Normal file
36
packages/samples/hello/src/server/index.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { InstallOptions, Plugin } from '@nocobase/server';
|
||||
|
||||
export class HelloPlugin extends Plugin {
|
||||
getName(): string {
|
||||
return this.getPackageName(__dirname);
|
||||
}
|
||||
|
||||
beforeLoad() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
async load() {
|
||||
// TODO
|
||||
// Visit: http://localhost:13000/api/testHello:getInfo
|
||||
this.app.resource({
|
||||
name: 'testHello',
|
||||
actions: {
|
||||
async getInfo(ctx, next) {
|
||||
ctx.body = `Hello hello!`;
|
||||
next();
|
||||
},
|
||||
},
|
||||
});
|
||||
this.app.acl.allow('testHello', 'getInfo');
|
||||
}
|
||||
|
||||
async disable() {
|
||||
// this.app.resourcer.removeResource('testHello');
|
||||
}
|
||||
|
||||
async install(options: InstallOptions) {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
export default HelloPlugin;
|
0
packages/samples/hello/src/server/models/.gitkeep
Normal file
0
packages/samples/hello/src/server/models/.gitkeep
Normal file
@ -20,6 +20,9 @@
|
||||
"@nocobase/app-*": [
|
||||
"packages/app/*/src"
|
||||
],
|
||||
"@nocobase/plugin-*-sample": [
|
||||
"packages/samples/*/src"
|
||||
],
|
||||
"@nocobase/plugin-*": [
|
||||
"packages/plugins/*/src"
|
||||
],
|
||||
|
Loading…
Reference in New Issue
Block a user