feat: support List and Grid Card block (#1753)

* feat: support details block

* feat: rename to list

* fix: imports

* feat: improve list block

* feat: support title hidden

* fix: showTitle default value

* feat: support table column actions

* feat: support appendsOnDemand

* feat: reuse same schema items

* feat: shouldn't display pagination when pages only one

* feat: block order

* feat: support card list

* fix: appends on demand

* fix: remove unused code

* fix: params

* fix: improve default pageSize

* feat: improve performance

* feat: add title designer

* feat: improve huge performance

* fix: better key

* feat: improve schema perf

* feat: improve initializer

* fix: improve grid perf

* fix: avoid haven't properties

* feat: support automatic run service when params changed

* fix: revert params

* fix: grid col width

* refactor: improve CardList performance

* fix: initializer related

* feat: i18n supports

* fix: improve get schema

* fix: create new form

* fix: revert code

* feat: css improve

* fix: i18n copy

* refactor: the CardList rename to GridCard

* feat: remove AssociationFilter item

* fix: edit and view data is incorrect

* feat: remove bulk actions

* feat: improve action bar

* feat: supports configuration of columns with different screen sizes

* fix: incorrect place to save the template

* feat: keep height in all card

* fix: remove console

* fix: rename

* refactor: improve GridCard implementation

* feat: support import/export actions

* fix: remove padding when actions is empty

* fix: remove incorrect import

* fix: remove unused props

* fix: action place

* feat: improve column count

* refactor: remove appendsOnDemand, and use useAssociationNames instated it

* fix: incorrect component

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
Dunqing 2023-05-16 09:05:47 +08:00 committed by GitHub
parent 26469f7aaf
commit 3a77e4376a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 2151 additions and 525 deletions

View File

@ -137,7 +137,7 @@ const getIgnoreScope = (options: any = {}) => {
const useAllowedActions = () => { const useAllowedActions = () => {
const result = useBlockRequestContext() || { service: useResourceActionContext() }; const result = useBlockRequestContext() || { service: useResourceActionContext() };
return result?.service?.data?.meta?.allowedActions; return result?.allowedActions ?? result?.service?.data?.meta?.allowedActions;
}; };
const useResourceName = () => { const useResourceName = () => {

View File

@ -2,9 +2,10 @@ import { css } from '@emotion/css';
import { Field } from '@formily/core'; import { Field } from '@formily/core';
import { RecursionField, useField, useFieldSchema } from '@formily/react'; import { RecursionField, useField, useFieldSchema } from '@formily/react';
import { useRequest } from 'ahooks'; import { useRequest } from 'ahooks';
import merge from 'deepmerge';
import { Col, Row } from 'antd'; import { Col, Row } from 'antd';
import template from 'lodash/template'; import template from 'lodash/template';
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { import {
ACLCollectionProvider, ACLCollectionProvider,
@ -19,6 +20,7 @@ import { CollectionProvider, useCollection, useCollectionManager } from '../coll
import { FilterBlockRecord } from '../filter-provider/FilterProvider'; import { FilterBlockRecord } from '../filter-provider/FilterProvider';
import { useRecordIndex } from '../record-provider'; import { useRecordIndex } from '../record-provider';
import { SharedFilterProvider } from './SharedFilterProvider'; import { SharedFilterProvider } from './SharedFilterProvider';
import _ from 'lodash';
export const BlockResourceContext = createContext(null); export const BlockResourceContext = createContext(null);
export const BlockAssociationContext = createContext(null); export const BlockAssociationContext = createContext(null);
@ -92,18 +94,20 @@ export const useResourceAction = (props, opts = {}) => {
/** /**
* fieldName: 来自 TableFieldProvider * fieldName: 来自 TableFieldProvider
*/ */
const { resource, action, fieldName: tableFieldName } = props; const { resource, action, fieldName: tableFieldName, runWhenParamsChanged = false } = props;
const { fields } = useCollection(); const { fields } = useCollection();
const appends = fields?.filter((field) => field.target).map((field) => field.name);
const params = useActionParams(props); const params = useActionParams(props);
const api = useAPIClient(); const api = useAPIClient();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { snapshot } = useActionContext(); const { snapshot } = useActionContext();
const record = useRecord(); const record = useRecord();
if (!Object.keys(params).includes('appends') && appends?.length) { if (!Reflect.has(params, 'appends')) {
const appends = fields?.filter((field) => field.target).map((field) => field.name);
if (appends?.length) {
params['appends'] = appends; params['appends'] = appends;
} }
}
const result = useRequest( const result = useRequest(
snapshot snapshot
? async () => ({ ? async () => ({
@ -113,10 +117,7 @@ export const useResourceAction = (props, opts = {}) => {
if (!action) { if (!action) {
return Promise.resolve({}); return Promise.resolve({});
} }
const actionParams = { ...opts }; const actionParams = { ...params, ...opts };
if (params.appends) {
actionParams.appends = params.appends;
}
return resource[action](actionParams).then((res) => res.data); return resource[action](actionParams).then((res) => res.data);
}, },
{ {
@ -128,9 +129,22 @@ export const useResourceAction = (props, opts = {}) => {
} }
}, },
defaultParams: [params], defaultParams: [params],
refreshDeps: [JSON.stringify(params.appends)], refreshDeps: [runWhenParamsChanged ? JSON.stringify(params.appends) : null],
}, },
); );
// automatic run service when params has changed
const firstRun = useRef(false);
useEffect(() => {
if (!runWhenParamsChanged) {
return;
}
if (firstRun.current) {
result?.run({ ...result?.params?.[0], ...params });
}
firstRun.current = true;
}, [JSON.stringify(params), runWhenParamsChanged]);
return result; return result;
}; };
@ -148,15 +162,29 @@ export const MaybeCollectionProvider = (props) => {
const BlockRequestProvider = (props) => { const BlockRequestProvider = (props) => {
const field = useField(); const field = useField();
const resource = useBlockResource(); const resource = useBlockResource();
const [allowedActions, setAllowedActions] = useState({});
const service = useResourceAction( const service = useResourceAction(
{ ...props, resource }, { ...props, resource },
{ {
...props.requestOptions, ...props.requestOptions,
}, },
); );
// Infinite scroll support
const serviceAllowedActions = (service?.data as any)?.meta?.allowedActions;
useEffect(() => {
if (!serviceAllowedActions) return;
setAllowedActions((last) => {
return merge(last, serviceAllowedActions ?? {});
});
}, [serviceAllowedActions]);
const __parent = useContext(BlockRequestContext); const __parent = useContext(BlockRequestContext);
return ( return (
<BlockRequestContext.Provider value={{ block: props.block, props, field, service, resource, __parent }}> <BlockRequestContext.Provider
value={{ allowedActions, block: props.block, props, field, service, resource, __parent }}
>
{props.children} {props.children}
</BlockRequestContext.Provider> </BlockRequestContext.Provider>
); );

View File

@ -1091,7 +1091,7 @@ export const useAssociationNames = (collection) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const associationValues = []; const associationValues = [];
const formSchema = fieldSchema.reduceProperties((buf, schema) => { const formSchema = fieldSchema.reduceProperties((buf, schema) => {
if (['FormV2', 'Details'].includes(schema['x-component'])) { if (['FormV2', 'Details', 'List', 'GridCard'].includes(schema['x-component'])) {
return schema; return schema;
} }
return buf; return buf;

View File

@ -69,7 +69,7 @@ const InternalField: React.FC = (props: Props) => {
if (!uiSchema) { if (!uiSchema) {
return null; return null;
} }
return React.createElement(Component, props, props.children); return <Component {...props} />;
}; };
export const InternalFallbackField = () => { export const InternalFallbackField = () => {

View File

@ -24,6 +24,7 @@ export * from './schema-component';
export * from './schema-initializer'; export * from './schema-initializer';
export * from './schema-settings'; export * from './schema-settings';
export * from './schema-templates'; export * from './schema-templates';
export * from './schema-items';
export * from './settings-form'; export * from './settings-form';
export * from './system-settings'; export * from './system-settings';
export * from './user'; export * from './user';

View File

@ -104,6 +104,17 @@ export default {
"Table": "Table", "Table": "Table",
"Table OID(Inheritance)":"Table OID(Inheritance)", "Table OID(Inheritance)":"Table OID(Inheritance)",
"Form": "Form", "Form": "Form",
"List": "List",
"Grid Card": "Grid Card",
"pixels": "pixels",
"Screen size": "Screen size",
"Display title": "Display title",
'Set the count of columns displayed in a row': "Set the count of columns displayed in a row",
'Column': 'Column',
'Phone device': 'Phone device',
'Tablet device': 'Tablet device',
'Desktop device': 'Desktop device',
'Large screen device': 'Large screen device',
"Collapse": "Collapse", "Collapse": "Collapse",
"Select data source": "Select data source", "Select data source": "Select data source",
"Calendar": "Calendar", "Calendar": "Calendar",

View File

@ -120,6 +120,17 @@ export default {
"Filter blocks": "筛选区块", "Filter blocks": "筛选区块",
"Table": "表格", "Table": "表格",
"Form": "表单", "Form": "表单",
"List": "列表",
"Grid Card": "网格卡片",
"Screen size": "屏幕尺寸",
"pixels": "像素",
"Display title": "显示标题",
'Set the count of columns displayed in a row': "设置一行展示的列数",
'Column': '列',
'Phone device': '手机设备',
'Tablet device': '平板设备',
'Desktop device': '电脑设备',
'Large screen device': '大屏幕设备',
"Table OID(Inheritance)":"数据表 OID(继承)", "Table OID(Inheritance)":"数据表 OID(继承)",
"Collapse": "折叠面板", "Collapse": "折叠面板",
"Select data source": "选择数据源", "Select data source": "选择数据源",
@ -217,7 +228,6 @@ export default {
"Edit collection": "编辑数据表", "Edit collection": "编辑数据表",
"Configure fields": "配置字段", "Configure fields": "配置字段",
"Configure columns": "配置字段", "Configure columns": "配置字段",
"Please select the records you want to delete": "请选择要删除的记录",
"Edit field": "编辑字段", "Edit field": "编辑字段",
"Override": "重写", "Override": "重写",
"Override field": "重写字段", "Override field": "重写字段",

View File

@ -12,6 +12,10 @@ export const RecordProvider: React.FC<{ record: any; parent?: any }> = (props) =
return <RecordContext.Provider value={value}>{children}</RecordContext.Provider>; return <RecordContext.Provider value={value}>{children}</RecordContext.Provider>;
}; };
export const RecordSimpleProvider: React.FC<{ value: Record<string, any>; children: React.ReactNode }> = (props) => {
return <RecordContext.Provider {...props} />;
};
export const RecordIndexProvider: React.FC<{ index: any }> = (props) => { export const RecordIndexProvider: React.FC<{ index: any }> = (props) => {
const { index, children } = props; const { index, children } = props;
return <RecordIndexContext.Provider value={index}>{children}</RecordIndexContext.Provider>; return <RecordIndexContext.Provider value={index}>{children}</RecordIndexContext.Provider>;

View File

@ -1,30 +1,34 @@
import { css } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { observer, RecursionField, useFieldSchema } from '@formily/react'; import { observer, RecursionField, useFieldSchema } from '@formily/react';
import { Space } from 'antd'; import { Space } from 'antd';
import React from 'react'; import React from 'react';
import { useSchemaInitializer } from '../../../schema-initializer'; import { useSchemaInitializer } from '../../../schema-initializer';
import { DndContext } from '../../common'; import { DndContext } from '../../common';
import { useDesignable } from '../../hooks'; import { useDesignable, useProps } from '../../hooks';
export const ActionBar = observer((props: any) => { export const ActionBar = observer((props: any) => {
const { layout = 'tow-columns', style, ...others } = props; const { layout = 'tow-columns', style, spaceProps, ...others } = useProps(props);
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializer(fieldSchema['x-initializer']); const { InitializerComponent } = useSchemaInitializer(fieldSchema['x-initializer']);
const { designable } = useDesignable(); const { designable } = useDesignable();
if (layout === 'one-column') { if (layout === 'one-column') {
return ( return (
<DndContext> <DndContext>
<div style={{ display: 'flex', ...style }} {...others}> <div
style={{ display: 'flex', alignItems: 'center', ...style }}
{...others}
className={cx(others.className, 'nb-action-bar')}
>
{props.children && ( {props.children && (
<div style={{ marginRight: 8 }}> <div style={{ marginRight: 8 }}>
<Space> <Space {...spaceProps}>
{fieldSchema.mapProperties((schema, key) => { {fieldSchema.mapProperties((schema, key) => {
return <RecursionField key={key} name={key} schema={schema} />; return <RecursionField key={key} name={key} schema={schema} />;
})} })}
</Space> </Space>
</div> </div>
)} )}
{render()} <InitializerComponent />
</div> </div>
</DndContext> </DndContext>
); );
@ -45,6 +49,7 @@ export const ActionBar = observer((props: any) => {
} }
} }
{...others} {...others}
className={cx(others.className, 'nb-action-bar')}
> >
<div <div
className={css` className={css`
@ -55,7 +60,7 @@ export const ActionBar = observer((props: any) => {
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}
> >
<DndContext> <DndContext>
<Space> <Space {...spaceProps}>
{fieldSchema.mapProperties((schema, key) => { {fieldSchema.mapProperties((schema, key) => {
if (schema['x-align'] !== 'left') { if (schema['x-align'] !== 'left') {
return null; return null;
@ -63,7 +68,7 @@ export const ActionBar = observer((props: any) => {
return <RecursionField key={key} name={key} schema={schema} />; return <RecursionField key={key} name={key} schema={schema} />;
})} })}
</Space> </Space>
<Space> <Space {...spaceProps}>
{fieldSchema.mapProperties((schema, key) => { {fieldSchema.mapProperties((schema, key) => {
if (schema['x-align'] === 'left') { if (schema['x-align'] === 'left') {
return null; return null;
@ -73,7 +78,7 @@ export const ActionBar = observer((props: any) => {
</Space> </Space>
</DndContext> </DndContext>
</div> </div>
{render()} <InitializerComponent />
</div> </div>
); );
}); });

View File

@ -23,6 +23,7 @@ import { defaultFieldNames } from '../select';
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent'; import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
import { ReadPretty } from './ReadPretty'; import { ReadPretty } from './ReadPretty';
import useServiceOptions from './useServiceOptions'; import useServiceOptions from './useServiceOptions';
import { GeneralSchemaItems } from '../../../schema-items';
export type AssociationSelectProps<P = any> = RemoteSelectProps<P> & { export type AssociationSelectProps<P = any> = RemoteSelectProps<P> & {
action?: string; action?: string;
@ -161,124 +162,7 @@ AssociationSelect.Designer = function Designer() {
return ( return (
<GeneralSchemaDesigner> <GeneralSchemaDesigner>
{collectionField && ( <GeneralSchemaItems />
<SchemaSettings.ModalItem
key="edit-field-title"
title={t('Edit field title')}
schema={
{
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',
'x-component-props': {},
},
},
} 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
key="edit-description"
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 && (
<SchemaSettings.ModalItem
key="edit-tooltip"
title={t('Edit tooltip')}
schema={
{
type: 'object',
title: t('Edit description'),
properties: {
tooltip: {
default: fieldSchema?.['x-decorator-props']?.tooltip,
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ tooltip }) => {
field.decoratorProps.tooltip = tooltip;
fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};
fieldSchema['x-decorator-props']['tooltip'] = tooltip;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
dn.refresh();
}}
/>
)}
{!field.readPretty && (
<SchemaSettings.SwitchItem
key="required"
title={t('Required')}
checked={fieldSchema.required as boolean}
onChange={(required) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
field.required = required;
fieldSchema['required'] = required;
schema['required'] = required;
dn.emit('patch', {
schema,
});
refresh();
}}
/>
)}
{form && !form?.readPretty && validateSchema && ( {form && !form?.readPretty && validateSchema && (
<SchemaSettings.ModalItem <SchemaSettings.ModalItem
title={t('Set validation rules')} title={t('Set validation rules')}
@ -795,105 +679,7 @@ AssociationSelect.FilterDesigner = function FilterDesigner() {
return ( return (
<GeneralSchemaDesigner> <GeneralSchemaDesigner>
{collectionField && ( <GeneralSchemaItems required={false} />
<SchemaSettings.ModalItem
key="edit-field-title"
title={t('Edit field title')}
schema={
{
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',
'x-component-props': {},
},
},
} 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
key="edit-description"
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 && (
<SchemaSettings.ModalItem
key="edit-tooltip"
title={t('Edit tooltip')}
schema={
{
type: 'object',
title: t('Edit description'),
properties: {
tooltip: {
default: fieldSchema?.['x-decorator-props']?.tooltip,
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ tooltip }) => {
field.decoratorProps.tooltip = tooltip;
fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};
fieldSchema['x-decorator-props']['tooltip'] = tooltip;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
dn.refresh();
}}
/>
)}
{form && !form?.readPretty && validateSchema && ( {form && !form?.readPretty && validateSchema && (
<SchemaSettings.ModalItem <SchemaSettings.ModalItem
title={t('Set validation rules')} title={t('Set validation rules')}

View File

@ -1,4 +1,4 @@
import { css } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { ArrayCollapse, ArrayItems, FormLayout, FormItem as Item } from '@formily/antd'; import { ArrayCollapse, ArrayItems, FormLayout, FormItem as Item } from '@formily/antd';
import { Field } from '@formily/core'; import { Field } from '@formily/core';
import { ISchema, Schema, observer, useField, useFieldSchema } from '@formily/react'; import { ISchema, Schema, observer, useField, useFieldSchema } from '@formily/react';
@ -18,6 +18,7 @@ import {
useSortFields, useSortFields,
} from '../../../collection-manager'; } from '../../../collection-manager';
import { isTitleField } from '../../../collection-manager/Configuration/CollectionFields'; import { isTitleField } from '../../../collection-manager/Configuration/CollectionFields';
import { GeneralSchemaItems } from '../../../schema-items/GeneralSchemaItems';
import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled, isShowDefaultValue } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled, isShowDefaultValue } from '../../../schema-settings';
import { VariableInput } from '../../../schema-settings/VariableInput/VariableInput'; import { VariableInput } from '../../../schema-settings/VariableInput/VariableInput';
import { useIsShowMultipleSwitch } from '../../../schema-settings/hooks/useIsShowMultipleSwitch'; import { useIsShowMultipleSwitch } from '../../../schema-settings/hooks/useIsShowMultipleSwitch';
@ -70,15 +71,25 @@ export const FormItem: any = observer((props: any) => {
} }
} }
}, []); }, []);
const { showTitle = true } = props;
return ( return (
<ACLCollectionFieldProvider> <ACLCollectionFieldProvider>
<BlockItem className={'nb-form-item'}> <BlockItem className={'nb-form-item'}>
<Item <Item
className={`${css` className={cx(
css`
& .ant-space { & .ant-space {
flex-wrap: wrap; flex-wrap: wrap;
} }
`}`} `,
{
[css`
& .ant-formily-item-label {
display: none;
}
`]: showTitle === false,
},
)}
{...props} {...props}
extra={ extra={
typeof field.description === 'string' ? ( typeof field.description === 'string' ? (
@ -162,124 +173,7 @@ FormItem.Designer = function Designer() {
return ( return (
<GeneralSchemaDesigner> <GeneralSchemaDesigner>
{collectionField && ( <GeneralSchemaItems />
<SchemaSettings.ModalItem
key="edit-field-title"
title={t('Edit field title')}
schema={
{
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',
'x-component-props': {},
},
},
} 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
key="edit-description"
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 && (
<SchemaSettings.ModalItem
key="edit-tooltip"
title={t('Edit tooltip')}
schema={
{
type: 'object',
title: t('Edit description'),
properties: {
tooltip: {
default: fieldSchema?.['x-decorator-props']?.tooltip,
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ tooltip }) => {
field.decoratorProps.tooltip = tooltip;
fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};
fieldSchema['x-decorator-props']['tooltip'] = tooltip;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
dn.refresh();
}}
/>
)}
{!field.readPretty && fieldSchema['x-component'] !== 'FormField' && (
<SchemaSettings.SwitchItem
key="required"
title={t('Required')}
checked={fieldSchema.required as boolean}
onChange={(required) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
field.required = required;
fieldSchema['required'] = required;
schema['required'] = required;
dn.emit('patch', {
schema,
});
refresh();
}}
/>
)}
{!form?.readPretty && isFileField ? ( {!form?.readPretty && isFileField ? (
<SchemaSettings.SwitchItem <SchemaSettings.SwitchItem
key="quick-upload" key="quick-upload"

View File

@ -0,0 +1,80 @@
import { createForm } from '@formily/core';
import { FormContext, useField, useForm } from '@formily/react';
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { BlockProvider, useBlockRequestContext } from '../../../block-provider';
import { FormLayout } from '@formily/antd';
import { css } from '@emotion/css';
import { useAssociationNames } from '../../../block-provider/hooks';
export const GridCardBlockContext = createContext<any>({});
const InternalGridCardBlockProvider = (props) => {
const { resource, service } = useBlockRequestContext();
const field = useField();
const form = useMemo(() => {
return createForm({
readPretty: true,
});
}, []);
useEffect(() => {
if (!service?.loading) {
form.setValuesIn(field.address.concat('list').toString(), service?.data?.data);
}
}, [service?.data?.data, service?.loading]);
return (
<GridCardBlockContext.Provider
value={{
service,
resource,
columnCount: props.columnCount,
}}
>
<FormContext.Provider value={form}>
<FormLayout layout={'vertical'}>
<div
className={css`
& > .nb-block-item {
margin-bottom: var(--nb-spacing);
& > .nb-action-bar:has(:first-child:not(:empty)) {
padding: var(--nb-spacing);
background: #fff;
}
.ant-list-pagination {
padding: var(--nb-spacing);
background: #fff;
}
}
`}
>
{props.children}
</div>
</FormLayout>
</FormContext.Provider>
</GridCardBlockContext.Provider>
);
};
export const GridCardBlockProvider = (props) => {
const params = { ...props.params };
const { collection } = props;
const { appends } = useAssociationNames(collection);
if (!Object.keys(params).includes('appends')) {
params['appends'] = appends;
}
return (
<BlockProvider {...props} params={params}>
<InternalGridCardBlockProvider {...props} />
</BlockProvider>
);
};
export const useGridCardBlockContext = () => {
return useContext(GridCardBlockContext);
};
export const useGridCardItemProps = () => {
return {};
};

View File

@ -0,0 +1,245 @@
import { useFieldSchema, useField, ISchema } from '@formily/react';
import React, { useMemo } from 'react';
import { ArrayItems } from '@formily/antd';
import { Slider } from 'antd';
import _ from 'lodash';
import { useTranslation } from 'react-i18next';
import { useCollection, useCollectionFilterOptions, useSortFields } from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates';
import { useDesignable } from '../../hooks';
import { removeNullCondition } from '../filter';
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
import { SchemaComponentOptions } from '../../core';
import { defaultColumnCount, gridSizes, pageSizeOptions, screenSizeMaps, screenSizeTitleMaps } from './options';
Slider;
const columnCountMarks = [1, 2, 3, 4, 6, 8, 12, 24].reduce((obj, cur) => {
obj[cur] = cur;
return obj;
}, {});
export const GridCardDesigner = () => {
const { name, title } = useCollection();
const template = useSchemaTemplate();
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const field = useField();
const dataSource = useCollectionFilterOptions(name);
const { dn } = useDesignable();
const sortFields = useSortFields(name);
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
const columnCount = field.decoratorProps.columnCount || defaultColumnCount;
const columnCountSchema = useMemo(() => {
return {
'x-component': 'Slider',
'x-decorator': 'FormItem',
'x-component-props': {
min: 1,
max: 24,
marks: columnCountMarks,
tooltip: {
formatter: (value) => `${value}${t('Column')}`,
},
step: null,
},
};
}, [t]);
const columnCountProperties = useMemo(() => {
return gridSizes.reduce((o, k) => {
o[k] = {
...columnCountSchema,
title: t(screenSizeTitleMaps[k]),
description: `${t('Screen size')} ${screenSizeMaps[k]} ${t('pixels')}`,
};
return o;
}, {});
}, [columnCountSchema, t]);
const sort = defaultSort?.map((item: string) => {
return item.startsWith('-')
? {
field: item.substring(1),
direction: 'desc',
}
: {
field: item,
direction: 'asc',
};
});
return (
<GeneralSchemaDesigner template={template} title={title || name}>
<SchemaComponentOptions components={{ Slider }}>
<SchemaSettings.BlockTitleItem />
<SchemaSettings.ModalItem
title={t('Set the count of columns displayed in a row')}
initialValues={columnCount}
schema={
{
type: 'object',
title: t('Set the count of columns displayed in a row'),
properties: columnCountProperties,
} as ISchema
}
onSubmit={(columnCount) => {
_.set(fieldSchema, 'x-decorator-props.columnCount', columnCount);
field.decoratorProps.columnCount = columnCount;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
}}
/>
<SchemaSettings.ModalItem
title={t('Set the data scope')}
schema={
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
default: defaultFilter,
// title: '数据范围',
enum: dataSource,
'x-component': 'Filter',
'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
},
},
},
} as ISchema
}
onSubmit={({ filter }) => {
filter = removeNullCondition(filter);
_.set(fieldSchema, 'x-decorator-props.params.filter', filter);
field.decoratorProps.params = { ...fieldSchema['x-decorator-props'].params };
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
}}
/>
<SchemaSettings.ModalItem
title={t('Set default sorting rules')}
components={{ ArrayItems }}
schema={
{
type: 'object',
title: t('Set default sorting rules'),
properties: {
sort: {
type: 'array',
default: sort,
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
properties: {
sort: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.SortHandle',
},
field: {
type: 'string',
enum: sortFields,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
style: {
width: 260,
},
},
},
direction: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-component-props': {
optionType: 'button',
},
enum: [
{
label: t('ASC'),
value: 'asc',
},
{
label: t('DESC'),
value: 'desc',
},
],
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: t('Add sort field'),
'x-component': 'ArrayItems.Addition',
},
},
},
},
} as ISchema
}
onSubmit={({ sort }) => {
const sortArr = sort.map((item) => {
return item.direction === 'desc' ? `-${item.field}` : item.field;
});
_.set(fieldSchema, 'x-decorator-props.params.sort', sortArr);
field.decoratorProps.params = { ...fieldSchema['x-decorator-props'].params };
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
}}
/>
<SchemaSettings.SelectItem
title={t('Records per page')}
value={field.decoratorProps?.params?.pageSize || 20}
options={pageSizeOptions.map((v) => ({ value: v }))}
onChange={(pageSize) => {
_.set(fieldSchema, 'x-decorator-props.params.pageSize', pageSize);
field.decoratorProps.params = { ...fieldSchema['x-decorator-props'].params, page: 1 };
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
}}
/>
<SchemaSettings.Template componentName={'GridCard'} collectionName={name} resourceName={defaultResource} />
<SchemaSettings.Divider />
<SchemaSettings.Remove
removeParentsIfNoChildren
breakRemoveOn={{
'x-component': 'Grid',
}}
/>
</SchemaComponentOptions>
</GeneralSchemaDesigner>
);
};

View File

@ -0,0 +1,33 @@
import React from 'react';
import { Card } from 'antd';
import { css } from '@emotion/css';
import { useField } from '@formily/react';
import { ObjectField } from '@formily/core';
import { RecordSimpleProvider } from '../../../record-provider';
const itemCss = css`
display: flex;
width: 100%;
height: 100%;
flex-direction: column;
justify-content: space-between;
gap: 8px;
`;
export const GridCardItem = (props) => {
const field = useField<ObjectField>();
return (
<Card
className={css`
&,
& .ant-card-body {
height: 100%;
}
`}
>
<div className={itemCss}>
<RecordSimpleProvider value={field.value}>{props.children}</RecordSimpleProvider>
</div>
</Card>
);
};

View File

@ -0,0 +1,150 @@
import React, { useCallback, useState } from 'react';
import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
import { css, cx } from '@emotion/css';
import { List as AntdList, PaginationProps, Col } from 'antd';
import { useGridCardActionBarProps } from './hooks';
import { SortableItem } from '../../common';
import { SchemaComponentOptions } from '../../core';
import { useDesigner } from '../../hooks';
import { GridCardItem } from './GridCard.Item';
import { useGridCardBlockContext, useGridCardItemProps, GridCardBlockProvider } from './GridCard.Decorator';
import { GridCardDesigner } from './GridCard.Designer';
import { ArrayField } from '@formily/core';
import { defaultColumnCount, pageSizeOptions } from './options';
const rowGutter = {
md: 16,
sm: 8,
xs: 8,
};
const designerCss = css`
width: 100%;
&:hover {
> .general-schema-designer {
display: block;
}
}
> .general-schema-designer {
position: absolute;
z-index: 999;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: none;
background: rgba(241, 139, 98, 0.06);
border: 0;
pointer-events: none;
> .general-schema-designer-icons {
position: absolute;
right: 2px;
top: 2px;
line-height: 16px;
pointer-events: all;
.ant-space-item {
background-color: #f18b62;
color: #fff;
line-height: 16px;
width: 16px;
padding-left: 1px;
}
}
}
`;
const InternalGridCard = (props) => {
const { service, columnCount = defaultColumnCount } = useGridCardBlockContext();
const { run, params } = service;
const meta = service?.data?.meta;
const fieldSchema = useFieldSchema();
const field = useField<ArrayField>();
const Designer = useDesigner();
const [schemaMap] = useState(new Map());
const getSchema = useCallback(
(key) => {
if (!schemaMap.has(key)) {
schemaMap.set(
key,
new Schema({
type: 'object',
properties: {
[key]: fieldSchema.properties['item'],
},
}),
);
}
return schemaMap.get(key);
},
[fieldSchema.properties, schemaMap],
);
const onPaginationChange: PaginationProps['onChange'] = useCallback(
(page, pageSize) => {
run({
...params?.[0],
page: page,
pageSize: pageSize,
});
},
[run, params],
);
return (
<SchemaComponentOptions
scope={{
useGridCardItemProps,
useGridCardActionBarProps,
}}
>
<SortableItem className={cx('nb-card-list', designerCss)}>
<AntdList
pagination={
!meta || meta.count <= meta.pageSize
? false
: {
onChange: onPaginationChange,
total: meta?.count || 0,
pageSize: meta?.pageSize || 10,
current: meta?.page || 1,
pageSizeOptions,
}
}
dataSource={field.value}
grid={{
...columnCount,
sm: columnCount.xs,
xl: columnCount.lg,
gutter: [rowGutter, rowGutter],
}}
renderItem={(item, index) => {
return (
<Col style={{ height: '100%' }}>
<RecursionField
key={index}
basePath={field.address}
name={index}
onlyRenderProperties
schema={getSchema(index)}
></RecursionField>
</Col>
);
}}
loading={service?.loading}
/>
<Designer />
</SortableItem>
</SchemaComponentOptions>
);
};
export const GridCard = InternalGridCard as typeof InternalGridCard & {
Item: typeof GridCardItem;
Designer: typeof GridCardDesigner;
Decorator: typeof GridCardBlockProvider;
};
GridCard.Item = GridCardItem;
GridCard.Designer = GridCardDesigner;
GridCard.Decorator = GridCardBlockProvider;

View File

@ -0,0 +1,12 @@
import { SpaceProps } from 'antd';
const spaceProps: SpaceProps = {
size: ['large', 'small'],
wrap: true,
};
export const useGridCardActionBarProps = () => {
return {
spaceProps,
};
};

View File

@ -0,0 +1 @@
export * from './GridCard';

View File

@ -0,0 +1,21 @@
export const pageSizeOptions = [12, 24, 60, 96, 120];
export const gridSizes = ['xs', 'md', 'lg', 'xxl'];
export const screenSizeTitleMaps = {
xs: 'Phone device',
md: 'Tablet device',
lg: 'Desktop device',
xxl: 'Large screen device',
};
export const screenSizeMaps = {
xs: '< 768',
md: '≥ 768',
lg: '≥ 992',
xxl: '≥ 1600',
};
export const defaultColumnCount = {
xs: 1,
md: 2,
lg: 3,
xxl: 4,
};

View File

@ -3,7 +3,7 @@ import { css } from '@emotion/css';
import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react'; import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import cls from 'classnames'; import cls from 'classnames';
import React, { createContext, useContext, useEffect, useRef, useState } from 'react'; import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useDesignable, useFormBlockContext, useSchemaInitializer } from '../../../'; import { useDesignable, useFormBlockContext, useSchemaInitializer } from '../../../';
import { DndContext } from '../../common/dnd-context'; import { DndContext } from '../../common/dnd-context';
@ -293,22 +293,26 @@ const wrapColSchema = (schema: Schema) => {
const useRowProperties = () => { const useRowProperties = () => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
return useMemo(() => {
return fieldSchema.reduceProperties((buf, s) => { return fieldSchema.reduceProperties((buf, s) => {
if (s['x-component'] === 'Grid.Row' && !s['x-hidden']) { if (s['x-component'] === 'Grid.Row' && !s['x-hidden']) {
buf.push(s); buf.push(s);
} }
return buf; return buf;
}, []); }, []);
}, [Object.keys(fieldSchema.properties || {}).join(',')]);
}; };
const useColProperties = () => { const useColProperties = () => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
return useMemo(() => {
return fieldSchema.reduceProperties((buf, s) => { return fieldSchema.reduceProperties((buf, s) => {
if (s['x-component'] === 'Grid.Col' && !s['x-hidden']) { if (s['x-component'] === 'Grid.Col' && !s['x-hidden']) {
buf.push(s); buf.push(s);
} }
return buf; return buf;
}, []); }, []);
}, [Object.keys(fieldSchema.properties || {}).join(',')]);
}; };
const DndWrapper = (props) => { const DndWrapper = (props) => {
@ -330,7 +334,7 @@ export const Grid: any = observer((props: any) => {
const gridRef = useRef(null); const gridRef = useRef(null);
const field = useField(); const field = useField();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializer(fieldSchema['x-initializer']); const { render, InitializerComponent } = useSchemaInitializer(fieldSchema['x-initializer']);
const addr = field.address.toString(); const addr = field.address.toString();
const rows = useRowProperties(); const rows = useRowProperties();
const { setPrintContent } = useFormBlockContext(); const { setPrintContent } = useFormBlockContext();
@ -338,8 +342,9 @@ export const Grid: any = observer((props: any) => {
useEffect(() => { useEffect(() => {
gridRef.current && setPrintContent?.(gridRef.current); gridRef.current && setPrintContent?.(gridRef.current);
}, [gridRef.current]); }, [gridRef.current]);
return ( return (
<GridContext.Provider value={{ ref: gridRef, fieldSchema, renderSchemaInitializer: render }}> <GridContext.Provider value={{ ref: gridRef, fieldSchema, renderSchemaInitializer: render, InitializerComponent }}>
<div className={'nb-grid'} style={{ position: 'relative' }} ref={gridRef}> <div className={'nb-grid'} style={{ position: 'relative' }} ref={gridRef}>
<DndWrapper dndContext={props.dndContext}> <DndWrapper dndContext={props.dndContext}>
<RowDivider <RowDivider
@ -355,7 +360,7 @@ export const Grid: any = observer((props: any) => {
/> />
{rows.map((schema, index) => { {rows.map((schema, index) => {
return ( return (
<React.Fragment key={schema.name}> <React.Fragment key={index}>
<RecursionField name={schema.name} schema={schema} /> <RecursionField name={schema.name} schema={schema} />
<RowDivider <RowDivider
rows={rows} rows={rows}
@ -372,7 +377,7 @@ export const Grid: any = observer((props: any) => {
); );
})} })}
</DndWrapper> </DndWrapper>
{render()} <InitializerComponent />
</div> </div>
</GridContext.Provider> </GridContext.Provider>
); );
@ -411,7 +416,7 @@ Grid.Row = observer(() => {
/> />
{cols.map((schema, index) => { {cols.map((schema, index) => {
return ( return (
<React.Fragment key={schema.name}> <React.Fragment key={index}>
<RecursionField name={schema.name} schema={schema} /> <RecursionField name={schema.name} schema={schema} />
<ColDivider <ColDivider
cols={cols} cols={cols}
@ -437,11 +442,16 @@ Grid.Col = observer((props: any) => {
const { cols = [] } = useContext(GridRowContext); const { cols = [] } = useContext(GridRowContext);
const schema = useFieldSchema(); const schema = useFieldSchema();
const field = useField(); const field = useField();
const width = useMemo(() => {
let width = ''; let width = '';
if (cols?.length) { if (cols?.length) {
const w = schema?.['x-component-props']?.['width'] || 100 / cols.length; const w = schema?.['x-component-props']?.['width'] || 100 / cols.length;
width = `calc(${w}% - var(--nb-spacing) * ${(cols.length + 1) / cols.length})`; width = `calc(${w}% - var(--nb-spacing) * ${(cols.length + 1) / cols.length})`;
} }
return width;
}, [cols?.length, schema?.['x-component-props']?.['width']]);
const { setNodeRef } = useDroppable({ const { setNodeRef } = useDroppable({
id: field.address.toString(), id: field.address.toString(),
data: { data: {

View File

@ -19,10 +19,12 @@ export * from './form-v2';
export * from './g2plot'; export * from './g2plot';
export * from './gantt'; export * from './gantt';
export * from './grid'; export * from './grid';
export * from './grid-card';
export * from './icon-picker'; export * from './icon-picker';
export * from './input'; export * from './input';
export * from './input-number'; export * from './input-number';
export * from './kanban'; export * from './kanban';
export * from './list';
export * from './markdown'; export * from './markdown';
export * from './menu'; export * from './menu';
export * from './page'; export * from './page';

View File

@ -0,0 +1,61 @@
import { createForm } from '@formily/core';
import { FormContext, useField } from '@formily/react';
import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { FormLayout } from '@formily/antd';
import { useAssociationNames } from '../../../block-provider/hooks';
import { BlockProvider, useBlockRequestContext } from '../../../block-provider';
export const ListBlockContext = createContext<any>({});
const InternalListBlockProvider = (props) => {
const { resource, service } = useBlockRequestContext();
const field = useField();
const form = useMemo(() => {
return createForm({
readPretty: true,
});
}, []);
useEffect(() => {
if (!service?.loading) {
form.setValuesIn(field.address.concat('list').toString(), service?.data?.data);
}
}, [service?.data?.data, service?.loading]);
return (
<ListBlockContext.Provider
value={{
service,
resource,
}}
>
<FormContext.Provider value={form}>
<FormLayout layout={'vertical'}>{props.children}</FormLayout>
</FormContext.Provider>
</ListBlockContext.Provider>
);
};
export const ListBlockProvider = (props) => {
const params = { ...props.params };
const { collection } = props;
const { appends } = useAssociationNames(collection);
if (!Object.keys(params).includes('appends')) {
params['appends'] = appends;
}
return (
<BlockProvider {...props} params={params}>
<InternalListBlockProvider {...props} />
</BlockProvider>
);
};
export const useListBlockContext = () => {
return useContext(ListBlockContext);
};
export const useListItemProps = () => {
return {};
};

View File

@ -0,0 +1,192 @@
import { useFieldSchema, useField, ISchema } from '@formily/react';
import React from 'react';
import { ArrayItems } from '@formily/antd';
import { useListBlockContext } from './List.Decorator';
import _ from 'lodash';
import { useTranslation } from 'react-i18next';
import { useCollection, useCollectionFilterOptions, useSortFields } from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates';
import { useDesignable } from '../../hooks';
import { removeNullCondition } from '../filter';
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
export const ListDesigner = () => {
const { name, title } = useCollection();
const template = useSchemaTemplate();
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const field = useField();
const dataSource = useCollectionFilterOptions(name);
const { service } = useListBlockContext();
const { dn } = useDesignable();
const sortFields = useSortFields(name);
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
const sort = defaultSort?.map((item: string) => {
return item.startsWith('-')
? {
field: item.substring(1),
direction: 'desc',
}
: {
field: item,
direction: 'asc',
};
});
return (
<GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettings.BlockTitleItem />
<SchemaSettings.ModalItem
title={t('Set the data scope')}
schema={
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
default: defaultFilter,
// title: '数据范围',
enum: dataSource,
'x-component': 'Filter',
'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
},
},
},
} as ISchema
}
onSubmit={({ filter }) => {
filter = removeNullCondition(filter);
_.set(fieldSchema, 'x-decorator-props.params.filter', filter);
field.decoratorProps.params = { ...fieldSchema['x-decorator-props'].params, page: 1 };
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
}}
/>
<SchemaSettings.ModalItem
title={t('Set default sorting rules')}
components={{ ArrayItems }}
schema={
{
type: 'object',
title: t('Set default sorting rules'),
properties: {
sort: {
type: 'array',
default: sort,
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
properties: {
sort: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.SortHandle',
},
field: {
type: 'string',
enum: sortFields,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
style: {
width: 260,
},
},
},
direction: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-component-props': {
optionType: 'button',
},
enum: [
{
label: t('ASC'),
value: 'asc',
},
{
label: t('DESC'),
value: 'desc',
},
],
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: t('Add sort field'),
'x-component': 'ArrayItems.Addition',
},
},
},
},
} as ISchema
}
onSubmit={({ sort }) => {
const sortArr = sort.map((item) => {
return item.direction === 'desc' ? `-${item.field}` : item.field;
});
_.set(fieldSchema, 'x-decorator-props.params.sort', sortArr);
field.decoratorProps.params = { ...fieldSchema['x-decorator-props'].params, page: 1 };
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
}}
/>
<SchemaSettings.SelectItem
title={t('Records per page')}
value={field.decoratorProps?.params?.pageSize || 20}
options={[
{ label: '10', value: 10 },
{ label: '20', value: 20 },
{ label: '50', value: 50 },
{ label: '80', value: 80 },
{ label: '100', value: 100 },
]}
onChange={(pageSize) => {
_.set(fieldSchema, 'x-decorator-props.params.pageSize', pageSize);
field.decoratorProps.params = { ...fieldSchema['x-decorator-props'].params, page: 1 };
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
}}
/>
<SchemaSettings.Template componentName={'List'} collectionName={name} resourceName={defaultResource} />
<SchemaSettings.Divider />
<SchemaSettings.Remove
removeParentsIfNoChildren
breakRemoveOn={{
'x-component': 'Grid',
}}
/>
</GeneralSchemaDesigner>
);
};

View File

@ -0,0 +1,23 @@
import React from 'react';
import { css } from '@emotion/css';
import { useField } from '@formily/react';
import { ObjectField } from '@formily/core';
import { RecordSimpleProvider } from '../../../record-provider';
const itemCss = css`
display: flex;
width: 100%;
flex-direction: column;
gap: 8px;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
`;
export const ListItem = (props) => {
const field = useField<ObjectField>();
return (
<div className={itemCss}>
<RecordSimpleProvider value={field.value}>{props.children}</RecordSimpleProvider>
</div>
);
};

View File

@ -0,0 +1,135 @@
import { ListDesigner } from './List.Designer';
import { ListBlockProvider, useListBlockContext, useListItemProps } from './List.Decorator';
import React, { useCallback, useEffect, useState } from 'react';
import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
import { css, cx } from '@emotion/css';
import { List as AntdList, PaginationProps } from 'antd';
import { useListActionBarProps } from './hooks';
import { SortableItem } from '../../common';
import { SchemaComponentOptions } from '../../core';
import { useDesigner } from '../../hooks';
import { ListItem } from './List.Item';
import { ArrayField } from '@formily/core';
const designerCss = css`
width: 100%;
margin-bottom: var(--nb-spacing);
&:hover {
> .general-schema-designer {
display: block;
}
}
> .general-schema-designer {
position: absolute;
z-index: 999;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: none;
background: rgba(241, 139, 98, 0.06);
border: 0;
pointer-events: none;
> .general-schema-designer-icons {
position: absolute;
right: 2px;
top: 2px;
line-height: 16px;
pointer-events: all;
.ant-space-item {
background-color: #f18b62;
color: #fff;
line-height: 16px;
width: 16px;
padding-left: 1px;
}
}
}
`;
const InternalList = (props) => {
const { service } = useListBlockContext();
const { run, params } = service;
const fieldSchema = useFieldSchema();
const Designer = useDesigner();
const meta = service?.data?.meta;
const field = useField<ArrayField>();
const [schemaMap] = useState(new Map());
const getSchema = useCallback(
(key) => {
if (!schemaMap.has(key)) {
schemaMap.set(
key,
new Schema({
type: 'object',
properties: {
[key]: fieldSchema.properties['item'],
},
}),
);
}
return schemaMap.get(key);
},
[fieldSchema.properties, schemaMap],
);
const onPaginationChange: PaginationProps['onChange'] = useCallback(
(page, pageSize) => {
run({
...params?.[0],
page: page,
pageSize: pageSize,
});
},
[run, params],
);
return (
<SchemaComponentOptions
scope={{
useListItemProps,
useListActionBarProps,
}}
>
<SortableItem className={cx('nb-list', designerCss)}>
<AntdList
pagination={
!meta || meta.count <= meta.pageSize
? false
: {
onChange: onPaginationChange,
total: meta?.count || 0,
pageSize: meta?.pageSize || 10,
current: meta?.page || 1,
}
}
loading={service?.loading}
>
{field.value?.map((item, index) => {
return (
<RecursionField
basePath={field.address}
key={index}
name={index}
onlyRenderProperties
schema={getSchema(index)}
></RecursionField>
);
})}
</AntdList>
<Designer />
</SortableItem>
</SchemaComponentOptions>
);
};
export const List = InternalList as typeof InternalList & {
Item: typeof ListItem;
Designer: typeof ListDesigner;
Decorator: typeof ListBlockProvider;
};
List.Item = ListItem;
List.Designer = ListDesigner;
List.Decorator = ListBlockProvider;

View File

@ -0,0 +1,12 @@
import { SpaceProps } from 'antd';
const spaceProps: SpaceProps = {
size: ['large', 'small'],
wrap: true,
};
export const useListActionBarProps = () => {
return {
spaceProps,
};
};

View File

@ -0,0 +1 @@
export * from './List';

View File

@ -1,7 +1,7 @@
import { useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import { isPlainObj } from '@formily/shared'; import { isPlainObj } from '@formily/shared';
import get from 'lodash/get'; import get from 'lodash/get';
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
import { SchemaComponentOptions } from '../schema-component'; import { SchemaComponentOptions } from '../schema-component';
import { SchemaInitializer } from './SchemaInitializer'; import { SchemaInitializer } from './SchemaInitializer';
import * as globals from './buttons'; import * as globals from './buttons';
@ -22,20 +22,32 @@ export const useSchemaInitializer = (name: string, props = {}) => {
return component && React.createElement(component, props); return component && React.createElement(component, props);
}; };
if (!name) { const initializerPropsRef = useRef({});
return { exists: false, render: (props?: any) => render(null) }; const initializer = name ? get(initializers, name || fieldSchema?.['x-initializer']) : null;
}
const initializer = get(initializers, name || fieldSchema?.['x-initializer']);
const initializerProps = { ...props, ...fieldSchema?.['x-initializer-props'] }; const initializerProps = { ...props, ...fieldSchema?.['x-initializer-props'] };
initializerPropsRef.current = initializerProps;
const InitializerComponent = useMemo(() => {
let C = React.Fragment;
if (!initializer) {
return C;
} else if (isPlainObj(initializer)) {
C = (initializer as any).component || SchemaInitializer.Button;
return (p) => <C {...initializer} {...initializerPropsRef.current} {...p} />;
} else {
C = initializer;
return (p) => <C {...initializerPropsRef.current} {...p} />;
}
}, [initializer]);
if (!initializer) { if (!initializer) {
return { exists: false, render: (props?: any) => render(null) }; return { exists: false, InitializerComponent, render: (props?: any) => render(null) };
} }
if (isPlainObj(initializer)) { if (isPlainObj(initializer)) {
return { return {
exists: true, exists: true,
InitializerComponent,
render: (props?: any) => { render: (props?: any) => {
const component = (initializer as any).component || SchemaInitializer.Button; const component = (initializer as any).component || SchemaInitializer.Button;
return render(component, { ...initializer, ...initializerProps, ...props }); return render(component, { ...initializer, ...initializerProps, ...props });
@ -45,6 +57,7 @@ export const useSchemaInitializer = (name: string, props = {}) => {
return { return {
exists: true, exists: true,
InitializerComponent,
render: (props?: any) => render(initializer, { ...initializerProps, ...props }), render: (props?: any) => render(initializer, { ...initializerProps, ...props }),
}; };
}; };

View File

@ -18,6 +18,18 @@ export const BlockInitializers = {
title: '{{t("Table")}}', title: '{{t("Table")}}',
component: 'TableBlockInitializer', component: 'TableBlockInitializer',
}, },
{
key: 'List',
type: 'item',
title: '{{t("List")}}',
component: 'ListBlockInitializer',
},
{
key: 'GridCard',
type: 'item',
title: '{{t("Grid Card")}}',
component: 'GridCardBlockInitializer',
},
{ {
key: 'form', key: 'form',
type: 'item', type: 'item',

View File

@ -0,0 +1,299 @@
import { useFieldSchema, Schema } from '@formily/react';
import { useCollection } from '../../collection-manager';
// 表单的操作配置
export const GridCardActionInitializers = {
title: "{{t('Configure actions')}}",
icon: 'SettingOutlined',
style: {
marginLeft: 8,
},
items: [
{
type: 'itemGroup',
title: "{{t('Enable actions')}}",
children: [
{
type: 'item',
title: "{{t('Filter')}}",
component: 'FilterActionInitializer',
schema: {
'x-align': 'left',
},
},
{
type: 'item',
title: "{{t('Add new')}}",
component: 'CreateActionInitializer',
schema: {
'x-align': 'right',
'x-decorator': 'ACLActionProvider',
'x-acl-action-props': {
skipScopeCheck: true,
},
},
visible: () => {
const collection = useCollection();
return collection.template !== 'view' && collection.template !== 'file';
},
},
{
type: 'item',
title: "{{t('Refresh')}}",
component: 'RefreshActionInitializer',
schema: {
'x-align': 'right',
},
},
{
type: 'item',
title: "{{t('Import')}}",
component: 'ImportActionInitializer',
schema: {
'x-align': 'right',
'x-decorator': 'ACLActionProvider',
'x-acl-action-props': {
skipScopeCheck: true,
},
},
},
{
type: 'item',
title: "{{t('Export')}}",
component: 'ExportActionInitializer',
schema: {
'x-align': 'right',
'x-decorator': 'ACLActionProvider',
'x-acl-action-props': {
skipScopeCheck: true,
},
},
},
],
},
// {
// type: 'divider',
// visible: () => {
// const collection = useCollection();
// return (collection as any).template !== 'view';
// },
// },
// {
// type: 'subMenu',
// title: '{{t("Customize")}}',
// children: [
// {
// type: 'item',
// title: '{{t("Bulk update")}}',
// component: 'CustomizeActionInitializer',
// schema: {
// type: 'void',
// title: '{{ t("Bulk update") }}',
// 'x-component': 'Action',
// 'x-align': 'right',
// 'x-acl-action': 'update',
// 'x-decorator': 'ACLActionProvider',
// 'x-acl-action-props': {
// skipScopeCheck: true,
// },
// 'x-action': 'customize:bulkUpdate',
// 'x-designer': 'Action.Designer',
// 'x-action-settings': {
// assignedValues: {},
// updateMode: 'selected',
// onSuccess: {
// manualClose: true,
// redirecting: false,
// successMessage: '{{t("Updated successfully")}}',
// },
// },
// 'x-component-props': {
// icon: 'EditOutlined',
// useProps: '{{ useCustomizeBulkUpdateActionProps }}',
// },
// },
// },
// {
// type: 'item',
// title: '{{t("Bulk edit")}}',
// component: 'CustomizeBulkEditActionInitializer',
// schema: {
// 'x-align': 'right',
// 'x-decorator': 'ACLActionProvider',
// 'x-acl-action': 'update',
// 'x-acl-action-props': {
// skipScopeCheck: true,
// },
// },
// },
// ],
// visible: () => {
// const collection = useCollection();
// return (collection as any).template !== 'view';
// },
// },
],
};
export const GridCardItemActionInitializers = {
title: '{{t("Configure actions")}}',
icon: 'SettingOutlined',
items: [
{
type: 'itemGroup',
title: '{{t("Enable actions")}}',
children: [
{
type: 'item',
title: '{{t("View")}}',
component: 'ViewActionInitializer',
schema: {
'x-component': 'Action.Link',
'x-action': 'view',
'x-decorator': 'ACLActionProvider',
'x-align': 'left',
},
},
{
type: 'item',
title: '{{t("Edit")}}',
component: 'UpdateActionInitializer',
schema: {
'x-component': 'Action.Link',
'x-action': 'update',
'x-decorator': 'ACLActionProvider',
'x-align': 'left',
},
visible: () => {
const collection = useCollection();
return (collection as any).template !== 'view';
},
},
{
type: 'item',
title: '{{t("Delete")}}',
component: 'DestroyActionInitializer',
schema: {
'x-component': 'Action.Link',
'x-action': 'destroy',
'x-decorator': 'ACLActionProvider',
'x-align': 'left',
},
},
],
},
{
type: 'divider',
},
{
type: 'subMenu',
title: '{{t("Customize")}}',
children: [
{
type: 'item',
title: '{{t("Popup")}}',
component: 'CustomizeActionInitializer',
schema: {
type: 'void',
title: '{{ t("Popup") }}',
'x-action': 'customize:popup',
'x-designer': 'Action.Designer',
'x-component': 'Action.Link',
'x-component-props': {
openMode: 'drawer',
},
properties: {
drawer: {
type: 'void',
title: '{{ t("Popup") }}',
'x-component': 'Action.Container',
'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: {},
},
},
},
},
},
},
},
},
},
},
{
type: 'item',
title: '{{t("Update record")}}',
component: 'CustomizeActionInitializer',
schema: {
title: '{{ t("Update record") }}',
'x-component': 'Action.Link',
'x-action': 'customize:update',
'x-decorator': 'ACLActionProvider',
'x-acl-action': 'update',
'x-designer': 'Action.Designer',
'x-action-settings': {
assignedValues: {},
onSuccess: {
manualClose: true,
redirecting: false,
successMessage: '{{t("Updated successfully")}}',
},
},
'x-component-props': {
useProps: '{{ useCustomizeUpdateActionProps }}',
},
},
visible: () => {
const collection = useCollection();
return (collection as any).template !== 'view';
},
},
{
type: 'item',
title: '{{t("Custom request")}}',
component: 'CustomizeActionInitializer',
schema: {
title: '{{ t("Custom request") }}',
'x-component': 'Action.Link',
'x-action': 'customize:table:request',
'x-designer': 'Action.Designer',
'x-action-settings': {
requestSettings: {},
onSuccess: {
manualClose: false,
redirecting: false,
successMessage: '{{t("Request success")}}',
},
},
'x-component-props': {
useProps: '{{ useCustomizeRequestActionProps }}',
},
},
visible: () => {
const collection = useCollection();
return (collection as any).template !== 'view';
},
},
],
},
],
};

View File

@ -0,0 +1,299 @@
import { useFieldSchema, Schema } from '@formily/react';
import { useCollection } from '../../collection-manager';
// 表单的操作配置
export const ListActionInitializers = {
title: "{{t('Configure actions')}}",
icon: 'SettingOutlined',
style: {
marginLeft: 8,
},
items: [
{
type: 'itemGroup',
title: "{{t('Enable actions')}}",
children: [
{
type: 'item',
title: "{{t('Filter')}}",
component: 'FilterActionInitializer',
schema: {
'x-align': 'left',
},
},
{
type: 'item',
title: "{{t('Add new')}}",
component: 'CreateActionInitializer',
schema: {
'x-align': 'right',
'x-decorator': 'ACLActionProvider',
'x-acl-action-props': {
skipScopeCheck: true,
},
},
visible: () => {
const collection = useCollection();
return collection.template !== 'view' && collection.template !== 'file';
},
},
{
type: 'item',
title: "{{t('Refresh')}}",
component: 'RefreshActionInitializer',
schema: {
'x-align': 'right',
},
},
{
type: 'item',
title: "{{t('Import')}}",
component: 'ImportActionInitializer',
schema: {
'x-align': 'right',
'x-decorator': 'ACLActionProvider',
'x-acl-action-props': {
skipScopeCheck: true,
},
},
},
{
type: 'item',
title: "{{t('Export')}}",
component: 'ExportActionInitializer',
schema: {
'x-align': 'right',
'x-decorator': 'ACLActionProvider',
'x-acl-action-props': {
skipScopeCheck: true,
},
},
},
],
},
// {
// type: 'divider',
// visible: () => {
// const collection = useCollection();
// return (collection as any).template !== 'view';
// },
// },
// {
// type: 'subMenu',
// title: '{{t("Customize")}}',
// children: [
// {
// type: 'item',
// title: '{{t("Bulk update")}}',
// component: 'CustomizeActionInitializer',
// schema: {
// type: 'void',
// title: '{{ t("Bulk update") }}',
// 'x-component': 'Action',
// 'x-align': 'right',
// 'x-acl-action': 'update',
// 'x-decorator': 'ACLActionProvider',
// 'x-acl-action-props': {
// skipScopeCheck: true,
// },
// 'x-action': 'customize:bulkUpdate',
// 'x-designer': 'Action.Designer',
// 'x-action-settings': {
// assignedValues: {},
// updateMode: 'selected',
// onSuccess: {
// manualClose: true,
// redirecting: false,
// successMessage: '{{t("Updated successfully")}}',
// },
// },
// 'x-component-props': {
// icon: 'EditOutlined',
// useProps: '{{ useCustomizeBulkUpdateActionProps }}',
// },
// },
// },
// {
// type: 'item',
// title: '{{t("Bulk edit")}}',
// component: 'CustomizeBulkEditActionInitializer',
// schema: {
// 'x-align': 'right',
// 'x-decorator': 'ACLActionProvider',
// 'x-acl-action': 'update',
// 'x-acl-action-props': {
// skipScopeCheck: true,
// },
// },
// },
// ],
// visible: () => {
// const collection = useCollection();
// return (collection as any).template !== 'view';
// },
// },
],
};
export const ListItemActionInitializers = {
title: '{{t("Configure actions")}}',
icon: 'SettingOutlined',
items: [
{
type: 'itemGroup',
title: '{{t("Enable actions")}}',
children: [
{
type: 'item',
title: '{{t("View")}}',
component: 'ViewActionInitializer',
schema: {
'x-component': 'Action.Link',
'x-action': 'view',
'x-decorator': 'ACLActionProvider',
'x-align': 'left',
},
},
{
type: 'item',
title: '{{t("Edit")}}',
component: 'UpdateActionInitializer',
schema: {
'x-component': 'Action.Link',
'x-action': 'update',
'x-decorator': 'ACLActionProvider',
'x-align': 'left',
},
visible: () => {
const collection = useCollection();
return (collection as any).template !== 'view';
},
},
{
type: 'item',
title: '{{t("Delete")}}',
component: 'DestroyActionInitializer',
schema: {
'x-component': 'Action.Link',
'x-action': 'destroy',
'x-decorator': 'ACLActionProvider',
'x-align': 'left',
},
},
],
},
{
type: 'divider',
},
{
type: 'subMenu',
title: '{{t("Customize")}}',
children: [
{
type: 'item',
title: '{{t("Popup")}}',
component: 'CustomizeActionInitializer',
schema: {
type: 'void',
title: '{{ t("Popup") }}',
'x-action': 'customize:popup',
'x-designer': 'Action.Designer',
'x-component': 'Action.Link',
'x-component-props': {
openMode: 'drawer',
},
properties: {
drawer: {
type: 'void',
title: '{{ t("Popup") }}',
'x-component': 'Action.Container',
'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: {},
},
},
},
},
},
},
},
},
},
},
{
type: 'item',
title: '{{t("Update record")}}',
component: 'CustomizeActionInitializer',
schema: {
title: '{{ t("Update record") }}',
'x-component': 'Action.Link',
'x-action': 'customize:update',
'x-decorator': 'ACLActionProvider',
'x-acl-action': 'update',
'x-designer': 'Action.Designer',
'x-action-settings': {
assignedValues: {},
onSuccess: {
manualClose: true,
redirecting: false,
successMessage: '{{t("Updated successfully")}}',
},
},
'x-component-props': {
useProps: '{{ useCustomizeUpdateActionProps }}',
},
},
visible: () => {
const collection = useCollection();
return (collection as any).template !== 'view';
},
},
{
type: 'item',
title: '{{t("Custom request")}}',
component: 'CustomizeActionInitializer',
schema: {
title: '{{ t("Custom request") }}',
'x-component': 'Action.Link',
'x-action': 'customize:table:request',
'x-designer': 'Action.Designer',
'x-action-settings': {
requestSettings: {},
onSuccess: {
manualClose: false,
redirecting: false,
successMessage: '{{t("Request success")}}',
},
},
'x-component-props': {
useProps: '{{ useCustomizeRequestActionProps }}',
},
},
visible: () => {
const collection = useCollection();
return (collection as any).template !== 'view';
},
},
],
},
],
};

View File

@ -21,5 +21,8 @@ export * from './TableColumnInitializers';
export * from './TableSelectorInitializers'; export * from './TableSelectorInitializers';
export * from './TabPaneInitializers'; export * from './TabPaneInitializers';
export * from './GanttActionInitializers'; export * from './GanttActionInitializers';
export * from './DetailsActionInitializers';
export * from './ListActionInitializers';
export * from './GridCardActionInitializers';
// association filter // association filter
export * from '../../schema-component/antd/association-filter/AssociationFilter'; export * from '../../schema-component/antd/association-filter/AssociationFilter';

View File

@ -0,0 +1,25 @@
import React from 'react';
import { OrderedListOutlined } from '@ant-design/icons';
import { createGridCardBlockSchema, createListBlockSchema } from '../utils';
import { DataBlockInitializer } from './DataBlockInitializer';
import { useCollectionManager } from '../../collection-manager';
export const GridCardBlockInitializer = (props) => {
const { insert } = props;
const { getCollection } = useCollectionManager();
return (
<DataBlockInitializer
{...props}
icon={<OrderedListOutlined />}
componentType={'GridCard'}
onCreateBlockSchema={async ({ item }) => {
const collection = getCollection(item.name);
const schema = createGridCardBlockSchema({
collection: item.name,
rowKey: collection.filterTargetKey || 'id',
});
insert(schema);
}}
/>
);
};

View File

@ -3,6 +3,8 @@ import React from 'react';
import { SchemaInitializer } from '..'; import { SchemaInitializer } from '..';
import { useCurrentSchema } from '../utils'; import { useCurrentSchema } from '../utils';
import { useBlockRequestContext } from '../../block-provider';
import { useCollection } from '../../collection-manager';
export const InitializerWithSwitch = (props) => { export const InitializerWithSwitch = (props) => {
const { type, schema, item, insert, remove: passInRemove } = props; const { type, schema, item, insert, remove: passInRemove } = props;

View File

@ -0,0 +1,25 @@
import React from 'react';
import { OrderedListOutlined } from '@ant-design/icons';
import { createListBlockSchema } from '../utils';
import { DataBlockInitializer } from './DataBlockInitializer';
import { useCollectionManager } from '../../collection-manager';
export const ListBlockInitializer = (props) => {
const { insert } = props;
const { getCollection } = useCollectionManager();
return (
<DataBlockInitializer
{...props}
icon={<OrderedListOutlined />}
componentType={'List'}
onCreateBlockSchema={async ({ item }) => {
const collection = getCollection(item.name);
const schema = createListBlockSchema({
collection: item.name,
rowKey: collection.filterTargetKey || 'id',
});
insert(schema);
}}
/>
);
};

View File

@ -29,8 +29,10 @@ export * from './FilterFormBlockInitializer';
export * from './FormBlockInitializer'; export * from './FormBlockInitializer';
export * from './G2PlotInitializer'; export * from './G2PlotInitializer';
export * from './GanttBlockInitializer'; export * from './GanttBlockInitializer';
export * from './GridCardBlockInitializer';
export * from './InitializerWithSwitch'; export * from './InitializerWithSwitch';
export * from './KanbanBlockInitializer'; export * from './KanbanBlockInitializer';
export * from './ListBlockInitializer';
export * from './MarkdownBlockInitializer'; export * from './MarkdownBlockInitializer';
export * from './PrintActionInitializer'; export * from './PrintActionInitializer';
export * from './RecordAssociationBlockInitializer'; export * from './RecordAssociationBlockInitializer';
@ -41,6 +43,7 @@ export * from './RecordFormBlockInitializer';
export * from './RecordReadPrettyAssociationFormBlockInitializer'; export * from './RecordReadPrettyAssociationFormBlockInitializer';
export * from './RecordReadPrettyFormBlockInitializer'; export * from './RecordReadPrettyFormBlockInitializer';
export * from './RefreshActionInitializer'; export * from './RefreshActionInitializer';
export * from './SelectActionInitializer';
export * from './SubmitActionInitializer'; export * from './SubmitActionInitializer';
export * from './TableActionColumnInitializer'; export * from './TableActionColumnInitializer';
export * from './TableBlockInitializer'; export * from './TableBlockInitializer';
@ -49,4 +52,3 @@ export * from './TableSelectorInitializer';
export * from './UpdateActionInitializer'; export * from './UpdateActionInitializer';
export * from './UpdateSubmitActionInitializer'; export * from './UpdateSubmitActionInitializer';
export * from './ViewActionInitializer'; export * from './ViewActionInitializer';
export * from './SelectActionInitializer';

View File

@ -950,6 +950,177 @@ export const createDetailsBlockSchema = (options) => {
return schema; return schema;
}; };
export const createListBlockSchema = (options) => {
const {
formItemInitializers = 'ReadPrettyFormItemInitializers',
actionInitializers = 'ListActionInitializers',
itemActionInitializers = 'ListItemActionInitializers',
collection,
association,
resource,
template,
...others
} = options;
const resourceName = resource || association || collection;
const schema: ISchema = {
type: 'void',
'x-acl-action': `${resourceName}:view`,
'x-decorator': 'List.Decorator',
'x-decorator-props': {
resource: resourceName,
collection,
association,
readPretty: true,
action: 'list',
params: {
pageSize: 10,
},
runWhenParamsChanged: true,
...others,
},
'x-component': 'CardItem',
'x-designer': 'List.Designer',
properties: {
actionBar: {
type: 'void',
'x-initializer': actionInitializers,
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--nb-spacing)',
},
},
properties: {},
},
list: {
type: 'array',
'x-component': 'List',
'x-component-props': {
props: '{{ useListBlockProps }}',
},
properties: {
item: {
type: 'object',
'x-component': 'List.Item',
'x-read-pretty': true,
'x-component-props': {
useProps: '{{ useListItemProps }}',
},
properties: {
grid: template || {
type: 'void',
'x-component': 'Grid',
'x-initializer': formItemInitializers,
'x-initializer-props': {
useProps: '{{ useListItemInitializerProps }}',
},
properties: {},
},
actionBar: {
type: 'void',
'x-align': 'left',
'x-initializer': itemActionInitializers,
'x-component': 'ActionBar',
'x-component-props': {
useProps: '{{ useListActionBarProps }}',
layout: 'one-column',
},
properties: {},
},
},
},
},
},
},
};
return schema;
};
export const createGridCardBlockSchema = (options) => {
const {
formItemInitializers = 'ReadPrettyFormItemInitializers',
actionInitializers = 'GridCardActionInitializers',
itemActionInitializers = 'GridCardItemActionInitializers',
collection,
association,
resource,
template,
...others
} = options;
const resourceName = resource || association || collection;
const schema: ISchema = {
type: 'void',
'x-acl-action': `${resourceName}:view`,
'x-decorator': 'GridCard.Decorator',
'x-decorator-props': {
resource: resourceName,
collection,
association,
readPretty: true,
action: 'list',
params: {
pageSize: 12,
},
runWhenParamsChanged: true,
...others,
},
'x-component': 'BlockItem',
'x-designer': 'GridCard.Designer',
properties: {
actionBar: {
type: 'void',
'x-initializer': actionInitializers,
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--nb-spacing)',
},
},
properties: {},
},
list: {
type: 'array',
'x-component': 'GridCard',
'x-component-props': {
props: '{{ useGridCardBlockProps }}',
},
properties: {
item: {
type: 'object',
'x-component': 'GridCard.Item',
'x-read-pretty': true,
'x-component-props': {
useProps: '{{ useGridCardItemProps }}',
},
properties: {
grid: template || {
type: 'void',
'x-component': 'Grid',
'x-initializer': formItemInitializers,
'x-initializer-props': {
useProps: '{{ useGridCardItemInitializerProps }}',
},
properties: {},
},
actionBar: {
type: 'void',
'x-align': 'left',
'x-initializer': itemActionInitializers,
'x-component': 'ActionBar',
'x-component-props': {
useProps: '{{ useGridCardActionBarProps }}',
layout: 'one-column',
},
properties: {},
},
},
},
},
},
},
};
return schema;
};
export const createFormBlockSchema = (options) => { export const createFormBlockSchema = (options) => {
const { const {
formItemInitializers = 'FormItemInitializers', formItemInitializers = 'FormItemInitializers',
@ -1177,7 +1348,7 @@ export const createTableBlockSchema = (options) => {
'x-component': 'ActionBar', 'x-component': 'ActionBar',
'x-component-props': { 'x-component-props': {
style: { style: {
marginBottom: 16, marginBottom: 'var(--nb-spacing)',
}, },
}, },
properties: {}, properties: {},
@ -1276,7 +1447,7 @@ export const createTableSelectorSchema = (options) => {
'x-component': 'ActionBar', 'x-component': 'ActionBar',
'x-component-props': { 'x-component-props': {
style: { style: {
marginBottom: 16, marginBottom: 'var(--nb-spacing)',
}, },
}, },
properties: {}, properties: {},
@ -1546,7 +1717,7 @@ export const createKanbanBlockSchema = (options) => {
'x-component': 'ActionBar', 'x-component': 'ActionBar',
'x-component-props': { 'x-component-props': {
style: { style: {
marginBottom: 16, marginBottom: 'var(--nb-spacing)',
}, },
}, },
properties: {}, properties: {},

View File

@ -0,0 +1,161 @@
import { Field } from '@formily/core';
import { ISchema, useField, useFieldSchema } from '@formily/react';
import _ from 'lodash';
import { useTranslation } from 'react-i18next';
import React from 'react';
import { useCollectionManager, useCollection } from '../collection-manager';
import { useDesignable } from '../schema-component';
import { SchemaSettings } from '../schema-settings';
export const GeneralSchemaItems: React.FC<{
required?: boolean;
}> = (props) => {
const { required = true } = props;
const { getCollectionJoinField } = useCollectionManager();
const { getField } = useCollection();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const { dn, refresh } = useDesignable();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
return (
<>
{collectionField && (
<SchemaSettings.ModalItem
key="edit-field-title"
title={t('Edit field title')}
schema={
{
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',
'x-component-props': {},
},
},
} 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();
}}
/>
)}
<SchemaSettings.SwitchItem
checked={field.decoratorProps.showTitle ?? true}
title={t('Display title')}
onChange={(checked) => {
field.decoratorProps.showTitle = checked;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-decorator-props': {
...fieldSchema['x-decorator-props'],
showTitle: checked,
},
},
});
dn.refresh();
}}
></SchemaSettings.SwitchItem>
{!field.readPretty && (
<SchemaSettings.ModalItem
key="edit-description"
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 && (
<SchemaSettings.ModalItem
key="edit-tooltip"
title={t('Edit tooltip')}
schema={
{
type: 'object',
title: t('Edit description'),
properties: {
tooltip: {
default: fieldSchema?.['x-decorator-props']?.tooltip,
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ tooltip }) => {
field.decoratorProps.tooltip = tooltip;
fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};
fieldSchema['x-decorator-props']['tooltip'] = tooltip;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
dn.refresh();
}}
/>
)}
{!field.readPretty && fieldSchema['x-component'] !== 'FormField' && required && (
<SchemaSettings.SwitchItem
key="required"
title={t('Required')}
checked={fieldSchema.required as boolean}
onChange={(required) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
field.required = required;
fieldSchema['required'] = required;
schema['required'] = required;
dn.emit('patch', {
schema,
});
refresh();
}}
/>
)}
</>
);
};

View File

@ -1 +1,2 @@
export * from './OpenModeSchemaItems'; export * from './OpenModeSchemaItems';
export * from './GeneralSchemaItems';

View File

@ -3,7 +3,7 @@ import { css } from '@emotion/css';
import { useField, useFieldSchema } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import { Space } from 'antd'; import { Space } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DragHandler, useCompile, useDesignable, useGridContext, useGridRowContext } from '../schema-component'; import { DragHandler, useCompile, useDesignable, useGridContext, useGridRowContext } from '../schema-component';
import { gridRowColWrap } from '../schema-initializer/utils'; import { gridRowColWrap } from '../schema-initializer/utils';
@ -43,14 +43,24 @@ export const GeneralSchemaDesigner = (props: any) => {
field, field,
fieldSchema, fieldSchema,
}; };
if (!designable) {
return null;
}
const rowCtx = useGridRowContext(); const rowCtx = useGridRowContext();
const ctx = useGridContext(); const ctx = useGridContext();
const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName) const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName)
? `${template?.name} ${t('(Fields only)')}` ? `${template?.name} ${t('(Fields only)')}`
: template?.name; : template?.name;
const initializerProps = useMemo(() => {
return {
insertPosition: 'afterEnd',
wrap: rowCtx?.cols?.length > 1 ? undefined : gridRowColWrap,
component: <PlusOutlined style={{ cursor: 'pointer', fontSize: 14 }} />,
};
}, [rowCtx?.cols?.length]);
if (!designable) {
return null;
}
return ( return (
<div className={'general-schema-designer'}> <div className={'general-schema-designer'}>
{title && ( {title && (
@ -73,11 +83,11 @@ export const GeneralSchemaDesigner = (props: any) => {
</DragHandler> </DragHandler>
)} )}
{!disableInitializer && {!disableInitializer &&
ctx?.renderSchemaInitializer?.({ (ctx?.InitializerComponent ? (
insertPosition: 'afterEnd', <ctx.InitializerComponent {...initializerProps} />
wrap: rowCtx?.cols?.length > 1 ? undefined : gridRowColWrap, ) : (
component: <PlusOutlined style={{ cursor: 'pointer', fontSize: 14 }} />, ctx?.renderSchemaInitializer?.(initializerProps)
})} ))}
<SchemaSettings title={<MenuOutlined style={{ cursor: 'pointer', fontSize: 12 }} />} {...schemaSettingsProps}> <SchemaSettings title={<MenuOutlined style={{ cursor: 'pointer', fontSize: 12 }} />} {...schemaSettingsProps}>
{props.children} {props.children}
</SchemaSettings> </SchemaSettings>

View File

@ -2,6 +2,7 @@ import { Field } from '@formily/core';
import { ISchema, useField, useFieldSchema } from '@formily/react'; import { ISchema, useField, useFieldSchema } from '@formily/react';
import { import {
GeneralSchemaDesigner, GeneralSchemaDesigner,
GeneralSchemaItems,
SchemaSettings, SchemaSettings,
isPatternDisabled, isPatternDisabled,
useCollection, useCollection,
@ -40,122 +41,7 @@ const Designer = () => {
return ( return (
<GeneralSchemaDesigner> <GeneralSchemaDesigner>
<SchemaSettings.ModalItem <GeneralSchemaItems />
key="edit-field-title"
title={t('Edit field title')}
schema={
{
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',
'x-component-props': {},
},
},
} 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
key="edit-description"
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 && (
<SchemaSettings.ModalItem
key="edit-tooltip"
title={t('Edit tooltip')}
schema={
{
type: 'object',
title: t('Edit description'),
properties: {
tooltip: {
default: fieldSchema?.['x-decorator-props']?.tooltip,
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ tooltip }) => {
field.decoratorProps.tooltip = tooltip;
fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};
fieldSchema['x-decorator-props']['tooltip'] = tooltip;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
dn.refresh();
}}
/>
)}
{!field.readPretty && (
<SchemaSettings.SwitchItem
key="required"
title={t('Required')}
checked={fieldSchema.required as boolean}
onChange={(required) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
field.required = required;
fieldSchema['required'] = required;
schema['required'] = required;
dn.emit('patch', {
schema,
});
refresh();
}}
/>
)}
{form && !form?.readPretty && !isPatternDisabled(fieldSchema) && ( {form && !form?.readPretty && !isPatternDisabled(fieldSchema) && (
<SchemaSettings.SelectItem <SchemaSettings.SelectItem
key="pattern" key="pattern"