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:
chenos 2022-04-04 00:30:40 +08:00 committed by GitHub
parent 036baaa443
commit 74779a0d46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
186 changed files with 15518 additions and 3529 deletions

View File

@ -5,6 +5,7 @@ import {
AntdSchemaComponentProvider,
APIClientProvider,
AuthLayout,
BlockSchemaComponentProvider,
BlockTemplateDetails,
BlockTemplatePage,
ChinaRegionProvider,
@ -95,6 +96,7 @@ const providers = [
},
},
],
BlockSchemaComponentProvider,
AntdSchemaComponentProvider,
ChinaRegionProvider,
WorkflowRouteProvider,

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -1,6 +0,0 @@
import { RoleTable } from '@nocobase/client';
import React from 'react';
export default () => {
return <RoleTable />;
};

View File

@ -1,6 +0,0 @@
import { RolePermissions } from '@nocobase/client';
import React from 'react';
export default () => {
return <RolePermissions />;
};

View File

@ -1,6 +0,0 @@
import { ResourceActionsForm } from '@nocobase/client';
import React from 'react';
export default () => {
return <ResourceActionsForm />;
};

View File

@ -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 的前端模块,详细文档待补充。

View File

@ -1,3 +1,3 @@
export * from './ACLProvider';
export * from './RolePermissionManager';
export * from './ACLShortcut';

View File

@ -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>;
};

View File

@ -12,5 +12,3 @@ group:
## compose
<code src="./demos/demo1/index.tsx"/>
<code src="./demos/demo2/index.tsx"/>

View 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,
};
};

View File

@ -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>
);
};

View 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,
};
};

View 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,
};
};

View 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);
},
};
};

View 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 });
},
};
};

View 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 });
},
};
};

View 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 });
},
};
};

View 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?.();
},
};
};

View 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';

View File

@ -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 (

View File

@ -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;
}

View File

@ -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>;
};

View File

@ -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;
},

View File

@ -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()}`;

View File

@ -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,
};
},
};

View File

@ -12,7 +12,7 @@ export const datetime: IField = {
type: 'date',
// name,
uiSchema: {
type: 'datetime',
type: 'string',
// title,
'x-component': 'DatePicker',
'x-component-props': {

View File

@ -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;
}
},
};

View File

@ -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) {

View File

@ -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;
}
},
};

View File

@ -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: {},
},
},
},
},
},
},
};

View File

@ -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()}`;

View File

@ -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;
}
},
};

View File

@ -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': {},

View File

@ -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,
};
},
};

View File

@ -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 = {

View File

@ -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';

View File

@ -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",

View 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,
},
},
};

View File

@ -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": "每页显示数量"
}

View File

@ -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) => {

View File

@ -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>;
};

View File

@ -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();

View File

@ -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={

View File

@ -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 (

View File

@ -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>
);
};

View File

@ -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>
);
});

View File

@ -0,0 +1,6 @@
import { observer } from '@formily/react';
import React from 'react';
export const Event = observer((props) => {
return <>{props.children}</>;
});

View File

@ -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>
);
});

View File

@ -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>
);
});

View File

@ -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>
);
});

View File

@ -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>
);
});

View File

@ -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

View File

@ -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',
},
],
};

View File

@ -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),
},
];

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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" />

View File

@ -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 };

View 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;
}
}

View File

@ -0,0 +1,8 @@
export interface ToolbarProps {
localizer?: any;
label?: any;
view?: any;
views?: any;
onNavigate?: (action: string) => void;
onView?: (view: string) => void;
}

View File

@ -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)),
};
});
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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>

View 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>
);
};

View File

@ -20,3 +20,7 @@ group:
### Custom Dynamic Component
<code src="./demos/demo4.tsx" />
### FilterAction
<code src="./demos/demo5.tsx" />

View File

@ -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 });
},
};
};

View File

@ -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);
},
};
};

View File

@ -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'] = {

View File

@ -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>
);
};

View 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

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View 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

View File

@ -0,0 +1,7 @@
import { Form as FormV2 } from './Form';
import { FormDesigner, ReadPrettyFormDesigner } from './Form.Designer';
FormV2.Designer = FormDesigner;
FormV2.ReadPrettyDesigner = ReadPrettyFormDesigner;
export { FormV2 };

View File

@ -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();

View File

@ -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';

View File

@ -29,7 +29,7 @@ export const EllipsisWithTooltip = (props) => {
overflowX: 'auto',
}}
>
{popoverContent}
{popoverContent || props.children}
</div>
}
>

View File

@ -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 = (

View File

@ -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>
);
};

View File

@ -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>
)}
</>
);
});

View File

@ -0,0 +1,5 @@
import { observer } from '@formily/react';
export const KanbanCardViewer: any = observer((props: any) => {
return props.children;
});

View File

@ -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>
);
};

View 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>
);
});

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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>
);
};

View File

@ -0,0 +1,12 @@
---
nav:
path: /client
group:
path: /schema-components
---
# KanbanV2
## 示例
<code src="./demos/demo1.tsx" />

View 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 };

View File

@ -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;

View File

@ -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();

View File

@ -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