feat(plugin-manager): better plugin manager experience (#1927)

This commit is contained in:
lincode 2023-05-29 11:06:54 +08:00 committed by GitHub
parent 82e6c7bb40
commit 4296db5859
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 594 additions and 317 deletions

View File

@ -0,0 +1,397 @@
import React, { useEffect, useMemo, useState, useCallback, MouseEventHandler } from 'react';
import { useAPIClient, useRequest } from '../api-client';
import {
Avatar,
Card,
message,
Modal,
Popconfirm,
Spin,
Switch,
Tabs,
TabsProps,
Tag,
Tooltip,
Typography,
} from 'antd';
import { css } from '@emotion/css';
import cls from 'classnames';
import { useHistory } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons';
import { useParseMarkdown } from '../schema-component/antd/markdown/util';
import type { IPluginData } from '.';
interface PluginDocumentProps {
path: string;
name: string;
}
interface ICommonCard {
onClick: () => void;
name: string;
description: string;
title: string;
displayName: string;
actions?: JSX.Element[];
}
interface IPluginDetail {
plugin: any;
onCancel: () => void;
items: TabsProps['items'];
}
/**
* get color by string
* TODO: real avatar
* @param str
*/
const stringToColor = function (str: string) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
let color = '#';
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff;
color += ('00' + value.toString(16)).substr(-2);
}
return color;
};
const PluginDocument: React.FC<PluginDocumentProps> = (props) => {
const [docLang, setDocLang] = useState('');
const { name, path } = props;
const { data, loading, error } = useRequest(
{
url: '/plugins:getTabInfo',
params: {
filterByTk: name,
path: path,
locale: docLang,
},
},
{
refreshDeps: [name, path, docLang],
},
);
const { html, loading: parseLoading } = useParseMarkdown(data?.data?.content);
const htmlWithOutRelativeDirect = useMemo(() => {
if (html) {
const pattern = /<a\s+href="\..*?\/([^/]+)"/g;
return html.replace(pattern, (match, $1) => match + `onclick="return false;"`); // prevent the default event of <a/>
}
}, [html]);
const handleSwitchDocLang = useCallback((e: MouseEvent) => {
const lang = (e.target as HTMLDivElement).innerHTML;
if (lang.trim() === '中文') {
setDocLang('zh-CN');
} else if (lang.trim() === 'English') {
setDocLang('en-US');
}
}, []);
useEffect(() => {
const md = document.getElementById('pm-md-preview');
md.addEventListener('click', handleSwitchDocLang);
return () => {
removeEventListener('click', handleSwitchDocLang);
};
}, [handleSwitchDocLang]);
return (
<div
className={css`
background: #ffffff;
padding: var(--nb-spacing); // if the antd can upgrade to v5.0, theme token will be better
height: 60vh;
overflow-y: auto;
`}
id="pm-md-preview"
>
{loading || parseLoading ? (
<Spin />
) : (
<div className="nb-markdown" dangerouslySetInnerHTML={{ __html: error ? '' : htmlWithOutRelativeDirect }}></div>
)}
</div>
);
};
function PluginDetail(props: IPluginDetail) {
const { plugin, onCancel, items } = props;
return (
<Modal
footer={false}
className={css`
.ant-modal-header {
background: #f0f2f5;
padding-bottom: 8px;
}
.ant-modal-body {
padding-top: 0;
}
.ant-modal-body {
background: #f0f2f5;
.plugin-desc {
padding-bottom: 8px;
}
}
`}
width="70%"
title={
<Typography.Title level={2} style={{ margin: 0 }}>
{plugin?.displayName || plugin?.name}
<Tag
className={css`
vertical-align: middle;
margin-top: -3px;
margin-left: 8px;
`}
>
v{plugin?.version}
</Tag>
</Typography.Title>
}
open={!!plugin}
onCancel={onCancel}
destroyOnClose
>
{plugin?.description && <div className={'plugin-desc'}>{plugin?.description}</div>}
<Tabs items={items}></Tabs>
</Modal>
);
}
function CommonCard(props: ICommonCard) {
const { onClick, name, displayName, actions, description, title } = props;
return (
<Card
bordered={false}
style={{ width: 'calc(20% - 24px)', marginRight: 24, marginBottom: 24, transition: 'all 0.35s ease-in-out' }}
onClick={onClick}
className={cls(css`
&:hover {
border: 1px solid var(--antd-wave-shadow-color);
cursor: pointer;
}
border: 1px solid transparent;
`)}
actions={actions}
// 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 style={{ background: `${stringToColor(name)}` }}>{name?.[0]}</Avatar>}
description={
<Tooltip title={description} placement="bottom">
<div
style={{
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{description || '-'}
</div>
</Tooltip>
}
title={
<span>
{displayName || name}
<span
className={css`
display: block;
color: rgba(0, 0, 0, 0.45);
font-weight: normal;
font-size: 13px;
// margin-left: 8px;
`}
>
{title}
</span>
</span>
}
/>
</Card>
);
}
export const PluginCard = (props: { data: IPluginData }) => {
const history = useHistory<any>();
const { data } = props;
const api = useAPIClient();
const { t } = useTranslation();
const { enabled, name, displayName, id, description, version } = data;
const [plugin, setPlugin] = useState<any>(null);
const { data: tabsData, run } = useRequest(
{
url: '/plugins:getTabs',
},
{
manual: true,
},
);
const items = useMemo<TabsProps['items']>(() => {
return tabsData?.data?.tabs.map((item) => {
return {
label: item.title,
key: item.path,
children: React.createElement(PluginDocument, {
name: tabsData?.data.filterByTk,
path: item.path,
}),
};
});
}, [tabsData?.data]);
const actions = useMemo(
() =>
[
enabled ? (
<SettingOutlined
onClick={(e) => {
e.stopPropagation();
history.push(`/admin/settings/${name}`);
}}
/>
) : null,
<Popconfirm
key={id}
title={t('Are you sure to delete this plugin?')}
onConfirm={async (e) => {
e.stopPropagation();
await api.request({
url: `pm:remove/${name}`,
});
message.success(t('插件删除成功'));
window.location.reload();
}}
onCancel={(e) => e.stopPropagation()}
okText={t('Yes')}
cancelText={t('No')}
>
<DeleteOutlined onClick={(e) => e.stopPropagation()} />
</Popconfirm>,
<Switch
key={id}
size={'small'}
onChange={async (checked, e) => {
e.stopPropagation();
Modal.warn({
title: checked ? t('Plugin staring') : t('Plugin stopping'),
content: t('The application is reloading, please do not close the page.'),
okButtonProps: {
style: {
display: 'none',
},
},
});
await api.request({
url: `pm:${checked ? 'enable' : 'disable'}/${name}`,
});
window.location.reload();
// message.success(checked ? t('插件激活成功') : t('插件禁用成功'));
}}
defaultChecked={enabled}
></Switch>,
].filter(Boolean),
[api, enabled, history, id, name, t],
);
return (
<>
<PluginDetail plugin={plugin} onCancel={() => setPlugin(null)} items={items} />
<CommonCard
onClick={() => {
setPlugin(data);
run({
params: {
filterByTk: name,
},
});
}}
name={name}
description={description}
title={version}
actions={actions}
displayName={displayName}
/>
</>
);
};
export const BuiltInPluginCard = (props: { data: IPluginData }) => {
const {
data: { description, name, version, displayName },
data,
} = props;
const history = useHistory();
const [plugin, setPlugin] = useState<any>(null);
const { data: tabsData, run } = useRequest(
{
url: '/plugins:getTabs',
},
{
manual: true,
},
);
const items = useMemo(() => {
return tabsData?.data?.tabs.map((item) => {
return {
label: item.title,
key: item.path,
children: React.createElement(PluginDocument, {
name: tabsData?.data.filterByTk,
path: item.path,
}),
};
});
}, [tabsData?.data]);
return (
<>
<PluginDetail plugin={plugin} onCancel={() => setPlugin(null)} items={items} />
<CommonCard
onClick={() => {
setPlugin(data);
run({
params: {
filterByTk: name,
},
});
}}
name={name}
displayName={displayName}
description={description}
title={version}
actions={[
<div key="placeholder-comp"></div>,
<SettingOutlined
key="settings"
onClick={(e) => {
e.stopPropagation();
history.push(`/admin/settings/${name}`);
}}
/>,
]}
/>
</>
);
};
function useCallabck(arg0: () => void, arg1: undefined[]) {
throw new Error('Function not implemented.');
}

View File

@ -16,7 +16,7 @@ export const PluginManagerLink = () => {
icon={<ApiOutlined />}
title={t('Plugin manager')}
onClick={() => {
history.push('/admin/pm/list');
history.push('/admin/pm/list/');
}}
/>
</Tooltip>

View File

@ -1,313 +1,105 @@
import { css } from '@emotion/css';
import {
Layout,
Menu,
message,
Modal,
PageHeader,
Popconfirm,
Result,
Space,
Spin,
Table,
TableProps,
Tabs,
TabsProps,
Tag,
Typography,
} from 'antd';
import { Layout, Menu, PageHeader, Result, Spin, Tabs } from 'antd';
import { sortBy } from 'lodash';
import React, { createContext, useContext, useMemo, useState } from 'react';
import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Redirect, useHistory, useRouteMatch } from 'react-router-dom';
import { ACLPane } from '../acl';
import { useACLRoleContext } from '../acl/ACLProvider';
import { useAPIClient, useRequest } from '../api-client';
import { useRequest } from '../api-client';
import { CollectionManagerPane } from '../collection-manager';
import { useDocumentTitle } from '../document-title';
import { Icon } from '../icon';
import { RouteSwitchContext } from '../route-switch';
import { useCompile, useTableSize } from '../schema-component';
import { useParseMarkdown } from '../schema-component/antd/markdown/util';
import { useCompile } from '../schema-component';
import { BlockTemplatesPane } from '../schema-templates';
import { SystemSettingsPane } from '../system-settings';
const { Link } = Typography;
import { BuiltInPluginCard, PluginCard } from './Card';
export interface TData {
data: IPluginData[];
meta: IMetaData;
}
export interface IPluginData {
id: number;
createdAt: Date;
updatedAt: Date;
name: string;
displayName: string;
version: string;
enabled: boolean;
installed: boolean;
builtIn: boolean;
options: Record<string, unknown>;
description?: string;
}
export interface IMetaData {
count: number;
page: number;
pageSize: number;
totalPage: number;
allowedActions: AllowedActions;
}
export interface AllowedActions {
view: number[];
update: number[];
destroy: number[];
}
// TODO: refactor card/built-int card
export const SettingsCenterContext = createContext<any>({});
interface PluginTableProps {
filter: any;
builtIn?: boolean;
}
interface PluginDocumentProps {
path: string;
name: string;
}
const PluginDocument: React.FC<PluginDocumentProps> = (props) => {
const { data, loading, error } = useRequest(
{
url: '/plugins:getTabInfo',
params: {
filterByTk: props.name,
path: props.path,
},
},
{
refreshDeps: [props.name, props.path],
},
);
const { html, loading: parseLoading } = useParseMarkdown(data?.data?.content);
return (
<div
className={css`
background: #ffffff;
padding: var(--nb-spacing);
height: 60vh;
overflow-y: auto;
`}
>
{loading || parseLoading ? (
<Spin />
) : (
<div className="nb-markdown" dangerouslySetInnerHTML={{ __html: error ? '' : html }}></div>
)}
</div>
);
};
const PluginTable: React.FC<PluginTableProps> = (props) => {
const { builtIn } = props;
const history = useHistory<any>();
const api = useAPIClient();
const [plugin, setPlugin] = useState<any>(null);
const { t, i18n } = useTranslation();
const settingItems = useContext(SettingsCenterContext);
const { data, loading } = useRequest({
const LocalPlugins = () => {
// TODO: useRequest types for data ts type
const { data, loading }: { data: TData; loading: boolean } = useRequest<TData>({
url: 'applicationPlugins:list',
params: {
filter: props.filter,
filter: {
'builtIn.$isFalsy': true,
},
sort: 'name',
paginate: false,
},
});
const { data: tabsData, run } = useRequest(
{
url: '/plugins:getTabs',
},
{
manual: true,
},
);
const columns = useMemo<TableProps<any>['columns']>(() => {
return [
{
title: t('Plugin name'),
dataIndex: 'displayName',
width: 300,
render: (displayName, record) => displayName || record.name,
},
{
title: t('Description'),
dataIndex: 'description',
ellipsis: true,
},
{
title: t('Version'),
dataIndex: 'version',
width: 300,
},
// {
// title: t('Author'),
// dataIndex: 'author',
// width: 200,
// },
{
title: t('Actions'),
width: 300,
render(data) {
return (
<Space>
<Link
onClick={() => {
setPlugin(data);
run({
params: {
filterByTk: data.name,
},
});
}}
>
{t('View')}
</Link>
{data.enabled && settingItems[data.name] ? (
<Link
onClick={() => {
history.push(`/admin/settings/${data.name}`);
}}
>
{t('Setting')}
</Link>
) : null}
{!builtIn ? (
<>
<Link
onClick={async () => {
const checked = !data.enabled;
Modal.warn({
title: checked ? t('Plugin starting') : t('Plugin stopping'),
content: t('The application is reloading, please do not close the page.'),
okButtonProps: {
style: {
display: 'none',
},
},
});
await api.request({
url: `pm:${checked ? 'enable' : 'disable'}/${data.name}`,
});
window.location.reload();
// message.success(checked ? t('插件激活成功') : t('插件禁用成功'));
}}
>
{t(data.enabled ? 'Disable' : 'Enable')}
</Link>
<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')}
>
<Link>{t('Delete')}</Link>
</Popconfirm>
</>
) : null}
</Space>
);
},
},
];
}, [t, builtIn]);
const items = useMemo<TabsProps['items']>(() => {
return tabsData?.data?.tabs.map((item) => {
return {
label: item.title,
key: item.path,
children: React.createElement(PluginDocument, {
name: tabsData?.data.filterByTk,
path: item.path,
}),
};
});
}, [tabsData?.data]);
const { height, tableSizeRefCallback } = useTableSize();
if (loading) {
return <Spin />;
}
return (
<div
className={css`
width: 100%;
height: 100%;
background: #fff;
padding: var(--nb-spacing);
`}
>
<Modal
footer={false}
className={css`
.ant-modal-header {
background: #f0f2f5;
padding-bottom: 8px;
}
.ant-modal-body {
padding-top: 0;
}
.ant-modal-body {
background: #f0f2f5;
.plugin-desc {
padding-bottom: 8px;
}
}
`}
width="70%"
title={
<Typography.Title level={2} style={{ margin: 0 }}>
{plugin?.displayName || plugin?.name}
<Tag
className={css`
vertical-align: middle;
margin-top: -3px;
margin-left: 8px;
`}
>
v{plugin?.version}
</Tag>
</Typography.Title>
}
open={!!plugin}
onCancel={() => setPlugin(null)}
>
{plugin?.description && <div className={'plugin-desc'}>{plugin?.description}</div>}
<Tabs items={items}></Tabs>
</Modal>
<Table
ref={tableSizeRefCallback}
pagination={false}
className={css`
.ant-spin-nested-loading {
height: 100%;
.ant-spin-container {
height: 100%;
display: flex;
flex-direction: column;
.ant-table {
flex: 1;
}
}
}
height: 100%;
`}
scroll={{
y: height,
}}
dataSource={data?.data || []}
loading={loading}
columns={columns}
/>
</div>
);
};
const LocalPlugins = () => {
return (
<PluginTable
filter={{
'builtIn.$isFalsy': true,
}}
></PluginTable>
<>
{data?.data?.map((item) => {
const { id } = item;
return <PluginCard data={item} key={id} />;
})}
</>
);
};
const BuiltinPlugins = () => {
return (
<PluginTable
builtIn
filter={{
const { data, loading } = useRequest({
url: 'applicationPlugins:list',
params: {
filter: {
'builtIn.$isTruly': true,
}}
></PluginTable>
},
sort: 'name',
paginate: false,
},
});
if (loading) {
return <Spin />;
}
return (
<>
{data?.data?.map((item) => {
const { id } = item;
return <BuiltInPluginCard data={item} key={id} />;
})}
</>
);
};
@ -324,15 +116,15 @@ const PluginList = (props) => {
const { t } = useTranslation();
const { snippets = [] } = useACLRoleContext();
useEffect(() => {
const { tabName } = match.params;
if (!tabName) {
history.replace(`/admin/pm/list/local/`);
}
}, []);
return snippets.includes('pm') ? (
<div
className={css`
flex: 1;
flex-direction: column;
overflow: hidden;
display: flex;
`}
>
<div>
<PageHeader
ghost={false}
title={t('Plugin manager')}
@ -340,7 +132,7 @@ const PluginList = (props) => {
<Tabs
activeKey={tabName}
onChange={(activeKey) => {
history.push(`/admin/pm/list/${activeKey}`);
history.push(`/admin/pm/list/${activeKey}/`);
}}
>
<Tabs.TabPane tab={t('Local')} key={'local'} />
@ -349,7 +141,7 @@ const PluginList = (props) => {
</Tabs>
}
/>
<div style={{ margin: 'var(--nb-spacing)', flex: 1, display: 'flex', flexFlow: 'row wrap' }}>
<div className={'m24'} style={{ margin: 24, display: 'flex', flexFlow: 'row wrap' }}>
{React.createElement(
{
local: LocalPlugins,
@ -529,7 +321,7 @@ const SettingsCenter = (props) => {
}
/>
)}
<div style={{ margin: 'var(--nb-spacing)' }}>
<div className={'m24'} style={{ margin: 24 }}>
{aclPluginTabCheck ? (
component && React.createElement(component)
) : (
@ -555,7 +347,7 @@ export const PMProvider = (props) => {
routes[1].routes.unshift(
{
type: 'route',
path: '/admin/pm/list/:tabName?',
path: '/admin/pm/list/:tabName?/:mdfile?',
component: PluginList,
},
{

View File

@ -3,7 +3,7 @@
"displayName": "ACL",
"displayName.zh-CN": "权限控制",
"description": "A simple access control based on roles, resources and actions",
"description.zh-CN": "基于角色、资源和操作的权限控制插件",
"description.zh-CN": "基于角色、资源和操作的权限控制",
"version": "0.9.4-alpha.2",
"license": "AGPL-3.0",
"main": "./lib/index.js",

View File

@ -1,6 +1,10 @@
{
"name": "@nocobase/plugin-audit-logs",
"version": "0.9.4-alpha.2",
"displayName": "audit-logs",
"displayName.zh-CN": "审计日志",
"description": "audit logs plugin",
"description.zh-CN": "审计日志。",
"main": "./lib/server/index.js",
"types": "./lib/server/index.d.ts",
"license": "AGPL-3.0",

View File

@ -1,5 +1,9 @@
{
"name": "@nocobase/plugin-charts",
"displayName": "charts",
"displayName.zh-CN": "图表",
"description": "Out-of-the-box, feature-rich chart plugins.",
"description.zh-CN": "开箱即用、丰富的报表。",
"version": "0.9.4-alpha.2",
"main": "lib/server/index.js",
"devDependencies": {

View File

@ -1,6 +1,10 @@
{
"name": "@nocobase/plugin-china-region",
"version": "0.9.4-alpha.2",
"displayName": "china-region",
"displayName.zh-CN": "中国行政区",
"description": "Chinese Administrative Division Plugin, including all administrative regions of China.",
"description.zh-CN": "中国行政区划插件,内含所有中国行政区域。",
"main": "lib/index.js",
"license": "AGPL-3.0",
"dependencies": {

View File

@ -1,5 +1,9 @@
{
"name": "@nocobase/plugin-client",
"displayName": "client",
"displayName.zh-CN": "客户端",
"description": "client",
"description.zh-CN": "客户端。",
"version": "0.9.4-alpha.2",
"main": "lib/index.js",
"license": "AGPL-3.0",

View File

@ -1,5 +1,9 @@
{
"name": "@nocobase/plugin-collection-manager",
"displayName": "collection manager plugin",
"displayName.zh-CN": "数据库管理",
"description": " database management plugin designed to simplify the process of managing and operating databases. It seamlessly integrates with various relational database systems such as MySQL and PostgreSQL, and provides an intuitive user interface for performing various database tasks.",
"description.zh-CN": "可以与多种关系型数据库系统如MySQL、PostgreSQL无缝集成并提供直观的用户界面来执行各种数据库任务。",
"version": "0.9.4-alpha.2",
"main": "lib/index.js",
"license": "AGPL-3.0",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-duplicator",
"displayName": "duplicator",
"displayName.zh-CN": "NocoBase应用备份与还原",
"description": "Backup and Restore Plugin for NocoBase applications, used for scenarios such as application replication, migration, and upgrade.",
"description.zh-CN": "NocoBase 应用的备份与还原插件,可用于应用的复制、迁移、升级等场景。",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-error-handler",
"displayName": "error handler",
"displayName.zh-CN": "错误处理",
"description": "Managing and handling errors and exceptions in an application.",
"description.zh-CN": "管理和处理应用程序中的错误和异常。",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-export",
"displayName": "export",
"displayName.zh-CN": "导出",
"description": "export data",
"description.zh-CN": "导出数据。",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-file-manager",
"version": "0.9.4-alpha.2",
"description": "",
"displayName": "file manager",
"displayName.zh-CN": "文件管理",
"description": "file management plugin.",
"description.zh-CN": "文件管理。",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-formula-field",
"displayName": "formula-field",
"displayName.zh-CN": "字段",
"description": "formula-field",
"description.zh-CN": "字段。",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-graph-collection-manager",
"displayName": "graph collection manager",
"displayName.zh-CN": "数据库可视化管理",
"description": "database collection manage",
"description.zh-CN": "数据库管理。",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-iframe-block",
"displayName": "iframe",
"displayName.zh-CN": "iframe",
"description": "create and manage IFrame blocks on the page for embedding and displaying external web pages or content.",
"description.zh-CN": "在页面上创建和管理iframe用于嵌入和展示外部网页或内容。",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-import",
"displayName": "import",
"displayName.zh-CN": "导入",
"description": "import data.",
"description.zh-CN": "导入数据。",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-math-formula-field",
"displayName": "math formula field",
"displayName.zh-CN": "数学公式字段",
"description": "A powerful mathematical formula field plugin designed to provide flexible mathematical and formula calculation capabilities for databases or data management systems. It seamlessly integrates with various database systems such as MySQL, PostgreSQL, and offers an intuitive user interface for defining and executing mathematical formula fields.",
"description.zh-CN": "一个功能强大的数学公式字段插件旨在为数据库或数据管理系统提供灵活的数学计算和公式计算功能。它可以与各种数据库系统如MySQL、PostgreSQL无缝集成并提供直观的用户界面来定义和执行数学公式字段。",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-multi-app-manager",
"displayName": "multi app manager",
"displayName.zh-CN": "多应用管理",
"description": "manage app",
"description.zh-CN": "管理应用",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,5 +1,9 @@
{
"name": "@nocobase/plugin-multi-app-share-collection",
"displayName": "multi app share collection",
"displayName.zh-CN": "多应用数据共享",
"description": "multi app share collection",
"description.zh-CN": "多应用数据共享",
"version": "0.9.4-alpha.2",
"main": "lib/server/index.js",
"devDependencies": {

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-oidc",
"displayName": "OpenID Connect",
"displayName.zh-CN": "OpenID Connect",
"description": " provide OIDC authentication and authorization functionality for applications or authentication systems.",
"description.zh-CN": "在为应用程序或身份验证系统提供OIDC认证和授权功能。",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-saml",
"displayName": "saml",
"displayName.zh-CN": "saml",
"description": "provide SAML authentication and authorization functionality for applications or authentication systems.",
"description.zh-CN": "为应用程序或身份验证系统提供SAML认证和授权功能。",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-sequence-field",
"displayName": "sequence field",
"displayName.zh-CN": "字段序列",
"description": "provide the functionality of automatically generating unique sequence values for databases or data management systems.",
"description.zh-CN": "为数据库或数据管理系统提供自动生成唯一序列值的功能。",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-snapshot-field",
"displayName": "snapshot field",
"displayName.zh-CN": "关系数据库快照",
"description": "provide fast and reliable database backup and recovery functionality for database systems.",
"description.zh-CN": "为数据库系统提供快速、可靠的数据库备份和恢复功能。。",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-system-settings",
"displayName": "system settings",
"displayName.zh-CN": "系统配置",
"description": "A plugin specifically designed to adjust the system-level settings of NocoBase.",
"description.zh-CN": "用于调整 nocobase 的系统级设置。",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-ui-routes-storage",
"displayName": "ui routes storage",
"displayName.zh-CN": "路由管理",
"description": "manage the routes of nocobase",
"description.zh-CN": "管理页面路由。",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-ui-schema-storage",
"displayName": "UI schema storage",
"displayName.zh-CN": "UI schema",
"description": "A plugin used for managing page UI schemas.",
"description.zh-CN": "UI schema 配置。",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-users",
"displayName": "users",
"displayName.zh-CN": "用户管理",
"description": "manage the uses of nocobase system",
"description.zh-CN": "管理系统用户",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-verification",
"displayName": "verification",
"displayName.zh-CN": "验证码",
"description": "verification setting.",
"description.zh-CN": "验证码配置。",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-workflow",
"displayName": "workflow",
"displayName.zh-CN": "工作流",
"description": " a powerful workflow plugin designed to support business process management and automation. .",
"description.zh-CN": "工作流插件,为业务流程管理和自动化提供支持。",
"version": "0.9.4-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -2,6 +2,10 @@
"name": "@nocobase/plugin-sample-hello",
"version": "0.9.4-alpha.2",
"main": "lib/server/index.js",
"displayName": "Sample-Hello",
"displayName.zh-CN": "插件demo-hello",
"description": "simple plugin demo",
"description.zh-CN": "这就是一个简单的插件demo",
"devDependencies": {
"@nocobase/client": "0.9.4-alpha.2",
"@nocobase/server": "0.9.4-alpha.2",