diff --git a/examples/app/custom-plugin.ts b/examples/app/custom-plugin.ts index e44650559a..d57aef0d98 100644 --- a/examples/app/custom-plugin.ts +++ b/examples/app/custom-plugin.ts @@ -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', diff --git a/examples/app/single-app.ts b/examples/app/single-app.ts index 74079365e2..93354d5dcd 100644 --- a/examples/app/single-app.ts +++ b/examples/app/single-app.ts @@ -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'); }, }, }); diff --git a/package.json b/package.json index cd08733fbe..a3565e4662 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ ], "scripts": { "nocobase": "nocobase", + "pm": "nocobase pm", "dev": "nocobase dev", "start": "nocobase start", "build": "nocobase build", diff --git a/packages/app/client/.umirc.ts b/packages/app/client/.umirc.ts index 156ba8d478..18d5d7736f 100644 --- a/packages/app/client/.umirc.ts +++ b/packages/app/client/.umirc.ts @@ -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: { diff --git a/packages/app/client/src/pages/index.tsx b/packages/app/client/src/pages/index.tsx index deaba138ba..edec62ec01 100644 --- a/packages/app/client/src/pages/index.tsx +++ b/packages/app/client/src/pages/index.tsx @@ -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(); diff --git a/packages/app/client/src/plugins/audit-logs.ts b/packages/app/client/src/plugins/audit-logs.ts new file mode 100644 index 0000000000..a4073dec38 --- /dev/null +++ b/packages/app/client/src/plugins/audit-logs.ts @@ -0,0 +1 @@ +export { default } from '@nocobase/plugin-audit-logs/client'; diff --git a/packages/app/client/src/plugins/china-region.ts b/packages/app/client/src/plugins/china-region.ts new file mode 100644 index 0000000000..1dc418003a --- /dev/null +++ b/packages/app/client/src/plugins/china-region.ts @@ -0,0 +1 @@ +export { default } from '@nocobase/plugin-china-region/client'; diff --git a/packages/app/client/src/plugins/export.ts b/packages/app/client/src/plugins/export.ts new file mode 100644 index 0000000000..48234cfd19 --- /dev/null +++ b/packages/app/client/src/plugins/export.ts @@ -0,0 +1 @@ +export { default } from '@nocobase/plugin-export/client'; diff --git a/packages/app/client/src/plugins/hello-sample.ts b/packages/app/client/src/plugins/hello-sample.ts new file mode 100644 index 0000000000..f0bf01164d --- /dev/null +++ b/packages/app/client/src/plugins/hello-sample.ts @@ -0,0 +1 @@ +export { default } from '@nocobase/plugin-hello-sample/client'; diff --git a/packages/app/client/src/plugins/workflow.ts b/packages/app/client/src/plugins/workflow.ts new file mode 100644 index 0000000000..1fb484e8af --- /dev/null +++ b/packages/app/client/src/plugins/workflow.ts @@ -0,0 +1 @@ +export { default } from '@nocobase/plugin-workflow/client'; diff --git a/packages/app/client/tsconfig.json b/packages/app/client/tsconfig.json index bbcb12f1d0..c1f4a369bd 100644 --- a/packages/app/client/tsconfig.json +++ b/packages/app/client/tsconfig.json @@ -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/"], diff --git a/packages/core/cli/src/commands/global.js b/packages/core/cli/src/commands/global.js index 5cc6e65231..de8d5e7bff 100644 --- a/packages/core/cli/src/commands/global.js +++ b/packages/core/cli/src/commands/global.js @@ -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)]); } }); diff --git a/packages/core/cli/src/util.js b/packages/core/cli/src/util.js index f9eff5d8e8..b34799a4e0 100644 --- a/packages/core/cli/src/util.js +++ b/packages/core/cli/src/util.js @@ -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); } diff --git a/packages/core/client/src/acl/ACLShortcut.tsx b/packages/core/client/src/acl/ACLShortcut.tsx index 25d795ec50..05c02e9eb8 100644 --- a/packages/core/client/src/acl/ACLShortcut.tsx +++ b/packages/core/client/src/acl/ACLShortcut.tsx @@ -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 ( + + + + ); +}; + export const ACLShortcut = () => { + const { t } = useTranslation(); + const history = useHistory(); + return ( + } + title={t('Roles & Permissions')} + onClick={() => { + history.push('/admin/settings/acl/roles'); + }} + /> + ); +}; + +export const ACLShortcut2 = () => { const [visible, setVisible] = useState(false); const { t } = useTranslation(); return ( diff --git a/packages/core/client/src/application/Application.tsx b/packages/core/client/src/application/Application.tsx index 160330d32d..d53598fb44 100644 --- a/packages/core/client/src/application/Application.tsx +++ b/packages/core/client/src/application/Application.tsx @@ -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; +const App = React.memo((props: any) => { + const C = compose(...props.providers)(() => { + const routes = useRoutes(); + return ( +
+ +
+ ); + }); + return ; +}); + 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 ( -
- -
- ); - }), - ); + 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 ; + } + return ; + }; } } diff --git a/packages/core/client/src/collection-manager/CollectionManagerShortcut.tsx b/packages/core/client/src/collection-manager/CollectionManagerShortcut.tsx index 961cb2a133..4a5360d6b0 100644 --- a/packages/core/client/src/collection-manager/CollectionManagerShortcut.tsx +++ b/packages/core/client/src/collection-manager/CollectionManagerShortcut.tsx @@ -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 ( + + + + ); +}; + export const CollectionManagerShortcut = () => { + const { t } = useTranslation(); + const history = useHistory(); + return ( + } + title={t('Collections & Fields')} + onClick={() => { + history.push('/admin/settings/collection-manager/collections'); + }} + /> + ); +}; + +export const CollectionManagerShortcut2 = () => { const [visible, setVisible] = useState(false); const { t } = useTranslation(); return ( diff --git a/packages/core/client/src/file-manager/FileStorage.tsx b/packages/core/client/src/file-manager/FileStorage.tsx new file mode 100644 index 0000000000..3ca927af8c --- /dev/null +++ b/packages/core/client/src/file-manager/FileStorage.tsx @@ -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 ( + + + + ); +}; + +// WZvC&6cR8@aAJu! diff --git a/packages/core/client/src/file-manager/FileStorageShortcut.tsx b/packages/core/client/src/file-manager/FileStorageShortcut.tsx index d28de15a25..27d063e928 100644 --- a/packages/core/client/src/file-manager/FileStorageShortcut.tsx +++ b/packages/core/client/src/file-manager/FileStorageShortcut.tsx @@ -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 ( + } + title={t('File storages')} + onClick={() => { + history.push('/admin/settings/file-manager/storages'); + }} + /> + ); +}; + +export const FileStorageShortcut2 = () => { const [visible, setVisible] = useState(false); const { t } = useTranslation(); return ( diff --git a/packages/core/client/src/file-manager/index.ts b/packages/core/client/src/file-manager/index.ts index 658bae5cb2..5430625558 100644 --- a/packages/core/client/src/file-manager/index.ts +++ b/packages/core/client/src/file-manager/index.ts @@ -1 +1,3 @@ +export * from './FileStorage'; export * from './FileStorageShortcut'; + diff --git a/packages/core/client/src/index.tsx b/packages/core/client/src/index.tsx index 196decddce..6bb4a82253 100644 --- a/packages/core/client/src/index.tsx +++ b/packages/core/client/src/index.tsx @@ -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'; + diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index 6cd2b718e2..b9f9967a8c 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -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': '管理所有配置', } diff --git a/packages/core/client/src/plugin-manager/PluginManager.tsx b/packages/core/client/src/plugin-manager/PluginManager.tsx index aad5058285..041543ba2d 100644 --- a/packages/core/client/src/plugin-manager/PluginManager.tsx +++ b/packages/core/client/src/plugin-manager/PluginManager.tsx @@ -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 & { - Item?: React.FC; + Item?: React.FC; }; }; @@ -55,6 +55,7 @@ PluginManager.Toolbar = (props: ToolbarProps) => { const { items = [] } = props; const [pinned, unpinned] = splitItems(items); const { t } = useTranslation(); + const history = useHistory(); return (
@@ -69,7 +70,7 @@ PluginManager.Toolbar = (props: ToolbarProps) => { ); })} {unpinned.length > 0 && ( - }> + }> {unpinned.map((item, index) => { const Action = get(components, item.component); return ( @@ -81,8 +82,14 @@ PluginManager.Toolbar = (props: ToolbarProps) => { ); })} {unpinned.length > 0 && } - }> - {t('View all plugins')} + { + history.push('/admin/settings'); + }} + icon={} + > + {t('Settings center')} )} @@ -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 && (
{subtitle}
- ) + > + {subtitle} +
+ ); const titleComponent = (
{title}
{subtitleComponent}
- ) + ); - return ( + return title ? ( {icon} + ) : ( + + {icon} + ); } 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 ; - } - return ; + // const api = useAPIClient(); + // const { data, loading } = useRequest({ + // resource: 'plugins', + // action: 'getPinned', + // }); + // if (loading) { + // return ; + // } + const items = [ + { component: 'DesignableSwitch', pin: true }, + { component: 'PluginManagerLink', pin: true }, + { component: 'SettingsCenterDropdown', pin: true }, + // ...data?.data, + ]; + return ; }; diff --git a/packages/core/client/src/pm/PluginManagerLink.tsx b/packages/core/client/src/pm/PluginManagerLink.tsx new file mode 100644 index 0000000000..505f30d7c0 --- /dev/null +++ b/packages/core/client/src/pm/PluginManagerLink.tsx @@ -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 ( + + } + title={t('Plugin manager')} + onClick={() => { + history.push('/admin/plugins'); + }} + /> + + ); +}; + +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 ( + + + + {items.map((item) => { + return ( + { + history.push('/admin/settings/' + item.path); + }} + > + {item.title} + + ); + })} + + + { + history.push('/admin/settings'); + }} + > + {t('Settings center')} + + + } + > + } + // title={t('Settings center')} + > + + + ); +}; diff --git a/packages/core/client/src/pm/index.tsx b/packages/core/client/src/pm/index.tsx new file mode 100644 index 0000000000..763cf1d112 --- /dev/null +++ b/packages/core/client/src/pm/index.tsx @@ -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({}); + +const PluginCard = (props) => { + const history = useHistory(); + const { data = {} } = props; + const api = useAPIClient(); + const { t } = useTranslation(); + return ( + { + history.push(`/admin/settings/${data.name}`); + }} + /> + ) : null, + { + await api.request({ + url: `pm:remove/${data.name}`, + }); + message.success(t('插件删除成功')); + window.location.reload(); + }} + onCancel={() => {}} + okText={t('Yes')} + cancelText={t('No')} + > + + , + { + await api.request({ + url: `pm:${checked ? 'enable' : 'disable'}/${data.name}`, + }); + message.success(checked ? t('插件激活成功') : t('插件禁用成功')); + window.location.reload(); + }} + defaultChecked={data.enabled} + >, + ].filter(Boolean)} + > + } + description={data.description} + title={ + + {data.name} + + {data.version} + + + } + /> + + ); +}; + +const BuiltInPluginCard = (props) => { + const { data } = props; + return ( + Settings, Remove, ]} + > + } + description={data.description} + title={ + + {data.name} + + {data.version} + + + } + /> + + ); +}; + +const LocalPlugins = () => { + const { data, loading } = useRequest({ + url: 'applicationPlugins:list', + params: { + filter: { + 'builtIn.$isFalsy': true, + }, + sort: 'name', + }, + }); + if (loading) { + return ; + } + return ( + <> + {data?.data?.map((item) => { + return ; + })} + + ); +}; + +const BuiltinPlugins = () => { + const { data, loading } = useRequest({ + url: 'applicationPlugins:list', + params: { + filter: { + 'builtIn.$isTruly': true, + }, + sort: 'name', + }, + }); + if (loading) { + return ; + } + return ( + <> + {data?.data?.map((item) => { + return ; + })} + + ); +}; + +const MarketplacePlugins = () => { + const { t } = useTranslation(); + return
{t('Coming soon...')}
; +}; + +const PluginList = (props) => { + const match = useRouteMatch(); + const history = useHistory(); + const { tabName = 'local' } = match.params || {}; + const { setTitle } = useDocumentTitle(); + const { t } = useTranslation(); + + return ( +
+ { + history.push(`/admin/plugins/${activeKey}`); + }} + > + + + + + } + /> +
+ {React.createElement( + { + local: LocalPlugins, + 'built-in': BuiltinPlugins, + marketplace: MarketplacePlugins, + }[tabName], + )} +
+
+ ); +}; + +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(); + const history = useHistory(); + 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 ; + } + if (!items[pluginName]) { + return ; + } + if (!tabName) { + const firstTabName = Object.keys(items[pluginName]?.tabs).shift(); + return ; + } + const component = items[pluginName]?.tabs?.[tabName]?.component; + return ( +
+ + + + {Object.keys(items) + .sort() + .map((key) => { + const item = items[key]; + const tabKey = Object.keys(item.tabs).shift(); + return ( + : null} + onClick={() => { + history.push(`/admin/settings/${key}/${tabKey}`); + }} + > + {compile(item.title)} + + ); + })} + + + + { + history.push(`/admin/settings/${pluginName}/${activeKey}`); + }} + > + {Object.keys(items[pluginName]?.tabs).map((tabKey) => { + const tab = items[pluginName].tabs?.[tabKey]; + return ; + })} + + } + /> +
{component && React.createElement(component)}
+
+
+
+ ); +}; + +export const SettingsCenterProvider = (props) => { + const { settings = {} } = props; + const items = useContext(SettingsCenterContext); + return ( + {props.children} + ); +}; + +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 ( + + {props.children} + + ); +}; + +export default PMProvider; + +export * from './PluginManagerLink'; diff --git a/packages/core/client/src/schema-templates/BlockTemplatePage.tsx b/packages/core/client/src/schema-templates/BlockTemplatePage.tsx index c41c6bad11..b7a5682801 100644 --- a/packages/core/client/src/schema-templates/BlockTemplatePage.tsx +++ b/packages/core/client/src/schema-templates/BlockTemplatePage.tsx @@ -19,3 +19,11 @@ export const BlockTemplatePage = () => { ); }; + +export const BlockTemplatesPane = () => { + return ( + + + + ); +}; diff --git a/packages/core/client/src/schema-templates/SchemaTemplateShortcut.tsx b/packages/core/client/src/schema-templates/SchemaTemplateShortcut.tsx index b561d822dd..aeefb7fad6 100644 --- a/packages/core/client/src/schema-templates/SchemaTemplateShortcut.tsx +++ b/packages/core/client/src/schema-templates/SchemaTemplateShortcut.tsx @@ -12,7 +12,7 @@ export const SchemaTemplateShortcut = () => { icon={} title={t('Block templates')} onClick={() => { - history.push('/admin/plugins/block-templates'); + history.push('/admin/settings/block-templates/list'); }} /> ); diff --git a/packages/core/client/src/system-settings/SystemSettingsShortcut.tsx b/packages/core/client/src/system-settings/SystemSettingsShortcut.tsx index c5bca9e120..00ac06f88b 100644 --- a/packages/core/client/src/system-settings/SystemSettingsShortcut.tsx +++ b/packages/core/client/src/system-settings/SystemSettingsShortcut.tsx @@ -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 ( + + + + ); +}; + export const SystemSettingsShortcut = () => { + const { t } = useTranslation(); + const history = useHistory(); + return ( + } + 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 ( diff --git a/packages/core/create-nocobase-app/templates/app/package.json.tpl b/packages/core/create-nocobase-app/templates/app/package.json.tpl index da031c8371..12e45ea77b 100644 --- a/packages/core/create-nocobase-app/templates/app/package.json.tpl +++ b/packages/core/create-nocobase-app/templates/app/package.json.tpl @@ -6,6 +6,7 @@ ], "scripts": { "nocobase": "nocobase", + "pm": "nocobase pm", "dev": "nocobase dev", "start": "nocobase start", "clean": "nocobase clean", diff --git a/packages/core/devtools/umiConfig.js b/packages/core/devtools/umiConfig.js index af7e28ca92..f194917e81 100644 --- a/packages/core/devtools/umiConfig.js +++ b/packages/core/devtools/umiConfig.js @@ -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; diff --git a/packages/core/resourcer/src/resourcer.ts b/packages/core/resourcer/src/resourcer.ts index 7e5ac17a17..d49a90fec5 100644 --- a/packages/core/resourcer/src/resourcer.ts +++ b/packages/core/resourcer/src/resourcer.ts @@ -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); } diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index 012c0c875c..5292b6018f 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -128,21 +128,21 @@ export class ApplicationVersion { } export class Application 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(); @@ -150,18 +150,61 @@ export class Application 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(); + 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 exten registerCli(this); - this.version = new ApplicationVersion(this); + this._version = new ApplicationVersion(this); } private createDatabase(options: ApplicationOptions) { @@ -242,6 +285,11 @@ export class Application exten await this.pm.load(); } + async reload() { + this.init(); + await this.pm.load(); + } + getPlugin

(name: string) { return this.pm.get(name) as P; } @@ -250,8 +298,10 @@ export class Application 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 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 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(); diff --git a/packages/core/server/src/commands/index.ts b/packages/core/server/src/commands/index.ts index d95d4b36e7..e12c954fcb 100644 --- a/packages/core/server/src/commands/index.ts +++ b/packages/core/server/src/commands/index.ts @@ -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...]'); diff --git a/packages/core/server/src/commands/pm.ts b/packages/core/server/src/commands/pm.ts new file mode 100644 index 0000000000..5a0ff35430 --- /dev/null +++ b/packages/core/server/src/commands/pm.ts @@ -0,0 +1,89 @@ +import axios from 'axios'; +import { resolve } from 'path'; +import Application from '../application'; + +export default (app: Application) => { + app + .command('pm') + .argument('') + .arguments('') + .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](); + }); +}; diff --git a/packages/core/server/src/plugin-manager.ts b/packages/core/server/src/plugin-manager.ts index 8d8ce5db47..433c333fb0 100644 --- a/packages/core/server/src/plugin-manager.ts +++ b/packages/core/server/src/plugin-manager.ts @@ -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 = { 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(); + collection: Collection; + repository: PluginManagerRepository; + plugins = new Map(); 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

(pluginClass: PluginConstructor, options?: O): P { - const instance = new pluginClass(this.app, options); + remove(name: string) { + return this.plugins.delete(name); + } + + add

(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); diff --git a/packages/core/server/src/plugin.ts b/packages/core/server/src/plugin.ts index 8e82188bde..0c26391f4a 100644 --- a/packages/core/server/src/plugin.ts +++ b/packages/core/server/src/plugin.ts @@ -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 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 implements PluginInterface { } } + async disable() { + + } + collectionPath() { return null; } diff --git a/packages/core/test/src/mockServer.ts b/packages/core/test/src/mockServer.ts index 3d96b6931b..fe8cb7a37f 100644 --- a/packages/core/test/src/mockServer.ts +++ b/packages/core/test/src/mockServer.ts @@ -56,7 +56,6 @@ interface Resource { export class MockServer extends Application { async loadAndInstall(options: any = {}) { - await this.load(); await this.install({ ...options, sync: { diff --git a/packages/plugins/acl/src/__tests__/acl.test.ts b/packages/plugins/acl/src/__tests__/acl.test.ts index 8e405504e1..23cc835953 100644 --- a/packages/plugins/acl/src/__tests__/acl.test.ts +++ b/packages/plugins/acl/src/__tests__/acl.test.ts @@ -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', () => { diff --git a/packages/plugins/acl/src/__tests__/prepare.ts b/packages/plugins/acl/src/__tests__/prepare.ts index 7c83d93d01..2887fada47 100644 --- a/packages/plugins/acl/src/__tests__/prepare.ts +++ b/packages/plugins/acl/src/__tests__/prepare.ts @@ -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'; diff --git a/packages/plugins/client/src/server.ts b/packages/plugins/client/src/server.ts index 43a0e0b507..7d855bcf0b 100644 --- a/packages/plugins/client/src/server.ts +++ b/packages/plugins/client/src/server.ts @@ -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' }, diff --git a/packages/plugins/collection-manager/src/__tests__/action.test.ts b/packages/plugins/collection-manager/src/__tests__/action.test.ts index dd5e64245f..dee0a8632e 100644 --- a/packages/plugins/collection-manager/src/__tests__/action.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/action.test.ts @@ -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; }); diff --git a/packages/plugins/collection-manager/src/__tests__/beforeInitOptions.test.ts b/packages/plugins/collection-manager/src/__tests__/beforeInitOptions.test.ts index 10ebfebd05..30542685ca 100644 --- a/packages/plugins/collection-manager/src/__tests__/beforeInitOptions.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/beforeInitOptions.test.ts @@ -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'); diff --git a/packages/plugins/collection-manager/src/__tests__/collections.repository.test.ts b/packages/plugins/collection-manager/src/__tests__/collections.repository.test.ts index 3ed1887a6d..6fa9869c12 100644 --- a/packages/plugins/collection-manager/src/__tests__/collections.repository.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/collections.repository.test.ts @@ -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'); diff --git a/packages/plugins/collection-manager/src/__tests__/field-options/defaultValue.test.ts b/packages/plugins/collection-manager/src/__tests__/field-options/defaultValue.test.ts index 306bad40fa..de07448f52 100644 --- a/packages/plugins/collection-manager/src/__tests__/field-options/defaultValue.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/field-options/defaultValue.test.ts @@ -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') diff --git a/packages/plugins/collection-manager/src/__tests__/field-options/indexes.test.ts b/packages/plugins/collection-manager/src/__tests__/field-options/indexes.test.ts index 269de0105f..7789c74caa 100644 --- a/packages/plugins/collection-manager/src/__tests__/field-options/indexes.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/field-options/indexes.test.ts @@ -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') diff --git a/packages/plugins/collection-manager/src/__tests__/fields.repository.test.ts b/packages/plugins/collection-manager/src/__tests__/fields.repository.test.ts index 43a5b885e2..eb71958e3e 100644 --- a/packages/plugins/collection-manager/src/__tests__/fields.repository.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/fields.repository.test.ts @@ -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'); diff --git a/packages/plugins/collection-manager/src/__tests__/fields/belongsTo.test.ts b/packages/plugins/collection-manager/src/__tests__/fields/belongsTo.test.ts index f28b1a39c0..1786056811 100644 --- a/packages/plugins/collection-manager/src/__tests__/fields/belongsTo.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/fields/belongsTo.test.ts @@ -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'); diff --git a/packages/plugins/collection-manager/src/__tests__/fields/belongsToMany.test.ts b/packages/plugins/collection-manager/src/__tests__/fields/belongsToMany.test.ts index 57211c24e0..88607e20d8 100644 --- a/packages/plugins/collection-manager/src/__tests__/fields/belongsToMany.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/fields/belongsToMany.test.ts @@ -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'); diff --git a/packages/plugins/collection-manager/src/__tests__/fields/children.test.ts b/packages/plugins/collection-manager/src/__tests__/fields/children.test.ts index 564b99b1ba..04bce1c1a0 100644 --- a/packages/plugins/collection-manager/src/__tests__/fields/children.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/fields/children.test.ts @@ -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'); diff --git a/packages/plugins/collection-manager/src/__tests__/fields/hasMany.test.ts b/packages/plugins/collection-manager/src/__tests__/fields/hasMany.test.ts index 661f2a5702..4095ebb924 100644 --- a/packages/plugins/collection-manager/src/__tests__/fields/hasMany.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/fields/hasMany.test.ts @@ -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'); diff --git a/packages/plugins/collection-manager/src/__tests__/fields/hasOne.test.ts b/packages/plugins/collection-manager/src/__tests__/fields/hasOne.test.ts index 2df4b5a3de..9b31f52647 100644 --- a/packages/plugins/collection-manager/src/__tests__/fields/hasOne.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/fields/hasOne.test.ts @@ -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'); diff --git a/packages/plugins/collection-manager/src/__tests__/fields/reverseField.test.ts b/packages/plugins/collection-manager/src/__tests__/fields/reverseField.test.ts index 64ed017910..de185f84b3 100644 --- a/packages/plugins/collection-manager/src/__tests__/fields/reverseField.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/fields/reverseField.test.ts @@ -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'); diff --git a/packages/plugins/collection-manager/src/__tests__/http-api/collections.test.ts b/packages/plugins/collection-manager/src/__tests__/http-api/collections.test.ts index 6b32557234..ad24931cc5 100644 --- a/packages/plugins/collection-manager/src/__tests__/http-api/collections.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/http-api/collections.test.ts @@ -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({ diff --git a/packages/plugins/collection-manager/src/__tests__/index.ts b/packages/plugins/collection-manager/src/__tests__/index.ts index 9926197ece..b8b1e9af4a 100644 --- a/packages/plugins/collection-manager/src/__tests__/index.ts +++ b/packages/plugins/collection-manager/src/__tests__/index.ts @@ -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; } diff --git a/packages/plugins/collection-manager/src/__tests__/remove-collection.test.ts b/packages/plugins/collection-manager/src/__tests__/remove-collection.test.ts index da915fac10..49292d6542 100644 --- a/packages/plugins/collection-manager/src/__tests__/remove-collection.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/remove-collection.test.ts @@ -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'); diff --git a/packages/plugins/collection-manager/src/__tests__/resources/collections.fields.test.ts b/packages/plugins/collection-manager/src/__tests__/resources/collections.fields.test.ts index 1a3762b6f9..584be2cc33 100644 --- a/packages/plugins/collection-manager/src/__tests__/resources/collections.fields.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/resources/collections.fields.test.ts @@ -6,7 +6,6 @@ describe('collections.fields', () => { beforeEach(async () => { app = await createApp(); - await app.install({ clean: true }); }); afterEach(async () => { diff --git a/packages/plugins/collection-manager/src/__tests__/resources/collections.test.ts b/packages/plugins/collection-manager/src/__tests__/resources/collections.test.ts index c789fc06d2..2fe99c2cc9 100644 --- a/packages/plugins/collection-manager/src/__tests__/resources/collections.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/resources/collections.test.ts @@ -7,7 +7,6 @@ describe('collections', () => { beforeEach(async () => { app = await createApp(); - await app.install({ clean: true }); }); afterEach(async () => { diff --git a/packages/plugins/collection-manager/src/__tests__/through.test.ts b/packages/plugins/collection-manager/src/__tests__/through.test.ts index 54aded2b58..a206d09cae 100644 --- a/packages/plugins/collection-manager/src/__tests__/through.test.ts +++ b/packages/plugins/collection-manager/src/__tests__/through.test.ts @@ -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 diff --git a/packages/plugins/multi-app-manager/src/__tests__/mock-get-schema.test.ts b/packages/plugins/multi-app-manager/src/__tests__/mock-get-schema.test.ts index e201755390..2fc1e89db1 100644 --- a/packages/plugins/multi-app-manager/src/__tests__/mock-get-schema.test.ts +++ b/packages/plugins/multi-app-manager/src/__tests__/mock-get-schema.test.ts @@ -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(); }); diff --git a/packages/plugins/multi-app-manager/src/__tests__/multiple-apps.test.ts b/packages/plugins/multi-app-manager/src/__tests__/multiple-apps.test.ts index f4b312ee74..709f36030f 100644 --- a/packages/plugins/multi-app-manager/src/__tests__/multiple-apps.test.ts +++ b/packages/plugins/multi-app-manager/src/__tests__/multiple-apps.test.ts @@ -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'], }, diff --git a/packages/plugins/multi-app-manager/src/models/application.ts b/packages/plugins/multi-app-manager/src/models/application.ts index 4a7d5ee587..0a89d282c9 100644 --- a/packages/plugins/multi-app-manager/src/models/application.ts +++ b/packages/plugins/multi-app-manager/src/models/application.ts @@ -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); diff --git a/packages/plugins/users/src/migrations/20220818072639-add-users-phone.ts b/packages/plugins/users/src/migrations/20220818072639-add-users-phone.ts index 5b6540ca13..fb1e863214 100644 --- a/packages/plugins/users/src/migrations/20220818072639-add-users-phone.ts +++ b/packages/plugins/users/src/migrations/20220818072639-add-users-phone.ts @@ -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() {} } diff --git a/packages/plugins/workflow/src/client/WorkflowProvider.tsx b/packages/plugins/workflow/src/client/WorkflowProvider.tsx index 44e05f261c..9597fddfa7 100644 --- a/packages/plugins/workflow/src/client/WorkflowProvider.tsx +++ b/packages/plugins/workflow/src/client/WorkflowProvider.tsx @@ -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 ( - - - {props.children} - - + + + {props.children} + + + ); }; diff --git a/packages/plugins/workflow/src/client/WorkflowShortcut.tsx b/packages/plugins/workflow/src/client/WorkflowShortcut.tsx index 04fd2a4d10..c6e7b12a6d 100644 --- a/packages/plugins/workflow/src/client/WorkflowShortcut.tsx +++ b/packages/plugins/workflow/src/client/WorkflowShortcut.tsx @@ -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 ( + + + + ); +}; + export const WorkflowShortcut = () => { + const { t } = useTranslation(); + const history = useHistory(); + return ( + } + 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, }} /> diff --git a/packages/presets/nocobase/src/index.ts b/packages/presets/nocobase/src/index.ts index 08c4c654ed..3f88bd451b 100644 --- a/packages/presets/nocobase/src/index.ts +++ b/packages/presets/nocobase/src/index.ts @@ -5,23 +5,33 @@ export class PresetNocoBase 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(); + } + } + }); } } diff --git a/packages/samples/hello/client.d.ts b/packages/samples/hello/client.d.ts new file mode 100755 index 0000000000..765db9222a --- /dev/null +++ b/packages/samples/hello/client.d.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +export * from './lib/client'; +export { default } from './lib/client'; + diff --git a/packages/samples/hello/client.js b/packages/samples/hello/client.js new file mode 100755 index 0000000000..238820257c --- /dev/null +++ b/packages/samples/hello/client.js @@ -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]; + } + }); +}); diff --git a/packages/samples/hello/package.json b/packages/samples/hello/package.json new file mode 100644 index 0000000000..064aa90fe6 --- /dev/null +++ b/packages/samples/hello/package.json @@ -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": "*" + } +} diff --git a/packages/samples/hello/server.d.ts b/packages/samples/hello/server.d.ts new file mode 100755 index 0000000000..e70edb928a --- /dev/null +++ b/packages/samples/hello/server.d.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +export * from './lib/server'; +export { default } from './lib/server'; + diff --git a/packages/samples/hello/server.js b/packages/samples/hello/server.js new file mode 100755 index 0000000000..d06a7eb92c --- /dev/null +++ b/packages/samples/hello/server.js @@ -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]; + } + }); +}); diff --git a/packages/samples/hello/src/client/HelloDesigner.tsx b/packages/samples/hello/src/client/HelloDesigner.tsx new file mode 100644 index 0000000000..ba5602092c --- /dev/null +++ b/packages/samples/hello/src/client/HelloDesigner.tsx @@ -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 ( + + + + ); +}; diff --git a/packages/samples/hello/src/client/index.tsx b/packages/samples/hello/src/client/index.tsx new file mode 100644 index 0000000000..5cddab1e8a --- /dev/null +++ b/packages/samples/hello/src/client/index.tsx @@ -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 ( + } + 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 ( + Hello Settings, + }, + }, + }, + }} + > + + {props.children} + + + ); +}); diff --git a/packages/samples/hello/src/index.ts b/packages/samples/hello/src/index.ts new file mode 100644 index 0000000000..7ddad58145 --- /dev/null +++ b/packages/samples/hello/src/index.ts @@ -0,0 +1 @@ +export { default } from './server'; diff --git a/packages/samples/hello/src/server/actions/.gitkeep b/packages/samples/hello/src/server/actions/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/samples/hello/src/server/collections/.gitkeep b/packages/samples/hello/src/server/collections/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/samples/hello/src/server/index.ts b/packages/samples/hello/src/server/index.ts new file mode 100644 index 0000000000..82f3baf021 --- /dev/null +++ b/packages/samples/hello/src/server/index.ts @@ -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; diff --git a/packages/samples/hello/src/server/models/.gitkeep b/packages/samples/hello/src/server/models/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/samples/hello/src/server/repositories/.gitkeep b/packages/samples/hello/src/server/repositories/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tsconfig.json b/tsconfig.json index 6f0e29ad0f..52ad39576e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,9 @@ "@nocobase/app-*": [ "packages/app/*/src" ], + "@nocobase/plugin-*-sample": [ + "packages/samples/*/src" + ], "@nocobase/plugin-*": [ "packages/plugins/*/src" ],