feat: support fixed block (#1267)

* feat: support fixed block

* feat: update locale

* fix: fix block not work in non-designer

* feat: improve padding

* feat: update scroll

* fix: the page effect is not normal when deleting fixed blocks

* feat: recalculate table scroll when resize

* fix: avoid scrolling effect when dragging the Kanban column

* feat: improve scroll size

* fix: column size

* fix: unused

* fix: configure action in designable

* fix: has page title

* fix: optimize

* fix: optimize

* feat: avoid fixed block

* fix: action column width

* fix: optimize

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
Dunqing 2022-12-20 21:43:27 +08:00 committed by GitHub
parent 217ecb27ae
commit c731abf82c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 451 additions and 129 deletions

View File

@ -5,6 +5,7 @@ import uniq from 'lodash/uniq';
import React, { createContext, useContext, useEffect } from 'react'; import React, { createContext, useContext, useEffect } from 'react';
import { useACLRoleContext } from '../acl'; import { useACLRoleContext } from '../acl';
import { useCollection, useCollectionManager } from '../collection-manager'; import { useCollection, useCollectionManager } from '../collection-manager';
import { useFixedSchema } from '../schema-component';
import { toColumns } from '../schema-component/antd/kanban/Kanban'; import { toColumns } from '../schema-component/antd/kanban/Kanban';
import { BlockProvider, useBlockRequestContext } from './BlockProvider'; import { BlockProvider, useBlockRequestContext } from './BlockProvider';
@ -23,6 +24,8 @@ const useGroupField = (props) => {
const InternalKanbanBlockProvider = (props) => { const InternalKanbanBlockProvider = (props) => {
const field = useField<any>(); const field = useField<any>();
const fieldSchema = useFieldSchema();
useFixedSchema();
const { resource, service } = useBlockRequestContext(); const { resource, service } = useBlockRequestContext();
const groupField = useGroupField(props); const groupField = useGroupField(props);
if (!groupField) { if (!groupField) {
@ -42,6 +45,7 @@ const InternalKanbanBlockProvider = (props) => {
service, service,
resource, resource,
groupField, groupField,
fixedBlock: fieldSchema?.['x-decorator-props']?.fixedBlock,
}} }}
> >
{props.children} {props.children}

View File

@ -3,6 +3,7 @@ import { FormContext, Schema, useField, useFieldSchema } from '@formily/react';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import React, { createContext, useContext, useEffect, useMemo } from 'react'; import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { useCollectionManager } from '../collection-manager'; import { useCollectionManager } from '../collection-manager';
import { useFixedSchema } from '../schema-component';
import { BlockProvider, useBlockRequestContext } from './BlockProvider'; import { BlockProvider, useBlockRequestContext } from './BlockProvider';
export const TableBlockContext = createContext<any>({}); export const TableBlockContext = createContext<any>({});
@ -14,6 +15,7 @@ const InternalTableBlockProvider = (props) => {
// if (service.loading) { // if (service.loading) {
// return <Spin />; // return <Spin />;
// } // }
useFixedSchema();
return ( return (
<TableBlockContext.Provider <TableBlockContext.Provider
value={{ value={{

View File

@ -1,3 +1,4 @@
import { useField } from '@formily/react';
import React, { forwardRef, useState } from 'react'; import React, { forwardRef, useState } from 'react';
import { DragDropContext } from 'react-beautiful-dnd'; import { DragDropContext } from 'react-beautiful-dnd';
import Column from './Column'; import Column from './Column';
@ -16,7 +17,9 @@ import {
import { partialRight, when } from './utils'; import { partialRight, when } from './utils';
import withDroppable from './withDroppable'; import withDroppable from './withDroppable';
const Columns = forwardRef((props, ref: any) => <div ref={ref} style={{ whiteSpace: 'nowrap' }} {...props} />); const Columns = forwardRef((props, ref: any) => (
<div ref={ref} style={{ whiteSpace: 'nowrap', height: '100%', overflowY: 'hidden' }} {...props} />
));
const DroppableBoard = withDroppable(Columns); const DroppableBoard = withDroppable(Columns);
@ -241,6 +244,7 @@ function BoardContainer(props) {
onCardNew, onCardNew,
allowAddCard, allowAddCard,
} = props; } = props;
function handleOnDragEnd(event) { function handleOnDragEnd(event) {
const coordinates = getCoordinates(event, board); const coordinates = getCoordinates(event, board);
if (!coordinates.source) return; if (!coordinates.source) return;

View File

@ -1,13 +1,24 @@
import React, { forwardRef } from 'react'; import React, { forwardRef, useState } from 'react';
import { Draggable } from 'react-beautiful-dnd'; import { Draggable } from 'react-beautiful-dnd';
import { useKanbanBlockContext } from '../block-provider';
import Card from './Card'; import Card from './Card';
import CardAdder from './CardAdder'; import CardAdder from './CardAdder';
import { pickPropOut } from './utils'; import { pickPropOut } from './utils';
import withDroppable from './withDroppable'; import withDroppable from './withDroppable';
const ColumnEmptyPlaceholder = forwardRef((props, ref: any) => ( const ColumnEmptyPlaceholder = forwardRef(
<div ref={ref} style={{ minHeight: 'inherit', height: 'inherit' }} {...props} /> (
)); props: {
children: React.ReactNode;
style?: React.CSSProperties;
},
ref: any,
) => {
return (
<div ref={ref} {...props} style={{ minHeight: 'inherit', height: 'var(--column-height)', ...props.style }} />
);
},
);
const DroppableColumn = withDroppable(ColumnEmptyPlaceholder); const DroppableColumn = withDroppable(ColumnEmptyPlaceholder);
@ -23,6 +34,8 @@ function Column({
allowAddCard, allowAddCard,
cardAdderPosition = 'top', cardAdderPosition = 'top',
}) { }) {
const { fixedBlock } = useKanbanBlockContext();
const [headerHeight, setHeaderHeight] = useState(0);
return ( return (
<Draggable draggableId={`column-draggable-${children.id}`} index={columnIndex} isDragDisabled={disableColumnDrag}> <Draggable draggableId={`column-draggable-${children.id}`} index={columnIndex} isDragDisabled={disableColumnDrag}>
{(columnProvided) => { {(columnProvided) => {
@ -38,15 +51,26 @@ function Column({
display: 'inline-block', display: 'inline-block',
verticalAlign: 'top', verticalAlign: 'top',
...columnProvided.draggableProps.style, ...columnProvided.draggableProps.style,
'--column-height': fixedBlock ? `calc(100% - ${headerHeight}px)` : 'inherit',
}} }}
className="react-kanban-column" className="react-kanban-column"
data-testid={`column-${children.id}`} data-testid={`column-${children.id}`}
> >
<div {...columnProvided.dragHandleProps}>{renderColumnHeader(children)}</div> <div
ref={fixedBlock ? (ref) => setHeaderHeight(Math.ceil(ref?.getBoundingClientRect().height || 0)) : null}
{...columnProvided.dragHandleProps}
>
{renderColumnHeader(children)}
</div>
{cardAdderPosition === 'top' && allowAddCard && renderCardAdder({ column: children, onConfirm: onCardNew })} {cardAdderPosition === 'top' && allowAddCard && renderCardAdder({ column: children, onConfirm: onCardNew })}
<DroppableColumn droppableId={String(children.id)}> <DroppableColumn droppableId={String(children.id)}>
{children?.cards?.length ? ( {children?.cards?.length ? (
<div className="react-kanban-card-skeleton"> <div
className="react-kanban-card-skeleton"
style={{
height: fixedBlock ? '100%' : undefined,
}}
>
{children.cards.map((card, index) => ( {children.cards.map((card, index) => (
<Card <Card
key={card.id} key={card.id}

View File

@ -1,6 +1,7 @@
.react-kanban-board { .react-kanban-board {
height: 100%;
// padding: 5px; // padding: 5px;
margin-bottom: 24px; // margin-bottom: 24px;
} }
.react-kanban-card { .react-kanban-card {

View File

@ -113,16 +113,16 @@ export default {
"Create collection": "Create collection", "Create collection": "Create collection",
"Collection display name": "Collection display name", "Collection display name": "Collection display name",
"Collection name": "Collection name", "Collection name": "Collection name",
"Inherits":"Inherits", "Inherits": "Inherits",
"AutoGenId":"Auto-generated ID field", "AutoGenId": "Auto-generated ID field",
"CreatedBy":"Recording a row's created user", "CreatedBy": "Recording a row's created user",
"UpdatedBy":"Recording a row's last updated user", "UpdatedBy": "Recording a row's last updated user",
"CreatedAt":"Recording a row's created time ", "CreatedAt": "Recording a row's created time ",
"UpdatedAt":"Recording a row's last updated user", "UpdatedAt": "Recording a row's last updated user",
"Records can be sorted": "Records can be sorted", "Records can be sorted": "Records can be sorted",
"Collection template":"Collection template", "Collection template": "Collection template",
"Calendar collection":"Calendar collection", "Calendar collection": "Calendar collection",
"General collection":"General collection", "General collection": "General collection",
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.", "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.",
"Storage type": "Storage type", "Storage type": "Storage type",
"Edit": "Edit", "Edit": "Edit",
@ -130,12 +130,12 @@ export default {
"Configure fields": "Configure fields", "Configure fields": "Configure fields",
"Configure columns": "Configure columns", "Configure columns": "Configure columns",
"Edit field": "Edit field", "Edit field": "Edit field",
"Override":"Override", "Override": "Override",
"Override field":"Override field", "Override field": "Override field",
"Configure fields of {{title}}": "Configure fields of {{title}}", "Configure fields of {{title}}": "Configure fields of {{title}}",
"PK & FK fields": "PK & FK fields", "PK & FK fields": "PK & FK fields",
"Association fields": "Association fields", "Association fields": "Association fields",
"Parent collection fields":"Parent collection fields", "Parent collection fields": "Parent collection fields",
"System fields": "System fields", "System fields": "System fields",
"General fields": "General fields", "General fields": "General fields",
"Basic": "Basic", "Basic": "Basic",
@ -508,11 +508,12 @@ export default {
"Selected": "Selected", "Selected": "Selected",
"Remains the same": "Remains the same", "Remains the same": "Remains the same",
"Changed to": "Changed to", "Changed to": "Changed to",
"Clear":"Clear", "Clear": "Clear",
"Add attach":"Add attach", "Add attach": "Add attach",
"Please select the records to be updated": "Please select the records to be updated", "Please select the records to be updated": "Please select the records to be updated",
"Selector": "Selector", "Selector": "Selector",
"Inner": "Inner", "Inner": "Inner",
"Search and select collection": "Search and select collection", "Search and select collection": "Search and select collection",
'Please fill in the iframe URL': 'Please fill in the iframe URL', 'Please fill in the iframe URL': 'Please fill in the iframe URL',
'Fix block': 'Fix block'
} }

View File

@ -117,17 +117,17 @@ export default {
"Create collection": "创建数据表", "Create collection": "创建数据表",
"Collection display name": "数据表名称", "Collection display name": "数据表名称",
"Collection name": "数据表标识", "Collection name": "数据表标识",
"Inherits":"继承", "Inherits": "继承",
"Generate ID field automatically":"自动生成 ID 字段", "Generate ID field automatically": "自动生成 ID 字段",
"Store the creation user of each record":"记录创建人", "Store the creation user of each record": "记录创建人",
"Store the last update user of each record":"记录最后更新人", "Store the last update user of each record": "记录最后更新人",
"Store the creation time of each record":"记录创建时间", "Store the creation time of each record": "记录创建时间",
"Store the last update time of each record":"记录最后更新时间", "Store the last update time of each record": "记录最后更新时间",
"More options": "更多选项", "More options": "更多选项",
"Records can be sorted":"可以对行记录进行排序", "Records can be sorted": "可以对行记录进行排序",
"Collection template":"数据表模板", "Collection template": "数据表模板",
"Calendar collection":"日历数据表", "Calendar collection": "日历数据表",
"General collection":"普通数据表", "General collection": "普通数据表",
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "随机生成,可修改。支持英文、数字和下划线,必须以英文字母开头。", "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "随机生成,可修改。支持英文、数字和下划线,必须以英文字母开头。",
"Storage type": "存储类型", "Storage type": "存储类型",
"Edit": "编辑", "Edit": "编辑",
@ -135,14 +135,14 @@ export default {
"Configure fields": "配置字段", "Configure fields": "配置字段",
"Configure columns": "配置字段", "Configure columns": "配置字段",
"Edit field": "编辑字段", "Edit field": "编辑字段",
"Override":"重写", "Override": "重写",
"Override field":"重写字段", "Override field": "重写字段",
"Configure fields of {{title}}": "「{{title}}」的字段配置", "Configure fields of {{title}}": "「{{title}}」的字段配置",
"PK & FK fields": "主外键字段", "PK & FK fields": "主外键字段",
"Association fields": "关系字段", "Association fields": "关系字段",
"System fields": "系统字段", "System fields": "系统字段",
"General fields": "普通字段", "General fields": "普通字段",
"Parent collection fields":"父表字段", "Parent collection fields": "父表字段",
"Basic": "基本类型", "Basic": "基本类型",
"Single line text": "单行文本", "Single line text": "单行文本",
"Long text": "多行文本", "Long text": "多行文本",
@ -604,11 +604,13 @@ export default {
"Update all data?": "更新全部数据吗?", "Update all data?": "更新全部数据吗?",
"Remains the same": "不更新", "Remains the same": "不更新",
"Changed to": "修改为", "Changed to": "修改为",
"Clear":"清空", "Clear": "清空",
"Add attach":"增加关联", "Add attach": "增加关联",
"Please select the records to be updated": "请选择要更新的记录", "Please select the records to be updated": "请选择要更新的记录",
"Selector": "选择器", "Selector": "选择器",
"Inner": "里面", "Inner": "里面",
"Search and select collection": "搜索并选择数据表", "Search and select collection": "搜索并选择数据表",
'Please fill in the iframe URL': '请填写嵌入的地址', 'Please fill in the iframe URL': '请填写嵌入的地址',
'Fix block': '固定区块'
} }

View File

@ -27,14 +27,24 @@ export const ActionBar = observer((props: any) => {
} }
return ( return (
<div <div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', overflowX: 'auto', ...style }} style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
overflowX: 'auto',
flexShrink: 0,
...style,
}}
{...others} {...others}
> >
<div className={css` <div
className={css`
.ant-space:last-child { .ant-space:last-child {
margin-left: 8px; margin-left: 8px;
} }
`} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}> `}
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}
>
<DndContext> <DndContext>
<Space> <Space>
{fieldSchema.mapProperties((schema, key) => { {fieldSchema.mapProperties((schema, key) => {

View File

@ -7,6 +7,7 @@ import { useCollectionFilterOptions } from '../../../collection-manager/action-h
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates'; import { useSchemaTemplate } from '../../../schema-templates';
import { useDesignable } from '../../hooks'; import { useDesignable } from '../../hooks';
import { useFixedBlockDesignerSetting } from '../page';
export const KanbanDesigner = () => { export const KanbanDesigner = () => {
const { name, title } = useCollection(); const { name, title } = useCollection();
@ -19,6 +20,8 @@ export const KanbanDesigner = () => {
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {}; const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
const template = useSchemaTemplate(); const template = useSchemaTemplate();
const fixedBlockDesignerSetting = useFixedBlockDesignerSetting();
return ( return (
<GeneralSchemaDesigner template={template} title={title || name}> <GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettings.BlockTitleItem /> <SchemaSettings.BlockTitleItem />
@ -52,6 +55,7 @@ export const KanbanDesigner = () => {
}); });
}} }}
/> />
{fixedBlockDesignerSetting}
<SchemaSettings.Divider /> <SchemaSettings.Divider />
<SchemaSettings.Template componentName={'Kanban'} collectionName={name} resourceName={defaultResource} /> <SchemaSettings.Template componentName={'Kanban'} collectionName={name} resourceName={defaultResource} />
<SchemaSettings.Divider /> <SchemaSettings.Divider />

View File

@ -1,5 +1,5 @@
.react-kanban-board { .react-kanban-board {
margin-bottom: 24px; // margin-bottom: 24px;
.nb-block-item { .nb-block-item {
.ant-formily-item-control .ant-space-item{ .ant-formily-item-control .ant-space-item{

View File

@ -0,0 +1,128 @@
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
import { css } from '@emotion/css';
import { SchemaSettings } from '../../../schema-settings';
import { useTranslation } from 'react-i18next';
import { useDesignable } from '../../hooks';
import { useGridContext } from '../grid';
import { useRecord } from '../../../record-provider';
const FixedBlockContext = React.createContext({
setFixedSchema: (schema: Schema) => {},
height: 0,
schema: {} as unknown as Schema,
});
export const useFixedSchema = () => {
const field = useField();
const fieldSchema = useFieldSchema();
const { setFixedSchema } = useFixedBlock();
const hasSet = useRef(false);
useEffect(() => {
if (fieldSchema?.['x-decorator-props']?.fixedBlock) {
setFixedSchema(fieldSchema);
hasSet.current = true;
}
}, [field?.decoratorProps?.fixedBlock, fieldSchema?.['x-decorator-props']?.fixedBlock]);
useEffect(
() => () => {
if (hasSet.current) {
setFixedSchema(null);
}
},
[],
);
};
export const useFixedBlock = () => {
return useContext(FixedBlockContext);
};
export const useFixedBlockDesignerSetting = () => {
const field = useField();
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const { dn } = useDesignable();
const record = useRecord();
return useMemo(() => {
if (Object.keys(record).length) {
return;
}
return (
<SchemaSettings.SwitchItem
title={t('Fix block')}
checked={fieldSchema['x-decorator-props']['fixedBlock']}
onChange={(fixedBlock) => {
field.decoratorProps.fixedBlock = fixedBlock;
fieldSchema['x-decorator-props'].fixedBlock = fixedBlock;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
}}
/>
);
}, [fieldSchema['x-decorator-props'], field.decoratorProps?.fixedBlock, dn, record]);
};
interface FixedBlockProps {
height: number;
}
const FixedBlock: React.FC<FixedBlockProps> = (props) => {
const { height } = props;
const [fixedSchema, setFixedSchema] = useState<Schema>();
const schema = useMemo<Schema>(() => {
if (!fixedSchema || fixedSchema['x-decorator-props']?.fixedBlock !== true) return;
return fixedSchema.parent;
}, [fixedSchema, fixedSchema?.['x-decorator-props']['fixedBlock']]);
return (
<FixedBlockContext.Provider value={{ schema: fixedSchema, height, setFixedSchema }}>
{schema ? (
<div
className={css`
height: 100%;
overflow: hidden;
.noco-card-item {
height: 100%;
.ant-card {
display: flex;
flex-direction: column;
height: 100%;
.ant-card-body {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
// padding-bottom: 0;
}
}
& .ant-spin-nested-loading {
height: 100%;
overflow: hidden;
}
& .ant-spin-container {
height: 100%;
}
}
`}
style={{
height: `calc(100vh - ${height}px)`,
}}
>
<RecursionField onlyRenderProperties={false} schema={schema} />
</div>
) : (
props.children
)}
</FixedBlockContext.Provider>
);
};
export default FixedBlock;

View File

@ -1,15 +1,17 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { FormDialog, FormLayout } from '@formily/antd'; import { FormDialog, FormLayout } from '@formily/antd';
import { RecursionField, SchemaOptionsContext, useField, useFieldSchema } from '@formily/react'; import { RecursionField, SchemaOptionsContext, useField, useFieldSchema } from '@formily/react';
import { useMutationObserver } from 'ahooks';
import { Button, PageHeader as AntdPageHeader, Spin, Tabs } from 'antd'; import { Button, PageHeader as AntdPageHeader, Spin, Tabs } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHistory, useLocation } from 'react-router-dom'; import { useHistory, useLocation } from 'react-router-dom';
import { useDocumentTitle } from '../../../document-title'; import { useDocumentTitle } from '../../../document-title';
import { Icon } from '../../../icon'; import { Icon } from '../../../icon';
import { SchemaComponent, SchemaComponentOptions } from '../../core'; import { SchemaComponent, SchemaComponentOptions } from '../../core';
import { useCompile, useDesignable } from '../../hooks'; import { useCompile, useDesignable } from '../../hooks';
import FixedBlock from './FixedBlock';
import { PageDesigner, PageTabDesigner } from './PageTabDesigner'; import { PageDesigner, PageTabDesigner } from './PageTabDesigner';
const designerCss = css` const designerCss = css`
@ -130,10 +132,17 @@ export const Page = (props) => {
// @ts-ignore // @ts-ignore
return location?.query?.tab || Object.keys(fieldSchema.properties).shift(); return location?.query?.tab || Object.keys(fieldSchema.properties).shift();
}); });
const [height, setHeight] = useState(0);
return ( return (
<>
<div className={pageDesignerCss}> <div className={pageDesignerCss}>
<PageDesigner title={fieldSchema.title || title} /> <PageDesigner title={fieldSchema.title || title} />
<div
ref={(ref) => {
setHeight(Math.floor(ref?.getBoundingClientRect().height || 0) + 1);
}}
>
{!disablePageHeader && ( {!disablePageHeader && (
<AntdPageHeader <AntdPageHeader
className={css` className={css`
@ -234,10 +243,18 @@ export const Page = (props) => {
} }
/> />
)} )}
</div>
<div style={{ margin: 24 }}> <div style={{ margin: 24 }}>
{loading ? ( {loading ? (
<Spin /> <Spin />
) : !disablePageHeader && enablePageTabs ? ( ) : (
<FixedBlock
height={
// header 46 margin 48
height + 46 + 48
}
>
{!disablePageHeader && enablePageTabs ? (
<RecursionField <RecursionField
schema={fieldSchema} schema={fieldSchema}
onlyRenderProperties onlyRenderProperties
@ -258,8 +275,9 @@ export const Page = (props) => {
{props.children} {props.children}
</div> </div>
)} )}
</FixedBlock>
)}
</div> </div>
</div> </div>
</>
); );
}; };

View File

@ -1 +1,2 @@
export * from './Page'; export * from './Page';
export * from './FixedBlock';

View File

@ -13,7 +13,7 @@ const useLabelFields = (collectionName?: any) => {
const { getCollectionFields } = useCollectionManager(); const { getCollectionFields } = useCollectionManager();
const targetFields = getCollectionFields(collectionName); const targetFields = getCollectionFields(collectionName);
return targetFields return targetFields
?.filter?.((field) => field?.interface && !field?.target && field.type !== 'boolean'&&!field.isForeignKey) ?.filter?.((field) => field?.interface && !field?.target && field.type !== 'boolean' && !field.isForeignKey)
?.map?.((field) => { ?.map?.((field) => {
return { return {
value: field.name, value: field.name,
@ -68,8 +68,38 @@ export const TableColumnDesigner = (props) => {
dn.refresh(); dn.refresh();
}} }}
/> />
<SchemaSettings.ModalItem
title={t('Column width')}
schema={
{ {
intefaceCfg && intefaceCfg.sortable === true && ( type: 'object',
title: t('Column width'),
properties: {
width: {
default: columnSchema?.['x-component-props']?.['width'] || 200,
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ width }) => {
const props = columnSchema['x-component-props'] || {};
props['width'] = width;
const schema: ISchema = {
['x-uid']: columnSchema['x-uid'],
};
schema['x-component-props'] = props;
columnSchema['x-component-props'] = props;
field.componentProps.width = width;
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
{intefaceCfg && intefaceCfg.sortable === true && (
<SchemaSettings.SwitchItem <SchemaSettings.SwitchItem
title={t('Sortable')} title={t('Sortable')}
checked={field.componentProps.sorter} checked={field.componentProps.sorter}
@ -79,18 +109,17 @@ export const TableColumnDesigner = (props) => {
}; };
columnSchema['x-component-props'] = { columnSchema['x-component-props'] = {
...columnSchema['x-component-props'], ...columnSchema['x-component-props'],
sorter: v sorter: v,
} };
schema['x-component-props'] = columnSchema['x-component-props']; schema['x-component-props'] = columnSchema['x-component-props'];
field.componentProps.sorter = v; field.componentProps.sorter = v;
dn.emit('patch', { dn.emit('patch', {
schema schema,
}); });
dn.refresh(); dn.refresh();
}} }}
/> />
) )}
}
{['linkTo', 'm2m', 'm2o', 'o2m', 'obo', 'oho'].includes(collectionField?.interface) && ( {['linkTo', 'm2m', 'm2o', 'o2m', 'obo', 'oho'].includes(collectionField?.interface) && (
<SchemaSettings.SelectItem <SchemaSettings.SelectItem
title={t('Title field')} title={t('Title field')}

View File

@ -4,12 +4,12 @@ import { css } from '@emotion/css';
import { ArrayField, Field } from '@formily/core'; import { ArrayField, Field } from '@formily/core';
import { ISchema, observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react'; import { ISchema, observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
import { reaction } from '@formily/reactive'; import { reaction } from '@formily/reactive';
import { useMemoizedFn } from 'ahooks'; import { useEventListener, useMemoizedFn } from 'ahooks';
import { Table as AntdTable, TableColumnProps } from 'antd'; import { Table as AntdTable, TableColumnProps } from 'antd';
import { default as classNames, default as cls } from 'classnames'; import { default as classNames, default as cls } from 'classnames';
import React, { useCallback, useEffect, useMemo } from 'react'; import React, { RefCallback, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DndContext } from '../..'; import { DndContext, useDesignable } from '../..';
import { RecordIndexProvider, RecordProvider, useSchemaInitializer } from '../../../'; import { RecordIndexProvider, RecordProvider, useSchemaInitializer } from '../../../';
const isColumnComponent = (schema: Schema) => { const isColumnComponent = (schema: Schema) => {
@ -24,6 +24,7 @@ const useTableColumns = () => {
const start = Date.now(); const start = Date.now();
const field = useField<ArrayField>(); const field = useField<ArrayField>();
const schema = useFieldSchema(); const schema = useFieldSchema();
const { designable } = useDesignable();
const { exists, render } = useSchemaInitializer(schema['x-initializer']); const { exists, render } = useSchemaInitializer(schema['x-initializer']);
const columns = schema const columns = schema
.reduceProperties((buf, s) => { .reduceProperties((buf, s) => {
@ -38,13 +39,13 @@ const useTableColumns = () => {
} }
}, []); }, []);
const dataIndex = collectionFields?.length > 0 ? collectionFields[0].name : s.name; const dataIndex = collectionFields?.length > 0 ? collectionFields[0].name : s.name;
return { return {
title: <RecursionField name={s.name} schema={s} onlyRenderSelf />, title: <RecursionField name={s.name} schema={s} onlyRenderSelf />,
dataIndex, dataIndex,
key: s.name, key: s.name,
sorter: s['x-component-props']?.['sorter'], sorter: s['x-component-props']?.['sorter'],
// width: 300, width: 200,
...s['x-component-props'],
render: (v, record) => { render: (v, record) => {
const index = field.value?.indexOf(record); const index = field.value?.indexOf(record);
// console.log((Date.now() - start) / 1000); // console.log((Date.now() - start) / 1000);
@ -65,6 +66,7 @@ const useTableColumns = () => {
title: render(), title: render(),
dataIndex: 'TABLE_COLUMN_INITIALIZER', dataIndex: 'TABLE_COLUMN_INITIALIZER',
key: 'TABLE_COLUMN_INITIALIZER', key: 'TABLE_COLUMN_INITIALIZER',
render: designable ? () => <div style={{ minWidth: 300 }} /> : null,
}); });
}; };
@ -169,6 +171,7 @@ export const Table: any = observer((props: any) => {
const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {})); const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {}));
const paginationProps = usePaginationProps(pagination1, pagination2); const paginationProps = usePaginationProps(pagination1, pagination2);
const requiredValidator = field.required || required; const requiredValidator = field.required || required;
useEffect(() => { useEffect(() => {
field.setValidator((value) => { field.setValidator((value) => {
if (requiredValidator) { if (requiredValidator) {
@ -360,10 +363,44 @@ export const Table: any = observer((props: any) => {
}, },
[field, dragSort], [field, dragSort],
); );
const fieldSchema = useFieldSchema();
const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock;
const [tableHeight, setTableHeight] = useState(0);
const [headerAndPaginationHeight, setHeaderAndPaginationHeight] = useState(0);
const scroll = useMemo(() => {
return fixedBlock
? {
x: 'max-content',
y: tableHeight - headerAndPaginationHeight,
}
: {
x: 'max-content',
};
}, [fixedBlock, tableHeight, headerAndPaginationHeight]);
const elementRef = useRef<HTMLDivElement>();
const calcTableSize = () => {
if (!elementRef.current) return;
const clientRect = elementRef.current?.getBoundingClientRect();
setTableHeight(Math.ceil(clientRect?.height || 0));
};
useEventListener('resize', calcTableSize);
const mountedRef: RefCallback<HTMLDivElement> = (ref) => {
elementRef.current = ref;
calcTableSize();
};
return ( return (
<div <div
ref={mountedRef}
className={css` className={css`
height: 100%;
overflow: hidden;
.ant-table-wrapper {
height: 100%;
}
.ant-table { .ant-table {
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
@ -372,6 +409,11 @@ export const Table: any = observer((props: any) => {
> >
<SortableWrapper> <SortableWrapper>
<AntdTable <AntdTable
ref={(ref) => {
const headerHeight = ref?.querySelector('.ant-table-header')?.getBoundingClientRect().height || 0;
const paginationHeight = ref?.querySelector('.ant-table-pagination')?.getBoundingClientRect().height || 0;
setHeaderAndPaginationHeight(Math.ceil(headerHeight + paginationHeight + 16));
}}
rowKey={rowKey ?? defaultRowKey} rowKey={rowKey ?? defaultRowKey}
{...others} {...others}
{...restProps} {...restProps}
@ -380,8 +422,8 @@ export const Table: any = observer((props: any) => {
onChange={(pagination, filters, sorter, extra) => { onChange={(pagination, filters, sorter, extra) => {
onTableChange?.(pagination, filters, sorter, extra); onTableChange?.(pagination, filters, sorter, extra);
}} }}
// tableLayout={'auto'} tableLayout={'auto'}
// scroll={{ x: 12 * 300 + 80 }} scroll={scroll}
columns={columns} columns={columns}
dataSource={field?.value?.slice?.()} dataSource={field?.value?.slice?.()}
/> />

View File

@ -1,13 +1,15 @@
import { ArrayItems } from '@formily/antd'; import { ArrayItems } from '@formily/antd';
import { ISchema, useField, useFieldSchema } from '@formily/react'; import { ISchema, observer, useField, useFieldSchema } from '@formily/react';
import React from 'react'; import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useTableBlockContext } from '../../../block-provider'; import { useTableBlockContext } from '../../../block-provider';
import { useCollection } from '../../../collection-manager'; import { useCollection } from '../../../collection-manager';
import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks'; import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks';
import { useRecord } from '../../../record-provider';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates'; import { useSchemaTemplate } from '../../../schema-templates';
import { useDesignable } from '../../hooks'; import { useDesignable } from '../../hooks';
import { useFixedBlockDesignerSetting } from '../page';
export const TableBlockDesigner = () => { export const TableBlockDesigner = () => {
const { name, title, sortable } = useCollection(); const { name, title, sortable } = useCollection();
@ -34,10 +36,13 @@ export const TableBlockDesigner = () => {
}); });
const template = useSchemaTemplate(); const template = useSchemaTemplate();
const { dragSort } = field.decoratorProps; const { dragSort } = field.decoratorProps;
const fixedBlockDesignerSetting = useFixedBlockDesignerSetting();
return ( return (
<GeneralSchemaDesigner template={template} title={title || name}> <GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettings.BlockTitleItem /> <SchemaSettings.BlockTitleItem />
{sortable&& <SchemaSettings.SwitchItem {sortable && (
<SchemaSettings.SwitchItem
title={t('Enable drag and drop sorting')} title={t('Enable drag and drop sorting')}
checked={field.decoratorProps.dragSort} checked={field.decoratorProps.dragSort}
onChange={(dragSort) => { onChange={(dragSort) => {
@ -51,7 +56,9 @@ export const TableBlockDesigner = () => {
}, },
}); });
}} }}
/>} />
)}
{fixedBlockDesignerSetting}
<SchemaSettings.ModalItem <SchemaSettings.ModalItem
title={t('Set the data scope')} title={t('Set the data scope')}
schema={ schema={
@ -176,7 +183,6 @@ export const TableBlockDesigner = () => {
}} }}
/> />
)} )}
<SchemaSettings.SelectItem <SchemaSettings.SelectItem
title={t('Records per page')} title={t('Records per page')}
value={field.decoratorProps?.params?.pageSize || 20} value={field.decoratorProps?.params?.pageSize || 20}

View File

@ -1,11 +1,49 @@
import { MenuOutlined } from '@ant-design/icons'; import { MenuOutlined } from '@ant-design/icons';
import { useFieldSchema } from '@formily/react'; import { ISchema, useFieldSchema } from '@formily/react';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SchemaInitializer } from '../..'; import { SchemaInitializer, SchemaSettings } from '../..';
import { useAPIClient } from '../../api-client'; import { useAPIClient } from '../../api-client';
import { createDesignable, useDesignable } from '../../schema-component'; import { createDesignable, useDesignable } from '../../schema-component';
const Resizable = (props) => {
const { t } = useTranslation();
const { dn } = useDesignable();
const fieldSchema = useFieldSchema();
return (
<SchemaSettings.ModalItem
title={t('Column width')}
schema={
{
type: 'object',
title: t('Column width'),
properties: {
width: {
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
'x-component-props': {},
default: fieldSchema?.['x-component-props']?.width || 200,
},
},
} as ISchema
}
onSubmit={({ width }) => {
const props = fieldSchema['x-component-props'] || {};
props['width'] = width;
const schema: ISchema = {
['x-uid']: fieldSchema['x-uid'],
};
schema['x-component-props'] = props;
fieldSchema['x-component-props'] = props;
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
);
};
export const TableActionColumnInitializers = (props: any) => { export const TableActionColumnInitializers = (props: any) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const api = useAPIClient(); const api = useAPIClient();
@ -174,6 +212,14 @@ export const TableActionColumnInitializers = (props: any) => {
}, },
], ],
}, },
{
type: 'divider',
},
{
type: 'item',
title: t('Column width'),
component: Resizable,
},
]} ]}
component={<MenuOutlined style={{ cursor: 'pointer' }} />} component={<MenuOutlined style={{ cursor: 'pointer' }} />}
/> />