feat: block provider (#261)
* feat: block schema * feat: improve code * feat: improve code * feat: improve code * feat: improve code * feat: styling for kanban * feat: improve code * feat: improve code * fix(client): auto collapse menu items when overflow * feat: improve code * fix: fieldNames prop for calendar * fix: improve code * feat: improve code * feat: improve code * feat: improve code * feat: improve code * feat: improve code * feat: improve code * feat: improve code * feat: improve code * fix: list the sub-table data source * fix: useFilterByTk * feat: improve code * feat: improve code * feat: improve code
This commit is contained in:
parent
036baaa443
commit
74779a0d46
@ -5,6 +5,7 @@ import {
|
|||||||
AntdSchemaComponentProvider,
|
AntdSchemaComponentProvider,
|
||||||
APIClientProvider,
|
APIClientProvider,
|
||||||
AuthLayout,
|
AuthLayout,
|
||||||
|
BlockSchemaComponentProvider,
|
||||||
BlockTemplateDetails,
|
BlockTemplateDetails,
|
||||||
BlockTemplatePage,
|
BlockTemplatePage,
|
||||||
ChinaRegionProvider,
|
ChinaRegionProvider,
|
||||||
@ -95,6 +96,7 @@ const providers = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
BlockSchemaComponentProvider,
|
||||||
AntdSchemaComponentProvider,
|
AntdSchemaComponentProvider,
|
||||||
ChinaRegionProvider,
|
ChinaRegionProvider,
|
||||||
WorkflowRouteProvider,
|
WorkflowRouteProvider,
|
||||||
|
@ -1,25 +1,12 @@
|
|||||||
import { LockOutlined } from '@ant-design/icons';
|
import { LockOutlined } from '@ant-design/icons';
|
||||||
import { ISchema, useForm } from '@formily/react';
|
import { ISchema } from '@formily/react';
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PluginManager } from '../plugin-manager';
|
import { PluginManager } from '../plugin-manager';
|
||||||
import { ActionContext, SchemaComponent, useActionContext } from '../schema-component';
|
import { ActionContext, SchemaComponent } from '../schema-component';
|
||||||
import * as components from './Configuration';
|
import * as components from './Configuration';
|
||||||
|
|
||||||
const useCloseAction = () => {
|
|
||||||
const { setVisible } = useActionContext();
|
|
||||||
const form = useForm();
|
|
||||||
return {
|
|
||||||
async run() {
|
|
||||||
setVisible(false);
|
|
||||||
form.submit((values) => {
|
|
||||||
console.log(values);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const schema: ISchema = {
|
const schema: ISchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
@ -49,7 +36,7 @@ export const ACLShortcut = () => {
|
|||||||
setVisible(true);
|
setVisible(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SchemaComponent components={components} scope={{ useCloseAction }} schema={schema} />
|
<SchemaComponent components={components} schema={schema} />
|
||||||
</ActionContext.Provider>
|
</ActionContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,233 +0,0 @@
|
|||||||
import { Button, Checkbox, Divider, Drawer, Space, Table, Tabs, Tag, Typography } from 'antd';
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { ScopeRecordPicker } from './ScopeRecordPicker';
|
|
||||||
|
|
||||||
export function RoleManager() {}
|
|
||||||
|
|
||||||
export const RoleTable = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Space style={{ justifyContent: 'flex-end', width: '100%', marginBottom: 16 }}>
|
|
||||||
<Button key="destroy">删除</Button>
|
|
||||||
<Button type={'primary'} key="create">
|
|
||||||
添加
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
<Table
|
|
||||||
rowSelection={{
|
|
||||||
type: 'checkbox',
|
|
||||||
}}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
title: '角色名称',
|
|
||||||
dataIndex: 'title',
|
|
||||||
key: 'title',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '角色描述',
|
|
||||||
dataIndex: 'description',
|
|
||||||
key: 'description',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '默认角色',
|
|
||||||
dataIndex: 'default',
|
|
||||||
key: 'default',
|
|
||||||
render: (value) => value && '是',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'actions',
|
|
||||||
key: 'actions',
|
|
||||||
render: () => (
|
|
||||||
<Space split={<Divider type="vertical" />}>
|
|
||||||
<ConfigurePermissions />
|
|
||||||
<Typography.Link>Edit role</Typography.Link>
|
|
||||||
<Typography.Link>Delete</Typography.Link>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
dataSource={[
|
|
||||||
{
|
|
||||||
name: 'root',
|
|
||||||
title: '管理员',
|
|
||||||
description: '描述',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'member',
|
|
||||||
title: '普通成员',
|
|
||||||
description: '描述',
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConfigurePermissions = () => {
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Typography.Link onClick={() => setVisible(true)}>Configure</Typography.Link>
|
|
||||||
<Drawer width={800} title={'权限配置'} visible={visible} destroyOnClose onClose={() => setVisible(false)}>
|
|
||||||
<RolePermissions />
|
|
||||||
</Drawer>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RolePermissions = () => {
|
|
||||||
return (
|
|
||||||
<Tabs defaultActiveKey={'actions'}>
|
|
||||||
<Tabs.TabPane key={'global'} tab={'系统全局权限'}></Tabs.TabPane>
|
|
||||||
<Tabs.TabPane key={'actions'} tab={'数据操作权限'}>
|
|
||||||
<Table
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
title: '数据表名称',
|
|
||||||
dataIndex: 'title',
|
|
||||||
key: 'title',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '数据表标识',
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '权限策略',
|
|
||||||
dataIndex: 'usingConfigure',
|
|
||||||
key: 'usingConfigure',
|
|
||||||
render: (value) => (value ? <Tag>单独配置</Tag> : <Tag>通用配置</Tag>),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'actions',
|
|
||||||
key: 'actions',
|
|
||||||
render: () => <ResourceActionsConfigure />,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
dataSource={[
|
|
||||||
{
|
|
||||||
title: '用户',
|
|
||||||
name: 'users',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
<Tabs.TabPane key={'accessible'} tab={'菜单访问权限'}>
|
|
||||||
<Table
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
title: '菜单页面',
|
|
||||||
dataIndex: 'title',
|
|
||||||
key: 'title',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: (
|
|
||||||
<>
|
|
||||||
<Checkbox /> 允许访问
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
dataIndex: 'accessible',
|
|
||||||
key: 'accessible',
|
|
||||||
render: () => <Checkbox />,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
dataSource={[
|
|
||||||
{
|
|
||||||
title: '页面1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '页面2',
|
|
||||||
accessible: true,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
</Tabs>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ResourceActionsConfigure = () => {
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Typography.Link onClick={() => setVisible(true)}>Configure</Typography.Link>
|
|
||||||
<Drawer
|
|
||||||
width={800}
|
|
||||||
title={'权限配置'}
|
|
||||||
visible={visible}
|
|
||||||
destroyOnClose
|
|
||||||
onClose={() => setVisible(false)}
|
|
||||||
footer={
|
|
||||||
<Space style={{ justifyContent: 'flex-end', width: '100%' }}>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setVisible(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type={'primary'}
|
|
||||||
onClick={() => {
|
|
||||||
setVisible(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ResourceActionsForm />
|
|
||||||
</Drawer>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ResourceActionsForm = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Table
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'title',
|
|
||||||
key: 'title',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '类型',
|
|
||||||
dataIndex: 'type',
|
|
||||||
key: 'type',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: (
|
|
||||||
<>
|
|
||||||
<Checkbox /> 允许操作
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
dataIndex: 'enable',
|
|
||||||
key: 'enable',
|
|
||||||
render: () => <Checkbox />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '可操作的数据范围',
|
|
||||||
dataIndex: 'scope',
|
|
||||||
key: 'scope',
|
|
||||||
render: () => <ScopeRecordPicker />,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
dataSource={[
|
|
||||||
{
|
|
||||||
title: '添加',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '导入',
|
|
||||||
enable: true,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,77 +0,0 @@
|
|||||||
import { createForm } from '@formily/core';
|
|
||||||
import { FormContext, ISchema } from '@formily/react';
|
|
||||||
import { Button, Space } from 'antd';
|
|
||||||
import React, { useMemo } from 'react';
|
|
||||||
import { SchemaComponent } from '../';
|
|
||||||
import { AntdSchemaComponentProvider } from '../schema-component';
|
|
||||||
|
|
||||||
const schema: ISchema = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
scope: {
|
|
||||||
type: 'array',
|
|
||||||
default: [
|
|
||||||
{ id: 1, name: 'name1' },
|
|
||||||
{ id: 2, name: 'name2' },
|
|
||||||
],
|
|
||||||
'x-component': 'RecordPicker',
|
|
||||||
properties: {
|
|
||||||
actions: {
|
|
||||||
'x-component': 'ScopeActions',
|
|
||||||
},
|
|
||||||
rowSelection: {
|
|
||||||
'x-component': 'RowSelection',
|
|
||||||
'x-component-props': {
|
|
||||||
rowKey: 'id',
|
|
||||||
objectValue: true,
|
|
||||||
rowSelection: {
|
|
||||||
type: 'radio',
|
|
||||||
},
|
|
||||||
dataSource: [
|
|
||||||
{ id: 1, title: '全部数据' },
|
|
||||||
{ id: 2, title: '用户自己创建的数据' },
|
|
||||||
{ id: 3, title: '待审核的文章' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
column1: {
|
|
||||||
type: 'void',
|
|
||||||
title: '数据范围',
|
|
||||||
'x-component': 'RowSelection.Column',
|
|
||||||
'x-read-pretty': true,
|
|
||||||
properties: {
|
|
||||||
title: {
|
|
||||||
type: 'string',
|
|
||||||
'x-component': 'Input',
|
|
||||||
'x-read-pretty': true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ScopeActions = () => {
|
|
||||||
return (
|
|
||||||
<Space style={{ justifyContent: 'flex-end', width: '100%', marginBottom: 16 }}>
|
|
||||||
<Button key="destroy">删除</Button>
|
|
||||||
<Button type={'primary'} key="create">
|
|
||||||
添加
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ScopeRecordPicker = () => {
|
|
||||||
const form = useMemo(() => createForm({}), []);
|
|
||||||
return (
|
|
||||||
<FormContext.Provider value={form}>
|
|
||||||
<AntdSchemaComponentProvider>
|
|
||||||
<SchemaComponent components={{ ScopeActions }} schema={schema} />
|
|
||||||
</AntdSchemaComponentProvider>
|
|
||||||
</FormContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,6 +0,0 @@
|
|||||||
import { RoleTable } from '@nocobase/client';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
return <RoleTable />;
|
|
||||||
};
|
|
@ -1,6 +0,0 @@
|
|||||||
import { RolePermissions } from '@nocobase/client';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
return <RolePermissions />;
|
|
||||||
};
|
|
@ -1,6 +0,0 @@
|
|||||||
import { ResourceActionsForm } from '@nocobase/client';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
return <ResourceActionsForm />;
|
|
||||||
};
|
|
@ -7,10 +7,6 @@ group:
|
|||||||
path: /client
|
path: /client
|
||||||
---
|
---
|
||||||
|
|
||||||
# ACL <Badge>待定</Badge>
|
# ACL
|
||||||
|
|
||||||
访问控制列表,plugin-acl 的前端模块
|
访问控制列表,plugin-acl 的前端模块,详细文档待补充。
|
||||||
|
|
||||||
<code src="./demos/demo1.tsx" />
|
|
||||||
<code src="./demos/demo2.tsx" />
|
|
||||||
<code src="./demos/demo3.tsx" />
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
export * from './ACLProvider';
|
export * from './ACLProvider';
|
||||||
export * from './RolePermissionManager';
|
|
||||||
export * from './ACLShortcut';
|
export * from './ACLShortcut';
|
||||||
|
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { SchemaInitializerProvider } from '../schema-initializer';
|
import { SchemaInitializerContext, SchemaInitializerProvider } from '../schema-initializer';
|
||||||
import { initializes } from '../schema-initializer/Initializers';
|
|
||||||
|
const actionLogsItemGroup = {
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: '{{t("Others")}}',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
title: '{{t("Action logs")}}',
|
||||||
|
component: 'ActionLogBlockInitializer',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
export const ActionLogProvider = (props: any) => {
|
export const ActionLogProvider = (props: any) => {
|
||||||
const actionLogsItemGroup = {
|
const initializes = useContext(SchemaInitializerContext);
|
||||||
type: 'itemGroup',
|
|
||||||
title: '{{t("Others")}}',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: 'item',
|
|
||||||
title: '{{t("Action logs")}}',
|
|
||||||
component: 'ActionLogBlockInitializer',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
initializes.BlockInitializers.items.push({ ...actionLogsItemGroup });
|
initializes.BlockInitializers.items.push({ ...actionLogsItemGroup });
|
||||||
return <SchemaInitializerProvider initializers={initializes}>{props.children}</SchemaInitializerProvider>;
|
return <SchemaInitializerProvider initializers={initializes}>{props.children}</SchemaInitializerProvider>;
|
||||||
};
|
};
|
||||||
|
@ -12,5 +12,3 @@ group:
|
|||||||
## compose
|
## compose
|
||||||
|
|
||||||
<code src="./demos/demo1/index.tsx"/>
|
<code src="./demos/demo1/index.tsx"/>
|
||||||
|
|
||||||
<code src="./demos/demo2/index.tsx"/>
|
|
||||||
|
177
packages/client/src/block-provider/BlockProvider.tsx
Normal file
177
packages/client/src/block-provider/BlockProvider.tsx
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import { Field } from '@formily/core';
|
||||||
|
import { useField } from '@formily/react';
|
||||||
|
import { useRequest } from 'ahooks';
|
||||||
|
import React, { createContext, useContext } from 'react';
|
||||||
|
import { TableFieldResource, useAPIClient, useRecord } from '../';
|
||||||
|
import { CollectionProvider, useCollection, useCollectionManager } from '../collection-manager';
|
||||||
|
import { useRecordIndex } from '../record-provider';
|
||||||
|
|
||||||
|
export const BlockResourceContext = createContext(null);
|
||||||
|
export const BlockAssociationContext = createContext(null);
|
||||||
|
const BlockRequestContext = createContext(null);
|
||||||
|
|
||||||
|
export const useBlockResource = () => {
|
||||||
|
return useContext(BlockResourceContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UseReousrceProps {
|
||||||
|
resource: any;
|
||||||
|
association?: any;
|
||||||
|
useSourceId?: any;
|
||||||
|
block?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useAssociation = (props) => {
|
||||||
|
const { association } = props;
|
||||||
|
const { getCollectionField } = useCollectionManager();
|
||||||
|
if (typeof association === 'string') {
|
||||||
|
return getCollectionField(association);
|
||||||
|
} else if (association?.collectionName && association?.name) {
|
||||||
|
return getCollectionField(`${association?.collectionName}.${association?.name}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const useReousrce = (props: UseReousrceProps) => {
|
||||||
|
const { block, resource, useSourceId } = props;
|
||||||
|
const record = useRecord();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const association = useAssociation(props);
|
||||||
|
const sourceId = useSourceId?.();
|
||||||
|
const field = useField<Field>();
|
||||||
|
if (block === 'TableField') {
|
||||||
|
const options = {
|
||||||
|
field,
|
||||||
|
api,
|
||||||
|
resource,
|
||||||
|
sourceId: sourceId || record[association?.sourceKey || 'id'],
|
||||||
|
};
|
||||||
|
return new TableFieldResource(options);
|
||||||
|
}
|
||||||
|
const __parent = useContext(BlockRequestContext);
|
||||||
|
if (__parent?.block === 'TableField' && __parent?.resource instanceof TableFieldResource) {
|
||||||
|
return __parent.resource;
|
||||||
|
}
|
||||||
|
if (!association) {
|
||||||
|
return api.resource(resource);
|
||||||
|
}
|
||||||
|
if (sourceId) {
|
||||||
|
return api.resource(resource, sourceId);
|
||||||
|
}
|
||||||
|
return api.resource(resource, record[association?.sourceKey || 'id']);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useActionParams = (props) => {
|
||||||
|
const { useParams } = props;
|
||||||
|
const params = useParams?.() || {};
|
||||||
|
return { ...props.params, ...params };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useResourceAction = (props, opts = {}) => {
|
||||||
|
const { resource, action } = props;
|
||||||
|
const { fields } = useCollection();
|
||||||
|
const appends = fields?.filter((field) => field.target).map((field) => field.name);
|
||||||
|
const params = useActionParams(props);
|
||||||
|
if (appends?.length) {
|
||||||
|
params['appends'] = appends;
|
||||||
|
}
|
||||||
|
const result = useRequest(
|
||||||
|
(params) => (action ? resource[action](params).then((res) => res.data) : Promise.resolve({})),
|
||||||
|
{
|
||||||
|
...opts,
|
||||||
|
defaultParams: [params],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MaybeCollectionProvider = (props) => {
|
||||||
|
const { collection } = props;
|
||||||
|
return collection ? (
|
||||||
|
<CollectionProvider collection={collection}>{props.children}</CollectionProvider>
|
||||||
|
) : (
|
||||||
|
<>{props.children}</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BlockRequestProvider = (props) => {
|
||||||
|
const field = useField();
|
||||||
|
const resource = useBlockResource();
|
||||||
|
const service = useResourceAction(
|
||||||
|
{ ...props, resource },
|
||||||
|
{
|
||||||
|
...props.requestOptions,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const __parent = useContext(BlockRequestContext);
|
||||||
|
return (
|
||||||
|
<BlockRequestContext.Provider value={{ block: props.block, field, service, resource, __parent }}>
|
||||||
|
{props.children}
|
||||||
|
</BlockRequestContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBlockRequestContext = () => {
|
||||||
|
return useContext(BlockRequestContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BlockProvider = (props) => {
|
||||||
|
const { collection, association } = props;
|
||||||
|
const resource = useReousrce(props);
|
||||||
|
return (
|
||||||
|
<MaybeCollectionProvider collection={collection}>
|
||||||
|
<BlockAssociationContext.Provider value={association}>
|
||||||
|
<BlockResourceContext.Provider value={resource}>
|
||||||
|
<BlockRequestProvider {...props}>{props.children}</BlockRequestProvider>
|
||||||
|
</BlockResourceContext.Provider>
|
||||||
|
</BlockAssociationContext.Provider>
|
||||||
|
</MaybeCollectionProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBlockAssociationContext = () => {
|
||||||
|
return useContext(BlockAssociationContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFilterByTk = () => {
|
||||||
|
const { resource, __parent } = useContext(BlockRequestContext);
|
||||||
|
const recordIndex = useRecordIndex();
|
||||||
|
const record = useRecord();
|
||||||
|
const collection = useCollection();
|
||||||
|
const { getCollectionField } = useCollectionManager();
|
||||||
|
const assoc = useContext(BlockAssociationContext);
|
||||||
|
if (resource instanceof TableFieldResource || __parent?.block === 'TableField') {
|
||||||
|
return recordIndex;
|
||||||
|
}
|
||||||
|
if (assoc) {
|
||||||
|
const association = getCollectionField(assoc);
|
||||||
|
return record?.[association.targetKey || 'id'];
|
||||||
|
}
|
||||||
|
return record?.[collection.filterTargetKey || 'id'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSourceIdFromRecord = () => {
|
||||||
|
const record = useRecord();
|
||||||
|
const { getCollectionField } = useCollectionManager();
|
||||||
|
const assoc = useContext(BlockAssociationContext);
|
||||||
|
if (assoc) {
|
||||||
|
const association = getCollectionField(assoc);
|
||||||
|
return record?.[association.sourceKey || 'id'];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSourceIdFromParentRecord = () => {
|
||||||
|
const record = useRecord();
|
||||||
|
const { getCollectionField } = useCollectionManager();
|
||||||
|
const assoc = useContext(BlockAssociationContext);
|
||||||
|
if (assoc) {
|
||||||
|
const association = getCollectionField(assoc);
|
||||||
|
return record?.__parent?.[association.sourceKey || 'id'];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useParamsFromRecord = () => {
|
||||||
|
const filterByTk = useFilterByTk();
|
||||||
|
return {
|
||||||
|
filterByTk: filterByTk,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { SchemaComponentOptions } from '../schema-component/core/SchemaComponentOptions';
|
||||||
|
import { useParamsFromRecord, useSourceIdFromParentRecord, useSourceIdFromRecord } from './BlockProvider';
|
||||||
|
import { CalendarBlockProvider, useCalendarBlockProps } from './CalendarBlockProvider';
|
||||||
|
import { FormBlockProvider, useFormBlockProps } from './FormBlockProvider';
|
||||||
|
import * as bp from './hooks';
|
||||||
|
import { KanbanBlockProvider, useKanbanBlockProps } from './KanbanBlockProvider';
|
||||||
|
import { TableBlockProvider, useTableBlockProps } from './TableBlockProvider';
|
||||||
|
import { TableFieldProvider, useTableFieldProps } from './TableFieldProvider';
|
||||||
|
import { TableSelectorProvider, useTableSelectorProps } from './TableSelectorProvider';
|
||||||
|
export const BlockSchemaComponentProvider: React.FC = (props) => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentOptions
|
||||||
|
components={{
|
||||||
|
CalendarBlockProvider,
|
||||||
|
TableFieldProvider,
|
||||||
|
TableBlockProvider,
|
||||||
|
TableSelectorProvider,
|
||||||
|
FormBlockProvider,
|
||||||
|
KanbanBlockProvider,
|
||||||
|
}}
|
||||||
|
scope={{
|
||||||
|
...bp,
|
||||||
|
useSourceIdFromRecord,
|
||||||
|
useSourceIdFromParentRecord,
|
||||||
|
useParamsFromRecord,
|
||||||
|
useCalendarBlockProps,
|
||||||
|
useFormBlockProps,
|
||||||
|
useTableFieldProps,
|
||||||
|
useTableBlockProps,
|
||||||
|
useTableSelectorProps,
|
||||||
|
useKanbanBlockProps,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
);
|
||||||
|
};
|
53
packages/client/src/block-provider/CalendarBlockProvider.tsx
Normal file
53
packages/client/src/block-provider/CalendarBlockProvider.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { ArrayField } from '@formily/core';
|
||||||
|
import { useField } from '@formily/react';
|
||||||
|
import { Spin } from 'antd';
|
||||||
|
import React, { createContext, useContext, useEffect } from 'react';
|
||||||
|
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
|
||||||
|
|
||||||
|
export const CalendarBlockContext = createContext<any>({});
|
||||||
|
|
||||||
|
const InternalCalendarBlockProvider = (props) => {
|
||||||
|
const { fieldNames } = props;
|
||||||
|
const field = useField();
|
||||||
|
const { resource, service } = useBlockRequestContext();
|
||||||
|
if (service.loading) {
|
||||||
|
return <Spin />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<CalendarBlockContext.Provider
|
||||||
|
value={{
|
||||||
|
field,
|
||||||
|
service,
|
||||||
|
resource,
|
||||||
|
fieldNames,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</CalendarBlockContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CalendarBlockProvider = (props) => {
|
||||||
|
return (
|
||||||
|
<BlockProvider {...props}>
|
||||||
|
<InternalCalendarBlockProvider {...props} />
|
||||||
|
</BlockProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCalendarBlockContext = () => {
|
||||||
|
return useContext(CalendarBlockContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCalendarBlockProps = () => {
|
||||||
|
const ctx = useCalendarBlockContext();
|
||||||
|
const field = useField<ArrayField>();
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ctx?.service?.loading) {
|
||||||
|
field.componentProps.dataSource = ctx?.service?.data?.data;
|
||||||
|
}
|
||||||
|
}, [ctx?.service?.loading]);
|
||||||
|
return {
|
||||||
|
fieldNames: ctx.fieldNames,
|
||||||
|
};
|
||||||
|
};
|
58
packages/client/src/block-provider/FormBlockProvider.tsx
Normal file
58
packages/client/src/block-provider/FormBlockProvider.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { createForm } from '@formily/core';
|
||||||
|
import { useField } from '@formily/react';
|
||||||
|
import { Spin } from 'antd';
|
||||||
|
import React, { createContext, useContext, useEffect, useMemo } from 'react';
|
||||||
|
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
|
||||||
|
|
||||||
|
export const FormBlockContext = createContext<any>({});
|
||||||
|
|
||||||
|
const InternalFormBlockProvider = (props) => {
|
||||||
|
const { action, readPretty } = props;
|
||||||
|
const field = useField();
|
||||||
|
const form = useMemo(
|
||||||
|
() =>
|
||||||
|
createForm({
|
||||||
|
readPretty,
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const { resource, service } = useBlockRequestContext();
|
||||||
|
if (service.loading) {
|
||||||
|
return <Spin />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FormBlockContext.Provider
|
||||||
|
value={{
|
||||||
|
action,
|
||||||
|
form,
|
||||||
|
field,
|
||||||
|
service,
|
||||||
|
resource,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</FormBlockContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FormBlockProvider = (props) => {
|
||||||
|
return (
|
||||||
|
<BlockProvider {...props}>
|
||||||
|
<InternalFormBlockProvider {...props} />
|
||||||
|
</BlockProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFormBlockContext = () => {
|
||||||
|
return useContext(FormBlockContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFormBlockProps = () => {
|
||||||
|
const ctx = useFormBlockContext();
|
||||||
|
useEffect(() => {
|
||||||
|
ctx.form.setInitialValues(ctx.service?.data?.data);
|
||||||
|
}, []);
|
||||||
|
return {
|
||||||
|
form: ctx.form,
|
||||||
|
};
|
||||||
|
};
|
88
packages/client/src/block-provider/KanbanBlockProvider.tsx
Normal file
88
packages/client/src/block-provider/KanbanBlockProvider.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { ArrayField } from '@formily/core';
|
||||||
|
import { useField } from '@formily/react';
|
||||||
|
import { Spin } from 'antd';
|
||||||
|
import React, { createContext, useContext, useEffect } from 'react';
|
||||||
|
import { useCollection } from '../collection-manager';
|
||||||
|
import { toColumns } from '../schema-component/antd/kanban-v2/Kanban';
|
||||||
|
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
|
||||||
|
|
||||||
|
export const KanbanBlockContext = createContext<any>({});
|
||||||
|
|
||||||
|
const useGroupField = (props) => {
|
||||||
|
const { getField } = useCollection();
|
||||||
|
const { groupField } = props;
|
||||||
|
if (typeof groupField === 'string') {
|
||||||
|
return getField(groupField);
|
||||||
|
}
|
||||||
|
if (groupField?.name) {
|
||||||
|
return getField(groupField?.name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const InternalKanbanBlockProvider = (props) => {
|
||||||
|
const field = useField();
|
||||||
|
const { resource, service } = useBlockRequestContext();
|
||||||
|
const groupField = useGroupField(props);
|
||||||
|
if (!groupField) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (service.loading) {
|
||||||
|
return <Spin />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<KanbanBlockContext.Provider
|
||||||
|
value={{
|
||||||
|
field,
|
||||||
|
service,
|
||||||
|
resource,
|
||||||
|
groupField,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</KanbanBlockContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KanbanBlockProvider = (props) => {
|
||||||
|
return (
|
||||||
|
<BlockProvider {...props}>
|
||||||
|
<InternalKanbanBlockProvider {...props} />
|
||||||
|
</BlockProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useKanbanBlockContext = () => {
|
||||||
|
return useContext(KanbanBlockContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useKanbanBlockProps = () => {
|
||||||
|
const field = useField<ArrayField>();
|
||||||
|
const ctx = useKanbanBlockContext();
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ctx?.service?.loading) {
|
||||||
|
field.value = toColumns(ctx.groupField, ctx?.service?.data?.data);
|
||||||
|
}
|
||||||
|
// field.loading = ctx?.service?.loading;
|
||||||
|
}, [ctx?.service?.loading]);
|
||||||
|
return {
|
||||||
|
groupField: ctx.groupField,
|
||||||
|
async onCardDragEnd({ columns, groupField }, { fromColumnId, fromPosition }, { toColumnId, toPosition }) {
|
||||||
|
const sourceColumn = columns.find((column) => column.id === fromColumnId);
|
||||||
|
const destinationColumn = columns.find((column) => column.id === toColumnId);
|
||||||
|
const sourceCard = sourceColumn?.cards?.[fromPosition];
|
||||||
|
const targetCard = destinationColumn?.cards?.[toPosition];
|
||||||
|
const values = {
|
||||||
|
sourceId: sourceCard.id,
|
||||||
|
sortField: `${groupField.name}_sort`,
|
||||||
|
};
|
||||||
|
if (targetCard) {
|
||||||
|
values['targetId'] = targetCard.id;
|
||||||
|
} else {
|
||||||
|
values['targetScope'] = {
|
||||||
|
[groupField.name]: toColumnId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await ctx.resource.move(values);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
123
packages/client/src/block-provider/TableBlockProvider.tsx
Normal file
123
packages/client/src/block-provider/TableBlockProvider.tsx
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { ArrayField } from '@formily/core';
|
||||||
|
import { Schema, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import React, { createContext, useContext, useEffect } from 'react';
|
||||||
|
import { useCollectionManager } from '../collection-manager';
|
||||||
|
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
|
||||||
|
|
||||||
|
export const TableBlockContext = createContext<any>({});
|
||||||
|
|
||||||
|
const InternalTableBlockProvider = (props) => {
|
||||||
|
const { params, showIndex, dragSort, rowKey } = props;
|
||||||
|
const field = useField();
|
||||||
|
const { resource, service } = useBlockRequestContext();
|
||||||
|
// if (service.loading) {
|
||||||
|
// return <Spin />;
|
||||||
|
// }
|
||||||
|
return (
|
||||||
|
<TableBlockContext.Provider
|
||||||
|
value={{
|
||||||
|
field,
|
||||||
|
service,
|
||||||
|
resource,
|
||||||
|
params,
|
||||||
|
showIndex,
|
||||||
|
dragSort,
|
||||||
|
rowKey,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</TableBlockContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useAssociationNames = (collection) => {
|
||||||
|
const { getCollectionFields } = useCollectionManager();
|
||||||
|
const names = getCollectionFields(collection)
|
||||||
|
?.filter((field) => field.target)
|
||||||
|
.map((field) => field.name);
|
||||||
|
return names;
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const tableSchema = fieldSchema.reduceProperties((buf, schema) => {
|
||||||
|
if (schema['x-component'] === 'TableV2') {
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}, null) as Schema;
|
||||||
|
return tableSchema.reduceProperties((buf, schema) => {
|
||||||
|
if (schema['x-component'] === 'TableV2.Column') {
|
||||||
|
const s = schema.reduceProperties((buf, s) => {
|
||||||
|
if (s['x-collection-field'] && names.includes(s.name)) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}, null);
|
||||||
|
if (s) {
|
||||||
|
buf.push(s.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TableBlockProvider = (props) => {
|
||||||
|
const params = { ...props.params };
|
||||||
|
const appends = useAssociationNames(props.collection);
|
||||||
|
if (props.dragSort) {
|
||||||
|
params['sort'] = ['sort'];
|
||||||
|
}
|
||||||
|
if (appends?.length) {
|
||||||
|
params['appends'] = appends;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<BlockProvider {...props} params={params}>
|
||||||
|
<InternalTableBlockProvider {...props} params={params} />
|
||||||
|
</BlockProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTableBlockContext = () => {
|
||||||
|
return useContext(TableBlockContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTableBlockProps = () => {
|
||||||
|
const field = useField<ArrayField>();
|
||||||
|
const ctx = useTableBlockContext();
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ctx?.service?.loading) {
|
||||||
|
field.value = ctx?.service?.data?.data;
|
||||||
|
field.data = field.data || {};
|
||||||
|
field.data.selectedRowKeys = ctx?.field?.data?.selectedRowKeys;
|
||||||
|
field.componentProps.pagination = field.componentProps.pagination || {};
|
||||||
|
field.componentProps.pagination.pageSize = ctx?.service?.data?.meta?.pageSize;
|
||||||
|
field.componentProps.pagination.total = ctx?.service?.data?.meta?.count;
|
||||||
|
field.componentProps.pagination.current = ctx?.service?.data?.meta?.page;
|
||||||
|
}
|
||||||
|
}, [ctx?.service?.loading]);
|
||||||
|
return {
|
||||||
|
loading: ctx?.service?.loading,
|
||||||
|
showIndex: ctx.showIndex,
|
||||||
|
dragSort: ctx.dragSort,
|
||||||
|
rowKey: ctx.rowKey || 'id',
|
||||||
|
pagination:
|
||||||
|
ctx?.params?.paginate !== false
|
||||||
|
? {
|
||||||
|
defaultCurrent: ctx?.params?.page || 1,
|
||||||
|
defaultPageSize: ctx?.params?.pageSize,
|
||||||
|
}
|
||||||
|
: false,
|
||||||
|
onRowSelectionChange(selectedRowKeys) {
|
||||||
|
ctx.field.data = ctx?.field?.data || {};
|
||||||
|
ctx.field.data.selectedRowKeys = selectedRowKeys;
|
||||||
|
},
|
||||||
|
async onRowDragEnd({ from, to }) {
|
||||||
|
await ctx.resource.move({
|
||||||
|
sourceId: from[ctx.rowKey || 'id'],
|
||||||
|
targetId: to[ctx.rowKey || 'id'],
|
||||||
|
});
|
||||||
|
ctx.service.refresh();
|
||||||
|
},
|
||||||
|
onChange({ current, pageSize }) {
|
||||||
|
ctx.service.run({ ...ctx.service.params?.[0], page: current, pageSize });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
147
packages/client/src/block-provider/TableFieldProvider.tsx
Normal file
147
packages/client/src/block-provider/TableFieldProvider.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import { ArrayField, Field } from '@formily/core';
|
||||||
|
import { useField } from '@formily/react';
|
||||||
|
import React, { createContext, useContext, useEffect } from 'react';
|
||||||
|
import { APIClient } from '../api-client';
|
||||||
|
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
|
||||||
|
|
||||||
|
export const TableFieldContext = createContext<any>({});
|
||||||
|
|
||||||
|
const InternalTableFieldProvider = (props) => {
|
||||||
|
const { params = {}, showIndex, dragSort } = props;
|
||||||
|
const field = useField();
|
||||||
|
const { resource, service } = useBlockRequestContext();
|
||||||
|
// if (service.loading) {
|
||||||
|
// return <Spin />;
|
||||||
|
// }
|
||||||
|
return (
|
||||||
|
<TableFieldContext.Provider
|
||||||
|
value={{
|
||||||
|
field,
|
||||||
|
service,
|
||||||
|
resource,
|
||||||
|
params,
|
||||||
|
showIndex,
|
||||||
|
dragSort,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</TableFieldContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export class TableFieldResource {
|
||||||
|
field: Field;
|
||||||
|
api: APIClient;
|
||||||
|
sourceId: any;
|
||||||
|
resource?: any;
|
||||||
|
|
||||||
|
constructor(options) {
|
||||||
|
this.field = options.field;
|
||||||
|
this.api = options.api;
|
||||||
|
this.sourceId = options.sourceId;
|
||||||
|
this.resource = this.api.resource(options.resource, this.sourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(options) {
|
||||||
|
this.field.data = this.field.data || {};
|
||||||
|
if (this.field?.data?.dataSource?.length) {
|
||||||
|
console.log('list', this.field.data.dataSource);
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
data: this.field.data.dataSource,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!this.sourceId) {
|
||||||
|
console.log('list.sourceId', this.field.data.dataSource);
|
||||||
|
this.field.data.dataSource = [];
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const response = await this.resource.list(options);
|
||||||
|
console.log('list', response);
|
||||||
|
this.field.data.dataSource = response.data.data;
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
data: response.data.data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(options) {
|
||||||
|
console.log('get', options);
|
||||||
|
const { filterByTk } = options;
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
data: this.field.data.dataSource[filterByTk],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(options) {
|
||||||
|
console.log('create', options);
|
||||||
|
const { values } = options;
|
||||||
|
this.field.data.dataSource.push(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(options) {
|
||||||
|
console.log('update', options);
|
||||||
|
const { filterByTk, values } = options;
|
||||||
|
this.field.data.dataSource[filterByTk] = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy(options) {
|
||||||
|
console.log('destroy', options);
|
||||||
|
let { filterByTk } = options;
|
||||||
|
if (!Array.isArray(filterByTk)) {
|
||||||
|
filterByTk = [filterByTk];
|
||||||
|
}
|
||||||
|
this.field.data.dataSource = this.field.data.dataSource.filter((item, index) => {
|
||||||
|
return !filterByTk.includes(index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TableFieldProvider = (props) => {
|
||||||
|
return (
|
||||||
|
<BlockProvider block={'TableField'} {...props}>
|
||||||
|
<InternalTableFieldProvider {...props} />
|
||||||
|
</BlockProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTableFieldContext = () => {
|
||||||
|
return useContext(TableFieldContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTableFieldProps = () => {
|
||||||
|
const field = useField<ArrayField>();
|
||||||
|
const ctx = useTableFieldContext();
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ctx?.service?.loading) {
|
||||||
|
field.value = ctx?.service?.data?.data;
|
||||||
|
field.data = field.data || {};
|
||||||
|
field.data.selectedRowKeys = ctx?.field?.data?.selectedRowKeys;
|
||||||
|
}
|
||||||
|
}, [ctx?.service?.loading]);
|
||||||
|
return {
|
||||||
|
size: 'middle',
|
||||||
|
loading: ctx?.service?.loading,
|
||||||
|
showIndex: ctx.showIndex,
|
||||||
|
dragSort: ctx.dragSort,
|
||||||
|
pagination: false,
|
||||||
|
rowKey: (record: any) => {
|
||||||
|
return field.value?.indexOf?.(record);
|
||||||
|
},
|
||||||
|
onRowSelectionChange(selectedRowKeys) {
|
||||||
|
ctx.field.data = ctx?.field?.data || {};
|
||||||
|
ctx.field.data.selectedRowKeys = selectedRowKeys;
|
||||||
|
},
|
||||||
|
onChange({ current, pageSize }) {
|
||||||
|
ctx.service.run({ page: current, pageSize });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
100
packages/client/src/block-provider/TableSelectorProvider.tsx
Normal file
100
packages/client/src/block-provider/TableSelectorProvider.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { ArrayField } from '@formily/core';
|
||||||
|
import { useField } from '@formily/react';
|
||||||
|
import React, { createContext, useContext, useEffect } from 'react';
|
||||||
|
import { useCollectionManager } from '../collection-manager';
|
||||||
|
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
|
||||||
|
|
||||||
|
export const TableSelectorContext = createContext<any>({});
|
||||||
|
|
||||||
|
const InternalTableSelectorProvider = (props) => {
|
||||||
|
const { params, rowKey } = props;
|
||||||
|
const field = useField();
|
||||||
|
const { resource, service } = useBlockRequestContext();
|
||||||
|
// if (service.loading) {
|
||||||
|
// return <Spin />;
|
||||||
|
// }
|
||||||
|
return (
|
||||||
|
<TableSelectorContext.Provider
|
||||||
|
value={{
|
||||||
|
field,
|
||||||
|
service,
|
||||||
|
resource,
|
||||||
|
params,
|
||||||
|
rowKey,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</TableSelectorContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useAssociationNames = (collection) => {
|
||||||
|
const { getCollectionFields } = useCollectionManager();
|
||||||
|
const names = getCollectionFields(collection)
|
||||||
|
?.filter((field) => field.target)
|
||||||
|
.map((field) => field.name);
|
||||||
|
return names;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TableSelectorProvider = (props) => {
|
||||||
|
const params = { ...props.params };
|
||||||
|
const appends = useAssociationNames(props.collection);
|
||||||
|
if (props.dragSort) {
|
||||||
|
params['sort'] = ['sort'];
|
||||||
|
}
|
||||||
|
if (appends?.length) {
|
||||||
|
params['appends'] = appends;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<BlockProvider {...props} params={params}>
|
||||||
|
<InternalTableSelectorProvider {...props} params={params} />
|
||||||
|
</BlockProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTableSelectorContext = () => {
|
||||||
|
return useContext(TableSelectorContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTableSelectorProps = () => {
|
||||||
|
const field = useField<ArrayField>();
|
||||||
|
const ctx = useTableSelectorContext();
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ctx?.service?.loading) {
|
||||||
|
field.value = ctx?.service?.data?.data;
|
||||||
|
field.data = field.data || {};
|
||||||
|
field.data.selectedRowKeys = ctx?.field?.data?.selectedRowKeys;
|
||||||
|
field.componentProps.pagination = field.componentProps.pagination || {};
|
||||||
|
field.componentProps.pagination.pageSize = ctx?.service?.data?.meta?.pageSize;
|
||||||
|
field.componentProps.pagination.total = ctx?.service?.data?.meta?.count;
|
||||||
|
field.componentProps.pagination.current = ctx?.service?.data?.meta?.page;
|
||||||
|
}
|
||||||
|
}, [ctx?.service?.loading]);
|
||||||
|
return {
|
||||||
|
loading: ctx?.service?.loading,
|
||||||
|
showIndex: false,
|
||||||
|
dragSort: false,
|
||||||
|
rowKey: ctx.rowKey || 'id',
|
||||||
|
pagination:
|
||||||
|
ctx?.params?.paginate !== false
|
||||||
|
? {
|
||||||
|
defaultCurrent: ctx?.params?.page || 1,
|
||||||
|
defaultPageSize: ctx?.params?.pageSize,
|
||||||
|
}
|
||||||
|
: false,
|
||||||
|
onRowSelectionChange(selectedRowKeys, selectedRows) {
|
||||||
|
ctx.field.data = ctx?.field?.data || {};
|
||||||
|
ctx.field.data.selectedRowKeys = selectedRowKeys;
|
||||||
|
},
|
||||||
|
async onRowDragEnd({ from, to }) {
|
||||||
|
await ctx.resource.move({
|
||||||
|
sourceId: from[ctx.rowKey || 'id'],
|
||||||
|
targetId: to[ctx.rowKey || 'id'],
|
||||||
|
});
|
||||||
|
ctx.service.refresh();
|
||||||
|
},
|
||||||
|
onChange({ current, pageSize }) {
|
||||||
|
ctx.service.run({ ...ctx.service.params?.[0], page: current, pageSize });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
76
packages/client/src/block-provider/hooks/index.ts
Normal file
76
packages/client/src/block-provider/hooks/index.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { useForm } from '@formily/react';
|
||||||
|
import { useActionContext } from '../../schema-component';
|
||||||
|
import { useBlockRequestContext, useFilterByTk } from '../BlockProvider';
|
||||||
|
|
||||||
|
export const usePickActionProps = () => {
|
||||||
|
const form = useForm();
|
||||||
|
return {
|
||||||
|
onClick() {
|
||||||
|
console.log('usePickActionProps', form.values);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateActionProps = () => {
|
||||||
|
const form = useForm();
|
||||||
|
const { resource, __parent } = useBlockRequestContext();
|
||||||
|
const { setVisible } = useActionContext();
|
||||||
|
return {
|
||||||
|
async onClick() {
|
||||||
|
await form.submit();
|
||||||
|
await resource.create({
|
||||||
|
values: form.values,
|
||||||
|
});
|
||||||
|
__parent?.service?.refresh?.();
|
||||||
|
setVisible?.(false);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateActionProps = () => {
|
||||||
|
const form = useForm();
|
||||||
|
const filterByTk = useFilterByTk();
|
||||||
|
const { resource, __parent } = useBlockRequestContext();
|
||||||
|
const { setVisible } = useActionContext();
|
||||||
|
return {
|
||||||
|
async onClick() {
|
||||||
|
await form.submit();
|
||||||
|
|
||||||
|
await resource.update({
|
||||||
|
filterByTk,
|
||||||
|
values: form.values,
|
||||||
|
});
|
||||||
|
__parent?.service?.refresh?.();
|
||||||
|
setVisible?.(false);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDestroyActionProps = () => {
|
||||||
|
const filterByTk = useFilterByTk();
|
||||||
|
const { resource, service } = useBlockRequestContext();
|
||||||
|
return {
|
||||||
|
async onClick() {
|
||||||
|
await resource.destroy({
|
||||||
|
filterByTk,
|
||||||
|
});
|
||||||
|
service?.refresh?.();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBulkDestroyActionProps = () => {
|
||||||
|
const { field } = useBlockRequestContext();
|
||||||
|
const { resource, service } = useBlockRequestContext();
|
||||||
|
return {
|
||||||
|
async onClick() {
|
||||||
|
if (!field?.data?.selectedRowKeys?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await resource.destroy({
|
||||||
|
filterByTk: field.data?.selectedRowKeys,
|
||||||
|
});
|
||||||
|
service?.refresh?.();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
9
packages/client/src/block-provider/index.tsx
Normal file
9
packages/client/src/block-provider/index.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
export * from './BlockProvider';
|
||||||
|
export * from './BlockSchemaComponentProvider';
|
||||||
|
export * from './CalendarBlockProvider';
|
||||||
|
export * from './FormBlockProvider';
|
||||||
|
export * from './KanbanBlockProvider';
|
||||||
|
export * from './TableBlockProvider';
|
||||||
|
export * from './TableFieldProvider';
|
||||||
|
|
@ -95,7 +95,7 @@ export default function App() {
|
|||||||
onNewCardConfirm={(draftCard) => draftCard}
|
onNewCardConfirm={(draftCard) => draftCard}
|
||||||
onCardNew={console.log}
|
onCardNew={console.log}
|
||||||
renderCard={(card, { column, dragging }) => {
|
renderCard={(card, { column, dragging }) => {
|
||||||
return <Card style={{ width: 220, marginBottom: 15 }}>{card.title}</Card>;
|
return <Card style={{ marginBottom: 15 }}>{card.title}</Card>;
|
||||||
}}
|
}}
|
||||||
renderCardAdder={({ column }) => {
|
renderCardAdder={({ column }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -10,7 +10,7 @@ import { useCollectionField } from './hooks';
|
|||||||
const InternalField: React.FC = (props) => {
|
const InternalField: React.FC = (props) => {
|
||||||
const field = useField<Field>();
|
const field = useField<Field>();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { name, uiSchema } = useCollectionField();
|
const { name, interface: interfaceType, uiSchema } = useCollectionField();
|
||||||
const component = useComponent(uiSchema?.['x-component']);
|
const component = useComponent(uiSchema?.['x-component']);
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const setFieldProps = (key, value) => {
|
const setFieldProps = (key, value) => {
|
||||||
@ -29,6 +29,9 @@ const InternalField: React.FC = (props) => {
|
|||||||
setFieldProps('title', uiSchema.title);
|
setFieldProps('title', uiSchema.title);
|
||||||
setFieldProps('description', uiSchema.description);
|
setFieldProps('description', uiSchema.description);
|
||||||
setFieldProps('initialValue', uiSchema.default);
|
setFieldProps('initialValue', uiSchema.default);
|
||||||
|
if (!field.validator && uiSchema['x-validator']) {
|
||||||
|
field.validator = uiSchema['x-validator'];
|
||||||
|
}
|
||||||
field.readPretty = uiSchema['x-read-pretty'];
|
field.readPretty = uiSchema['x-read-pretty'];
|
||||||
setRequired();
|
setRequired();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -36,8 +39,16 @@ const InternalField: React.FC = (props) => {
|
|||||||
const originalProps = compile(uiSchema['x-component-props']) || {};
|
const originalProps = compile(uiSchema['x-component-props']) || {};
|
||||||
const componentProps = merge(originalProps, field.componentProps || {});
|
const componentProps = merge(originalProps, field.componentProps || {});
|
||||||
field.component = [component, componentProps];
|
field.component = [component, componentProps];
|
||||||
// console.log('componentProps', uiSchema?.title, field.componentProps);
|
// if (interfaceType === 'input') {
|
||||||
}, [uiSchema?.title, uiSchema?.description, uiSchema?.required]);
|
// field.componentProps.ellipsis = true;
|
||||||
|
// } else if (interfaceType === 'textarea') {
|
||||||
|
// field.componentProps.ellipsis = true;
|
||||||
|
// } else if (interfaceType === 'markdown') {
|
||||||
|
// field.componentProps.ellipsis = true;
|
||||||
|
// } else if (interfaceType === 'attachment') {
|
||||||
|
// field.componentProps.size = 'small';
|
||||||
|
// }
|
||||||
|
}, [JSON.stringify(uiSchema)]);
|
||||||
if (!uiSchema) {
|
if (!uiSchema) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,6 @@ import { CollectionOptions } from './types';
|
|||||||
|
|
||||||
export const CollectionProvider: React.FC<{ name?: string; collection?: CollectionOptions }> = (props) => {
|
export const CollectionProvider: React.FC<{ name?: string; collection?: CollectionOptions }> = (props) => {
|
||||||
const { name, collection, children } = props;
|
const { name, collection, children } = props;
|
||||||
const { get } = useCollectionManager();
|
const { getCollection } = useCollectionManager();
|
||||||
return (
|
return <CollectionContext.Provider value={getCollection(collection || name)}>{children}</CollectionContext.Provider>;
|
||||||
<CollectionContext.Provider value={collection || get(collection?.name || name)}>
|
|
||||||
{children}
|
|
||||||
</CollectionContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -12,13 +12,27 @@ export const useCollectionManager = () => {
|
|||||||
get(name: string) {
|
get(name: string) {
|
||||||
return collections?.find((collection) => collection.name === name);
|
return collections?.find((collection) => collection.name === name);
|
||||||
},
|
},
|
||||||
getCollection(name: string) {
|
getCollection(name: any) {
|
||||||
|
if (typeof name !== 'string') {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
return collections?.find((collection) => collection.name === name);
|
return collections?.find((collection) => collection.name === name);
|
||||||
},
|
},
|
||||||
getCollectionFields(name: string) {
|
getCollectionFields(name: string) {
|
||||||
const collection = collections?.find((collection) => collection.name === name);
|
const collection = collections?.find((collection) => collection.name === name);
|
||||||
return collection?.fields || [];
|
return collection?.fields || [];
|
||||||
},
|
},
|
||||||
|
getCollectionField(name: string) {
|
||||||
|
const [collectionName, fieldName] = name.split('.');
|
||||||
|
if (!fieldName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const collection = collections?.find((collection) => collection.name === collectionName);
|
||||||
|
if (!collection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return collection?.fields?.find((field) => field.name === fieldName);
|
||||||
|
},
|
||||||
getInterface(name: string) {
|
getInterface(name: string) {
|
||||||
return interfaces[name] ? clone(interfaces[name]) : null;
|
return interfaces[name] ? clone(interfaces[name]) : null;
|
||||||
},
|
},
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ISchema } from '@formily/react';
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import { defaultProps, operators } from './properties';
|
import { defaultProps, operators } from './properties';
|
||||||
import { IField } from './types';
|
import { IField } from './types';
|
||||||
@ -21,6 +22,12 @@ export const attachment: IField = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
schemaInitialize(schema: ISchema, { block }) {
|
||||||
|
if (['Table', 'Kanban'].includes(block)) {
|
||||||
|
schema['x-component-props'] = schema['x-component-props'] || {};
|
||||||
|
schema['x-component-props']['size'] = 'small';
|
||||||
|
}
|
||||||
|
},
|
||||||
initialize: (values: any) => {
|
initialize: (values: any) => {
|
||||||
if (!values.through) {
|
if (!values.through) {
|
||||||
values.through = `t_${uid()}`;
|
values.through = `t_${uid()}`;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { defaultProps, operators } from './properties';
|
import { ISchema } from '@formily/react';
|
||||||
|
import { defaultProps, operators, recordPickerViewer } from './properties';
|
||||||
import { IField } from './types';
|
import { IField } from './types';
|
||||||
|
|
||||||
export const createdBy: IField = {
|
export const createdBy: IField = {
|
||||||
@ -43,4 +44,9 @@ export const createdBy: IField = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
schemaInitialize(schema: ISchema, { readPretty }) {
|
||||||
|
schema['properties'] = {
|
||||||
|
viewer: recordPickerViewer,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -12,7 +12,7 @@ export const datetime: IField = {
|
|||||||
type: 'date',
|
type: 'date',
|
||||||
// name,
|
// name,
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
type: 'datetime',
|
type: 'string',
|
||||||
// title,
|
// title,
|
||||||
'x-component': 'DatePicker',
|
'x-component': 'DatePicker',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ISchema } from '@formily/react';
|
||||||
import { defaultProps, operators } from './properties';
|
import { defaultProps, operators } from './properties';
|
||||||
import { IField } from './types';
|
import { IField } from './types';
|
||||||
|
|
||||||
@ -22,4 +23,10 @@ export const input: IField = {
|
|||||||
filterable: {
|
filterable: {
|
||||||
operators: operators.string,
|
operators: operators.string,
|
||||||
},
|
},
|
||||||
|
schemaInitialize(schema: ISchema, { block }) {
|
||||||
|
if (['Table', 'Kanban'].includes(block)) {
|
||||||
|
schema['x-component-props'] = schema['x-component-props'] || {};
|
||||||
|
schema['x-component-props']['ellipsis'] = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
import { ISchema } from '@formily/react';
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import { defaultProps } from './properties';
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { defaultProps, recordPickerSelector, recordPickerViewer } from './properties';
|
||||||
import { IField } from './types';
|
import { IField } from './types';
|
||||||
|
|
||||||
export const linkTo: IField = {
|
export const linkTo: IField = {
|
||||||
@ -42,6 +44,17 @@ export const linkTo: IField = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
schemaInitialize(schema: ISchema, { readPretty }) {
|
||||||
|
if (readPretty) {
|
||||||
|
schema['properties'] = {
|
||||||
|
viewer: cloneDeep(recordPickerViewer),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
schema['properties'] = {
|
||||||
|
selector: cloneDeep(recordPickerSelector),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
initialize: (values: any) => {
|
initialize: (values: any) => {
|
||||||
if (values.type === 'belongsToMany') {
|
if (values.type === 'belongsToMany') {
|
||||||
if (!values.through) {
|
if (!values.through) {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ISchema } from '@formily/react';
|
||||||
import { defaultProps } from './properties';
|
import { defaultProps } from './properties';
|
||||||
import { IField } from './types';
|
import { IField } from './types';
|
||||||
|
|
||||||
@ -18,4 +19,10 @@ export const markdown: IField = {
|
|||||||
properties: {
|
properties: {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
},
|
},
|
||||||
|
schemaInitialize(schema: ISchema, { block }) {
|
||||||
|
if (['Table', 'Kanban'].includes(block)) {
|
||||||
|
schema['x-component-props'] = schema['x-component-props'] || {};
|
||||||
|
schema['x-component-props']['ellipsis'] = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { ISchema } from '@formily/react';
|
import { ISchema } from '@formily/react';
|
||||||
|
|
||||||
|
export * as operators from './operators';
|
||||||
|
|
||||||
export const type: ISchema = {
|
export const type: ISchema = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
title: '{{t("Storage type")}}',
|
title: '{{t("Storage type")}}',
|
||||||
@ -199,5 +201,77 @@ export const defaultProps = {
|
|||||||
type,
|
type,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const recordPickerSelector: ISchema = {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Select record") }}',
|
||||||
|
'x-component': 'RecordPicker.Selector',
|
||||||
|
'x-component-props': {
|
||||||
|
className: 'nb-record-picker-selector'
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
grid: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'TableSelectorInitializers',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
'x-component': 'Action.Container.Footer',
|
||||||
|
'x-component-props': {},
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
'x-component-props': {},
|
||||||
|
properties: {
|
||||||
|
submit: {
|
||||||
|
title: '{{ t("Submit") }}',
|
||||||
|
'x-action': 'submit',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-designer': 'Action.Designer',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
htmlType: 'submit',
|
||||||
|
useProps: '{{ usePickActionProps }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export * as operators from './operators';
|
export const recordPickerViewer = {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("View record") }}',
|
||||||
|
'x-component': 'RecordPicker.Viewer',
|
||||||
|
'x-component-props': {
|
||||||
|
className: 'nb-action-popup',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
tabs: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Tabs',
|
||||||
|
'x-component-props': {},
|
||||||
|
'x-initializer': 'TabPaneInitializers',
|
||||||
|
properties: {
|
||||||
|
tab1: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Details")}}',
|
||||||
|
'x-component': 'Tabs.TabPane',
|
||||||
|
'x-designer': 'Tabs.Designer',
|
||||||
|
'x-component-props': {},
|
||||||
|
properties: {
|
||||||
|
grid: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'RecordBlockInitializers',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ISchema } from '@formily/react';
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import { defaultProps } from './properties';
|
import { defaultProps } from './properties';
|
||||||
import { IField } from './types';
|
import { IField } from './types';
|
||||||
@ -13,13 +14,53 @@ export const subTable: IField = {
|
|||||||
type: 'hasMany',
|
type: 'hasMany',
|
||||||
// name,
|
// name,
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
type: 'array',
|
type: 'void',
|
||||||
// title,
|
// title,
|
||||||
'x-component': 'Table',
|
'x-component': 'TableField',
|
||||||
'x-component-props': {},
|
'x-component-props': {},
|
||||||
enum: [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
schemaInitialize(schema: ISchema, { field, readPretty }) {
|
||||||
|
const association = `${field.collectionName}.${field.name}`;
|
||||||
|
schema['type'] = 'void';
|
||||||
|
schema['x-component'] = 'TableField';
|
||||||
|
schema['properties'] = {
|
||||||
|
block: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'TableFieldProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
collection: field.target,
|
||||||
|
association: association,
|
||||||
|
resource: association,
|
||||||
|
action: 'list',
|
||||||
|
params: {
|
||||||
|
paginate: false,
|
||||||
|
},
|
||||||
|
showIndex: true,
|
||||||
|
dragSort: false,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-initializer': 'SubTableActionInitializers',
|
||||||
|
'x-component': 'TableField.ActionBar',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
[field.name]: {
|
||||||
|
type: 'array',
|
||||||
|
'x-initializer': 'TableColumnInitializers',
|
||||||
|
'x-component': 'TableV2',
|
||||||
|
'x-component-props': {
|
||||||
|
rowSelection: {
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
|
useProps: '{{ useTableFieldProps }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
initialize: (values: any) => {
|
initialize: (values: any) => {
|
||||||
if (!values.target) {
|
if (!values.target) {
|
||||||
values.target = `t_${uid()}`;
|
values.target = `t_${uid()}`;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ISchema } from '@formily/react';
|
||||||
import { defaultProps } from './properties';
|
import { defaultProps } from './properties';
|
||||||
import { IField } from './types';
|
import { IField } from './types';
|
||||||
|
|
||||||
@ -19,4 +20,10 @@ export const textarea: IField = {
|
|||||||
properties: {
|
properties: {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
},
|
},
|
||||||
|
schemaInitialize(schema: ISchema, { block }) {
|
||||||
|
if (['Table', 'Kanban'].includes(block)) {
|
||||||
|
schema['x-component-props'] = schema['x-component-props'] || {};
|
||||||
|
schema['x-component-props']['ellipsis'] = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -13,7 +13,7 @@ export const updatedAt: IField = {
|
|||||||
field: 'updatedAt',
|
field: 'updatedAt',
|
||||||
// name,
|
// name,
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
type: 'datetime',
|
type: 'string',
|
||||||
title: '{{t("Last updated at")}}',
|
title: '{{t("Last updated at")}}',
|
||||||
'x-component': 'DatePicker',
|
'x-component': 'DatePicker',
|
||||||
'x-component-props': {},
|
'x-component-props': {},
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { defaultProps, operators } from './properties';
|
import { ISchema } from '@formily/react';
|
||||||
|
import { defaultProps, operators, recordPickerViewer } from './properties';
|
||||||
import { IField } from './types';
|
import { IField } from './types';
|
||||||
|
|
||||||
export const updatedBy: IField = {
|
export const updatedBy: IField = {
|
||||||
@ -42,4 +43,9 @@ export const updatedBy: IField = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
schemaInitialize(schema: ISchema, { readPretty }) {
|
||||||
|
schema['properties'] = {
|
||||||
|
viewer: recordPickerViewer,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,34 +1,21 @@
|
|||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { initReactI18next } from 'react-i18next';
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
import { resources } from '../locale';
|
||||||
const zhCN = require('../locale/zh_CN');
|
|
||||||
const enUS = require('../locale/en_US');
|
|
||||||
const log = require('debug')('i18next');
|
const log = require('debug')('i18next');
|
||||||
|
|
||||||
export const i18n = i18next.createInstance();
|
export const i18n = i18next.createInstance();
|
||||||
|
|
||||||
i18n.use(initReactI18next).init({
|
i18n.use(initReactI18next).init({
|
||||||
lng: localStorage.getItem('locale') || 'en-US',
|
lng: localStorage.getItem('locale') || 'en-US',
|
||||||
debug: false,
|
// debug: true,
|
||||||
defaultNS: 'client',
|
defaultNS: 'client',
|
||||||
// parseMissingKeyHandler: (key) => {
|
// parseMissingKeyHandler: (key) => {
|
||||||
// console.log('parseMissingKeyHandler', `'${key}': '${key}',`);
|
// console.log('parseMissingKeyHandler', `'${key}': '${key}',`);
|
||||||
// return key;
|
// return key;
|
||||||
// },
|
// },
|
||||||
// ns: ['client'],
|
// ns: ['client'],
|
||||||
resources: {
|
resources,
|
||||||
'en-US': {
|
|
||||||
client: {
|
|
||||||
...enUS,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'zh-CN': {
|
|
||||||
client: {
|
|
||||||
...zhCN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const momentLngs = {
|
const momentLngs = {
|
||||||
|
@ -6,6 +6,7 @@ export * from './antd-config-provider';
|
|||||||
export * from './api-client';
|
export * from './api-client';
|
||||||
export * from './application';
|
export * from './application';
|
||||||
export * from './async-data-provider';
|
export * from './async-data-provider';
|
||||||
|
export * from './block-provider';
|
||||||
export * from './board';
|
export * from './board';
|
||||||
export * from './china-region';
|
export * from './china-region';
|
||||||
export * from './collection-manager';
|
export * from './collection-manager';
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
{
|
export default {
|
||||||
"{{count}} filter items_one": "{{count}} filter item",
|
"{{count}} filter items_one": "{{count}} filter item",
|
||||||
"{{count}} filter items_other": "{{count}} filter items",
|
"{{count}} filter items_other": "{{count}} filter items",
|
||||||
"{{count}} more items_one": "{{count}} more item",
|
"{{count}} more items_one": "{{count}} more item",
|
||||||
"{{count}} more items_other": "{{count}} more items",
|
"{{count}} more items_other": "{{count}} more items",
|
||||||
|
"Total {{count}} items_one": "Total {{count}} item",
|
||||||
|
"Total {{count}} items_other": "Total {{count}} items",
|
||||||
"Delete menu item": "Delete menu item",
|
"Delete menu item": "Delete menu item",
|
||||||
"Today": "Today",
|
"Today": "Today",
|
||||||
"Month": "Month",
|
"Month": "Month",
|
15
packages/client/src/locale/index.ts
Normal file
15
packages/client/src/locale/index.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import enUS from './en_US';
|
||||||
|
import zhCN from './zh_CN';
|
||||||
|
|
||||||
|
export const resources = {
|
||||||
|
'en-US': {
|
||||||
|
client: {
|
||||||
|
...enUS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'zh-CN': {
|
||||||
|
client: {
|
||||||
|
...zhCN,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -1,10 +1,10 @@
|
|||||||
{
|
export default {
|
||||||
"Display <1><0>10</0><1>20</1><2>50</2><3>100</3></1> items per page": "每页显示 <1><0>10</0><1>20</1><2>50</2><3>100</3></1> 条",
|
"Display <1><0>10</0><1>20</1><2>50</2><3>100</3></1> items per page": "每页显示 <1><0>10</0><1>20</1><2>50</2><3>100</3></1> 条",
|
||||||
"Meet <1><0>All</0><1>Any</1></1> conditions in the group": "满足组内 <1><0>全部</0><1>任意</1></1> 条件",
|
"Meet <1><0>All</0><1>Any</1></1> conditions in the group": "满足组内 <1><0>全部</0><1>任意</1></1> 条件",
|
||||||
"Open in<1><0>Modal</0><1>Drawer</1><2>Window</2></1>": "在 <1><0>对话框</0><1>抽屉</1><2>窗口</2></1> 内打开",
|
"Open in<1><0>Modal</0><1>Drawer</1><2>Window</2></1>": "在 <1><0>对话框</0><1>抽屉</1><2>窗口</2></1> 内打开",
|
||||||
"{{count}} filter items": "{{count}} 个筛选项",
|
"{{count}} filter items": "{{count}} 个筛选项",
|
||||||
"{{count}} more items": "还有 {{count}} 项",
|
"{{count}} more items": "还有 {{count}} 项",
|
||||||
|
"Total {{count}} items": "总共 {{count}} 条",
|
||||||
"Today": "今天",
|
"Today": "今天",
|
||||||
"Month": "月",
|
"Month": "月",
|
||||||
"Week": "周",
|
"Week": "周",
|
||||||
@ -285,5 +285,7 @@
|
|||||||
"Block templates": "区块模板",
|
"Block templates": "区块模板",
|
||||||
"Convert reference to duplicate": "模板引用转为复制",
|
"Convert reference to duplicate": "模板引用转为复制",
|
||||||
"Template name": "模板名称",
|
"Template name": "模板名称",
|
||||||
"Block type": "区块类型"
|
"Block type": "区块类型",
|
||||||
|
"Action column": "操作列",
|
||||||
|
"Records per page": "每页显示数量"
|
||||||
}
|
}
|
@ -5,7 +5,8 @@ export const RecordIndexContext = createContext(null);
|
|||||||
|
|
||||||
export const RecordProvider: React.FC<{ record: any }> = (props) => {
|
export const RecordProvider: React.FC<{ record: any }> = (props) => {
|
||||||
const { record, children } = props;
|
const { record, children } = props;
|
||||||
return <RecordContext.Provider value={record}>{children}</RecordContext.Provider>;
|
const __parent = useContext(RecordContext);
|
||||||
|
return <RecordContext.Provider value={{...record, __parent}}>{children}</RecordContext.Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecordIndexProvider: React.FC<{ index: any }> = (props) => {
|
export const RecordIndexProvider: React.FC<{ index: any }> = (props) => {
|
||||||
|
@ -2,8 +2,9 @@ import React from 'react';
|
|||||||
import * as components from '.';
|
import * as components from '.';
|
||||||
import * as common from '../common';
|
import * as common from '../common';
|
||||||
import { SchemaComponentOptions } from '../core/SchemaComponentOptions';
|
import { SchemaComponentOptions } from '../core/SchemaComponentOptions';
|
||||||
|
import { useFilterActionProps } from './filter/useFilterActionProps';
|
||||||
|
|
||||||
export const AntdSchemaComponentProvider = (props) => {
|
export const AntdSchemaComponentProvider = (props) => {
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
return <SchemaComponentOptions components={{ ...components, ...common } as any}>{children}</SchemaComponentOptions>;
|
return <SchemaComponentOptions scope={{ useFilterActionProps }} components={{ ...components, ...common } as any}>{children}</SchemaComponentOptions>;
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,6 @@ import { useDesignable } from '../..';
|
|||||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||||
|
|
||||||
export const ActionDesigner = (props) => {
|
export const ActionDesigner = (props) => {
|
||||||
const initialValue = {};
|
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
@ -22,21 +21,35 @@ export const ActionDesigner = (props) => {
|
|||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
title: '按钮标题',
|
title: '按钮标题',
|
||||||
|
default: fieldSchema.title,
|
||||||
|
'x-component-props': {},
|
||||||
|
// description: `原字段标题:${collectionField?.uiSchema?.title}`,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'IconPicker',
|
||||||
|
title: '按钮图标',
|
||||||
|
default: fieldSchema?.['x-component-props']?.icon,
|
||||||
'x-component-props': {},
|
'x-component-props': {},
|
||||||
// description: `原字段标题:${collectionField?.uiSchema?.title}`,
|
// description: `原字段标题:${collectionField?.uiSchema?.title}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as ISchema
|
} as ISchema
|
||||||
}
|
}
|
||||||
initialValues={{ title: field.title }}
|
onSubmit={({ title, icon }) => {
|
||||||
onSubmit={({ title }) => {
|
|
||||||
if (title) {
|
if (title) {
|
||||||
fieldSchema.title = title;
|
fieldSchema.title = title;
|
||||||
field.title = title;
|
field.title = title;
|
||||||
|
field.componentProps.icon = icon;
|
||||||
|
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||||
|
fieldSchema['x-component-props'].icon = icon;
|
||||||
dn.emit('patch', {
|
dn.emit('patch', {
|
||||||
schema: {
|
schema: {
|
||||||
['x-uid']: fieldSchema['x-uid'],
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
title,
|
title,
|
||||||
|
'x-component-props': {
|
||||||
|
...fieldSchema['x-component-props'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
dn.refresh();
|
dn.refresh();
|
||||||
|
@ -36,6 +36,17 @@ export const ActionDrawer: ComposedActionDrawer = observer((props) => {
|
|||||||
background: #f0f2f5;
|
background: #f0f2f5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.nb-record-picker-selector {
|
||||||
|
.nb-block-item {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
.general-schema-designer {
|
||||||
|
top: -8px;
|
||||||
|
bottom: -8px;
|
||||||
|
left: -8px;
|
||||||
|
right: -8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
footer={
|
footer={
|
||||||
|
@ -4,8 +4,10 @@ import { Button, Modal, Popover } from 'antd';
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useActionContext } from '../..';
|
import { useActionContext } from '../..';
|
||||||
|
import { Icon } from '../../../icon';
|
||||||
import { SortableItem } from '../../common';
|
import { SortableItem } from '../../common';
|
||||||
import { useDesigner } from '../../hooks';
|
import { useDesigner } from '../../hooks';
|
||||||
|
import { useProps } from '../../hooks/useProps';
|
||||||
import ActionContainer from './Action.Container';
|
import ActionContainer from './Action.Container';
|
||||||
import { ActionDesigner } from './Action.Designer';
|
import { ActionDesigner } from './Action.Designer';
|
||||||
import { ActionDrawer } from './Action.Drawer';
|
import { ActionDrawer } from './Action.Drawer';
|
||||||
@ -71,10 +73,12 @@ export const Action: ComposedAction = observer((props: any) => {
|
|||||||
containerRefKey,
|
containerRefKey,
|
||||||
component,
|
component,
|
||||||
useAction = useA,
|
useAction = useA,
|
||||||
onClick,
|
|
||||||
className,
|
className,
|
||||||
|
disabled,
|
||||||
|
icon,
|
||||||
...others
|
...others
|
||||||
} = props;
|
} = props;
|
||||||
|
const { onClick } = useProps(props);
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const Designer = useDesigner();
|
const Designer = useDesigner();
|
||||||
const field = useField();
|
const field = useField();
|
||||||
@ -84,6 +88,7 @@ export const Action: ComposedAction = observer((props: any) => {
|
|||||||
const renderButton = () => (
|
const renderButton = () => (
|
||||||
<SortableItem
|
<SortableItem
|
||||||
{...others}
|
{...others}
|
||||||
|
icon={<Icon type={icon} />}
|
||||||
onClick={(e: React.MouseEvent) => {
|
onClick={(e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -104,8 +109,8 @@ export const Action: ComposedAction = observer((props: any) => {
|
|||||||
component={component || Button}
|
component={component || Button}
|
||||||
className={classnames(className, actionDesignerCss)}
|
className={classnames(className, actionDesignerCss)}
|
||||||
>
|
>
|
||||||
<Designer {...designerProps} />
|
|
||||||
{field.title}
|
{field.title}
|
||||||
|
<Designer {...designerProps} />
|
||||||
</SortableItem>
|
</SortableItem>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
|
@ -0,0 +1,142 @@
|
|||||||
|
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useCompile, useDesignable } from '../..';
|
||||||
|
import { useCalendarBlockContext } from '../../../block-provider';
|
||||||
|
import { useCollection } from '../../../collection-manager';
|
||||||
|
import { useCollectionFilterOptions } from '../../../collection-manager/action-hooks';
|
||||||
|
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||||
|
import { useSchemaTemplate } from '../../../schema-templates';
|
||||||
|
|
||||||
|
const useOptions = () => {
|
||||||
|
const compile = useCompile();
|
||||||
|
const { fields } = useCollection();
|
||||||
|
const options = fields?.map((field) => {
|
||||||
|
return {
|
||||||
|
value: field.name,
|
||||||
|
label: compile(field?.uiSchema?.title),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CalendarDesigner = () => {
|
||||||
|
const field = useField();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { name, title, fields } = useCollection();
|
||||||
|
const dataSource = useCollectionFilterOptions(name);
|
||||||
|
const { service } = useCalendarBlockContext();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const compile = useCompile();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const template = useSchemaTemplate();
|
||||||
|
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
|
||||||
|
const options = useOptions();
|
||||||
|
const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {};
|
||||||
|
return (
|
||||||
|
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||||
|
<SchemaSettings.SelectItem
|
||||||
|
title={t('Title field')}
|
||||||
|
value={fieldNames.title}
|
||||||
|
options={options}
|
||||||
|
onChange={(title) => {
|
||||||
|
const fieldNames = field.decoratorProps.fieldNames || {};
|
||||||
|
fieldNames['title'] = title;
|
||||||
|
field.decoratorProps.params = fieldNames;
|
||||||
|
fieldSchema['x-decorator-props']['params'] = fieldNames;
|
||||||
|
service.refresh();
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-decorator-props': field.decoratorProps,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettings.SelectItem
|
||||||
|
title={t('Start date field')}
|
||||||
|
value={fieldNames.start}
|
||||||
|
options={options}
|
||||||
|
onChange={(start) => {
|
||||||
|
const fieldNames = field.decoratorProps.fieldNames || {};
|
||||||
|
fieldNames['start'] = start;
|
||||||
|
field.decoratorProps.params = fieldNames;
|
||||||
|
fieldSchema['x-decorator-props']['params'] = fieldNames;
|
||||||
|
service.refresh();
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-decorator-props': field.decoratorProps,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettings.SelectItem
|
||||||
|
title={t('End date field')}
|
||||||
|
value={fieldNames.end}
|
||||||
|
options={options}
|
||||||
|
onChange={(end) => {
|
||||||
|
const fieldNames = field.decoratorProps.fieldNames || {};
|
||||||
|
fieldNames['end'] = end;
|
||||||
|
field.decoratorProps.params = fieldNames;
|
||||||
|
fieldSchema['x-decorator-props']['params'] = fieldNames;
|
||||||
|
service.refresh();
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-decorator-props': field.decoratorProps,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettings.ModalItem
|
||||||
|
title={t('Set the data scope')}
|
||||||
|
schema={
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
title: t('Set the data scope'),
|
||||||
|
properties: {
|
||||||
|
filter: {
|
||||||
|
default: defaultFilter,
|
||||||
|
enum: dataSource,
|
||||||
|
'x-component': 'Filter',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ISchema
|
||||||
|
}
|
||||||
|
initialValues={
|
||||||
|
{
|
||||||
|
// title: field.title,
|
||||||
|
// icon: field.componentProps.icon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSubmit={({ filter }) => {
|
||||||
|
const params = field.decoratorProps.params || {};
|
||||||
|
params.filter = filter;
|
||||||
|
field.decoratorProps.params = params;
|
||||||
|
fieldSchema['x-decorator-props']['params'] = params;
|
||||||
|
service.run({ ...service?.params?.[0], filter });
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-decorator-props': field.decoratorProps,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettings.Divider />
|
||||||
|
<SchemaSettings.Template componentName={'Calendar'} collectionName={name} />
|
||||||
|
<SchemaSettings.Divider />
|
||||||
|
<SchemaSettings.Remove
|
||||||
|
removeParentsIfNoChildren
|
||||||
|
breakRemoveOn={{
|
||||||
|
'x-component': 'Grid',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</GeneralSchemaDesigner>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,156 @@
|
|||||||
|
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||||
|
import { createForm } from '@formily/core';
|
||||||
|
import { observer, RecursionField, Schema, useFieldSchema } from '@formily/react';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import moment from 'moment';
|
||||||
|
import React, { useMemo, useState } from 'react';
|
||||||
|
import { Calendar as BigCalendar, momentLocalizer } from 'react-big-calendar';
|
||||||
|
import * as dates from 'react-big-calendar/lib/utils/dates';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { RecordProvider } from '../../../';
|
||||||
|
import { i18n } from '../../../i18n';
|
||||||
|
import { useProps } from '../../hooks/useProps';
|
||||||
|
import { ActionContext } from '../action';
|
||||||
|
import { CalendarToolbarContext } from './context';
|
||||||
|
import './style.less';
|
||||||
|
import type { ToolbarProps } from './types';
|
||||||
|
|
||||||
|
const localizer = momentLocalizer(moment);
|
||||||
|
|
||||||
|
function Toolbar(props: ToolbarProps) {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const toolBarSchema: Schema = useMemo(
|
||||||
|
() =>
|
||||||
|
fieldSchema.reduceProperties((buf, current) => {
|
||||||
|
if (current['x-component'].endsWith('.ActionBar')) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}, null),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<CalendarToolbarContext.Provider value={props}>
|
||||||
|
<RecursionField name={toolBarSchema.name} schema={toolBarSchema} />
|
||||||
|
</CalendarToolbarContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages: any = {
|
||||||
|
allDay: '',
|
||||||
|
previous: (
|
||||||
|
<div>
|
||||||
|
<LeftOutlined />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
next: (
|
||||||
|
<div>
|
||||||
|
<RightOutlined />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
today: i18n.t('Today'),
|
||||||
|
month: i18n.t('Month'),
|
||||||
|
week: i18n.t('Week'),
|
||||||
|
work_week: i18n.t('Work week'),
|
||||||
|
day: i18n.t('Day'),
|
||||||
|
agenda: i18n.t('Agenda'),
|
||||||
|
date: i18n.t('Date'),
|
||||||
|
time: i18n.t('Time'),
|
||||||
|
event: i18n.t('Event'),
|
||||||
|
noEventsInRange: i18n.t('None'),
|
||||||
|
showMore: (count) => i18n.t('{{count}} more items', { count }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const useEvents = (dataSource: any, fieldNames: any) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(dataSource)
|
||||||
|
? dataSource?.map((item) => {
|
||||||
|
return {
|
||||||
|
id: get(item, fieldNames.id || 'id'),
|
||||||
|
title: get(item, fieldNames.title) || t('Untitle'),
|
||||||
|
start: new Date(get(item, fieldNames.start)),
|
||||||
|
end: new Date(get(item, fieldNames.end || fieldNames.start)),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
[dataSource, fieldNames],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CalendarRecordViewer = (props) => {
|
||||||
|
const { visible, setVisible, record } = props;
|
||||||
|
const form = useMemo(() => createForm(), [record]);
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const eventSchema: Schema = useMemo(
|
||||||
|
() =>
|
||||||
|
fieldSchema.reduceProperties((buf, current) => {
|
||||||
|
if (current['x-component'].endsWith('.Event')) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}, null),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
eventSchema && (
|
||||||
|
<ActionContext.Provider value={{ visible, setVisible }}>
|
||||||
|
<RecordProvider record={record}>
|
||||||
|
<RecursionField schema={eventSchema} name={eventSchema.name} />
|
||||||
|
</RecordProvider>
|
||||||
|
</ActionContext.Provider>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Calendar: any = observer((props: any) => {
|
||||||
|
const { dataSource, fieldNames } = useProps(props);
|
||||||
|
const events = useEvents(dataSource, fieldNames);
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [record, setRecord] = useState<any>({});
|
||||||
|
return (
|
||||||
|
<div {...props} style={{ height: 700 }}>
|
||||||
|
<CalendarRecordViewer visible={visible} setVisible={setVisible} record={record} />
|
||||||
|
<BigCalendar
|
||||||
|
popup
|
||||||
|
selectable
|
||||||
|
events={events}
|
||||||
|
views={['month', 'week', 'day']}
|
||||||
|
step={60}
|
||||||
|
showMultiDayTimes
|
||||||
|
messages={messages}
|
||||||
|
onSelectSlot={(slotInfo) => {
|
||||||
|
console.log('onSelectSlot', slotInfo);
|
||||||
|
}}
|
||||||
|
onDoubleClickEvent={(event) => {
|
||||||
|
console.log('onDoubleClickEvent');
|
||||||
|
}}
|
||||||
|
onSelectEvent={(event) => {
|
||||||
|
const record = dataSource?.find((item) => item[fieldNames.id] === event.id);
|
||||||
|
if (!record) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRecord(record);
|
||||||
|
setVisible(true);
|
||||||
|
}}
|
||||||
|
formats={{
|
||||||
|
monthHeaderFormat: 'Y-M',
|
||||||
|
agendaDateFormat: 'M-DD',
|
||||||
|
dayHeaderFormat: 'Y-M-DD',
|
||||||
|
dayRangeHeaderFormat: ({ start, end }, culture, local) => {
|
||||||
|
if (dates.eq(start, end, 'month')) {
|
||||||
|
return local.format(start, 'Y-M', culture);
|
||||||
|
}
|
||||||
|
return `${local.format(start, 'Y-M', culture)} - ${local.format(end, 'Y-M', culture)}`;
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
defaultDate={new Date()}
|
||||||
|
components={{
|
||||||
|
toolbar: Toolbar,
|
||||||
|
}}
|
||||||
|
localizer={localizer}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,6 @@
|
|||||||
|
import { observer } from '@formily/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const Event = observer((props) => {
|
||||||
|
return <>{props.children}</>;
|
||||||
|
});
|
@ -0,0 +1,19 @@
|
|||||||
|
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||||
|
import { observer } from '@formily/react';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { navigate } from 'react-big-calendar/lib/utils/constants';
|
||||||
|
import { useDesignable } from '../../hooks';
|
||||||
|
import { CalendarToolbarContext } from './context';
|
||||||
|
|
||||||
|
export const Nav = observer((props) => {
|
||||||
|
const { DesignableBar } = useDesignable();
|
||||||
|
const { onNavigate } = useContext(CalendarToolbarContext);
|
||||||
|
return (
|
||||||
|
<div className="ant-btn-group">
|
||||||
|
<Button icon={<LeftOutlined />} onClick={() => onNavigate(navigate.PREVIOUS)}></Button>
|
||||||
|
<Button icon={<RightOutlined />} onClick={() => onNavigate(navigate.NEXT)}></Button>
|
||||||
|
<DesignableBar />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,15 @@
|
|||||||
|
import { observer } from '@formily/react';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { useDesignable } from '../../hooks';
|
||||||
|
import { CalendarToolbarContext } from './context';
|
||||||
|
|
||||||
|
export const Title = observer(() => {
|
||||||
|
const { DesignableBar } = useDesignable();
|
||||||
|
const { label } = useContext(CalendarToolbarContext);
|
||||||
|
return (
|
||||||
|
<div className="ant-btn-group" style={{ fontSize: '1.75em', fontWeight: 300 }}>
|
||||||
|
{label}
|
||||||
|
<DesignableBar />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,23 @@
|
|||||||
|
import { observer } from '@formily/react';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { navigate } from 'react-big-calendar/lib/utils/constants';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useDesignable } from '../../hooks';
|
||||||
|
import { CalendarToolbarContext } from './context';
|
||||||
|
|
||||||
|
export const Today = observer((props) => {
|
||||||
|
const { DesignableBar } = useDesignable();
|
||||||
|
const { onNavigate } = useContext(CalendarToolbarContext);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
onNavigate(navigate.TODAY);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Today')}
|
||||||
|
<DesignableBar />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,25 @@
|
|||||||
|
import { observer } from '@formily/react';
|
||||||
|
import { Select } from 'antd';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { useDesignable } from '../../hooks';
|
||||||
|
import { CalendarToolbarContext } from './context';
|
||||||
|
|
||||||
|
export const ViewSelect = observer((props) => {
|
||||||
|
const { DesignableBar } = useDesignable();
|
||||||
|
const {
|
||||||
|
views,
|
||||||
|
view,
|
||||||
|
onView,
|
||||||
|
localizer: { messages },
|
||||||
|
} = useContext(CalendarToolbarContext);
|
||||||
|
return (
|
||||||
|
<div className="ant-btn-group">
|
||||||
|
<Select value={view} onChange={onView}>
|
||||||
|
{views.map((name) => (
|
||||||
|
<Select.Option value={name}>{messages[name]}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<DesignableBar />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,5 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
import type { ToolbarProps } from './types';
|
||||||
|
|
||||||
|
export const CalendarToolbarContext = createContext<ToolbarProps>(null);
|
||||||
|
export const CalendarContext = createContext(null);
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,131 @@
|
|||||||
|
export default {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
createdAt: '2022-03-06T07:22:21.163Z',
|
||||||
|
updatedAt: '2022-03-12T04:34:19.147Z',
|
||||||
|
createdById: 1,
|
||||||
|
updatedById: 1,
|
||||||
|
f_1ckuegfab9s: null,
|
||||||
|
f_a4z4h45vi5b: 'hgobsqbfnd4',
|
||||||
|
f_cht6rsiiiko: 'ftfccbha25a',
|
||||||
|
f_d93g4r08krl: '1',
|
||||||
|
f_f7txg1oc3nt: '2022-03-07T07:22:21.163Z',
|
||||||
|
f_g8j5jvalqh0: '子表格字段缺少标题',
|
||||||
|
f_hpmvdltzs6m: null,
|
||||||
|
f_tegyd222bcc: '见附件,在此位置显示标题,包括表单和详情',
|
||||||
|
f_yc8jbfiqfvh: 'v25527dxseu',
|
||||||
|
f_z27302tl2bf: '9yyv2tawmry',
|
||||||
|
f_zek99qhv0vc: '2022-03-12T04:34:19.147Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
createdAt: '2022-03-07T11:10:50.869Z',
|
||||||
|
updatedAt: '2022-03-12T04:34:37.792Z',
|
||||||
|
createdById: 1,
|
||||||
|
updatedById: 1,
|
||||||
|
f_1ckuegfab9s: null,
|
||||||
|
f_a4z4h45vi5b: 'r3fxlrunwd8',
|
||||||
|
f_cht6rsiiiko: 'ftfccbha25a',
|
||||||
|
f_d93g4r08krl: '2',
|
||||||
|
f_f7txg1oc3nt: '2022-03-07T11:10:50.869Z',
|
||||||
|
f_g8j5jvalqh0: '抽屉关闭时,如果内容已更改未保存,弹出提示',
|
||||||
|
f_hpmvdltzs6m: '67snt3e6yld',
|
||||||
|
f_tegyd222bcc: null,
|
||||||
|
f_yc8jbfiqfvh: 'c4hobfb5k07',
|
||||||
|
f_z27302tl2bf: 'mkvgt7ndsqd',
|
||||||
|
f_zek99qhv0vc: '2022-03-12T04:34:37.792Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
createdAt: '2022-03-07T11:12:49.853Z',
|
||||||
|
updatedAt: '2022-03-11T15:37:00.663Z',
|
||||||
|
createdById: 1,
|
||||||
|
updatedById: 1,
|
||||||
|
f_1ckuegfab9s: null,
|
||||||
|
f_a4z4h45vi5b: 't8prrpfk7kl',
|
||||||
|
f_cht6rsiiiko: 'ftfccbha25a',
|
||||||
|
f_d93g4r08krl: null,
|
||||||
|
f_f7txg1oc3nt: '2022-03-07T11:12:49.853Z',
|
||||||
|
f_g8j5jvalqh0: '字段设置必填后,未验证必填',
|
||||||
|
f_hpmvdltzs6m: 'irepvdzp1ac',
|
||||||
|
f_tegyd222bcc: null,
|
||||||
|
f_yc8jbfiqfvh: 'v25527dxseu',
|
||||||
|
f_z27302tl2bf: 'mkvgt7ndsqd',
|
||||||
|
f_zek99qhv0vc: '2022-03-11T15:37:00.663Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
createdAt: '2022-03-07T11:13:45.123Z',
|
||||||
|
updatedAt: '2022-03-10T16:02:25.805Z',
|
||||||
|
createdById: 1,
|
||||||
|
updatedById: 1,
|
||||||
|
f_1ckuegfab9s: null,
|
||||||
|
f_a4z4h45vi5b: null,
|
||||||
|
f_cht6rsiiiko: null,
|
||||||
|
f_d93g4r08krl: null,
|
||||||
|
f_f7txg1oc3nt: '2022-03-07T11:13:45.123Z',
|
||||||
|
f_g8j5jvalqh0: '菜单项移动功能',
|
||||||
|
f_hpmvdltzs6m: 'irepvdzp1ac',
|
||||||
|
f_tegyd222bcc: '同上一个版本',
|
||||||
|
f_yc8jbfiqfvh: 's7hocw6twfk',
|
||||||
|
f_z27302tl2bf: 'mkvgt7ndsqd',
|
||||||
|
f_zek99qhv0vc: '2022-03-10T16:02:25.805Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
createdAt: '2022-03-07T11:16:15.876Z',
|
||||||
|
updatedAt: '2022-03-10T16:02:25.805Z',
|
||||||
|
createdById: 1,
|
||||||
|
updatedById: 1,
|
||||||
|
f_1ckuegfab9s: null,
|
||||||
|
f_a4z4h45vi5b: null,
|
||||||
|
f_cht6rsiiiko: null,
|
||||||
|
f_d93g4r08krl: null,
|
||||||
|
f_f7txg1oc3nt: '2022-03-07T11:16:15.876Z',
|
||||||
|
f_g8j5jvalqh0: '附件样式',
|
||||||
|
f_hpmvdltzs6m: 'irepvdzp1ac',
|
||||||
|
f_tegyd222bcc:
|
||||||
|
'修改后,有附件的行跟没附件的行的高度还是不一样\n\n1. 表格里显示更小的缩略图,同之前版本\n2. 还有子表格里\n3. 鼠标移上去之后的图标位置\n\n备注:行高不一的未解决,以后统一解决,需要给每个 td 加上字段类型的 className 方便调整不同字段的样式问题',
|
||||||
|
f_yc8jbfiqfvh: 's7hocw6twfk',
|
||||||
|
f_z27302tl2bf: '9yyv2tawmry',
|
||||||
|
f_zek99qhv0vc: '2022-03-10T16:02:25.805Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
createdAt: '2022-03-07T11:17:11.423Z',
|
||||||
|
updatedAt: '2022-03-12T06:11:09.330Z',
|
||||||
|
createdById: 1,
|
||||||
|
updatedById: 1,
|
||||||
|
f_1ckuegfab9s: null,
|
||||||
|
f_a4z4h45vi5b: null,
|
||||||
|
f_cht6rsiiiko: null,
|
||||||
|
f_d93g4r08krl: null,
|
||||||
|
f_f7txg1oc3nt: '2022-03-07T11:17:11.423Z',
|
||||||
|
f_g8j5jvalqh0: '日志',
|
||||||
|
f_hpmvdltzs6m: 'ht963n365al',
|
||||||
|
f_tegyd222bcc: '同原来版本的日志功能。\n- ActionLogBlockInitializer;\n- ActionLog.Designer;',
|
||||||
|
f_yc8jbfiqfvh: 'v25527dxseu',
|
||||||
|
f_z27302tl2bf: 'mkvgt7ndsqd',
|
||||||
|
f_zek99qhv0vc: '2022-03-12T06:11:09.330Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
createdAt: '2022-03-07T11:18:40.719Z',
|
||||||
|
updatedAt: '2022-03-12T04:24:31.402Z',
|
||||||
|
createdById: 1,
|
||||||
|
updatedById: 1,
|
||||||
|
f_1ckuegfab9s: null,
|
||||||
|
f_a4z4h45vi5b: null,
|
||||||
|
f_cht6rsiiiko: 'r6l2wg1snxc',
|
||||||
|
f_d93g4r08krl: null,
|
||||||
|
f_f7txg1oc3nt: '2022-03-07T11:18:40.719Z',
|
||||||
|
f_g8j5jvalqh0: '配置图标没有居中',
|
||||||
|
f_hpmvdltzs6m: 'irepvdzp1ac',
|
||||||
|
f_tegyd222bcc: null,
|
||||||
|
f_yc8jbfiqfvh: 'v25527dxseu',
|
||||||
|
f_z27302tl2bf: '9yyv2tawmry',
|
||||||
|
f_zek99qhv0vc: '2022-03-12T04:24:31.402Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@ -0,0 +1,180 @@
|
|||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
title: 'All Day Event very long title',
|
||||||
|
allDay: true,
|
||||||
|
start: new Date(2015, 3, 0),
|
||||||
|
end: new Date(2015, 3, 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Long Event',
|
||||||
|
start: new Date(2015, 3, 7),
|
||||||
|
end: new Date(2015, 3, 10),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'DTS STARTS',
|
||||||
|
start: new Date(2016, 2, 13, 0, 0, 0),
|
||||||
|
end: new Date(2016, 2, 20, 0, 0, 0),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'DTS ENDS',
|
||||||
|
start: new Date(2016, 10, 6, 0, 0, 0),
|
||||||
|
end: new Date(2016, 10, 13, 0, 0, 0),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'Some Event',
|
||||||
|
start: new Date(2015, 3, 9, 0, 0, 0),
|
||||||
|
end: new Date(2015, 3, 10, 0, 0, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: 'Conference',
|
||||||
|
start: new Date(2015, 3, 11),
|
||||||
|
end: new Date(2015, 3, 13),
|
||||||
|
desc: 'Big conference for important people',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
title: 'Meeting',
|
||||||
|
start: new Date(2015, 3, 12, 10, 30, 0, 0),
|
||||||
|
end: new Date(2015, 3, 12, 12, 30, 0, 0),
|
||||||
|
desc: 'Pre-meeting meeting, to prepare for the meeting',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
title: 'Lunch',
|
||||||
|
start: new Date(2015, 3, 12, 12, 0, 0, 0),
|
||||||
|
end: new Date(2015, 3, 12, 13, 0, 0, 0),
|
||||||
|
desc: 'Power lunch',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
title: 'Meeting',
|
||||||
|
start: new Date(2015, 3, 12, 14, 0, 0, 0),
|
||||||
|
end: new Date(2015, 3, 12, 15, 0, 0, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
title: 'Happy Hour',
|
||||||
|
start: new Date(2015, 3, 12, 17, 0, 0, 0),
|
||||||
|
end: new Date(2015, 3, 12, 17, 30, 0, 0),
|
||||||
|
desc: 'Most important meal of the day',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
title: 'Dinner',
|
||||||
|
start: new Date(2015, 3, 12, 20, 0, 0, 0),
|
||||||
|
end: new Date(2015, 3, 12, 21, 0, 0, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
title: 'Planning Meeting with Paige',
|
||||||
|
start: new Date(2015, 3, 13, 8, 0, 0),
|
||||||
|
end: new Date(2015, 3, 13, 10, 30, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11.1,
|
||||||
|
title: 'Inconvenient Conference Call',
|
||||||
|
start: new Date(2015, 3, 13, 9, 30, 0),
|
||||||
|
end: new Date(2015, 3, 13, 12, 0, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11.2,
|
||||||
|
title: "Project Kickoff - Lou's Shoes",
|
||||||
|
start: new Date(2015, 3, 13, 11, 30, 0),
|
||||||
|
end: new Date(2015, 3, 13, 14, 0, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11.3,
|
||||||
|
title: 'Quote Follow-up - Tea by Tina',
|
||||||
|
start: new Date(2015, 3, 13, 15, 30, 0),
|
||||||
|
end: new Date(2015, 3, 13, 16, 0, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
title: 'Late Night Event',
|
||||||
|
start: new Date(2015, 3, 17, 19, 30, 0),
|
||||||
|
end: new Date(2015, 3, 18, 2, 0, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12.5,
|
||||||
|
title: 'Late Same Night Event',
|
||||||
|
start: new Date(2015, 3, 17, 19, 30, 0),
|
||||||
|
end: new Date(2015, 3, 17, 23, 30, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
title: 'Multi-day Event',
|
||||||
|
start: new Date(2015, 3, 20, 19, 30, 0),
|
||||||
|
end: new Date(2015, 3, 22, 2, 0, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 14,
|
||||||
|
title: 'Today',
|
||||||
|
start: new Date(new Date().setHours(new Date().getHours() - 3)),
|
||||||
|
end: new Date(new Date().setHours(new Date().getHours() + 3)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 15,
|
||||||
|
title: 'Point in Time Event',
|
||||||
|
start: now,
|
||||||
|
end: now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 16,
|
||||||
|
title: 'Video Record',
|
||||||
|
start: new Date(2015, 3, 14, 15, 30, 0),
|
||||||
|
end: new Date(2015, 3, 14, 19, 0, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 17,
|
||||||
|
title: 'Dutch Song Producing',
|
||||||
|
start: new Date(2015, 3, 14, 16, 30, 0),
|
||||||
|
end: new Date(2015, 3, 14, 20, 0, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 18,
|
||||||
|
title: 'Itaewon Halloween Meeting',
|
||||||
|
start: new Date(2015, 3, 14, 16, 30, 0),
|
||||||
|
end: new Date(2015, 3, 14, 17, 30, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 19,
|
||||||
|
title: 'Online Coding Test',
|
||||||
|
start: new Date(2015, 3, 14, 17, 30, 0),
|
||||||
|
end: new Date(2015, 3, 14, 20, 30, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 20,
|
||||||
|
title: 'An overlapped Event',
|
||||||
|
start: new Date(2015, 3, 14, 17, 0, 0),
|
||||||
|
end: new Date(2015, 3, 14, 18, 30, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 21,
|
||||||
|
title: 'Phone Interview',
|
||||||
|
start: new Date(2015, 3, 14, 17, 0, 0),
|
||||||
|
end: new Date(2015, 3, 14, 18, 30, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 22,
|
||||||
|
title: 'Cooking Class',
|
||||||
|
start: new Date(2015, 3, 14, 17, 30, 0),
|
||||||
|
end: new Date(2015, 3, 14, 19, 0, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 23,
|
||||||
|
title: 'Go to the gym',
|
||||||
|
start: new Date(2015, 3, 14, 18, 30, 0),
|
||||||
|
end: new Date(2015, 3, 14, 20, 0, 0),
|
||||||
|
},
|
||||||
|
];
|
@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* title: Calendar
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
AntdSchemaComponentProvider,
|
||||||
|
SchemaComponent,
|
||||||
|
SchemaComponentProvider,
|
||||||
|
SchemaInitializerProvider
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import defaultValues from './defaultValues';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'array',
|
||||||
|
name: 'calendar1',
|
||||||
|
'x-component': 'CalendarV2',
|
||||||
|
'x-component-props': {},
|
||||||
|
default: defaultValues,
|
||||||
|
properties: {
|
||||||
|
toolBar: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'CalendarV2.ActionBar',
|
||||||
|
properties: {
|
||||||
|
today: {
|
||||||
|
type: 'void',
|
||||||
|
title: '今天',
|
||||||
|
'x-component': 'CalendarV2.Today',
|
||||||
|
'x-action': 'calendar:today',
|
||||||
|
'x-align': 'left',
|
||||||
|
},
|
||||||
|
nav: {
|
||||||
|
type: 'void',
|
||||||
|
title: '翻页',
|
||||||
|
'x-component': 'CalendarV2.Nav',
|
||||||
|
'x-action': 'calendar:nav',
|
||||||
|
'x-align': 'left',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'void',
|
||||||
|
title: '标题',
|
||||||
|
'x-component': 'CalendarV2.Title',
|
||||||
|
'x-action': 'calendar:title',
|
||||||
|
'x-align': 'left',
|
||||||
|
},
|
||||||
|
viewSelect: {
|
||||||
|
type: 'void',
|
||||||
|
title: '视图切换',
|
||||||
|
'x-component': 'CalendarV2.ViewSelect',
|
||||||
|
'x-action': 'calendar:viewSelect',
|
||||||
|
'x-align': 'right',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider>
|
||||||
|
<SchemaInitializerProvider>
|
||||||
|
<AntdSchemaComponentProvider>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</AntdSchemaComponentProvider>
|
||||||
|
</SchemaInitializerProvider>
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,117 @@
|
|||||||
|
import { ISchema } from '@formily/react';
|
||||||
|
import {
|
||||||
|
AntdSchemaComponentProvider,
|
||||||
|
APIClient,
|
||||||
|
APIClientProvider,
|
||||||
|
BlockSchemaComponentProvider,
|
||||||
|
CollectionManagerProvider,
|
||||||
|
SchemaComponent,
|
||||||
|
SchemaComponentProvider
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import React from 'react';
|
||||||
|
import collections from './collections';
|
||||||
|
import data from './data';
|
||||||
|
|
||||||
|
const schema: ISchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
block: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'CalendarBlockProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
collection: 't_j6omof6tza8',
|
||||||
|
resource: 't_j6omof6tza8',
|
||||||
|
action: 'list',
|
||||||
|
fieldNames: {
|
||||||
|
id: 'id',
|
||||||
|
start: 'createdAt',
|
||||||
|
end: 'createdAt',
|
||||||
|
title: 'f_g8j5jvalqh0',
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
paginate: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
calendar: {
|
||||||
|
type: 'array',
|
||||||
|
name: 'calendar1',
|
||||||
|
'x-component': 'CalendarV2',
|
||||||
|
'x-component-props': {
|
||||||
|
useProps: '{{ useCalendarBlockProps }}',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
toolBar: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'CalendarV2.ActionBar',
|
||||||
|
properties: {
|
||||||
|
today: {
|
||||||
|
type: 'void',
|
||||||
|
title: '今天',
|
||||||
|
'x-component': 'CalendarV2.Today',
|
||||||
|
'x-action': 'calendar:today',
|
||||||
|
'x-align': 'left',
|
||||||
|
},
|
||||||
|
nav: {
|
||||||
|
type: 'void',
|
||||||
|
title: '翻页',
|
||||||
|
'x-component': 'CalendarV2.Nav',
|
||||||
|
'x-action': 'calendar:nav',
|
||||||
|
'x-align': 'left',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'void',
|
||||||
|
title: '标题',
|
||||||
|
'x-component': 'CalendarV2.Title',
|
||||||
|
'x-action': 'calendar:title',
|
||||||
|
'x-align': 'left',
|
||||||
|
},
|
||||||
|
viewSelect: {
|
||||||
|
type: 'void',
|
||||||
|
title: '视图切换',
|
||||||
|
'x-component': 'CalendarV2.ViewSelect',
|
||||||
|
'x-action': 'calendar:viewSelect',
|
||||||
|
'x-align': 'right',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiClient = new APIClient({
|
||||||
|
baseURL: 'http://localhost:3000/api',
|
||||||
|
});
|
||||||
|
|
||||||
|
const sleep = (value: number) => new Promise((resolve) => setTimeout(resolve, value));
|
||||||
|
|
||||||
|
const mock = (api: APIClient) => {
|
||||||
|
const mock = new MockAdapter(api.axios);
|
||||||
|
|
||||||
|
mock.onGet('/t_j6omof6tza8:list').reply(async (config) => {
|
||||||
|
await sleep(2000);
|
||||||
|
return [200, data];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
mock(apiClient);
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<APIClientProvider apiClient={apiClient}>
|
||||||
|
<SchemaComponentProvider>
|
||||||
|
<CollectionManagerProvider collections={collections.data}>
|
||||||
|
<AntdSchemaComponentProvider>
|
||||||
|
<BlockSchemaComponentProvider>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</BlockSchemaComponentProvider>
|
||||||
|
</AntdSchemaComponentProvider>
|
||||||
|
</CollectionManagerProvider>
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
</APIClientProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
nav:
|
||||||
|
path: /client
|
||||||
|
group:
|
||||||
|
path: /schema-components
|
||||||
|
---
|
||||||
|
|
||||||
|
# CalendarV2
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
#### Basic
|
||||||
|
|
||||||
|
<code src="./demos/demo1.tsx" />
|
||||||
|
|
||||||
|
### CalendarBlockProvider
|
||||||
|
|
||||||
|
<code src="./demos/demo2.tsx" />
|
@ -0,0 +1,19 @@
|
|||||||
|
import { ActionBar } from '../action';
|
||||||
|
import { Calendar as CalendarV2 } from './Calendar';
|
||||||
|
import { CalendarDesigner } from './Calendar.Designer';
|
||||||
|
import { Event } from './Event';
|
||||||
|
import { Nav } from './Nav';
|
||||||
|
import './style.less';
|
||||||
|
import { Title } from './Title';
|
||||||
|
import { Today } from './Today';
|
||||||
|
import { ViewSelect } from './ViewSelect';
|
||||||
|
|
||||||
|
CalendarV2.ActionBar = ActionBar;
|
||||||
|
CalendarV2.Event = Event;
|
||||||
|
CalendarV2.Title = Title;
|
||||||
|
CalendarV2.Today = Today;
|
||||||
|
CalendarV2.Nav = Nav;
|
||||||
|
CalendarV2.ViewSelect = ViewSelect;
|
||||||
|
CalendarV2.Designer = CalendarDesigner;
|
||||||
|
|
||||||
|
export { CalendarV2 };
|
767
packages/client/src/schema-component/antd/calendar-v2/style.less
Normal file
767
packages/client/src/schema-component/antd/calendar-v2/style.less
Normal file
@ -0,0 +1,767 @@
|
|||||||
|
@charset "UTF-8";
|
||||||
|
.rbc-btn {
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.rbc-btn {
|
||||||
|
overflow: visible;
|
||||||
|
text-transform: none;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[disabled].rbc-btn {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.rbc-input::-moz-focus-inner {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-calendar {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-calendar *,
|
||||||
|
.rbc-calendar *:before,
|
||||||
|
.rbc-calendar *:after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-abs-full,
|
||||||
|
.rbc-row-bg {
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-ellipsis,
|
||||||
|
.rbc-event-label,
|
||||||
|
.rbc-row-segment .rbc-event-content,
|
||||||
|
.rbc-show-more {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-rtl {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-off-range {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-off-range-bg {
|
||||||
|
// background: #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-header {
|
||||||
|
// font-size: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1 0 0%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 4px 12px;
|
||||||
|
text-align: right;
|
||||||
|
vertical-align: middle;
|
||||||
|
// font-weight: 500;
|
||||||
|
min-height: 32px;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
margin: 0 4px;
|
||||||
|
border-bottom: 2px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.rbc-header + .rbc-header {
|
||||||
|
// border-left: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-header + .rbc-header {
|
||||||
|
border-left-width: 0;
|
||||||
|
border-right: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.rbc-header > a,
|
||||||
|
.rbc-header > a:active,
|
||||||
|
.rbc-header > a:visited {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-row-content {
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-row-content-scrollable {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.rbc-row-content-scrollable .rbc-row-content-scroll-container {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
/* IE and Edge */
|
||||||
|
scrollbar-width: none;
|
||||||
|
/* Firefox */
|
||||||
|
}
|
||||||
|
.rbc-row-content-scrollable .rbc-row-content-scroll-container::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.rbc-toolbar .rbc-toolbar-label {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 0 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.rbc-toolbar button {
|
||||||
|
outline: none;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5715;
|
||||||
|
height: 32px;
|
||||||
|
color: #373a3c;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
background: none;
|
||||||
|
background-image: none;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
padding: 4px 15px;
|
||||||
|
border-radius: 2px;
|
||||||
|
line-height: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.rbc-toolbar button:active,
|
||||||
|
.rbc-toolbar button.rbc-active {
|
||||||
|
// background-image: none;
|
||||||
|
// box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
|
||||||
|
// background-color: #e6e6e6;
|
||||||
|
z-index: 2;
|
||||||
|
color: #1890ff;
|
||||||
|
border-color: #1890ff;
|
||||||
|
}
|
||||||
|
.rbc-toolbar button:active:hover,
|
||||||
|
.rbc-toolbar button:active:focus,
|
||||||
|
.rbc-toolbar button.rbc-active:hover,
|
||||||
|
.rbc-toolbar button.rbc-active:focus {
|
||||||
|
// color: #373a3c;
|
||||||
|
// background-color: #d4d4d4;
|
||||||
|
// border-color: #8c8c8c;
|
||||||
|
z-index: 2;
|
||||||
|
color: #40a9ff;
|
||||||
|
border-color: #40a9ff;
|
||||||
|
}
|
||||||
|
// .rbc-toolbar button:focus {
|
||||||
|
// color: #373a3c;
|
||||||
|
// background-color: #e6e6e6;
|
||||||
|
// border-color: #adadad; }
|
||||||
|
.rbc-toolbar button:hover {
|
||||||
|
// color: #373a3c;
|
||||||
|
// background-color: #e6e6e6;
|
||||||
|
z-index: 2;
|
||||||
|
color: #40a9ff;
|
||||||
|
border-color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-btn-group {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.rbc-btn-group > button:first-child:not(:last-child) {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
.rbc-btn-group > button:last-child:not(:first-child) {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-btn-group > button:first-child:not(:last-child) {
|
||||||
|
border-radius: 4px;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-btn-group > button:last-child:not(:first-child) {
|
||||||
|
border-radius: 4px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
.rbc-btn-group > button:not(:first-child):not(:last-child) {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.rbc-btn-group button + button {
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-btn-group button + button {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: -1px;
|
||||||
|
}
|
||||||
|
.rbc-btn-group + .rbc-btn-group,
|
||||||
|
.rbc-btn-group + button {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-event {
|
||||||
|
border: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
box-shadow: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 2px 5px;
|
||||||
|
background-color: rgba(240, 240, 240, 0.65);
|
||||||
|
border-radius: 2px;
|
||||||
|
// color: #1890ff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
&:hover {
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rbc-slot-selecting .rbc-event {
|
||||||
|
cursor: inherit;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.rbc-event.rbc-selected {
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
.rbc-event:focus {
|
||||||
|
// outline: 5px auto #3b99fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-event-label {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-event-overlaps {
|
||||||
|
box-shadow: -1px 1px 5px 0px rgba(51, 51, 51, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-event-continues-prior {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-event-continues-after {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-event-continues-earlier {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-event-continues-later {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-row-segment {
|
||||||
|
padding: 0 4px 1px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-selected-cell {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-show-more {
|
||||||
|
z-index: 4;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 85%;
|
||||||
|
height: auto;
|
||||||
|
line-height: normal;
|
||||||
|
color: inherit;
|
||||||
|
padding: 2px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-month-view {
|
||||||
|
position: relative;
|
||||||
|
// border: 1px solid #f0f0f0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 0 0;
|
||||||
|
width: 100%;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
height: 68vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-month-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-month-row {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 0 0;
|
||||||
|
flex-basis: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.rbc-month-row + .rbc-month-row {
|
||||||
|
// border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-date-cell {
|
||||||
|
// cursor: pointer;
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
padding-right: 12px;
|
||||||
|
padding-top: 4px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.rbc-date-cell.rbc-now a {
|
||||||
|
// font-weight: bold;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
.rbc-date-cell > a,
|
||||||
|
.rbc-date-cell > a:active,
|
||||||
|
.rbc-date-cell > a:visited {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.rbc-date-cell > a:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-row-bg {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex: 1 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-day-bg {
|
||||||
|
flex: 1 0 0%;
|
||||||
|
// border-top: 2px solid #f0f0f0;
|
||||||
|
margin: 0 4px;
|
||||||
|
&:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
// cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-month-view {
|
||||||
|
.rbc-day-bg {
|
||||||
|
border-top: 2px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.rbc-today {
|
||||||
|
border-color: #1890ff !important;
|
||||||
|
background-color: #e6f7ff !important;
|
||||||
|
}
|
||||||
|
.rbc-header {
|
||||||
|
border-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-day-bg + .rbc-day-bg {
|
||||||
|
// margin-left: 8px;
|
||||||
|
// border-left: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-day-bg + .rbc-day-bg {
|
||||||
|
// border-left-width: 0;
|
||||||
|
// border-right: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-overlay {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 5;
|
||||||
|
margin-top: 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
// border: 1px solid #e5e5e5;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
.rbc-overlay > * + * {
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-overlay-header {
|
||||||
|
font-weight: 500;
|
||||||
|
min-height: 32px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
margin: -12px -16px 12px -16px;
|
||||||
|
padding: 5px 16px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-agenda-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 0 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.rbc-agenda-view table.rbc-agenda-table {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-spacing: 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.rbc-agenda-view table.rbc-agenda-table tbody > tr > td {
|
||||||
|
padding: 5px 10px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.rbc-agenda-view table.rbc-agenda-table .rbc-agenda-time-cell {
|
||||||
|
padding-left: 15px;
|
||||||
|
padding-right: 15px;
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
.rbc-agenda-view table.rbc-agenda-table tbody > tr > td + td {
|
||||||
|
border-left: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-agenda-view table.rbc-agenda-table tbody > tr > td + td {
|
||||||
|
border-left-width: 0;
|
||||||
|
border-right: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.rbc-agenda-view table.rbc-agenda-table tbody > tr + tr {
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.rbc-agenda-view table.rbc-agenda-table thead > tr > th {
|
||||||
|
padding: 3px 5px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-agenda-view table.rbc-agenda-table thead > tr > th {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-agenda-time-cell {
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
.rbc-agenda-time-cell .rbc-continues-after:after {
|
||||||
|
content: ' »';
|
||||||
|
}
|
||||||
|
.rbc-agenda-time-cell .rbc-continues-prior:before {
|
||||||
|
content: '« ';
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-agenda-date-cell,
|
||||||
|
.rbc-agenda-time-cell {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-agenda-event-cell {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-time-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
.rbc-time-column .rbc-timeslot-group {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-timeslot-group {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
min-height: 40px;
|
||||||
|
line-height: 39px;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
// cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-time-gutter,
|
||||||
|
.rbc-header-gutter {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-label {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-day-slot {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.rbc-day-slot .rbc-events-container {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
margin-right: 10px;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.rbc-day-slot .rbc-events-container.rbc-rtl {
|
||||||
|
left: 10px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.rbc-day-slot .rbc-event {
|
||||||
|
border: 1px solid #265985;
|
||||||
|
display: flex;
|
||||||
|
max-height: 100%;
|
||||||
|
min-height: 20px;
|
||||||
|
flex-flow: column wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.rbc-day-slot .rbc-event-label {
|
||||||
|
flex: none;
|
||||||
|
padding-right: 5px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.rbc-day-slot .rbc-event-content {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1 1 0;
|
||||||
|
word-wrap: break-word;
|
||||||
|
line-height: 1;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 1em;
|
||||||
|
}
|
||||||
|
.rbc-day-slot .rbc-time-slot {
|
||||||
|
// border-top: 1px solid #f7f7f7;
|
||||||
|
}
|
||||||
|
.rbc-time-header-gutter {
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
.rbc-time-header-cell {
|
||||||
|
min-height: 32px !important;
|
||||||
|
}
|
||||||
|
.rbc-calendar.view-week {
|
||||||
|
.rbc-time-header-cell {
|
||||||
|
margin-top: -32px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.rbc-time-view {
|
||||||
|
padding-top: 32px;
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
.rbc-header {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
.rbc-time-header-content {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.rbc-time-header-gutter {
|
||||||
|
padding: 0;
|
||||||
|
padding-top: 2px;
|
||||||
|
> div {
|
||||||
|
border-top: 2px solid #f0f0f0;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rbc-time-view-resources .rbc-time-gutter,
|
||||||
|
.rbc-time-view-resources .rbc-time-header-gutter {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
background-color: white;
|
||||||
|
border-right: 1px solid #f0f0f0;
|
||||||
|
z-index: 10;
|
||||||
|
margin-right: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-time-view-resources .rbc-time-header {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-time-view-resources .rbc-time-header-content {
|
||||||
|
min-width: auto;
|
||||||
|
flex: 1 0 0;
|
||||||
|
flex-basis: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-time-view-resources .rbc-time-header-cell-single-day {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-time-view-resources .rbc-day-slot {
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-time-view-resources .rbc-header,
|
||||||
|
.rbc-time-view-resources .rbc-day-bg {
|
||||||
|
width: 140px;
|
||||||
|
flex: 1 1 0;
|
||||||
|
flex-basis: 0 px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-time-header-content + .rbc-time-header-content {
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-time-slot {
|
||||||
|
flex: 1 0 0;
|
||||||
|
}
|
||||||
|
.rbc-time-slot.rbc-now {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-day-header {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-slot-selection {
|
||||||
|
z-index: 10;
|
||||||
|
position: absolute;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
color: white;
|
||||||
|
font-size: 75%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-slot-selecting {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-time-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.rbc-time-view .rbc-time-gutter {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.rbc-time-view .rbc-allday-cell {
|
||||||
|
box-sizing: content-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.rbc-time-view .rbc-allday-cell + .rbc-allday-cell {
|
||||||
|
border-left: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.rbc-time-view .rbc-allday-events {
|
||||||
|
position: relative;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
.rbc-time-view .rbc-row {
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-time-header {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.rbc-time-header.rbc-overflowing {
|
||||||
|
border-right: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-time-header.rbc-overflowing {
|
||||||
|
border-right-width: 0;
|
||||||
|
border-left: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.rbc-time-header > .rbc-row:first-child {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.rbc-time-header > .rbc-row.rbc-row-resource {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-time-header-cell-single-day {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-time-header-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
// border-left: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-time-header-content {
|
||||||
|
border-left-width: 0;
|
||||||
|
border-right: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.rbc-time-header-content > .rbc-row.rbc-row-resource {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-time-content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0 0%;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.rbc-time-content > .rbc-time-gutter {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.rbc-time-content > * + * > * {
|
||||||
|
// border-left: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.rbc-rtl .rbc-time-content > * + * > * {
|
||||||
|
border-left-width: 0;
|
||||||
|
border-right: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.rbc-time-content > .rbc-day-slot {
|
||||||
|
width: 100%;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbc-current-time-indicator {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #74ad31;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 800px) {
|
||||||
|
.view-type-calendar .action-buttons {
|
||||||
|
position: relative !important;
|
||||||
|
left: 0 !important;
|
||||||
|
.filter-action-button {
|
||||||
|
left: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rbc-toolbar .rbc-toolbar-label {
|
||||||
|
position: absolute;
|
||||||
|
top: 28px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
.rbc-toolbar {
|
||||||
|
justify-content: space-between !important;
|
||||||
|
}
|
||||||
|
.rbc-header {
|
||||||
|
padding: 4px !important;
|
||||||
|
}
|
||||||
|
}
|
8
packages/client/src/schema-component/antd/calendar-v2/types.d.ts
vendored
Normal file
8
packages/client/src/schema-component/antd/calendar-v2/types.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export interface ToolbarProps {
|
||||||
|
localizer?: any;
|
||||||
|
label?: any;
|
||||||
|
view?: any;
|
||||||
|
views?: any;
|
||||||
|
onNavigate?: (action: string) => void;
|
||||||
|
onView?: (view: string) => void;
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import { get } from 'lodash';
|
||||||
|
import { i18n } from '../../../i18n';
|
||||||
|
|
||||||
|
export const toEvents = (data: any[], fieldNames: any) => {
|
||||||
|
return data?.map((item) => {
|
||||||
|
return {
|
||||||
|
id: get(item, fieldNames.id || 'id'),
|
||||||
|
title: get(item, fieldNames.title) || i18n.t('Untitle'),
|
||||||
|
start: new Date(get(item, fieldNames.start)),
|
||||||
|
end: new Date(get(item, fieldNames.end || fieldNames.start)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
@ -1,42 +1,43 @@
|
|||||||
import { createForm, onFieldValueChange } from '@formily/core';
|
import { createForm, onFieldValueChange } from '@formily/core';
|
||||||
|
import { FieldContext, FormContext } from '@formily/react';
|
||||||
import React, { useContext, useMemo } from 'react';
|
import React, { useContext, useMemo } from 'react';
|
||||||
import { FormProvider, SchemaComponent } from '../../../schema-component';
|
import { SchemaComponent } from '../../core';
|
||||||
import { useComponent } from '../../hooks';
|
import { useComponent } from '../../hooks';
|
||||||
import { FilterContext } from './context';
|
import { FilterContext } from './context';
|
||||||
|
|
||||||
export const DynamicComponent = (props) => {
|
export const DynamicComponent = (props) => {
|
||||||
const { dynamicComponent } = useContext(FilterContext);
|
const { dynamicComponent } = useContext(FilterContext);
|
||||||
const component = useComponent(dynamicComponent);
|
const component = useComponent(dynamicComponent);
|
||||||
const form = useMemo(
|
const form = useMemo(() => {
|
||||||
() =>
|
return createForm({
|
||||||
createForm({
|
values: {
|
||||||
values: {
|
value: props.value,
|
||||||
value: props.value,
|
},
|
||||||
},
|
effects() {
|
||||||
effects() {
|
onFieldValueChange('value', (field) => {
|
||||||
onFieldValueChange('value', (field) => {
|
props?.onChange?.(field.value);
|
||||||
props?.onChange?.(field.value);
|
});
|
||||||
});
|
},
|
||||||
},
|
});
|
||||||
}),
|
}, [JSON.stringify(props.schema), JSON.stringify(props.value)]);
|
||||||
[JSON.stringify(props.schema), JSON.stringify(props.value)],
|
|
||||||
);
|
|
||||||
const renderSchemaComponent = () => {
|
const renderSchemaComponent = () => {
|
||||||
return (
|
return (
|
||||||
<SchemaComponent
|
<FieldContext.Provider value={null}>
|
||||||
schema={{
|
<SchemaComponent
|
||||||
'x-component': 'Input',
|
schema={{
|
||||||
...props.schema,
|
'x-component': 'Input',
|
||||||
name: 'value',
|
...props.schema,
|
||||||
'x-read-pretty': false,
|
name: 'value',
|
||||||
'x-validator': undefined,
|
'x-read-pretty': false,
|
||||||
'x-decorator': undefined,
|
'x-validator': undefined,
|
||||||
}}
|
'x-decorator': undefined,
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</FieldContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<FormProvider form={form}>
|
<FormContext.Provider value={form}>
|
||||||
{component
|
{component
|
||||||
? React.createElement(component, {
|
? React.createElement(component, {
|
||||||
value: props.value,
|
value: props.value,
|
||||||
@ -44,6 +45,6 @@ export const DynamicComponent = (props) => {
|
|||||||
renderSchemaComponent,
|
renderSchemaComponent,
|
||||||
})
|
})
|
||||||
: renderSchemaComponent()}
|
: renderSchemaComponent()}
|
||||||
</FormProvider>
|
</FormContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,90 @@
|
|||||||
|
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { useDesignable } from '../..';
|
||||||
|
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||||
|
|
||||||
|
export const FilterActionDesigner = (props) => {
|
||||||
|
const initialValue = {};
|
||||||
|
const field = useField();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const isPopupAction = ['create', 'update', 'view'].includes(fieldSchema['x-action'] || '');
|
||||||
|
return (
|
||||||
|
<GeneralSchemaDesigner {...props}>
|
||||||
|
<SchemaSettings.ModalItem
|
||||||
|
title={'编辑'}
|
||||||
|
schema={
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
title: '编辑按钮',
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
title: '按钮标题',
|
||||||
|
default: fieldSchema.title,
|
||||||
|
'x-component-props': {},
|
||||||
|
// description: `原字段标题:${collectionField?.uiSchema?.title}`,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'IconPicker',
|
||||||
|
title: '按钮图标',
|
||||||
|
default: fieldSchema?.['x-component-props']?.icon,
|
||||||
|
'x-component-props': {},
|
||||||
|
// description: `原字段标题:${collectionField?.uiSchema?.title}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ISchema
|
||||||
|
}
|
||||||
|
onSubmit={({ title, icon }) => {
|
||||||
|
if (title) {
|
||||||
|
fieldSchema.title = title;
|
||||||
|
field.title = title;
|
||||||
|
field.componentProps.icon = icon;
|
||||||
|
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||||
|
fieldSchema['x-component-props'].icon = icon;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
title,
|
||||||
|
'x-component-props': {
|
||||||
|
...fieldSchema['x-component-props'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{isPopupAction && (
|
||||||
|
<SchemaSettings.SelectItem
|
||||||
|
title={'打开方式'}
|
||||||
|
options={[
|
||||||
|
{ label: '抽屉', value: 'drawer' },
|
||||||
|
{ label: '对话框', value: 'modal' },
|
||||||
|
]}
|
||||||
|
value={field.componentProps.openMode}
|
||||||
|
onChange={(value) => {
|
||||||
|
field.componentProps.openMode = value;
|
||||||
|
fieldSchema['x-component-props']['openMode'] = value;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
'x-uid': fieldSchema['x-uid'],
|
||||||
|
'x-component-props': fieldSchema['x-component-props'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<SchemaSettings.Divider />
|
||||||
|
<SchemaSettings.Remove
|
||||||
|
removeParentsIfNoChildren
|
||||||
|
breakRemoveOn={(s) => {
|
||||||
|
return s['x-component'] === 'Space' || s['x-component'] === 'ActionBar';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</GeneralSchemaDesigner>
|
||||||
|
);
|
||||||
|
};
|
@ -3,6 +3,8 @@ import { observer, useField, useFieldSchema } from '@formily/react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useRequest } from '../../../api-client';
|
import { useRequest } from '../../../api-client';
|
||||||
import { FilterContext } from './context';
|
import { FilterContext } from './context';
|
||||||
|
import { FilterActionDesigner } from './Filter.Action.Designer';
|
||||||
|
import { FilterAction } from './FilterAction';
|
||||||
import { FilterGroup } from './FilterGroup';
|
import { FilterGroup } from './FilterGroup';
|
||||||
import { SaveDefaultValue } from './SaveDefaultValue';
|
import { SaveDefaultValue } from './SaveDefaultValue';
|
||||||
|
|
||||||
@ -17,7 +19,6 @@ export const Filter: any = observer((props: any) => {
|
|||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
useDataSource({
|
useDataSource({
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
console.log('onSuccess', data?.data);
|
|
||||||
field.dataSource = data?.data || [];
|
field.dataSource = data?.data || [];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -26,9 +27,11 @@ export const Filter: any = observer((props: any) => {
|
|||||||
<FilterContext.Provider value={{ field, fieldSchema, dynamicComponent, options: field.dataSource || [] }}>
|
<FilterContext.Provider value={{ field, fieldSchema, dynamicComponent, options: field.dataSource || [] }}>
|
||||||
<FilterGroup {...props} />
|
<FilterGroup {...props} />
|
||||||
</FilterContext.Provider>
|
</FilterContext.Provider>
|
||||||
{/* <pre>{JSON.stringify(field.value, null, 2)}</pre> */}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Filter.SaveDefaultValue = SaveDefaultValue;
|
Filter.SaveDefaultValue = SaveDefaultValue;
|
||||||
|
|
||||||
|
Filter.Action = FilterAction;
|
||||||
|
Filter.Action.Designer = FilterActionDesigner;
|
||||||
|
@ -0,0 +1,124 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { createForm, Field, Form } from '@formily/core';
|
||||||
|
import { observer, useField, useFieldSchema, useForm } from '@formily/react';
|
||||||
|
import { Button, Popover, Space } from 'antd';
|
||||||
|
import React, { createContext, useContext, useMemo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FormProvider, SchemaComponent } from '../../core';
|
||||||
|
import { useDesignable } from '../../hooks';
|
||||||
|
import { useProps } from '../../hooks/useProps';
|
||||||
|
import { Action } from '../action';
|
||||||
|
|
||||||
|
export const FilterActionContext = createContext<any>(null);
|
||||||
|
|
||||||
|
export const FilterAction = observer((props: any) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const field = useField<Field>();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const { designable, dn } = useDesignable();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const form = useMemo<Form>(() => props.form || createForm(), [visible]);
|
||||||
|
const { options, onSubmit, onReset, ...others } = useProps(props);
|
||||||
|
return (
|
||||||
|
<FilterActionContext.Provider value={{ field, fieldSchema, designable, dn }}>
|
||||||
|
<Popover
|
||||||
|
destroyTooltipOnHide
|
||||||
|
placement={'bottomLeft'}
|
||||||
|
visible={visible}
|
||||||
|
onVisibleChange={(visible) => {
|
||||||
|
setVisible(visible);
|
||||||
|
}}
|
||||||
|
trigger={'click'}
|
||||||
|
content={
|
||||||
|
<form>
|
||||||
|
<FormProvider form={form}>
|
||||||
|
<SchemaComponent
|
||||||
|
schema={{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
filter: {
|
||||||
|
type: 'string',
|
||||||
|
enum: options || field.dataSource,
|
||||||
|
default: fieldSchema.default,
|
||||||
|
'x-component': 'Filter',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
<SaveConditions />
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
await form.reset();
|
||||||
|
onReset?.();
|
||||||
|
field.title = t('Filter');
|
||||||
|
setVisible(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Reset')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type={'primary'}
|
||||||
|
htmlType={'submit'}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onSubmit?.(form.values);
|
||||||
|
const items = form.values?.filter?.$and || form.values?.filter?.$or;
|
||||||
|
console.log('form.values.filter', form.values, items);
|
||||||
|
field.title = t('{{count}} filter items', { count: items?.length || 0 });
|
||||||
|
setVisible(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Submit')}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</FormProvider>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Action {...others} />
|
||||||
|
</Popover>
|
||||||
|
</FilterActionContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const SaveConditions = () => {
|
||||||
|
const { fieldSchema, field, designable, dn } = useContext(FilterActionContext);
|
||||||
|
const form = useForm();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
if (!designable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type={'dashed'}
|
||||||
|
className={css`
|
||||||
|
border-color: rgb(241, 139, 98);
|
||||||
|
color: rgb(241, 139, 98);
|
||||||
|
`}
|
||||||
|
onClick={() => {
|
||||||
|
const defaultValue = { ...form.values.filter };
|
||||||
|
fieldSchema.default = defaultValue;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
'x-uid': fieldSchema['x-uid'],
|
||||||
|
default: defaultValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Save conditions')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import { CloseCircleOutlined } from '@ant-design/icons';
|
import { CloseCircleOutlined } from '@ant-design/icons';
|
||||||
import { observer, useField } from '@formily/react';
|
import { observer } from '@formily/react';
|
||||||
import { Cascader, Select, Space } from 'antd';
|
import { Cascader, Select, Space } from 'antd';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useCompile } from '../..';
|
import { useCompile } from '../..';
|
||||||
@ -8,10 +8,9 @@ import { DynamicComponent } from './DynamicComponent';
|
|||||||
import { useValues } from './useValues';
|
import { useValues } from './useValues';
|
||||||
|
|
||||||
export const FilterItem = observer((props: any) => {
|
export const FilterItem = observer((props: any) => {
|
||||||
const field = useField<any>();
|
|
||||||
const remove = useContext(RemoveConditionContext);
|
|
||||||
const { option, options, dataIndex, operator, setDataIndex, setOperator, value, setValue } = useValues();
|
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
|
const remove = useContext(RemoveConditionContext);
|
||||||
|
const { schema, fields, operators, dataIndex, operator, setDataIndex, setOperator, value, setValue } = useValues();
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: 8 }}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<Space>
|
<Space>
|
||||||
@ -25,16 +24,15 @@ export const FilterItem = observer((props: any) => {
|
|||||||
width: 150,
|
width: 150,
|
||||||
}}
|
}}
|
||||||
changeOnSelect={false}
|
changeOnSelect={false}
|
||||||
key={field.address.toString()}
|
|
||||||
value={dataIndex}
|
value={dataIndex}
|
||||||
options={compile(options)}
|
options={compile(fields)}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setDataIndex(value);
|
setDataIndex(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={operator}
|
value={operator?.value}
|
||||||
options={compile(option?.operators)}
|
options={compile(operators)}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setOperator(value);
|
setOperator(value);
|
||||||
}}
|
}}
|
||||||
@ -42,13 +40,14 @@ export const FilterItem = observer((props: any) => {
|
|||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{React.createElement(DynamicComponent, {
|
{!operator?.noValue &&
|
||||||
value,
|
React.createElement(DynamicComponent, {
|
||||||
schema: option?.schema,
|
value,
|
||||||
onChange(value) {
|
schema,
|
||||||
setValue(value);
|
onChange(value) {
|
||||||
},
|
setValue(value);
|
||||||
})}
|
},
|
||||||
|
})}
|
||||||
<CloseCircleOutlined onClick={() => remove()} />
|
<CloseCircleOutlined onClick={() => remove()} />
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
104
packages/client/src/schema-component/antd/filter/demos/demo5.tsx
Normal file
104
packages/client/src/schema-component/antd/filter/demos/demo5.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import {
|
||||||
|
AntdSchemaComponentProvider,
|
||||||
|
FilterAction, SchemaComponent,
|
||||||
|
SchemaComponentProvider
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const schema: any = {
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
demo: {
|
||||||
|
type: 'object',
|
||||||
|
enum: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
title: 'Name',
|
||||||
|
operators: [
|
||||||
|
{ label: 'eq', value: '$eq' },
|
||||||
|
{ label: 'ne', value: '$ne' },
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Name',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'age',
|
||||||
|
title: 'Age',
|
||||||
|
operators: [
|
||||||
|
{ label: 'in', value: '$in' },
|
||||||
|
{ label: 'not', value: '$not' },
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Age',
|
||||||
|
'x-component': 'InputNumber',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tags',
|
||||||
|
title: 'Tags',
|
||||||
|
schema: {
|
||||||
|
title: 'Tags',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'slug',
|
||||||
|
title: 'Slug',
|
||||||
|
operators: [
|
||||||
|
{ label: 'in', value: '$in' },
|
||||||
|
{ label: 'not', value: '$not' },
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
title: 'Slug',
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
title: 'Title',
|
||||||
|
operators: [
|
||||||
|
{ label: 'eq', value: '$eq' },
|
||||||
|
{ label: 'ne', value: '$ne' },
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
title: 'Title',
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: {
|
||||||
|
$or: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
$ne: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'tags.title': {
|
||||||
|
$eq: 'aaa',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'x-component': 'FilterAction',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider>
|
||||||
|
<AntdSchemaComponentProvider>
|
||||||
|
<SchemaComponent components={{ FilterAction }} schema={schema} />
|
||||||
|
</AntdSchemaComponentProvider>
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -20,3 +20,7 @@ group:
|
|||||||
### Custom Dynamic Component
|
### Custom Dynamic Component
|
||||||
|
|
||||||
<code src="./demos/demo4.tsx" />
|
<code src="./demos/demo4.tsx" />
|
||||||
|
|
||||||
|
### FilterAction
|
||||||
|
|
||||||
|
<code src="./demos/demo5.tsx" />
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
import { useBlockRequestContext } from '../../../block-provider';
|
||||||
|
import { useCollection, useCollectionManager } from '../../../collection-manager';
|
||||||
|
|
||||||
|
export const useFilterOptions = (collectionName: string) => {
|
||||||
|
const { getCollectionFields, getInterface } = useCollectionManager();
|
||||||
|
const fields = getCollectionFields(collectionName);
|
||||||
|
const field2option = (field, nochildren) => {
|
||||||
|
if (!field.interface) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fieldInterface = getInterface(field.interface);
|
||||||
|
if (!fieldInterface.filterable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { nested, children, operators } = fieldInterface.filterable;
|
||||||
|
const option = {
|
||||||
|
name: field.name,
|
||||||
|
title: field?.uiSchema?.title || field.name,
|
||||||
|
schema: field?.uiSchema,
|
||||||
|
operators: operators || [],
|
||||||
|
};
|
||||||
|
if (nochildren) {
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
if (children?.length) {
|
||||||
|
option['children'] = children;
|
||||||
|
}
|
||||||
|
if (nested) {
|
||||||
|
const targetFields = getCollectionFields(field.target);
|
||||||
|
const options = getOptions(targetFields, true);
|
||||||
|
option['children'] = option['children'] || [];
|
||||||
|
option['children'].push(...options);
|
||||||
|
}
|
||||||
|
return option;
|
||||||
|
};
|
||||||
|
const getOptions = (fields, nochildren = false) => {
|
||||||
|
const options = [];
|
||||||
|
fields.forEach((field) => {
|
||||||
|
const option = field2option(field, nochildren);
|
||||||
|
if (option) {
|
||||||
|
options.push(option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
return getOptions(fields);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFilterActionProps = () => {
|
||||||
|
const { name } = useCollection();
|
||||||
|
const options = useFilterOptions(name);
|
||||||
|
const { service } = useBlockRequestContext();
|
||||||
|
return {
|
||||||
|
options,
|
||||||
|
onSubmit({ filter }) {
|
||||||
|
console.log('filter', filter)
|
||||||
|
service.run({ ...service.params?.[0], filter });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -1,69 +1,88 @@
|
|||||||
import { useField } from '@formily/react';
|
import { useField } from '@formily/react';
|
||||||
import { Input } from 'antd';
|
import { merge } from '@formily/shared';
|
||||||
import flat from 'flat';
|
import flat from 'flat';
|
||||||
import { useContext } from 'react';
|
import get from 'lodash/get';
|
||||||
|
import { useContext, useEffect } from 'react';
|
||||||
import { FilterContext } from './context';
|
import { FilterContext } from './context';
|
||||||
|
|
||||||
|
// import { useValues } from './useValues';
|
||||||
|
const findOption = (dataIndex = [], options) => {
|
||||||
|
let items = options;
|
||||||
|
let option;
|
||||||
|
dataIndex?.forEach?.((name, index) => {
|
||||||
|
const item = items.find((item) => item.name === name);
|
||||||
|
if (item) {
|
||||||
|
option = item;
|
||||||
|
}
|
||||||
|
items = item?.children || [];
|
||||||
|
});
|
||||||
|
return option;
|
||||||
|
};
|
||||||
|
|
||||||
export const useValues = () => {
|
export const useValues = () => {
|
||||||
const { options } = useContext(FilterContext);
|
|
||||||
const field = useField<any>();
|
const field = useField<any>();
|
||||||
const obj = flat(field.value || {});
|
const { options } = useContext(FilterContext);
|
||||||
const key = Object.keys(obj).shift() || '';
|
const data2value = () => {
|
||||||
const [path, others = ''] = key.split('.$');
|
field.value = flat.unflatten({
|
||||||
let [operator] = others.split('.');
|
[`${field.data.dataIndex.join('.')}.${field.data?.operator?.value}`]: field.data?.value,
|
||||||
const dataIndex = path.split('.');
|
|
||||||
let maxDepth = dataIndex.length;
|
|
||||||
if (operator) {
|
|
||||||
operator = '$' + operator;
|
|
||||||
++maxDepth;
|
|
||||||
}
|
|
||||||
const values = flat(field.value || {}, { maxDepth });
|
|
||||||
const value = Object.values<any>(values).shift();
|
|
||||||
const findOption = (dataIndex = []) => {
|
|
||||||
let items = options;
|
|
||||||
let option;
|
|
||||||
dataIndex?.forEach?.((name, index) => {
|
|
||||||
const item = items.find((item) => item.name === name);
|
|
||||||
if (item) {
|
|
||||||
option = item;
|
|
||||||
}
|
|
||||||
items = item?.children || [];
|
|
||||||
});
|
});
|
||||||
return option;
|
|
||||||
};
|
};
|
||||||
const option = findOption(dataIndex);
|
const value2data = () => {
|
||||||
console.log('option', option);
|
const values = flat(field.value);
|
||||||
const operators = option?.operators;
|
const path = Object.keys(values).shift() || '';
|
||||||
|
if (!path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [fieldPath, otherPath] = path.split('.$');
|
||||||
|
const [operatorValue] = otherPath.split('.', 2);
|
||||||
|
const dataIndex = fieldPath.split('.');
|
||||||
|
const option = findOption(dataIndex, options);
|
||||||
|
const operators = option.operators;
|
||||||
|
const operator = operators?.find?.((item) => item.value === `$${operatorValue}`);
|
||||||
|
field.data = field.data || {};
|
||||||
|
field.data.dataIndex = dataIndex;
|
||||||
|
field.data.operators = operators;
|
||||||
|
field.data.operator = operator;
|
||||||
|
field.data.schema = merge(merge(option?.schema, operator?.schema), {
|
||||||
|
'x-component-props': {
|
||||||
|
style: {
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
field.data.value = get(field.value, `${fieldPath}.$${operatorValue}`);
|
||||||
|
console.log('option', operator, field.data.value);
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
value2data();
|
||||||
|
}, []);
|
||||||
return {
|
return {
|
||||||
option,
|
fields: options,
|
||||||
dataIndex,
|
...field.data,
|
||||||
options,
|
setDataIndex(dataIndex) {
|
||||||
operators,
|
const option = findOption(dataIndex, options);
|
||||||
operator,
|
const operator = option?.operators?.[0];
|
||||||
value,
|
field.data = field.data || {};
|
||||||
component: [Input, {}],
|
field.data.operators = option.operators;
|
||||||
// 当 dataIndex 变化,value 清空
|
field.data.operator = operator;
|
||||||
setDataIndex(di: string[]) {
|
field.data.schema = merge(option.schema, operator.schema);
|
||||||
if (!di) {
|
field.data.dataIndex = dataIndex;
|
||||||
field.value = {};
|
field.data.value = null;
|
||||||
return;
|
data2value();
|
||||||
}
|
console.log('setDataIndex', field.data);
|
||||||
const option = findOption(di);
|
|
||||||
const op = option?.operators?.[0]?.value || '$eq';
|
|
||||||
field.value = flat.unflatten({
|
|
||||||
[`${di.join('.')}.${op}`]: null,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
// 如果只是 Operator 变化,value 要保留
|
setOperator(operatorValue) {
|
||||||
setOperator(op: string) {
|
const operator = field.data?.operators?.find?.((item) => item.value === operatorValue);
|
||||||
field.value = flat.unflatten({
|
field.data.operator = operator;
|
||||||
[`${dataIndex.join('.')}.${op}`]: value,
|
field.data.schema = merge(field.data.schema, operator.schema);
|
||||||
});
|
field.data.value = null;
|
||||||
|
data2value();
|
||||||
|
console.log('setOperator', field.data);
|
||||||
},
|
},
|
||||||
setValue(v: any) {
|
setValue(value) {
|
||||||
field.value = flat.unflatten({
|
field.data.value = value;
|
||||||
[`${dataIndex.join('.')}.${operator || '$eq'}`]: v,
|
data2value();
|
||||||
});
|
console.log('setValue', field.data);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { FormItem as Item } from '@formily/antd';
|
import { FormItem as Item } from '@formily/antd';
|
||||||
import { Field } from '@formily/core';
|
import { Field } from '@formily/core';
|
||||||
import { ISchema, useField, useFieldSchema, useForm } from '@formily/react';
|
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useActionContext } from '..';
|
|
||||||
import { useCompile, useDesignable } from '../..';
|
import { useCompile, useDesignable } from '../..';
|
||||||
import { useCollection, useCollectionManager } from '../../../collection-manager';
|
import { useCollection, useCollectionManager } from '../../../collection-manager';
|
||||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||||
@ -43,85 +42,68 @@ FormItem.Designer = () => {
|
|||||||
return (
|
return (
|
||||||
<GeneralSchemaDesigner>
|
<GeneralSchemaDesigner>
|
||||||
{collectionField && (
|
{collectionField && (
|
||||||
<SchemaSettings.PopupItem
|
<SchemaSettings.ModalItem
|
||||||
title={'编辑'}
|
title={t('Edit field title')}
|
||||||
schema={
|
schema={
|
||||||
{
|
{
|
||||||
title: '编辑字段',
|
type: 'object',
|
||||||
'x-component': 'Action.Modal',
|
title: t('Edit field title'),
|
||||||
'x-component-props': {
|
|
||||||
width: 520,
|
|
||||||
},
|
|
||||||
'x-decorator': 'Form',
|
|
||||||
'x-decorator-props': {
|
|
||||||
initialValue,
|
|
||||||
},
|
|
||||||
type: 'void',
|
|
||||||
properties: {
|
properties: {
|
||||||
title: {
|
title: {
|
||||||
|
title: t('Field title'),
|
||||||
|
default: field?.title,
|
||||||
|
description: `${t('Original field title: ')}${collectionField?.uiSchema?.title}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
title: '字段标题',
|
|
||||||
'x-component-props': {},
|
'x-component-props': {},
|
||||||
description: `原字段标题:${collectionField?.uiSchema?.title}`,
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'Action.Modal.Footer',
|
|
||||||
properties: {
|
|
||||||
cancel: {
|
|
||||||
type: 'void',
|
|
||||||
title: '{{t("Cancel")}}',
|
|
||||||
'x-component': 'Action',
|
|
||||||
'x-component-props': {
|
|
||||||
useAction() {
|
|
||||||
const ctx = useActionContext();
|
|
||||||
return {
|
|
||||||
async run() {
|
|
||||||
ctx.setVisible(false);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
submit: {
|
|
||||||
type: 'void',
|
|
||||||
title: 'Submit',
|
|
||||||
'x-component': 'Action',
|
|
||||||
'x-component-props': {
|
|
||||||
type: 'primary',
|
|
||||||
useAction() {
|
|
||||||
const form = useForm();
|
|
||||||
const ctx = useActionContext();
|
|
||||||
return {
|
|
||||||
async run() {
|
|
||||||
const { title } = form.values;
|
|
||||||
|
|
||||||
const schema = {
|
|
||||||
['x-uid']: fieldSchema['x-uid'],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (title) {
|
|
||||||
field.title = title;
|
|
||||||
fieldSchema['title'] = title;
|
|
||||||
schema['title'] = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.setVisible(false);
|
|
||||||
dn.emit('patch', {
|
|
||||||
schema,
|
|
||||||
});
|
|
||||||
refresh();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as ISchema
|
} as ISchema
|
||||||
}
|
}
|
||||||
|
onSubmit={({ title }) => {
|
||||||
|
if (title) {
|
||||||
|
field.title = title;
|
||||||
|
fieldSchema.title = title;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
'x-uid': fieldSchema['x-uid'],
|
||||||
|
title: fieldSchema.title,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!field.readPretty && (
|
||||||
|
<SchemaSettings.ModalItem
|
||||||
|
title={t('Edit description')}
|
||||||
|
schema={
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
title: t('Edit description'),
|
||||||
|
properties: {
|
||||||
|
description: {
|
||||||
|
// title: t('Description'),
|
||||||
|
default: field?.description,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input.TextArea',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ISchema
|
||||||
|
}
|
||||||
|
onSubmit={({ description }) => {
|
||||||
|
field.description = description;
|
||||||
|
fieldSchema.description = description;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
'x-uid': fieldSchema['x-uid'],
|
||||||
|
description: fieldSchema.description,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!field.readPretty && (
|
{!field.readPretty && (
|
||||||
@ -155,6 +137,7 @@ FormItem.Designer = () => {
|
|||||||
...field.componentProps.fieldNames,
|
...field.componentProps.fieldNames,
|
||||||
label,
|
label,
|
||||||
};
|
};
|
||||||
|
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||||
fieldSchema['x-component-props']['fieldNames'] = fieldNames;
|
fieldSchema['x-component-props']['fieldNames'] = fieldNames;
|
||||||
field.componentProps.fieldNames = fieldNames;
|
field.componentProps.fieldNames = fieldNames;
|
||||||
schema['x-component-props'] = {
|
schema['x-component-props'] = {
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useFormBlockContext } from '../../../block-provider';
|
||||||
|
import { useCollection } from '../../../collection-manager';
|
||||||
|
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||||
|
import { useSchemaTemplate } from '../../../schema-templates';
|
||||||
|
|
||||||
|
export const FormDesigner = () => {
|
||||||
|
const { name, title } = useCollection();
|
||||||
|
const template = useSchemaTemplate();
|
||||||
|
const ctx = useFormBlockContext();
|
||||||
|
return (
|
||||||
|
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||||
|
<SchemaSettings.Template componentName={ctx?.action ? 'RecordForm' : 'CreateForm'} collectionName={name} />
|
||||||
|
<SchemaSettings.Divider />
|
||||||
|
<SchemaSettings.Remove
|
||||||
|
removeParentsIfNoChildren
|
||||||
|
breakRemoveOn={{
|
||||||
|
'x-component': 'Grid',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</GeneralSchemaDesigner>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReadPrettyFormDesigner = () => {
|
||||||
|
const { name, title } = useCollection();
|
||||||
|
const template = useSchemaTemplate();
|
||||||
|
return (
|
||||||
|
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||||
|
<SchemaSettings.Template componentName={'ReadPrettyForm'} collectionName={name} />
|
||||||
|
<SchemaSettings.Divider />
|
||||||
|
<SchemaSettings.Remove
|
||||||
|
removeParentsIfNoChildren
|
||||||
|
breakRemoveOn={{
|
||||||
|
'x-component': 'Grid',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</GeneralSchemaDesigner>
|
||||||
|
);
|
||||||
|
};
|
80
packages/client/src/schema-component/antd/form-v2/Form.tsx
Normal file
80
packages/client/src/schema-component/antd/form-v2/Form.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { FormLayout } from '@formily/antd';
|
||||||
|
import { createForm, Field } from '@formily/core';
|
||||||
|
import { FieldContext, FormContext, observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import { Spin } from 'antd';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useAttach, useComponent } from '../..';
|
||||||
|
import { useProps } from '../../hooks/useProps';
|
||||||
|
|
||||||
|
export interface FormProps {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormComponent: React.FC<FormProps> = (props) => {
|
||||||
|
const { form, children, ...others } = props;
|
||||||
|
const field = useField();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
// TODO: component 里 useField 会与当前 field 存在偏差
|
||||||
|
const f = useAttach(form.createVoidField({ ...field.props, basePath: '' }));
|
||||||
|
return (
|
||||||
|
<FieldContext.Provider value={undefined}>
|
||||||
|
<FormContext.Provider value={form}>
|
||||||
|
<FormLayout layout={'vertical'} {...others}>
|
||||||
|
<RecursionField basePath={f.address} schema={fieldSchema} onlyRenderProperties />
|
||||||
|
</FormLayout>
|
||||||
|
</FormContext.Provider>
|
||||||
|
</FieldContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Def = (props: any) => props.children;
|
||||||
|
|
||||||
|
const FormDecorator: React.FC<FormProps> = (props) => {
|
||||||
|
const { form, children, ...others } = props;
|
||||||
|
const field = useField();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
// TODO: component 里 useField 会与当前 field 存在偏差
|
||||||
|
const f = useAttach(form.createVoidField({ ...field.props, basePath: '' }));
|
||||||
|
const Component = useComponent(fieldSchema['x-component'], Def);
|
||||||
|
return (
|
||||||
|
<FieldContext.Provider value={undefined}>
|
||||||
|
<FormContext.Provider value={form}>
|
||||||
|
<FormLayout layout={'vertical'} {...others}>
|
||||||
|
<FieldContext.Provider value={f}>
|
||||||
|
<Component {...field.componentProps}>
|
||||||
|
<RecursionField basePath={f.address} schema={fieldSchema} onlyRenderProperties />
|
||||||
|
</Component>
|
||||||
|
</FieldContext.Provider>
|
||||||
|
{/* <FieldContext.Provider value={f}>{children}</FieldContext.Provider> */}
|
||||||
|
</FormLayout>
|
||||||
|
</FormContext.Provider>
|
||||||
|
</FieldContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const WithForm = (props) => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
return fieldSchema['x-decorator'] === 'Form' ? <FormDecorator {...props} /> : <FormComponent {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WithoutForm = (props) => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const form = useMemo(() => createForm(), []);
|
||||||
|
return fieldSchema['x-decorator'] === 'Form' ? (
|
||||||
|
<FormDecorator form={form} {...props} />
|
||||||
|
) : (
|
||||||
|
<FormComponent form={form} {...props} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Form: React.FC<FormProps> & { Designer?: any; ReadPrettyDesigner?: any } = observer((props) => {
|
||||||
|
const field = useField<Field>();
|
||||||
|
const { form, ...others } = useProps(props);
|
||||||
|
return (
|
||||||
|
<form>
|
||||||
|
<Spin spinning={field.loading || false}>
|
||||||
|
{form ? <WithForm form={form} {...others} /> : <WithoutForm {...others} />}
|
||||||
|
</Spin>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
});
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,83 @@
|
|||||||
|
import { ISchema, useForm } from '@formily/react';
|
||||||
|
import {
|
||||||
|
AntdSchemaComponentProvider,
|
||||||
|
APIClient,
|
||||||
|
APIClientProvider,
|
||||||
|
BlockSchemaComponentProvider,
|
||||||
|
CollectionManagerProvider,
|
||||||
|
SchemaComponent,
|
||||||
|
SchemaComponentProvider,
|
||||||
|
useFormBlockContext
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import collections from './collections';
|
||||||
|
|
||||||
|
function useAction() {
|
||||||
|
const ctx = useFormBlockContext();
|
||||||
|
const form = useForm();
|
||||||
|
return {
|
||||||
|
async run() {
|
||||||
|
console.log('form.values', form.values);
|
||||||
|
// await ctx.resource.create({
|
||||||
|
// values: form.values,
|
||||||
|
// });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema: ISchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
block: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'FormBlockProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
collection: 'users',
|
||||||
|
resource: 'users',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
form: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'FormV2',
|
||||||
|
'x-component-props': {},
|
||||||
|
properties: {
|
||||||
|
nickname: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
title: '提交',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
htmlType: 'submit',
|
||||||
|
type: 'primary',
|
||||||
|
useAction,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiClient = new APIClient({
|
||||||
|
baseURL: 'http://localhost:3000/api',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<APIClientProvider apiClient={apiClient}>
|
||||||
|
<SchemaComponentProvider>
|
||||||
|
<CollectionManagerProvider collections={collections.data}>
|
||||||
|
<AntdSchemaComponentProvider>
|
||||||
|
<BlockSchemaComponentProvider>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</BlockSchemaComponentProvider>
|
||||||
|
</AntdSchemaComponentProvider>
|
||||||
|
</CollectionManagerProvider>
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
</APIClientProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,103 @@
|
|||||||
|
import { ISchema, useForm } from '@formily/react';
|
||||||
|
import {
|
||||||
|
AntdSchemaComponentProvider,
|
||||||
|
APIClient,
|
||||||
|
APIClientProvider,
|
||||||
|
BlockSchemaComponentProvider,
|
||||||
|
CollectionManagerProvider,
|
||||||
|
RecordProvider,
|
||||||
|
SchemaComponent,
|
||||||
|
SchemaComponentProvider,
|
||||||
|
useFilterByTk,
|
||||||
|
useFormBlockContext
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import collections from './collections';
|
||||||
|
|
||||||
|
const useAction = () => {
|
||||||
|
const ctx = useFormBlockContext();
|
||||||
|
const form = useForm();
|
||||||
|
const filterByTk = useFilterByTk();
|
||||||
|
return {
|
||||||
|
async run() {
|
||||||
|
console.log('form.values', form.values);
|
||||||
|
await ctx.resource.update({
|
||||||
|
filterByTk,
|
||||||
|
values: form.values,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const schema: ISchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
block: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'FormBlockProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
collection: 'users',
|
||||||
|
resource: 'users',
|
||||||
|
action: 'get',
|
||||||
|
useParams: '{{ useParamsFromRecord }}',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
form: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'FormV2',
|
||||||
|
'x-component-props': {
|
||||||
|
useProps: '{{ useFormBlockProps }}',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
nickname: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-designer': 'FormItem.Designer',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
title: '提交',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
htmlType: 'submit',
|
||||||
|
type: 'primary',
|
||||||
|
useAction,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiClient = new APIClient({
|
||||||
|
baseURL: 'http://localhost:3000/api',
|
||||||
|
});
|
||||||
|
|
||||||
|
const record = {
|
||||||
|
id: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<APIClientProvider apiClient={apiClient}>
|
||||||
|
<SchemaComponentProvider>
|
||||||
|
<CollectionManagerProvider collections={collections.data}>
|
||||||
|
<AntdSchemaComponentProvider>
|
||||||
|
<RecordProvider record={record}>
|
||||||
|
<BlockSchemaComponentProvider>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</BlockSchemaComponentProvider>
|
||||||
|
</RecordProvider>
|
||||||
|
</AntdSchemaComponentProvider>
|
||||||
|
</CollectionManagerProvider>
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
</APIClientProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,103 @@
|
|||||||
|
import { ISchema } from '@formily/react';
|
||||||
|
import {
|
||||||
|
AntdSchemaComponentProvider,
|
||||||
|
APIClient,
|
||||||
|
APIClientProvider,
|
||||||
|
BlockSchemaComponentProvider,
|
||||||
|
CollectionManagerProvider,
|
||||||
|
RecordProvider,
|
||||||
|
SchemaComponent,
|
||||||
|
SchemaComponentProvider
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import collections from './collections';
|
||||||
|
|
||||||
|
const schema: ISchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
block: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'FormBlockProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
collection: 'users',
|
||||||
|
resource: 'users',
|
||||||
|
action: 'get',
|
||||||
|
useParams: '{{ useParamsFromRecord }}',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
form: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'FormV2',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-component-props': {
|
||||||
|
useProps: '{{ useFormBlockProps }}',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
grid: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
properties: {
|
||||||
|
row1: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
properties: {
|
||||||
|
col11: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Col',
|
||||||
|
properties: {
|
||||||
|
nickname: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-designer': 'FormItem.Designer',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
col12: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Col',
|
||||||
|
properties: {
|
||||||
|
password: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-designer': 'FormItem.Designer',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiClient = new APIClient({
|
||||||
|
baseURL: 'http://localhost:3000/api',
|
||||||
|
});
|
||||||
|
|
||||||
|
const record = {
|
||||||
|
id: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<APIClientProvider apiClient={apiClient}>
|
||||||
|
<SchemaComponentProvider>
|
||||||
|
<CollectionManagerProvider collections={collections.data}>
|
||||||
|
<AntdSchemaComponentProvider>
|
||||||
|
<BlockSchemaComponentProvider>
|
||||||
|
<RecordProvider record={record}>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</RecordProvider>
|
||||||
|
</BlockSchemaComponentProvider>
|
||||||
|
</AntdSchemaComponentProvider>
|
||||||
|
</CollectionManagerProvider>
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
</APIClientProvider>
|
||||||
|
);
|
||||||
|
};
|
30
packages/client/src/schema-component/antd/form-v2/index.md
Normal file
30
packages/client/src/schema-component/antd/form-v2/index.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
nav:
|
||||||
|
path: /client
|
||||||
|
group:
|
||||||
|
path: /schema-components
|
||||||
|
---
|
||||||
|
|
||||||
|
# FormV2
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### 基础用法
|
||||||
|
|
||||||
|
<code src="./demos/demo1.tsx"/>
|
||||||
|
|
||||||
|
### Initial values
|
||||||
|
|
||||||
|
<code src="./demos/demo2.tsx"/>
|
||||||
|
|
||||||
|
### Read pretty
|
||||||
|
|
||||||
|
<code src="./demos/demo3.tsx"/>
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
属性说明
|
||||||
|
|
||||||
|
- `form` form 实例
|
||||||
|
- `useProps` 动态 props
|
||||||
|
- [FormLayout](https://antd.formilyjs.org/zh-CN/components/form-layout) 相关 props
|
@ -0,0 +1,7 @@
|
|||||||
|
import { Form as FormV2 } from './Form';
|
||||||
|
import { FormDesigner, ReadPrettyFormDesigner } from './Form.Designer';
|
||||||
|
|
||||||
|
FormV2.Designer = FormDesigner;
|
||||||
|
FormV2.ReadPrettyDesigner = ReadPrettyFormDesigner;
|
||||||
|
|
||||||
|
export { FormV2 };
|
@ -78,11 +78,6 @@ const useDef = (opts: any = {}, props: FormProps = {}) => {
|
|||||||
|
|
||||||
const FormBlockContext = createContext<any>(null);
|
const FormBlockContext = createContext<any>(null);
|
||||||
|
|
||||||
export const useFormBlockContext = () => {
|
|
||||||
const ctx = useContext(FormBlockContext);
|
|
||||||
return ctx;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Form: React.FC<FormProps> & { Designer?: any } = observer((props) => {
|
export const Form: React.FC<FormProps> & { Designer?: any } = observer((props) => {
|
||||||
const { request, effects, initialValue, useValues = useDef, ...others } = props;
|
const { request, effects, initialValue, useValues = useDef, ...others } = props;
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
export * from './action';
|
export * from './action';
|
||||||
export * from './block-item';
|
export * from './block-item';
|
||||||
|
// export * from './blocks';
|
||||||
export * from './calendar';
|
export * from './calendar';
|
||||||
|
export * from './calendar-v2';
|
||||||
export * from './card-item';
|
export * from './card-item';
|
||||||
export * from './cascader';
|
export * from './cascader';
|
||||||
export * from './chart';
|
export * from './chart';
|
||||||
@ -10,11 +12,13 @@ export * from './date-picker';
|
|||||||
export * from './filter';
|
export * from './filter';
|
||||||
export * from './form';
|
export * from './form';
|
||||||
export * from './form-item';
|
export * from './form-item';
|
||||||
|
export * from './form-v2';
|
||||||
export * from './grid';
|
export * from './grid';
|
||||||
export * from './icon-picker';
|
export * from './icon-picker';
|
||||||
export * from './input';
|
export * from './input';
|
||||||
export * from './input-number';
|
export * from './input-number';
|
||||||
export * from './kanban';
|
export * from './kanban';
|
||||||
|
export * from './kanban-v2';
|
||||||
export * from './markdown';
|
export * from './markdown';
|
||||||
export * from './menu';
|
export * from './menu';
|
||||||
export * from './page';
|
export * from './page';
|
||||||
@ -24,6 +28,7 @@ export * from './record-picker';
|
|||||||
export * from './select';
|
export * from './select';
|
||||||
export * from './space';
|
export * from './space';
|
||||||
export * from './table';
|
export * from './table';
|
||||||
|
export * from './table-v2';
|
||||||
export * from './tabs';
|
export * from './tabs';
|
||||||
export * from './time-picker';
|
export * from './time-picker';
|
||||||
export * from './tree-select';
|
export * from './tree-select';
|
||||||
|
@ -29,7 +29,7 @@ export const EllipsisWithTooltip = (props) => {
|
|||||||
overflowX: 'auto',
|
overflowX: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{popoverContent}
|
{popoverContent || props.children}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -34,7 +34,7 @@ ReadPretty.TextArea = (props) => {
|
|||||||
const prefixCls = usePrefixCls('description-textarea', props);
|
const prefixCls = usePrefixCls('description-textarea', props);
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const value = compile(props.value ?? '');
|
const value = compile(props.value ?? '');
|
||||||
const { autop = false } = props;
|
const { autop = true } = props;
|
||||||
let content = null;
|
let content = null;
|
||||||
const values = HTMLEncode(value).split('\n').join('<br/>');
|
const values = HTMLEncode(value).split('\n').join('<br/>');
|
||||||
content = (
|
content = (
|
||||||
|
@ -0,0 +1,103 @@
|
|||||||
|
import { MenuOutlined } from '@ant-design/icons';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
|
import { Space } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useAPIClient } from '../../../api-client';
|
||||||
|
import { createDesignable, useDesignable } from '../../../schema-component';
|
||||||
|
import { SchemaInitializer } from '../../../schema-initializer';
|
||||||
|
import { useFormItemInitializerFields } from '../../../schema-initializer/utils';
|
||||||
|
|
||||||
|
const titleCss = css`
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #f18b62;
|
||||||
|
color: #fff;
|
||||||
|
padding: 0 5px;
|
||||||
|
line-height: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const gridRowColWrap = (schema: ISchema) => {
|
||||||
|
schema['x-read-pretty'] = true;
|
||||||
|
return {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
properties: {
|
||||||
|
[uid()]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Col',
|
||||||
|
properties: {
|
||||||
|
[schema.name || uid()]: schema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// export const removeGridFormItem = (schema, cb) => {
|
||||||
|
// cb(schema, {
|
||||||
|
// removeParentsIfNoChildren: true,
|
||||||
|
// breakRemoveOn: {
|
||||||
|
// 'x-component': 'Kanban.Card',
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
export const KanbanCardDesigner = (props: any) => {
|
||||||
|
const { dn, designable } = useDesignable();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const { refresh } = useDesignable();
|
||||||
|
const field = useField();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const fields = useFormItemInitializerFields();
|
||||||
|
if (!designable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={'general-schema-designer'}>
|
||||||
|
<div className={'general-schema-designer-icons'}>
|
||||||
|
<Space size={2} align={'center'}>
|
||||||
|
<SchemaInitializer.Button
|
||||||
|
wrap={gridRowColWrap}
|
||||||
|
insert={(schema) => {
|
||||||
|
const gridSchema = fieldSchema.reduceProperties((buf, schema) => {
|
||||||
|
if (schema['x-component'] === 'Grid') {
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}, null);
|
||||||
|
console.log('schema', gridSchema)
|
||||||
|
if (!gridSchema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dn = createDesignable({
|
||||||
|
api,
|
||||||
|
refresh,
|
||||||
|
current: gridSchema,
|
||||||
|
});
|
||||||
|
dn.loadAPIClientEvents();
|
||||||
|
dn.insertBeforeEnd(schema);
|
||||||
|
}}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: t('Display fields'),
|
||||||
|
children: fields,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
component={<MenuOutlined style={{ cursor: 'pointer', fontSize: 12 }} />}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,98 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { FormLayout } from '@formily/antd';
|
||||||
|
import { observer, RecursionField, useFieldSchema } from '@formily/react';
|
||||||
|
import { Card } from 'antd';
|
||||||
|
import React, { useContext, useState } from 'react';
|
||||||
|
import { ActionContext, BlockItem } from '..';
|
||||||
|
import { DndContext } from '../..';
|
||||||
|
import { RecordProvider } from '../../../record-provider';
|
||||||
|
import { SchemaComponentOptions } from '../../core/SchemaComponentOptions';
|
||||||
|
import { KanbanCardContext } from './context';
|
||||||
|
|
||||||
|
const FormItem = observer((props) => {
|
||||||
|
return <BlockItem {...props} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const KanbanCard: any = observer((props: any) => {
|
||||||
|
const { setDisableCardDrag, cardViewerSchema, card, cardField, columnIndex, cardIndex } =
|
||||||
|
useContext(KanbanCardContext);
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card
|
||||||
|
onClick={(e) => {
|
||||||
|
setVisible(true);
|
||||||
|
}}
|
||||||
|
bordered={false}
|
||||||
|
hoverable
|
||||||
|
style={{ cursor: 'pointer', overflow: 'hidden' }}
|
||||||
|
// bodyStyle={{ paddingBottom: 0 }}
|
||||||
|
className={css`
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.nb-row-divider {
|
||||||
|
height: 16px;
|
||||||
|
margin-top: -16px;
|
||||||
|
&:last-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-description-input {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.ant-description-textarea {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.ant-formily-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.nb-grid-row:last-of-type {
|
||||||
|
.nb-grid-col {
|
||||||
|
.nb-form-item:last-of-type {
|
||||||
|
.ant-formily-item {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<SchemaComponentOptions components={{}}>
|
||||||
|
<DndContext
|
||||||
|
onDragStart={() => {
|
||||||
|
setDisableCardDrag(true);
|
||||||
|
}}
|
||||||
|
onDragEnd={() => {
|
||||||
|
setDisableCardDrag(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormLayout layout={'vertical'}>
|
||||||
|
<RecursionField
|
||||||
|
basePath={cardField.address.concat(`${columnIndex}.cards.${cardIndex}`)}
|
||||||
|
schema={fieldSchema}
|
||||||
|
onlyRenderProperties
|
||||||
|
/>
|
||||||
|
</FormLayout>
|
||||||
|
</DndContext>
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
</Card>
|
||||||
|
{cardViewerSchema && (
|
||||||
|
<ActionContext.Provider value={{ openMode: 'drawer', visible, setVisible }}>
|
||||||
|
<RecordProvider record={card}>
|
||||||
|
<RecursionField
|
||||||
|
basePath={cardField.address.concat(`${columnIndex}.cardViewer.${cardIndex}`)}
|
||||||
|
schema={cardViewerSchema}
|
||||||
|
onlyRenderProperties
|
||||||
|
/>
|
||||||
|
</RecordProvider>
|
||||||
|
</ActionContext.Provider>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,5 @@
|
|||||||
|
import { observer } from '@formily/react';
|
||||||
|
|
||||||
|
export const KanbanCardViewer: any = observer((props: any) => {
|
||||||
|
return props.children;
|
||||||
|
});
|
@ -0,0 +1,64 @@
|
|||||||
|
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useKanbanBlockContext } from '../../../block-provider';
|
||||||
|
import { useCollection } from '../../../collection-manager';
|
||||||
|
import { useCollectionFilterOptions } from '../../../collection-manager/action-hooks';
|
||||||
|
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||||
|
import { useSchemaTemplate } from '../../../schema-templates';
|
||||||
|
import { useDesignable } from '../../hooks';
|
||||||
|
|
||||||
|
export const KanbanDesigner = () => {
|
||||||
|
const { name, title } = useCollection();
|
||||||
|
const field = useField();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const dataSource = useCollectionFilterOptions(name);
|
||||||
|
const { service } = useKanbanBlockContext();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
|
||||||
|
const template = useSchemaTemplate();
|
||||||
|
return (
|
||||||
|
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||||
|
<SchemaSettings.ModalItem
|
||||||
|
title={t('Set the data scope')}
|
||||||
|
schema={
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
title: t('Set the data scope'),
|
||||||
|
properties: {
|
||||||
|
filter: {
|
||||||
|
default: defaultFilter,
|
||||||
|
enum: dataSource,
|
||||||
|
'x-component': 'Filter',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ISchema
|
||||||
|
}
|
||||||
|
onSubmit={({ filter }) => {
|
||||||
|
const params = field.decoratorProps.params || {};
|
||||||
|
params.filter = filter;
|
||||||
|
field.decoratorProps.params = params;
|
||||||
|
fieldSchema['x-decorator-props']['params'] = params;
|
||||||
|
service.run({ ...service.params?.[0], filter });
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettings.Divider />
|
||||||
|
<SchemaSettings.Template componentName={'Kanban'} collectionName={name} />
|
||||||
|
<SchemaSettings.Divider />
|
||||||
|
<SchemaSettings.Remove
|
||||||
|
removeParentsIfNoChildren
|
||||||
|
breakRemoveOn={{
|
||||||
|
'x-component': 'Grid',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</GeneralSchemaDesigner>
|
||||||
|
);
|
||||||
|
};
|
141
packages/client/src/schema-component/antd/kanban-v2/Kanban.tsx
Normal file
141
packages/client/src/schema-component/antd/kanban-v2/Kanban.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import { ArrayField } from '@formily/core';
|
||||||
|
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
|
||||||
|
import { Spin, Tag } from 'antd';
|
||||||
|
import React, { useContext, useMemo, useState } from 'react';
|
||||||
|
import { SchemaComponentOptions } from '../..';
|
||||||
|
import { RecordProvider } from '../../../';
|
||||||
|
import { useCreateActionProps as useCAP } from '../../../block-provider/hooks';
|
||||||
|
import { Board } from '../../../board';
|
||||||
|
import '../../../board/style.less';
|
||||||
|
import { useProps } from '../../hooks/useProps';
|
||||||
|
import { KanbanCardContext, KanbanColumnContext } from './context';
|
||||||
|
|
||||||
|
const useCreateActionProps = () => {
|
||||||
|
const form = useForm();
|
||||||
|
const { column, groupField } = useContext(KanbanColumnContext);
|
||||||
|
const { onClick } = useCAP();
|
||||||
|
return {
|
||||||
|
async onClick() {
|
||||||
|
form.setValuesIn(groupField.name, column.id);
|
||||||
|
await onClick();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toColumns = (groupField: any, dataSource: Array<any> = []) => {
|
||||||
|
const columns = {
|
||||||
|
__unknown__: {
|
||||||
|
id: '__unknown__',
|
||||||
|
title: 'Unknown',
|
||||||
|
color: 'default',
|
||||||
|
cards: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
groupField.uiSchema.enum.forEach((item) => {
|
||||||
|
columns[item.value] = {
|
||||||
|
id: item.value,
|
||||||
|
title: item.label,
|
||||||
|
color: item.color,
|
||||||
|
cards: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
dataSource.forEach((ds) => {
|
||||||
|
const value = ds[groupField.name];
|
||||||
|
if (value && columns[value]) {
|
||||||
|
columns[value].cards.push(ds);
|
||||||
|
} else {
|
||||||
|
columns.__unknown__.cards.push(ds);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (columns.__unknown__.cards.length === 0) {
|
||||||
|
delete columns.__unknown__;
|
||||||
|
}
|
||||||
|
return Object.values(columns);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Kanban: ComposedKanban = observer((props: any) => {
|
||||||
|
const { groupField, onCardDragEnd, ...restProps } = useProps(props);
|
||||||
|
const field = useField<ArrayField>();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const [disableCardDrag, setDisableCardDrag] = useState(false);
|
||||||
|
const schemas = useMemo(
|
||||||
|
() =>
|
||||||
|
fieldSchema.reduceProperties(
|
||||||
|
(buf, current) => {
|
||||||
|
if (current['x-component'].endsWith('.Card')) {
|
||||||
|
buf.card = current;
|
||||||
|
} else if (current['x-component'].endsWith('.CardAdder')) {
|
||||||
|
buf.cardAdder = current;
|
||||||
|
} else if (current['x-component'].endsWith('.CardViewer')) {
|
||||||
|
buf.cardViewer = current;
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
},
|
||||||
|
{ card: null, cardAdder: null, cardViewer: null },
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const handleCardRemove = (card, column) => {
|
||||||
|
const updatedBoard = Board.removeCard({ columns: field.value }, column, card);
|
||||||
|
field.value = updatedBoard.columns;
|
||||||
|
};
|
||||||
|
const handleCardDragEnd = (card, fromColumn, toColumn) => {
|
||||||
|
onCardDragEnd?.({ columns: field.value, groupField }, fromColumn, toColumn);
|
||||||
|
const updatedBoard = Board.moveCard({ columns: field.value }, fromColumn, toColumn);
|
||||||
|
field.value = updatedBoard.columns;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Spin spinning={field.loading || false}>
|
||||||
|
<Board
|
||||||
|
allowAddCard
|
||||||
|
cardAdderPosition={'bottom'}
|
||||||
|
disableCardDrag={disableCardDrag}
|
||||||
|
onCardRemove={handleCardRemove}
|
||||||
|
onCardDragEnd={handleCardDragEnd}
|
||||||
|
renderColumnHeader={({ title, color }) => (
|
||||||
|
<div className={'react-kanban-column-header'}>
|
||||||
|
<Tag color={color}>{title}</Tag>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
renderCard={(card, { column, dragging }) => {
|
||||||
|
const columnIndex = field.value?.indexOf(column);
|
||||||
|
const cardIndex = column?.cards?.indexOf(card);
|
||||||
|
return (
|
||||||
|
schemas.card && (
|
||||||
|
<RecordProvider record={card}>
|
||||||
|
<KanbanCardContext.Provider
|
||||||
|
value={{
|
||||||
|
setDisableCardDrag,
|
||||||
|
cardViewerSchema: schemas.cardViewer,
|
||||||
|
cardField: field,
|
||||||
|
card,
|
||||||
|
column,
|
||||||
|
dragging,
|
||||||
|
columnIndex,
|
||||||
|
cardIndex,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RecursionField name={schemas.card.name} schema={schemas.card} />
|
||||||
|
</KanbanCardContext.Provider>
|
||||||
|
</RecordProvider>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
renderCardAdder={({ column }) => {
|
||||||
|
return (
|
||||||
|
<KanbanColumnContext.Provider value={{ column, groupField }}>
|
||||||
|
<SchemaComponentOptions scope={{ useCreateActionProps }}>
|
||||||
|
<RecursionField name={schemas.cardAdder.name} schema={schemas.cardAdder} />
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
</KanbanColumnContext.Provider>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
columns: field.value || [],
|
||||||
|
}}
|
||||||
|
</Board>
|
||||||
|
</Spin>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,4 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export const KanbanCardContext = createContext(null);
|
||||||
|
export const KanbanColumnContext = createContext(null);
|
File diff suppressed because it is too large
Load Diff
2145
packages/client/src/schema-component/antd/kanban-v2/demos/data.ts
Normal file
2145
packages/client/src/schema-component/antd/kanban-v2/demos/data.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,92 @@
|
|||||||
|
import { ISchema } from '@formily/react';
|
||||||
|
import {
|
||||||
|
AntdSchemaComponentProvider,
|
||||||
|
APIClient,
|
||||||
|
APIClientProvider,
|
||||||
|
BlockSchemaComponentProvider,
|
||||||
|
CollectionManagerProvider,
|
||||||
|
SchemaComponent,
|
||||||
|
SchemaComponentProvider
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import React from 'react';
|
||||||
|
import collections from './collections';
|
||||||
|
import data from './data';
|
||||||
|
|
||||||
|
const schema: ISchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
block: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'KanbanBlockProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
collection: 't_j6omof6tza8',
|
||||||
|
resource: 't_j6omof6tza8',
|
||||||
|
action: 'list',
|
||||||
|
groupField: 'f_hpmvdltzs6m',
|
||||||
|
params: {
|
||||||
|
paginate: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
kanban: {
|
||||||
|
type: 'array',
|
||||||
|
'x-component': 'KanbanV2',
|
||||||
|
'x-component-props': {
|
||||||
|
useProps: '{{ useKanbanBlockProps }}',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
card: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'KanbanV2.Card',
|
||||||
|
properties: {
|
||||||
|
f_g8j5jvalqh0: {
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
f_tegyd222bcc: {
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const sleep = (value: number) => new Promise((resolve) => setTimeout(resolve, value));
|
||||||
|
|
||||||
|
const apiClient = new APIClient({
|
||||||
|
baseURL: 'http://localhost:3000/api',
|
||||||
|
});
|
||||||
|
|
||||||
|
const mock = (api: APIClient) => {
|
||||||
|
const mock = new MockAdapter(api.axios);
|
||||||
|
mock.onGet('/t_j6omof6tza8:list').reply(async (config) => {
|
||||||
|
await sleep(200);
|
||||||
|
return [200, data];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
mock(apiClient);
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<APIClientProvider apiClient={apiClient}>
|
||||||
|
<SchemaComponentProvider>
|
||||||
|
<CollectionManagerProvider collections={collections.data}>
|
||||||
|
<AntdSchemaComponentProvider>
|
||||||
|
<BlockSchemaComponentProvider>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</BlockSchemaComponentProvider>
|
||||||
|
</AntdSchemaComponentProvider>
|
||||||
|
</CollectionManagerProvider>
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
</APIClientProvider>
|
||||||
|
);
|
||||||
|
};
|
12
packages/client/src/schema-component/antd/kanban-v2/index.md
Normal file
12
packages/client/src/schema-component/antd/kanban-v2/index.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
nav:
|
||||||
|
path: /client
|
||||||
|
group:
|
||||||
|
path: /schema-components
|
||||||
|
---
|
||||||
|
|
||||||
|
# KanbanV2
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
<code src="./demos/demo1.tsx" />
|
16
packages/client/src/schema-component/antd/kanban-v2/index.ts
Normal file
16
packages/client/src/schema-component/antd/kanban-v2/index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Action } from '../action';
|
||||||
|
import { Kanban } from './Kanban';
|
||||||
|
import { KanbanCard } from './Kanban.Card';
|
||||||
|
import { KanbanCardDesigner } from './Kanban.Card.Designer';
|
||||||
|
import { KanbanCardViewer } from './Kanban.CardViewer';
|
||||||
|
import { KanbanDesigner } from './Kanban.Designer';
|
||||||
|
|
||||||
|
const KanbanV2 = Kanban;
|
||||||
|
|
||||||
|
KanbanV2.Card = KanbanCard;
|
||||||
|
KanbanV2.CardAdder = Action;
|
||||||
|
KanbanV2.CardViewer = KanbanCardViewer;
|
||||||
|
KanbanV2.Card.Designer = KanbanCardDesigner;
|
||||||
|
KanbanV2.Designer = KanbanDesigner;
|
||||||
|
|
||||||
|
export { KanbanV2 };
|
@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useAPIClient } from '../../../api-client';
|
import { useAPIClient } from '../../../api-client';
|
||||||
import { createDesignable, useDesignable } from '../../../schema-component';
|
import { createDesignable, useDesignable } from '../../../schema-component';
|
||||||
import { SchemaInitializer } from '../../../schema-initializer';
|
import { SchemaInitializer } from '../../../schema-initializer';
|
||||||
import { useFormItemInitializerFields } from '../../../schema-initializer/Initializers/utils';
|
import { useFormItemInitializerFields } from '../../../schema-initializer/utils';
|
||||||
|
|
||||||
const titleCss = css`
|
const titleCss = css`
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useCollection } from '../../../../collection-manager';
|
import { useCollection } from '../../../../collection-manager';
|
||||||
import type { SchemaInitializerItemOptions } from '../../../../schema-initializer';
|
import type { SchemaInitializerItemOptions } from '../../../../schema-initializer';
|
||||||
import { removeGridFormItem } from '../../../../schema-initializer/Initializers/utils';
|
import { removeGridFormItem } from '../../../../schema-initializer/utils';
|
||||||
|
|
||||||
export const useCardItemInitializerFields = () => {
|
export const useCardItemInitializerFields = () => {
|
||||||
const { name, fields } = useCollection();
|
const { name, fields } = useCollection();
|
||||||
|
@ -134,19 +134,6 @@ const useSideMenuRef = () => {
|
|||||||
|
|
||||||
const MenuItemDesignerContext = createContext(null);
|
const MenuItemDesignerContext = createContext(null);
|
||||||
|
|
||||||
const AddMenuItemButton = () => {
|
|
||||||
const schema = useFieldSchema();
|
|
||||||
const { render } = useSchemaInitializer(schema['x-initializer']);
|
|
||||||
const { designable } = useDesignable();
|
|
||||||
return (
|
|
||||||
designable && (
|
|
||||||
<AntdMenu.Item disabled style={{ padding: '0 8px', order: 9999 }}>
|
|
||||||
{render({ style: { background: 'none' } })}
|
|
||||||
</AntdMenu.Item>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Menu: ComposedMenu = observer((props) => {
|
export const Menu: ComposedMenu = observer((props) => {
|
||||||
let {
|
let {
|
||||||
onSelect,
|
onSelect,
|
||||||
@ -201,6 +188,7 @@ export const Menu: ComposedMenu = observer((props) => {
|
|||||||
}
|
}
|
||||||
sideMenuElement.style.display = sideMenuSchema?.['x-component'] === 'Menu.SubMenu' ? 'block' : 'none';
|
sideMenuElement.style.display = sideMenuSchema?.['x-component'] === 'Menu.SubMenu' ? 'block' : 'none';
|
||||||
}, [sideMenuSchema?.name, sideMenuRef]);
|
}, [sideMenuSchema?.name, sideMenuRef]);
|
||||||
|
const { designable } = useDesignable();
|
||||||
return (
|
return (
|
||||||
<DndContext>
|
<DndContext>
|
||||||
<MenuItemDesignerContext.Provider value={Designer}>
|
<MenuItemDesignerContext.Provider value={Designer}>
|
||||||
@ -257,7 +245,11 @@ export const Menu: ComposedMenu = observer((props) => {
|
|||||||
defaultOpenKeys={defaultOpenKeys}
|
defaultOpenKeys={defaultOpenKeys}
|
||||||
defaultSelectedKeys={defaultSelectedKeys}
|
defaultSelectedKeys={defaultSelectedKeys}
|
||||||
>
|
>
|
||||||
<AddMenuItemButton />
|
{designable && (
|
||||||
|
<AntdMenu.Item disabled style={{ padding: '0 8px', order: 9999 }}>
|
||||||
|
{render({ style: { background: 'none' } })}
|
||||||
|
</AntdMenu.Item>
|
||||||
|
)}
|
||||||
{props.children}
|
{props.children}
|
||||||
</AntdMenu>
|
</AntdMenu>
|
||||||
{loading
|
{loading
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user