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:
parent
7c06a929ef
commit
1fdc456c0b
@ -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",
|
||||
|
@ -38,6 +38,9 @@ export default {
|
||||
"Unconnected": "未连接",
|
||||
"System settings": "系统设置",
|
||||
"System title": "系统名称",
|
||||
"Setting": "设置",
|
||||
"Enable": "启用",
|
||||
"Disable": "禁用",
|
||||
"Logo": "Logo",
|
||||
"Add menu item": "添加菜单项",
|
||||
"Page": "页面",
|
||||
|
@ -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)
|
||||
) : (
|
||||
|
@ -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;
|
||||
|
@ -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)}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -29,7 +29,4 @@
|
||||
.ant-card {
|
||||
margin-bottom: 16px !important;
|
||||
}
|
||||
.m24 {
|
||||
margin: 16px !important;
|
||||
}
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -8,3 +8,4 @@ export * from './useSchemaComponentContext';
|
||||
export * from './useFieldComponentOptions';
|
||||
export * from './useFieldTitle';
|
||||
export * from './useProps';
|
||||
export * from './useTableSize';
|
||||
|
@ -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 };
|
||||
};
|
@ -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`
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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'],
|
||||
|
@ -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",
|
||||
|
@ -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`;
|
||||
|
@ -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 () => {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
1
packages/plugins/map/docs/en-US/changelog.md
Normal file
1
packages/plugins/map/docs/en-US/changelog.md
Normal file
@ -0,0 +1 @@
|
||||
# Map Changelog
|
1
packages/plugins/map/docs/en-US/index.md
Normal file
1
packages/plugins/map/docs/en-US/index.md
Normal file
@ -0,0 +1 @@
|
||||
# Map overview
|
1
packages/plugins/map/docs/en-US/installation.md
Normal file
1
packages/plugins/map/docs/en-US/installation.md
Normal file
@ -0,0 +1 @@
|
||||
# Map Installation
|
18
packages/plugins/map/docs/en-US/tabs.json
Normal file
18
packages/plugins/map/docs/en-US/tabs.json
Normal file
@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"title": "Introduction",
|
||||
"path": "index"
|
||||
},
|
||||
{
|
||||
"title": "Installation",
|
||||
"path": "installation"
|
||||
},
|
||||
{
|
||||
"title": "Usage",
|
||||
"path": "usage"
|
||||
},
|
||||
{
|
||||
"title": "Changelog",
|
||||
"path": "changelog"
|
||||
}
|
||||
]
|
1
packages/plugins/map/docs/en-US/usage.md
Normal file
1
packages/plugins/map/docs/en-US/usage.md
Normal file
@ -0,0 +1 @@
|
||||
# Map Usage
|
1
packages/plugins/map/docs/zh-CN/changelog.md
Normal file
1
packages/plugins/map/docs/zh-CN/changelog.md
Normal file
@ -0,0 +1 @@
|
||||
# 地图更新日志
|
1
packages/plugins/map/docs/zh-CN/index.md
Normal file
1
packages/plugins/map/docs/zh-CN/index.md
Normal file
@ -0,0 +1 @@
|
||||
# 地图
|
1
packages/plugins/map/docs/zh-CN/installation.md
Normal file
1
packages/plugins/map/docs/zh-CN/installation.md
Normal file
@ -0,0 +1 @@
|
||||
# 地图安装方法
|
18
packages/plugins/map/docs/zh-CN/tabs.json
Normal file
18
packages/plugins/map/docs/zh-CN/tabs.json
Normal file
@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"title": "介绍",
|
||||
"path": "index"
|
||||
},
|
||||
{
|
||||
"title": "安装",
|
||||
"path": "installation"
|
||||
},
|
||||
{
|
||||
"title": "用法",
|
||||
"path": "usage"
|
||||
},
|
||||
{
|
||||
"title": "日志",
|
||||
"path": "changelog"
|
||||
}
|
||||
]
|
1
packages/plugins/map/docs/zh-CN/usage.md
Normal file
1
packages/plugins/map/docs/zh-CN/usage.md
Normal file
@ -0,0 +1 @@
|
||||
# 地图用法
|
@ -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",
|
||||
|
@ -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'],
|
||||
},
|
||||
|
@ -11,7 +11,7 @@ export default function (props) {
|
||||
<SigninPageExtensionProvider component={OIDCList}>
|
||||
<SettingsCenterProvider
|
||||
settings={{
|
||||
'oidc-manager': {
|
||||
oidc: {
|
||||
title: t('OIDC manager'),
|
||||
icon: 'FileOutlined',
|
||||
tabs: {
|
||||
|
@ -12,7 +12,7 @@ export default function (props) {
|
||||
<SigninPageExtensionProvider component={SAMLList}>
|
||||
<SettingsCenterProvider
|
||||
settings={{
|
||||
'saml-manager': {
|
||||
saml: {
|
||||
title: t('SAML manager'),
|
||||
icon: 'FileOutlined',
|
||||
tabs: {
|
||||
|
@ -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);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user