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,
|
||||
APIClientProvider,
|
||||
AuthLayout,
|
||||
BlockSchemaComponentProvider,
|
||||
BlockTemplateDetails,
|
||||
BlockTemplatePage,
|
||||
ChinaRegionProvider,
|
||||
@ -95,6 +96,7 @@ const providers = [
|
||||
},
|
||||
},
|
||||
],
|
||||
BlockSchemaComponentProvider,
|
||||
AntdSchemaComponentProvider,
|
||||
ChinaRegionProvider,
|
||||
WorkflowRouteProvider,
|
||||
|
@ -1,25 +1,12 @@
|
||||
import { LockOutlined } from '@ant-design/icons';
|
||||
import { ISchema, useForm } from '@formily/react';
|
||||
import { ISchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PluginManager } from '../plugin-manager';
|
||||
import { ActionContext, SchemaComponent, useActionContext } from '../schema-component';
|
||||
import { ActionContext, SchemaComponent } from '../schema-component';
|
||||
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 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@ -49,7 +36,7 @@ export const ACLShortcut = () => {
|
||||
setVisible(true);
|
||||
}}
|
||||
/>
|
||||
<SchemaComponent components={components} scope={{ useCloseAction }} schema={schema} />
|
||||
<SchemaComponent components={components} schema={schema} />
|
||||
</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
|
||||
---
|
||||
|
||||
# ACL <Badge>待定</Badge>
|
||||
# ACL
|
||||
|
||||
访问控制列表,plugin-acl 的前端模块
|
||||
|
||||
<code src="./demos/demo1.tsx" />
|
||||
<code src="./demos/demo2.tsx" />
|
||||
<code src="./demos/demo3.tsx" />
|
||||
访问控制列表,plugin-acl 的前端模块,详细文档待补充。
|
||||
|
@ -1,3 +1,3 @@
|
||||
export * from './ACLProvider';
|
||||
export * from './RolePermissionManager';
|
||||
export * from './ACLShortcut';
|
||||
|
||||
|
@ -1,19 +1,20 @@
|
||||
import React from 'react';
|
||||
import { SchemaInitializerProvider } from '../schema-initializer';
|
||||
import { initializes } from '../schema-initializer/Initializers';
|
||||
import React, { useContext } from 'react';
|
||||
import { SchemaInitializerContext, SchemaInitializerProvider } from '../schema-initializer';
|
||||
|
||||
const actionLogsItemGroup = {
|
||||
type: 'itemGroup',
|
||||
title: '{{t("Others")}}',
|
||||
children: [
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Action logs")}}',
|
||||
component: 'ActionLogBlockInitializer',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const ActionLogProvider = (props: any) => {
|
||||
const actionLogsItemGroup = {
|
||||
type: 'itemGroup',
|
||||
title: '{{t("Others")}}',
|
||||
children: [
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Action logs")}}',
|
||||
component: 'ActionLogBlockInitializer',
|
||||
},
|
||||
],
|
||||
};
|
||||
const initializes = useContext(SchemaInitializerContext);
|
||||
initializes.BlockInitializers.items.push({ ...actionLogsItemGroup });
|
||||
return <SchemaInitializerProvider initializers={initializes}>{props.children}</SchemaInitializerProvider>;
|
||||
};
|
||||
|
@ -12,5 +12,3 @@ group:
|
||||
## compose
|
||||
|
||||
<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}
|
||||
onCardNew={console.log}
|
||||
renderCard={(card, { column, dragging }) => {
|
||||
return <Card style={{ width: 220, marginBottom: 15 }}>{card.title}</Card>;
|
||||
return <Card style={{ marginBottom: 15 }}>{card.title}</Card>;
|
||||
}}
|
||||
renderCardAdder={({ column }) => {
|
||||
return (
|
||||
|
@ -10,7 +10,7 @@ import { useCollectionField } from './hooks';
|
||||
const InternalField: React.FC = (props) => {
|
||||
const field = useField<Field>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { name, uiSchema } = useCollectionField();
|
||||
const { name, interface: interfaceType, uiSchema } = useCollectionField();
|
||||
const component = useComponent(uiSchema?.['x-component']);
|
||||
const compile = useCompile();
|
||||
const setFieldProps = (key, value) => {
|
||||
@ -29,6 +29,9 @@ const InternalField: React.FC = (props) => {
|
||||
setFieldProps('title', uiSchema.title);
|
||||
setFieldProps('description', uiSchema.description);
|
||||
setFieldProps('initialValue', uiSchema.default);
|
||||
if (!field.validator && uiSchema['x-validator']) {
|
||||
field.validator = uiSchema['x-validator'];
|
||||
}
|
||||
field.readPretty = uiSchema['x-read-pretty'];
|
||||
setRequired();
|
||||
// @ts-ignore
|
||||
@ -36,8 +39,16 @@ const InternalField: React.FC = (props) => {
|
||||
const originalProps = compile(uiSchema['x-component-props']) || {};
|
||||
const componentProps = merge(originalProps, field.componentProps || {});
|
||||
field.component = [component, componentProps];
|
||||
// console.log('componentProps', uiSchema?.title, field.componentProps);
|
||||
}, [uiSchema?.title, uiSchema?.description, uiSchema?.required]);
|
||||
// if (interfaceType === 'input') {
|
||||
// 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) {
|
||||
return null;
|
||||
}
|
||||
|
@ -5,10 +5,6 @@ import { CollectionOptions } from './types';
|
||||
|
||||
export const CollectionProvider: React.FC<{ name?: string; collection?: CollectionOptions }> = (props) => {
|
||||
const { name, collection, children } = props;
|
||||
const { get } = useCollectionManager();
|
||||
return (
|
||||
<CollectionContext.Provider value={collection || get(collection?.name || name)}>
|
||||
{children}
|
||||
</CollectionContext.Provider>
|
||||
);
|
||||
const { getCollection } = useCollectionManager();
|
||||
return <CollectionContext.Provider value={getCollection(collection || name)}>{children}</CollectionContext.Provider>;
|
||||
};
|
||||
|
@ -12,13 +12,27 @@ export const useCollectionManager = () => {
|
||||
get(name: string) {
|
||||
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);
|
||||
},
|
||||
getCollectionFields(name: string) {
|
||||
const collection = collections?.find((collection) => collection.name === name);
|
||||
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) {
|
||||
return interfaces[name] ? clone(interfaces[name]) : null;
|
||||
},
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { defaultProps, operators } from './properties';
|
||||
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) => {
|
||||
if (!values.through) {
|
||||
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';
|
||||
|
||||
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',
|
||||
// name,
|
||||
uiSchema: {
|
||||
type: 'datetime',
|
||||
type: 'string',
|
||||
// title,
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
import { defaultProps, operators } from './properties';
|
||||
import { IField } from './types';
|
||||
|
||||
@ -22,4 +23,10 @@ export const input: IField = {
|
||||
filterable: {
|
||||
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 { defaultProps } from './properties';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { defaultProps, recordPickerSelector, recordPickerViewer } from './properties';
|
||||
import { IField } from './types';
|
||||
|
||||
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) => {
|
||||
if (values.type === 'belongsToMany') {
|
||||
if (!values.through) {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
import { defaultProps } from './properties';
|
||||
import { IField } from './types';
|
||||
|
||||
@ -18,4 +19,10 @@ export const markdown: IField = {
|
||||
properties: {
|
||||
...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';
|
||||
|
||||
export * as operators from './operators';
|
||||
|
||||
export const type: ISchema = {
|
||||
type: 'string',
|
||||
title: '{{t("Storage type")}}',
|
||||
@ -199,5 +201,77 @@ export const defaultProps = {
|
||||
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 { defaultProps } from './properties';
|
||||
import { IField } from './types';
|
||||
@ -13,13 +14,53 @@ export const subTable: IField = {
|
||||
type: 'hasMany',
|
||||
// name,
|
||||
uiSchema: {
|
||||
type: 'array',
|
||||
type: 'void',
|
||||
// title,
|
||||
'x-component': 'Table',
|
||||
'x-component': 'TableField',
|
||||
'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) => {
|
||||
if (!values.target) {
|
||||
values.target = `t_${uid()}`;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
import { defaultProps } from './properties';
|
||||
import { IField } from './types';
|
||||
|
||||
@ -19,4 +20,10 @@ export const textarea: IField = {
|
||||
properties: {
|
||||
...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',
|
||||
// name,
|
||||
uiSchema: {
|
||||
type: 'datetime',
|
||||
type: 'string',
|
||||
title: '{{t("Last updated at")}}',
|
||||
'x-component': 'DatePicker',
|
||||
'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';
|
||||
|
||||
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 moment from 'moment';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
const zhCN = require('../locale/zh_CN');
|
||||
const enUS = require('../locale/en_US');
|
||||
import { resources } from '../locale';
|
||||
const log = require('debug')('i18next');
|
||||
|
||||
export const i18n = i18next.createInstance();
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
lng: localStorage.getItem('locale') || 'en-US',
|
||||
debug: false,
|
||||
// debug: true,
|
||||
defaultNS: 'client',
|
||||
// parseMissingKeyHandler: (key) => {
|
||||
// console.log('parseMissingKeyHandler', `'${key}': '${key}',`);
|
||||
// return key;
|
||||
// },
|
||||
// ns: ['client'],
|
||||
resources: {
|
||||
'en-US': {
|
||||
client: {
|
||||
...enUS,
|
||||
},
|
||||
},
|
||||
'zh-CN': {
|
||||
client: {
|
||||
...zhCN,
|
||||
},
|
||||
},
|
||||
},
|
||||
resources,
|
||||
});
|
||||
|
||||
const momentLngs = {
|
||||
|
@ -6,6 +6,7 @@ export * from './antd-config-provider';
|
||||
export * from './api-client';
|
||||
export * from './application';
|
||||
export * from './async-data-provider';
|
||||
export * from './block-provider';
|
||||
export * from './board';
|
||||
export * from './china-region';
|
||||
export * from './collection-manager';
|
||||
|
@ -1,8 +1,10 @@
|
||||
{
|
||||
export default {
|
||||
"{{count}} filter items_one": "{{count}} filter item",
|
||||
"{{count}} filter items_other": "{{count}} filter items",
|
||||
"{{count}} more items_one": "{{count}} more item",
|
||||
"{{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",
|
||||
"Today": "Today",
|
||||
"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> 条",
|
||||
"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> 内打开",
|
||||
"{{count}} filter items": "{{count}} 个筛选项",
|
||||
"{{count}} more items": "还有 {{count}} 项",
|
||||
|
||||
"Total {{count}} items": "总共 {{count}} 条",
|
||||
"Today": "今天",
|
||||
"Month": "月",
|
||||
"Week": "周",
|
||||
@ -285,5 +285,7 @@
|
||||
"Block templates": "区块模板",
|
||||
"Convert reference to duplicate": "模板引用转为复制",
|
||||
"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) => {
|
||||
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) => {
|
||||
|
@ -2,8 +2,9 @@ import React from 'react';
|
||||
import * as components from '.';
|
||||
import * as common from '../common';
|
||||
import { SchemaComponentOptions } from '../core/SchemaComponentOptions';
|
||||
import { useFilterActionProps } from './filter/useFilterActionProps';
|
||||
|
||||
export const AntdSchemaComponentProvider = (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';
|
||||
|
||||
export const ActionDesigner = (props) => {
|
||||
const initialValue = {};
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { dn } = useDesignable();
|
||||
@ -22,21 +21,35 @@ export const ActionDesigner = (props) => {
|
||||
'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
|
||||
}
|
||||
initialValues={{ title: field.title }}
|
||||
onSubmit={({ title }) => {
|
||||
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();
|
||||
|
@ -36,6 +36,17 @@ export const ActionDrawer: ComposedActionDrawer = observer((props) => {
|
||||
background: #f0f2f5;
|
||||
}
|
||||
}
|
||||
&.nb-record-picker-selector {
|
||||
.nb-block-item {
|
||||
margin-bottom: 24px;
|
||||
.general-schema-designer {
|
||||
top: -8px;
|
||||
bottom: -8px;
|
||||
left: -8px;
|
||||
right: -8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
)}
|
||||
footer={
|
||||
|
@ -4,8 +4,10 @@ import { Button, Modal, Popover } from 'antd';
|
||||
import classnames from 'classnames';
|
||||
import React, { useState } from 'react';
|
||||
import { useActionContext } from '../..';
|
||||
import { Icon } from '../../../icon';
|
||||
import { SortableItem } from '../../common';
|
||||
import { useDesigner } from '../../hooks';
|
||||
import { useProps } from '../../hooks/useProps';
|
||||
import ActionContainer from './Action.Container';
|
||||
import { ActionDesigner } from './Action.Designer';
|
||||
import { ActionDrawer } from './Action.Drawer';
|
||||
@ -71,10 +73,12 @@ export const Action: ComposedAction = observer((props: any) => {
|
||||
containerRefKey,
|
||||
component,
|
||||
useAction = useA,
|
||||
onClick,
|
||||
className,
|
||||
disabled,
|
||||
icon,
|
||||
...others
|
||||
} = props;
|
||||
const { onClick } = useProps(props);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const Designer = useDesigner();
|
||||
const field = useField();
|
||||
@ -84,6 +88,7 @@ export const Action: ComposedAction = observer((props: any) => {
|
||||
const renderButton = () => (
|
||||
<SortableItem
|
||||
{...others}
|
||||
icon={<Icon type={icon} />}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@ -104,8 +109,8 @@ export const Action: ComposedAction = observer((props: any) => {
|
||||
component={component || Button}
|
||||
className={classnames(className, actionDesignerCss)}
|
||||
>
|
||||
<Designer {...designerProps} />
|
||||
{field.title}
|
||||
<Designer {...designerProps} />
|
||||
</SortableItem>
|
||||
);
|
||||
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 { FieldContext, FormContext } from '@formily/react';
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { FormProvider, SchemaComponent } from '../../../schema-component';
|
||||
import { SchemaComponent } from '../../core';
|
||||
import { useComponent } from '../../hooks';
|
||||
import { FilterContext } from './context';
|
||||
|
||||
export const DynamicComponent = (props) => {
|
||||
const { dynamicComponent } = useContext(FilterContext);
|
||||
const component = useComponent(dynamicComponent);
|
||||
const form = useMemo(
|
||||
() =>
|
||||
createForm({
|
||||
values: {
|
||||
value: props.value,
|
||||
},
|
||||
effects() {
|
||||
onFieldValueChange('value', (field) => {
|
||||
props?.onChange?.(field.value);
|
||||
});
|
||||
},
|
||||
}),
|
||||
[JSON.stringify(props.schema), JSON.stringify(props.value)],
|
||||
);
|
||||
const form = useMemo(() => {
|
||||
return createForm({
|
||||
values: {
|
||||
value: props.value,
|
||||
},
|
||||
effects() {
|
||||
onFieldValueChange('value', (field) => {
|
||||
props?.onChange?.(field.value);
|
||||
});
|
||||
},
|
||||
});
|
||||
}, [JSON.stringify(props.schema), JSON.stringify(props.value)]);
|
||||
const renderSchemaComponent = () => {
|
||||
return (
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
'x-component': 'Input',
|
||||
...props.schema,
|
||||
name: 'value',
|
||||
'x-read-pretty': false,
|
||||
'x-validator': undefined,
|
||||
'x-decorator': undefined,
|
||||
}}
|
||||
/>
|
||||
<FieldContext.Provider value={null}>
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
'x-component': 'Input',
|
||||
...props.schema,
|
||||
name: 'value',
|
||||
'x-read-pretty': false,
|
||||
'x-validator': undefined,
|
||||
'x-decorator': undefined,
|
||||
}}
|
||||
/>
|
||||
</FieldContext.Provider>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<FormProvider form={form}>
|
||||
<FormContext.Provider value={form}>
|
||||
{component
|
||||
? React.createElement(component, {
|
||||
value: props.value,
|
||||
@ -44,6 +45,6 @@ export const DynamicComponent = (props) => {
|
||||
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 { useRequest } from '../../../api-client';
|
||||
import { FilterContext } from './context';
|
||||
import { FilterActionDesigner } from './Filter.Action.Designer';
|
||||
import { FilterAction } from './FilterAction';
|
||||
import { FilterGroup } from './FilterGroup';
|
||||
import { SaveDefaultValue } from './SaveDefaultValue';
|
||||
|
||||
@ -17,7 +19,6 @@ export const Filter: any = observer((props: any) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
useDataSource({
|
||||
onSuccess(data) {
|
||||
console.log('onSuccess', 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 || [] }}>
|
||||
<FilterGroup {...props} />
|
||||
</FilterContext.Provider>
|
||||
{/* <pre>{JSON.stringify(field.value, null, 2)}</pre> */}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
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 { observer, useField } from '@formily/react';
|
||||
import { observer } from '@formily/react';
|
||||
import { Cascader, Select, Space } from 'antd';
|
||||
import React, { useContext } from 'react';
|
||||
import { useCompile } from '../..';
|
||||
@ -8,10 +8,9 @@ import { DynamicComponent } from './DynamicComponent';
|
||||
import { useValues } from './useValues';
|
||||
|
||||
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 remove = useContext(RemoveConditionContext);
|
||||
const { schema, fields, operators, dataIndex, operator, setDataIndex, setOperator, value, setValue } = useValues();
|
||||
return (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Space>
|
||||
@ -25,16 +24,15 @@ export const FilterItem = observer((props: any) => {
|
||||
width: 150,
|
||||
}}
|
||||
changeOnSelect={false}
|
||||
key={field.address.toString()}
|
||||
value={dataIndex}
|
||||
options={compile(options)}
|
||||
options={compile(fields)}
|
||||
onChange={(value) => {
|
||||
setDataIndex(value);
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
value={operator}
|
||||
options={compile(option?.operators)}
|
||||
value={operator?.value}
|
||||
options={compile(operators)}
|
||||
onChange={(value) => {
|
||||
setOperator(value);
|
||||
}}
|
||||
@ -42,13 +40,14 @@ export const FilterItem = observer((props: any) => {
|
||||
minWidth: 100,
|
||||
}}
|
||||
/>
|
||||
{React.createElement(DynamicComponent, {
|
||||
value,
|
||||
schema: option?.schema,
|
||||
onChange(value) {
|
||||
setValue(value);
|
||||
},
|
||||
})}
|
||||
{!operator?.noValue &&
|
||||
React.createElement(DynamicComponent, {
|
||||
value,
|
||||
schema,
|
||||
onChange(value) {
|
||||
setValue(value);
|
||||
},
|
||||
})}
|
||||
<CloseCircleOutlined onClick={() => remove()} />
|
||||
</Space>
|
||||
</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
|
||||
|
||||
<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 { Input } from 'antd';
|
||||
import { merge } from '@formily/shared';
|
||||
import flat from 'flat';
|
||||
import { useContext } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useContext, useEffect } from 'react';
|
||||
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 = () => {
|
||||
const { options } = useContext(FilterContext);
|
||||
const field = useField<any>();
|
||||
const obj = flat(field.value || {});
|
||||
const key = Object.keys(obj).shift() || '';
|
||||
const [path, others = ''] = key.split('.$');
|
||||
let [operator] = others.split('.');
|
||||
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 || [];
|
||||
const { options } = useContext(FilterContext);
|
||||
const data2value = () => {
|
||||
field.value = flat.unflatten({
|
||||
[`${field.data.dataIndex.join('.')}.${field.data?.operator?.value}`]: field.data?.value,
|
||||
});
|
||||
return option;
|
||||
};
|
||||
const option = findOption(dataIndex);
|
||||
console.log('option', option);
|
||||
const operators = option?.operators;
|
||||
const value2data = () => {
|
||||
const values = flat(field.value);
|
||||
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 {
|
||||
option,
|
||||
dataIndex,
|
||||
options,
|
||||
operators,
|
||||
operator,
|
||||
value,
|
||||
component: [Input, {}],
|
||||
// 当 dataIndex 变化,value 清空
|
||||
setDataIndex(di: string[]) {
|
||||
if (!di) {
|
||||
field.value = {};
|
||||
return;
|
||||
}
|
||||
const option = findOption(di);
|
||||
const op = option?.operators?.[0]?.value || '$eq';
|
||||
field.value = flat.unflatten({
|
||||
[`${di.join('.')}.${op}`]: null,
|
||||
});
|
||||
fields: options,
|
||||
...field.data,
|
||||
setDataIndex(dataIndex) {
|
||||
const option = findOption(dataIndex, options);
|
||||
const operator = option?.operators?.[0];
|
||||
field.data = field.data || {};
|
||||
field.data.operators = option.operators;
|
||||
field.data.operator = operator;
|
||||
field.data.schema = merge(option.schema, operator.schema);
|
||||
field.data.dataIndex = dataIndex;
|
||||
field.data.value = null;
|
||||
data2value();
|
||||
console.log('setDataIndex', field.data);
|
||||
},
|
||||
// 如果只是 Operator 变化,value 要保留
|
||||
setOperator(op: string) {
|
||||
field.value = flat.unflatten({
|
||||
[`${dataIndex.join('.')}.${op}`]: value,
|
||||
});
|
||||
setOperator(operatorValue) {
|
||||
const operator = field.data?.operators?.find?.((item) => item.value === operatorValue);
|
||||
field.data.operator = operator;
|
||||
field.data.schema = merge(field.data.schema, operator.schema);
|
||||
field.data.value = null;
|
||||
data2value();
|
||||
console.log('setOperator', field.data);
|
||||
},
|
||||
setValue(v: any) {
|
||||
field.value = flat.unflatten({
|
||||
[`${dataIndex.join('.')}.${operator || '$eq'}`]: v,
|
||||
});
|
||||
setValue(value) {
|
||||
field.data.value = value;
|
||||
data2value();
|
||||
console.log('setValue', field.data);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { FormItem as Item } from '@formily/antd';
|
||||
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 { useTranslation } from 'react-i18next';
|
||||
import { useActionContext } from '..';
|
||||
import { useCompile, useDesignable } from '../..';
|
||||
import { useCollection, useCollectionManager } from '../../../collection-manager';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
@ -43,85 +42,68 @@ FormItem.Designer = () => {
|
||||
return (
|
||||
<GeneralSchemaDesigner>
|
||||
{collectionField && (
|
||||
<SchemaSettings.PopupItem
|
||||
title={'编辑'}
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Edit field title')}
|
||||
schema={
|
||||
{
|
||||
title: '编辑字段',
|
||||
'x-component': 'Action.Modal',
|
||||
'x-component-props': {
|
||||
width: 520,
|
||||
},
|
||||
'x-decorator': 'Form',
|
||||
'x-decorator-props': {
|
||||
initialValue,
|
||||
},
|
||||
type: 'void',
|
||||
type: 'object',
|
||||
title: t('Edit field title'),
|
||||
properties: {
|
||||
title: {
|
||||
title: t('Field title'),
|
||||
default: field?.title,
|
||||
description: `${t('Original field title: ')}${collectionField?.uiSchema?.title}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
title: '字段标题',
|
||||
'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
|
||||
}
|
||||
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 && (
|
||||
@ -155,6 +137,7 @@ FormItem.Designer = () => {
|
||||
...field.componentProps.fieldNames,
|
||||
label,
|
||||
};
|
||||
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||
fieldSchema['x-component-props']['fieldNames'] = fieldNames;
|
||||
field.componentProps.fieldNames = fieldNames;
|
||||
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);
|
||||
|
||||
export const useFormBlockContext = () => {
|
||||
const ctx = useContext(FormBlockContext);
|
||||
return ctx;
|
||||
};
|
||||
|
||||
export const Form: React.FC<FormProps> & { Designer?: any } = observer((props) => {
|
||||
const { request, effects, initialValue, useValues = useDef, ...others } = props;
|
||||
const fieldSchema = useFieldSchema();
|
||||
|
@ -1,6 +1,8 @@
|
||||
export * from './action';
|
||||
export * from './block-item';
|
||||
// export * from './blocks';
|
||||
export * from './calendar';
|
||||
export * from './calendar-v2';
|
||||
export * from './card-item';
|
||||
export * from './cascader';
|
||||
export * from './chart';
|
||||
@ -10,11 +12,13 @@ export * from './date-picker';
|
||||
export * from './filter';
|
||||
export * from './form';
|
||||
export * from './form-item';
|
||||
export * from './form-v2';
|
||||
export * from './grid';
|
||||
export * from './icon-picker';
|
||||
export * from './input';
|
||||
export * from './input-number';
|
||||
export * from './kanban';
|
||||
export * from './kanban-v2';
|
||||
export * from './markdown';
|
||||
export * from './menu';
|
||||
export * from './page';
|
||||
@ -24,6 +28,7 @@ export * from './record-picker';
|
||||
export * from './select';
|
||||
export * from './space';
|
||||
export * from './table';
|
||||
export * from './table-v2';
|
||||
export * from './tabs';
|
||||
export * from './time-picker';
|
||||
export * from './tree-select';
|
||||
|
@ -29,7 +29,7 @@ export const EllipsisWithTooltip = (props) => {
|
||||
overflowX: 'auto',
|
||||
}}
|
||||
>
|
||||
{popoverContent}
|
||||
{popoverContent || props.children}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
@ -34,7 +34,7 @@ ReadPretty.TextArea = (props) => {
|
||||
const prefixCls = usePrefixCls('description-textarea', props);
|
||||
const compile = useCompile();
|
||||
const value = compile(props.value ?? '');
|
||||
const { autop = false } = props;
|
||||
const { autop = true } = props;
|
||||
let content = null;
|
||||
const values = HTMLEncode(value).split('\n').join('<br/>');
|
||||
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 { createDesignable, useDesignable } from '../../../schema-component';
|
||||
import { SchemaInitializer } from '../../../schema-initializer';
|
||||
import { useFormItemInitializerFields } from '../../../schema-initializer/Initializers/utils';
|
||||
import { useFormItemInitializerFields } from '../../../schema-initializer/utils';
|
||||
|
||||
const titleCss = css`
|
||||
pointer-events: none;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useCollection } from '../../../../collection-manager';
|
||||
import type { SchemaInitializerItemOptions } from '../../../../schema-initializer';
|
||||
import { removeGridFormItem } from '../../../../schema-initializer/Initializers/utils';
|
||||
import { removeGridFormItem } from '../../../../schema-initializer/utils';
|
||||
|
||||
export const useCardItemInitializerFields = () => {
|
||||
const { name, fields } = useCollection();
|
||||
|
@ -134,19 +134,6 @@ const useSideMenuRef = () => {
|
||||
|
||||
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) => {
|
||||
let {
|
||||
onSelect,
|
||||
@ -201,6 +188,7 @@ export const Menu: ComposedMenu = observer((props) => {
|
||||
}
|
||||
sideMenuElement.style.display = sideMenuSchema?.['x-component'] === 'Menu.SubMenu' ? 'block' : 'none';
|
||||
}, [sideMenuSchema?.name, sideMenuRef]);
|
||||
const { designable } = useDesignable();
|
||||
return (
|
||||
<DndContext>
|
||||
<MenuItemDesignerContext.Provider value={Designer}>
|
||||
@ -257,7 +245,11 @@ export const Menu: ComposedMenu = observer((props) => {
|
||||
defaultOpenKeys={defaultOpenKeys}
|
||||
defaultSelectedKeys={defaultSelectedKeys}
|
||||
>
|
||||
<AddMenuItemButton />
|
||||
{designable && (
|
||||
<AntdMenu.Item disabled style={{ padding: '0 8px', order: 9999 }}>
|
||||
{render({ style: { background: 'none' } })}
|
||||
</AntdMenu.Item>
|
||||
)}
|
||||
{props.children}
|
||||
</AntdMenu>
|
||||
{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