feat: improve plugin manager ui (#1650)

* feat: improve plugin view

* feat: work compatibility

* feat: avoid cause error

* feat: complete

* docs: revert

* fix: header cannot displayed

* feat: improve

* feat: update page css

* feat: update fixedblock design

* chore: upgrade antd

* fix: improve code

* fix: build error

* fix: build error

* fix: pagination cannot be fully displayed

* feat: improve

* fix: ts error

* chore: sqlite view field test

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
Co-authored-by: chareice <chareice@live.com>
This commit is contained in:
Dunqing 2023-04-12 12:24:09 +08:00 committed by GitHub
parent 7c06a929ef
commit 1fdc456c0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 8349 additions and 8717 deletions

View File

@ -17,7 +17,7 @@
"@nocobase/sdk": "0.9.1-alpha.2",
"@nocobase/utils": "0.9.1-alpha.2",
"ahooks": "^3.7.2",
"antd": "4.22.8",
"antd": "^4.24.8",
"axios": "^0.26.1",
"classnames": "^2.3.1",
"cron-parser": "^4.6.0",

View File

@ -38,6 +38,9 @@ export default {
"Unconnected": "未连接",
"System settings": "系统设置",
"System title": "系统名称",
"Setting": "设置",
"Enable": "启用",
"Disable": "禁用",
"Logo": "Logo",
"Add menu item": "添加菜单项",
"Page": "页面",

View File

@ -1,8 +1,23 @@
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons';
import { css } from '@emotion/css';
import { Avatar, Card, Layout, Menu, message, Modal, PageHeader, Popconfirm, Result, Spin, Switch, Tabs } from 'antd';
import {
Layout,
Menu,
message,
Modal,
PageHeader,
Popconfirm,
Result,
Space,
Spin,
Table,
TableProps,
Tabs,
TabsProps,
Tag,
Typography,
} from 'antd';
import { sortBy } from 'lodash';
import React, { createContext, useContext, useMemo } from 'react';
import React, { createContext, useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Redirect, useHistory, useRouteMatch } from 'react-router-dom';
import { ACLPane } from '../acl';
@ -12,179 +27,287 @@ import { CollectionManagerPane } from '../collection-manager';
import { useDocumentTitle } from '../document-title';
import { Icon } from '../icon';
import { RouteSwitchContext } from '../route-switch';
import { useCompile } from '../schema-component';
import { useCompile, useTableSize } from '../schema-component';
import { useParseMarkdown } from '../schema-component/antd/markdown/util';
import { BlockTemplatesPane } from '../schema-templates';
import { SystemSettingsPane } from '../system-settings';
const { Link } = Typography;
export const SettingsCenterContext = createContext<any>({});
const PluginCard = (props) => {
const history = useHistory<any>();
const { data = {} } = props;
const api = useAPIClient();
const { t } = useTranslation();
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 (
<Card
bordered={false}
style={{ width: 'calc(20% - 24px)', marginRight: 24, marginBottom: 24 }}
actions={[
data.enabled ? (
<SettingOutlined
onClick={() => {
history.push(`/admin/settings/${data.name}`);
}}
/>
) : null,
<Popconfirm
title={t('Are you sure to delete this plugin?')}
onConfirm={async () => {
await api.request({
url: `pm:remove/${data.name}`,
});
message.success(t('插件删除成功'));
window.location.reload();
}}
onCancel={() => {}}
okText={t('Yes')}
cancelText={t('No')}
>
<DeleteOutlined />
</Popconfirm>,
<Switch
size={'small'}
onChange={async (checked) => {
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'}/${data.name}`,
});
window.location.reload();
// message.success(checked ? t('插件激活成功') : t('插件禁用成功'));
}}
defaultChecked={data.enabled}
></Switch>,
].filter(Boolean)}
<div
className={css`
background: #ffffff;
padding: var(--nb-spacing);
height: 60vh;
overflow-y: auto;
`}
>
<Card.Meta
className={css`
.ant-card-meta-avatar {
margin-top: 8px;
.ant-avatar {
border-radius: 2px;
}
}
`}
avatar={<Avatar />}
description={data.description}
title={
<span>
{data.name}
<span
className={css`
display: block;
color: rgba(0, 0, 0, 0.45);
font-weight: normal;
font-size: 13px;
// margin-left: 8px;
`}
>
{data.version}
</span>
</span>
}
/>
</Card>
{loading || parseLoading ? (
<Spin />
) : (
<div className="nb-markdown" dangerouslySetInnerHTML={{ __html: error ? '' : html }}></div>
)}
</div>
);
};
const BuiltInPluginCard = (props) => {
const { data } = props;
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({
url: 'applicationPlugins:list',
params: {
filter: props.filter,
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 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'}/${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();
return (
<Card
bordered={false}
style={{ width: 'calc(20% - 24px)', marginRight: 24, marginBottom: 24 }}
// actions={[<a>Settings</a>, <a>Remove</a>, <Switch size={'small'} defaultChecked={true}></Switch>]}
<div
className={css`
width: 100%;
height: 100%;
background: #fff;
padding: var(--nb-spacing);
`}
>
<Card.Meta
<Modal
footer={false}
className={css`
.ant-card-meta-avatar {
margin-top: 8px;
.ant-avatar {
border-radius: 2px;
.ant-modal-header {
background: #f0f2f5;
padding-bottom: 8px;
}
.ant-modal-body {
padding-top: 0;
}
.ant-modal-body {
background: #f0f2f5;
.plugin-desc {
padding-bottom: 8px;
}
}
`}
avatar={<Avatar />}
description={data.description}
width="70%"
title={
<span>
{data.name}
<span
<Typography.Title level={2} style={{ margin: 0 }}>
{plugin?.displayName || plugin?.name}
<Tag
className={css`
display: block;
color: rgba(0, 0, 0, 0.45);
font-weight: normal;
font-size: 13px;
// margin-left: 8px;
vertical-align: middle;
margin-top: -3px;
margin-left: 8px;
`}
>
{data.version}
</span>
</span>
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}
/>
</Card>
</div>
);
};
const LocalPlugins = () => {
const { data, loading } = useRequest({
url: 'applicationPlugins:list',
params: {
filter: {
'builtIn.$isFalsy': true,
},
sort: 'name',
},
});
if (loading) {
return <Spin />;
}
return (
<>
{data?.data?.map((item) => {
return <PluginCard data={item} />;
})}
</>
<PluginTable
filter={{
'builtIn.$isFalsy': true,
}}
></PluginTable>
);
};
const BuiltinPlugins = () => {
const { data, loading } = useRequest({
url: 'applicationPlugins:list',
params: {
filter: {
'builtIn.$isTruly': true,
},
sort: 'name',
},
});
if (loading) {
return <Spin />;
}
return (
<>
{data?.data?.map((item) => {
return <BuiltInPluginCard data={item} />;
})}
</>
<PluginTable
builtIn
filter={{
'builtIn.$isTruly': true,
}}
></PluginTable>
);
};
@ -202,7 +325,14 @@ const PluginList = (props) => {
const { snippets = [] } = useACLRoleContext();
return snippets.includes('pm') ? (
<div>
<div
className={css`
flex: 1;
flex-direction: column;
overflow: hidden;
display: flex;
`}
>
<PageHeader
ghost={false}
title={t('Plugin manager')}
@ -219,7 +349,7 @@ const PluginList = (props) => {
</Tabs>
}
/>
<div className={'m24'} style={{ margin: 24, display: 'flex', flexFlow: 'row wrap' }}>
<div style={{ margin: 'var(--nb-spacing)', flex: 1, display: 'flex', flexFlow: 'row wrap' }}>
{React.createElement(
{
local: LocalPlugins,
@ -399,7 +529,7 @@ const SettingsCenter = (props) => {
}
/>
)}
<div className={'m24'} style={{ margin: 24 }}>
<div style={{ margin: 'var(--nb-spacing)' }}>
{aclPluginTabCheck ? (
component && React.createElement(component)
) : (

View File

@ -18,7 +18,7 @@ import {
useDocumentTitle,
useRequest,
useRoute,
useSystemSettings
useSystemSettings,
} from '../../../';
import { useCollectionManager } from '../../../collection-manager';
@ -251,6 +251,8 @@ export const InternalAdminLayout = (props: any) => {
></Layout.Sider>
<Layout.Content
className={css`
display: flex;
flex-direction: column;
position: relative;
overflow-y: auto;
height: 100vh;
@ -271,6 +273,7 @@ export const InternalAdminLayout = (props: any) => {
>
<header
className={css`
flex-shrink: 0;
height: 46px;
line-height: 46px;
background: transparent;

View File

@ -1,6 +1,6 @@
import { css } from '@emotion/css';
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
import { Modal } from 'antd';
import { Modal, ModalProps } from 'antd';
import classNames from 'classnames';
import React from 'react';
import { createPortal } from 'react-dom';
@ -12,7 +12,7 @@ const openSizeWidthMap = new Map<OpenSize, string>([
['middle', '60%'],
['large', '80%'],
]);
export const ActionModal: ComposedActionDrawer = observer((props) => {
export const ActionModal: ComposedActionDrawer<ModalProps> = observer((props) => {
const { footerNodeName = 'Action.Modal.Footer', width, ...others } = props;
const { visible, setVisible, openSize = 'large' } = useActionContext();
const actualWidth = width ?? openSizeWidthMap.get(openSize);
@ -35,7 +35,7 @@ export const ActionModal: ComposedActionDrawer = observer((props) => {
<Modal
width={actualWidth}
title={field.title}
{...others}
{...(others as ModalProps)}
destroyOnClose
visible={visible}
onCancel={() => setVisible(false, true)}

View File

@ -1,4 +1,4 @@
import { ButtonProps, DrawerProps } from 'antd';
import { ButtonProps, DrawerProps, ModalProps } from 'antd';
export type ActionProps = ButtonProps & {
component?: any;
@ -12,6 +12,6 @@ export type ComposedAction = React.FC<ActionProps> & {
[key: string]: any;
};
export type ComposedActionDrawer = React.FC<DrawerProps & { footerNodeName?: string }> & {
export type ComposedActionDrawer<T = DrawerProps> = React.FC<T & { footerNodeName?: string }> & {
Footer?: React.FC;
};

View File

@ -29,7 +29,4 @@
.ant-card {
margin-bottom: 16px !important;
}
.m24 {
margin: 16px !important;
}
}

View File

@ -1,6 +1,6 @@
import { ArrayField } from '@formily/core';
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
import { css } from '@emotion/css'
import { css } from '@emotion/css';
import { Spin, Tag } from 'antd';
import React, { useContext, useMemo, useState } from 'react';
import { SchemaComponentOptions } from '../..';
@ -88,12 +88,16 @@ export const Kanban: any = observer((props: any) => {
};
return (
<Spin wrapperClassName={css`
overflow: hidden;
> .ant-spin-container {
<Spin
wrapperClassName={css`
overflow: hidden;
height: 100%;
}
`} spinning={field.loading || false}>
> .ant-spin-container {
height: 100%;
}
`}
spinning={field.loading || false}
>
<Board
{...restProps}
allowAddCard={!!schemas.cardAdder}

View File

@ -8,7 +8,7 @@ import { useRecord } from '../../../record-provider';
const FixedBlockContext = React.createContext<{
setFixedBlock: (value: string | false) => void;
height: number;
height: number | string;
fixedBlockUID: boolean | string;
fixedBlockUIDRef: React.MutableRefObject<boolean | string>;
}>({
@ -55,7 +55,7 @@ export const FixedBlockWrapper: React.FC = (props) => {
<div
className="nb-fixed-block"
style={{
height: fixedBlockUID ? `calc(100vh - ${height}px)` : undefined,
height: fixedBlockUID ? `calc(100vh - ${height})` : undefined,
}}
>
{props.children}
@ -95,7 +95,7 @@ export const FixedBlockDesignerItem = () => {
};
interface FixedBlockProps {
height: number;
height: number | string;
}
const fixedBlockCss = css`
@ -131,7 +131,7 @@ const FixedBlock: React.FC<FixedBlockProps> = (props) => {
<div
className={fixedBlockUID ? fixedBlockCss : ''}
style={{
height: fixedBlockUID ? `calc(100vh - ${height}px)` : undefined,
height: fixedBlockUID ? `calc(100vh - ${height})` : undefined,
}}
>
{props.children}

View File

@ -63,7 +63,9 @@ const designerCss = css`
const pageDesignerCss = css`
position: relative;
z-index: 20;
padding-top: 1px;
flex: 1;
display: flex;
flex-direction: column;
&:hover {
> .general-schema-designer {
@ -104,8 +106,9 @@ const pageDesignerCss = css`
`;
const pageWithFixedBlockCss = classNames([
'nb-page',
'nb-page-content',
css`
height: 100%;
> .nb-grid:not(:last-child) {
> .nb-schema-initializer-button {
display: none;
@ -256,7 +259,7 @@ export const Page = (props) => {
/>
)}
</div>
<div className={'m24'} style={{ margin: 24 }}>
<div className="nb-page-wrapper" style={{ margin: 'var(--nb-spacing)', flex: 1 }}>
{loading ? (
<Spin />
) : !disablePageHeader && enablePageTabs ? (
@ -266,8 +269,8 @@ export const Page = (props) => {
<FixedBlock
key={schema.name}
height={
// header 46 margin 48
height + 46 + 48
// header 46 margin --nb-spacing * 2
`calc(${height}px + 46px + var(--nb-spacing) * 2)`
}
>
<SchemaComponent
@ -285,8 +288,8 @@ export const Page = (props) => {
) : (
<FixedBlock
height={
// header 46 margin 48
height + 46 + 48
// header 46 margin --nb-spacing * 2
`calc(${height}px + 46px + var(--nb-spacing) * 2)`
}
>
<div className={pageWithFixedBlockCss}>{props.children}</div>

View File

@ -9,7 +9,7 @@ import { Table as AntdTable, TableColumnProps } from 'antd';
import { default as classNames, default as cls } from 'classnames';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { DndContext, useDesignable } from '../..';
import { DndContext, useDesignable, useTableSize } from '../..';
import {
RecordIndexProvider,
RecordProvider,
@ -417,43 +417,35 @@ export const Table: any = observer((props: any) => {
);
const fieldSchema = useFieldSchema();
const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock;
const [tableHeight, setTableHeight] = useState(0);
const [headerAndPaginationHeight, setHeaderAndPaginationHeight] = useState(0);
const { height: tableHeight, tableSizeRefCallback } = useTableSize();
const scroll = useMemo(() => {
return fixedBlock
? {
x: 'max-content',
y: tableHeight - headerAndPaginationHeight,
y: tableHeight,
}
: {
x: 'max-content',
};
}, [fixedBlock, tableHeight, headerAndPaginationHeight]);
const elementRef = useRef<HTMLDivElement>();
const calcTableSize = () => {
if (!elementRef.current) return;
const clientRect = elementRef.current?.getBoundingClientRect();
setTableHeight(Math.ceil(clientRect?.height || 0));
};
useEventListener('resize', calcTableSize);
const mountedRef: React.RefCallback<HTMLDivElement> = (ref) => {
elementRef.current = ref;
calcTableSize();
};
}, [fixedBlock, tableHeight]);
return (
<div
ref={mountedRef}
className={css`
height: 100%;
overflow: hidden;
.ant-table-wrapper {
height: 100%;
.ant-spin-nested-loading {
height: 100%;
.ant-spin-container {
height: 100%;
display: flex;
flex-direction: column;
}
}
}
.ant-table {
overflow-x: auto;
@ -463,13 +455,7 @@ export const Table: any = observer((props: any) => {
>
<SortableWrapper>
<AntdTable
ref={(ref) => {
if (ref) {
const headerHeight = ref.querySelector('.ant-table-header')?.getBoundingClientRect().height || 0;
const paginationHeight = ref.querySelector('.ant-table-pagination')?.getBoundingClientRect().height || 0;
setHeaderAndPaginationHeight(Math.ceil(headerHeight + paginationHeight + 16));
}
}}
ref={tableSizeRefCallback}
rowKey={rowKey ?? defaultRowKey}
dataSource={field?.value?.slice?.()}
{...others}

View File

@ -8,3 +8,4 @@ export * from './useSchemaComponentContext';
export * from './useFieldComponentOptions';
export * from './useFieldTitle';
export * from './useProps';
export * from './useTableSize';

View File

@ -0,0 +1,31 @@
import { useEventListener } from 'ahooks';
import { useCallback, useRef, useState } from 'react';
export const useTableSize = () => {
const [height, setTableHeight] = useState(0);
const [width, setTableWidth] = useState(0);
const elementRef = useRef<HTMLDivElement>();
const calcTableSize = useCallback(() => {
if (!elementRef.current) return;
const clientRect = elementRef.current.getBoundingClientRect();
const tableHeight = Math.ceil(clientRect?.height || 0);
const headerHeight = elementRef.current.querySelector('.ant-table-header')?.getBoundingClientRect().height || 0;
const tableContentRect = elementRef.current.querySelector('.ant-table')?.getBoundingClientRect();
if (!tableContentRect) return;
const paginationRect = elementRef.current.querySelector('.ant-table-pagination')?.getBoundingClientRect();
const paginationHeight = paginationRect
? paginationRect.y - tableContentRect.height - tableContentRect.y + paginationRect.height
: 0;
setTableWidth(clientRect.width);
setTableHeight(tableHeight - headerHeight - paginationHeight);
}, []);
const tableSizeRefCallback: React.RefCallback<HTMLDivElement> = (ref) => {
elementRef.current = ref;
calcTableSize();
};
useEventListener('resize', calcTableSize);
return { height, width, tableSizeRefCallback };
};

View File

@ -1,15 +1,14 @@
import React, { useState, useEffect } from 'react';
import { DownOutlined, PlusOutlined } from '@ant-design/icons';
import { RecursionField, useFieldSchema, useField } from '@formily/react';
import { Dropdown, Menu, Button } from 'antd';
import { css } from '@emotion/css';
import { observer } from '@formily/react';
import { RecursionField, observer, useField, useFieldSchema } from '@formily/react';
import { Button, Dropdown, Menu } from 'antd';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useCollectionManager, useCollection, CollectionProvider } from '../../collection-manager';
import { ActionContext, useCompile, useActionContext } from '../../schema-component';
import { useRecordPkValue, useACLRolesCheck } from '../../acl/ACLProvider';
import { useRecord } from '../../record-provider';
import { useDesignable } from '../../';
import { useACLRolesCheck, useRecordPkValue } from '../../acl/ACLProvider';
import { CollectionProvider, useCollection, useCollectionManager } from '../../collection-manager';
import { useRecord } from '../../record-provider';
import { ActionContext, useActionContext, useCompile } from '../../schema-component';
import { linkageAction } from '../../schema-component/antd/action/utils';
export const actionDesignerCss = css`

View File

@ -81,7 +81,7 @@ export const BlockTemplateDetails = () => {
ghost={false}
title={<EditableTitle filterByTk={key} title={data?.data?.name} />}
/>
<div className={'m24'} style={{ margin: 24 }}>
<div style={{ margin: 'var(--nb-spacing)' }}>
<SchemaComponentContext.Provider value={{ ...value, designable: true }}>
<RemoteSchemaComponent uid={data?.data?.uid} />
</SchemaComponentContext.Provider>

View File

@ -11,7 +11,7 @@ export const BlockTemplatePage = () => {
return (
<div>
<AntdPageHeader ghost={false} title={t('Block templates')} />
<div className={'m24'} style={{ margin: 24 }}>
<div style={{ margin: 'var(--nb-spacing)' }}>
<CollectionManagerProvider collections={[uiSchemaTemplatesCollection]}>
<SchemaComponent schema={uiSchemaTemplatesSchema} />
</CollectionManagerProvider>

View File

@ -45,6 +45,26 @@ export class PluginManager {
this.repository.setPluginManager(this);
this.app.resourcer.define(resourceOptions);
this.app.resourcer.use(async (ctx, next) => {
await next();
const { resourceName, actionName } = ctx.action;
if (resourceName === 'applicationPlugins' && actionName === 'list') {
const lng = ctx.getCurrentLocale();
if (Array.isArray(ctx.body)) {
ctx.body = ctx.body.map((plugin) => {
const json = plugin.toJSON();
const packageName = PluginManager.getPackageName(json.name);
const packageJson = PluginManager.getPackageJson(packageName);
return {
displayName: packageJson[`displayName.${lng}`] || packageJson.displayName,
description: packageJson[`description.${lng}`] || packageJson.description,
...json,
};
});
}
}
});
this.app.acl.registerSnippet({
name: 'pm',
actions: ['pm:*', 'applicationPlugins:list'],

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-acl",
"displayName": "ACL",
"displayName.zh-CN": "权限控制",
"description": "A simple access control based on roles, resources and actions",
"description.zh-CN": "基于角色、资源和操作的权限控制插件",
"version": "0.9.1-alpha.2",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -13,12 +13,51 @@ import { getResourceLocale } from './resource';
async function getReadMe(name: string, locale: string) {
const packageName = PluginManager.getPackageName(name);
const dir = resolve(process.cwd(), 'node_modules', packageName);
let file = resolve(dir, `README.${locale}.md`);
if (fs.existsSync(file)) {
return (await fs.promises.readFile(file)).toString();
let files = [resolve(dir, `README.${locale}.md`), resolve(dir, `README.md`)];
const file = files.find((file) => {
return fs.existsSync(file);
});
return file ? (await fs.promises.readFile(file)).toString() : '';
}
async function getTabs(name: string, locale: string) {
const packageName = PluginManager.getPackageName(name);
const dir = resolve(process.cwd(), 'node_modules', packageName);
let file = resolve(dir, 'docs', locale, 'tabs.json');
if (!fs.existsSync(file)) {
// TODO: compatible README, remove it in all plugin has tabs.json
return [
{
title: 'README',
path: '__README__',
},
];
}
file = resolve(dir, `README.md`);
return (await fs.promises.readFile(file)).toString();
return JSON.parse((await fs.promises.readFile(file)).toString());
}
interface TabInfoParams {
filterByTk: string;
path: string;
locale: string;
}
async function getTabInfo({ filterByTk, path, locale }: TabInfoParams) {
const packageName = PluginManager.getPackageName(filterByTk);
const dir = resolve(process.cwd(), 'node_modules', packageName);
if (path === '__README__') {
return await getReadMe(filterByTk, locale);
}
const files = [
resolve(dir, 'docs', locale, `${path}.md`),
// default
resolve(dir, 'docs', 'en-US', `${path}.md`),
resolve(dir, 'docs', 'zh-CN', `${path}.md`),
];
const file = files.find((file) => {
return fs.existsSync(file);
});
return file ? (await fs.promises.readFile(file)).toString() : '';
}
async function getLang(ctx) {
@ -148,6 +187,24 @@ export class ClientPlugin extends Plugin {
};
await next();
},
async getTabs(ctx, next) {
const lang = await getLang(ctx);
const { filterByTk } = ctx.action.params;
ctx.body = {
filterByTk,
tabs: await getTabs(filterByTk, lang),
};
await next();
},
async getTabInfo(ctx, next) {
const locale = await getLang(ctx);
const { filterByTk } = ctx.action.params;
ctx.body = {
filterByTk,
content: await getTabInfo({ ...(ctx.action.params as any), locale }),
};
await next();
},
},
});
let root = this.options.dist || `./packages/app/client/dist`;

View File

@ -93,10 +93,20 @@ SELECT * FROM numbers;
expect(data.fields.n.type).toBe('integer');
}
console.log(
JSON.stringify(
{
nField: data.fields.n,
},
null,
2,
),
);
// cannot get field type in sqlite
if (app.db.options.dialect === 'sqlite') {
expect(data.fields.n.possibleTypes).toBeTruthy();
}
// if (app.db.options.dialect === 'sqlite') {
// expect(data.fields.n.possibleTypes).toBeTruthy();
// }
});
it('should return possible types for json fields', async () => {

View File

@ -2,7 +2,7 @@ import { Context, Next } from '@nocobase/actions';
import xlsx from 'node-xlsx';
export async function downloadXlsxTemplate(ctx: Context, next: Next) {
let { columns, explain, title } = ctx.request.body;
let { columns, explain, title } = ctx.request.body as any;
if (typeof columns === 'string') {
columns = JSON.parse(columns);
}

View File

@ -35,7 +35,7 @@ class Importer {
parseXlsx() {
const rows = this.getRows();
let columns = this.context.request.body.columns as any[];
let columns = (this.context.request.body as any).columns as any[];
if (typeof columns === 'string') {
columns = JSON.parse(columns);
}

View File

@ -0,0 +1 @@
# Map Changelog

View File

@ -0,0 +1 @@
# Map overview

View File

@ -0,0 +1 @@
# Map Installation

View File

@ -0,0 +1,18 @@
[
{
"title": "Introduction",
"path": "index"
},
{
"title": "Installation",
"path": "installation"
},
{
"title": "Usage",
"path": "usage"
},
{
"title": "Changelog",
"path": "changelog"
}
]

View File

@ -0,0 +1 @@
# Map Usage

View File

@ -0,0 +1 @@
# 地图更新日志

View File

@ -0,0 +1 @@
# 地图

View File

@ -0,0 +1 @@
# 地图安装方法

View File

@ -0,0 +1,18 @@
[
{
"title": "介绍",
"path": "index"
},
{
"title": "安装",
"path": "installation"
},
{
"title": "用法",
"path": "usage"
},
{
"title": "日志",
"path": "changelog"
}
]

View File

@ -0,0 +1 @@
# 地图用法

View File

@ -1,7 +1,10 @@
{
"name": "@nocobase/plugin-map",
"displayName": "Map",
"displayName.zh-CN": "地图",
"version": "0.9.1-alpha.2",
"description": "",
"description": "Provide map fields and blocks",
"description.zh-CN": "提供地图字段和区块",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",

View File

@ -98,7 +98,11 @@ export class PluginMultiAppManager extends Plugin {
return req.headers['x-app'];
}
if (req.headers['x-hostname']) {
const appInstance = await this.db.getRepository('applications').findOne({
const repository = this.db.getRepository('applications');
if (!repository) {
return null;
}
const appInstance = await repository.findOne({
filter: {
cname: req.headers['x-hostname'],
},

View File

@ -11,7 +11,7 @@ export default function (props) {
<SigninPageExtensionProvider component={OIDCList}>
<SettingsCenterProvider
settings={{
'oidc-manager': {
oidc: {
title: t('OIDC manager'),
icon: 'FileOutlined',
tabs: {

View File

@ -12,7 +12,7 @@ export default function (props) {
<SigninPageExtensionProvider component={SAMLList}>
<SettingsCenterProvider
settings={{
'saml-manager': {
saml: {
title: t('SAML manager'),
icon: 'FileOutlined',
tabs: {

View File

@ -531,7 +531,7 @@ export function SchemaConfig({ value, onChange }) {
<SchemaInitializerProvider initializers={{ AddBlockButton, AddFormField, AddActionButton, ...trigger.initializers, ...nodeInitializers }}>
<SchemaComponentRefreshProvider
onRefresh={() => {
const { tabs, footer } = get(schema.toJSON(), 'properties.drawer.properties');
const { tabs, footer } = get(schema.toJSON(), 'properties.drawer.properties') as any;
const fields: any[] = [];
findFormFields(tabs, fields);

16310
yarn.lock

File diff suppressed because it is too large Load Diff