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:
parent
217ecb27ae
commit
c731abf82c
@ -5,6 +5,7 @@ import uniq from 'lodash/uniq';
|
||||
import React, { createContext, useContext, useEffect } from 'react';
|
||||
import { useACLRoleContext } from '../acl';
|
||||
import { useCollection, useCollectionManager } from '../collection-manager';
|
||||
import { useFixedSchema } from '../schema-component';
|
||||
import { toColumns } from '../schema-component/antd/kanban/Kanban';
|
||||
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
|
||||
|
||||
@ -23,6 +24,8 @@ const useGroupField = (props) => {
|
||||
|
||||
const InternalKanbanBlockProvider = (props) => {
|
||||
const field = useField<any>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
useFixedSchema();
|
||||
const { resource, service } = useBlockRequestContext();
|
||||
const groupField = useGroupField(props);
|
||||
if (!groupField) {
|
||||
@ -42,6 +45,7 @@ const InternalKanbanBlockProvider = (props) => {
|
||||
service,
|
||||
resource,
|
||||
groupField,
|
||||
fixedBlock: fieldSchema?.['x-decorator-props']?.fixedBlock,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
|
@ -3,6 +3,7 @@ import { FormContext, Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import uniq from 'lodash/uniq';
|
||||
import React, { createContext, useContext, useEffect, useMemo } from 'react';
|
||||
import { useCollectionManager } from '../collection-manager';
|
||||
import { useFixedSchema } from '../schema-component';
|
||||
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
|
||||
|
||||
export const TableBlockContext = createContext<any>({});
|
||||
@ -14,6 +15,7 @@ const InternalTableBlockProvider = (props) => {
|
||||
// if (service.loading) {
|
||||
// return <Spin />;
|
||||
// }
|
||||
useFixedSchema();
|
||||
return (
|
||||
<TableBlockContext.Provider
|
||||
value={{
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { useField } from '@formily/react';
|
||||
import React, { forwardRef, useState } from 'react';
|
||||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
import Column from './Column';
|
||||
@ -16,7 +17,9 @@ import {
|
||||
import { partialRight, when } from './utils';
|
||||
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);
|
||||
|
||||
@ -241,6 +244,7 @@ function BoardContainer(props) {
|
||||
onCardNew,
|
||||
allowAddCard,
|
||||
} = props;
|
||||
|
||||
function handleOnDragEnd(event) {
|
||||
const coordinates = getCoordinates(event, board);
|
||||
if (!coordinates.source) return;
|
||||
|
@ -1,13 +1,24 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import React, { forwardRef, useState } from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { useKanbanBlockContext } from '../block-provider';
|
||||
import Card from './Card';
|
||||
import CardAdder from './CardAdder';
|
||||
import { pickPropOut } from './utils';
|
||||
import withDroppable from './withDroppable';
|
||||
|
||||
const ColumnEmptyPlaceholder = forwardRef((props, ref: any) => (
|
||||
<div ref={ref} style={{ minHeight: 'inherit', height: 'inherit' }} {...props} />
|
||||
));
|
||||
const ColumnEmptyPlaceholder = forwardRef(
|
||||
(
|
||||
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);
|
||||
|
||||
@ -23,6 +34,8 @@ function Column({
|
||||
allowAddCard,
|
||||
cardAdderPosition = 'top',
|
||||
}) {
|
||||
const { fixedBlock } = useKanbanBlockContext();
|
||||
const [headerHeight, setHeaderHeight] = useState(0);
|
||||
return (
|
||||
<Draggable draggableId={`column-draggable-${children.id}`} index={columnIndex} isDragDisabled={disableColumnDrag}>
|
||||
{(columnProvided) => {
|
||||
@ -38,15 +51,26 @@ function Column({
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'top',
|
||||
...columnProvided.draggableProps.style,
|
||||
'--column-height': fixedBlock ? `calc(100% - ${headerHeight}px)` : 'inherit',
|
||||
}}
|
||||
className="react-kanban-column"
|
||||
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 })}
|
||||
<DroppableColumn droppableId={String(children.id)}>
|
||||
{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) => (
|
||||
<Card
|
||||
key={card.id}
|
||||
|
@ -1,6 +1,7 @@
|
||||
.react-kanban-board {
|
||||
height: 100%;
|
||||
// padding: 5px;
|
||||
margin-bottom: 24px;
|
||||
// margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.react-kanban-card {
|
||||
|
@ -113,16 +113,16 @@ export default {
|
||||
"Create collection": "Create collection",
|
||||
"Collection display name": "Collection display name",
|
||||
"Collection name": "Collection name",
|
||||
"Inherits":"Inherits",
|
||||
"AutoGenId":"Auto-generated ID field",
|
||||
"CreatedBy":"Recording a row's created user",
|
||||
"UpdatedBy":"Recording a row's last updated user",
|
||||
"CreatedAt":"Recording a row's created time ",
|
||||
"UpdatedAt":"Recording a row's last updated user",
|
||||
"Inherits": "Inherits",
|
||||
"AutoGenId": "Auto-generated ID field",
|
||||
"CreatedBy": "Recording a row's created user",
|
||||
"UpdatedBy": "Recording a row's last updated user",
|
||||
"CreatedAt": "Recording a row's created time ",
|
||||
"UpdatedAt": "Recording a row's last updated user",
|
||||
"Records can be sorted": "Records can be sorted",
|
||||
"Collection template":"Collection template",
|
||||
"Calendar collection":"Calendar collection",
|
||||
"General collection":"General collection",
|
||||
"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",
|
||||
@ -130,12 +130,12 @@ export default {
|
||||
"Configure fields": "Configure fields",
|
||||
"Configure columns": "Configure columns",
|
||||
"Edit field": "Edit field",
|
||||
"Override":"Override",
|
||||
"Override field":"Override field",
|
||||
"Override": "Override",
|
||||
"Override field": "Override field",
|
||||
"Configure fields of {{title}}": "Configure fields of {{title}}",
|
||||
"PK & FK fields": "PK & FK fields",
|
||||
"Association fields": "Association fields",
|
||||
"Parent collection fields":"Parent collection fields",
|
||||
"Parent collection fields": "Parent collection fields",
|
||||
"System fields": "System fields",
|
||||
"General fields": "General fields",
|
||||
"Basic": "Basic",
|
||||
@ -508,11 +508,12 @@ export default {
|
||||
"Selected": "Selected",
|
||||
"Remains the same": "Remains the same",
|
||||
"Changed to": "Changed to",
|
||||
"Clear":"Clear",
|
||||
"Add attach":"Add attach",
|
||||
"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': 'Fix block'
|
||||
}
|
||||
|
@ -117,17 +117,17 @@ export default {
|
||||
"Create collection": "创建数据表",
|
||||
"Collection display name": "数据表名称",
|
||||
"Collection name": "数据表标识",
|
||||
"Inherits":"继承",
|
||||
"Generate ID field automatically":"自动生成 ID 字段",
|
||||
"Store the creation user of each record":"记录创建人",
|
||||
"Store the last update user of each record":"记录最后更新人",
|
||||
"Store the creation time of each record":"记录创建时间",
|
||||
"Store the last update time of each record":"记录最后更新时间",
|
||||
"Inherits": "继承",
|
||||
"Generate ID field automatically": "自动生成 ID 字段",
|
||||
"Store the creation user of each record": "记录创建人",
|
||||
"Store the last update user of each record": "记录最后更新人",
|
||||
"Store the creation time of each record": "记录创建时间",
|
||||
"Store the last update time of each record": "记录最后更新时间",
|
||||
"More options": "更多选项",
|
||||
"Records can be sorted":"可以对行记录进行排序",
|
||||
"Collection template":"数据表模板",
|
||||
"Calendar collection":"日历数据表",
|
||||
"General collection":"普通数据表",
|
||||
"Records can be sorted": "可以对行记录进行排序",
|
||||
"Collection template": "数据表模板",
|
||||
"Calendar collection": "日历数据表",
|
||||
"General collection": "普通数据表",
|
||||
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "随机生成,可修改。支持英文、数字和下划线,必须以英文字母开头。",
|
||||
"Storage type": "存储类型",
|
||||
"Edit": "编辑",
|
||||
@ -135,14 +135,14 @@ export default {
|
||||
"Configure fields": "配置字段",
|
||||
"Configure columns": "配置字段",
|
||||
"Edit field": "编辑字段",
|
||||
"Override":"重写",
|
||||
"Override field":"重写字段",
|
||||
"Override": "重写",
|
||||
"Override field": "重写字段",
|
||||
"Configure fields of {{title}}": "「{{title}}」的字段配置",
|
||||
"PK & FK fields": "主外键字段",
|
||||
"Association fields": "关系字段",
|
||||
"System fields": "系统字段",
|
||||
"General fields": "普通字段",
|
||||
"Parent collection fields":"父表字段",
|
||||
"Parent collection fields": "父表字段",
|
||||
"Basic": "基本类型",
|
||||
"Single line text": "单行文本",
|
||||
"Long text": "多行文本",
|
||||
@ -399,8 +399,8 @@ export default {
|
||||
"Action column": "操作列",
|
||||
"Records per page": "每页显示数量",
|
||||
"(Fields only)": "(仅字段)",
|
||||
"Button title": "按钮标题",
|
||||
"Button icon": "按钮图标",
|
||||
"Button title": "按钮标题",
|
||||
"Button icon": "按钮图标",
|
||||
"Submitted successfully": "提交成功",
|
||||
"Operation succeeded": "操作成功",
|
||||
"Operation failed": "操作失败",
|
||||
@ -604,11 +604,13 @@ export default {
|
||||
"Update all data?": "更新全部数据吗?",
|
||||
"Remains the same": "不更新",
|
||||
"Changed to": "修改为",
|
||||
"Clear":"清空",
|
||||
"Add attach":"增加关联",
|
||||
"Clear": "清空",
|
||||
"Add attach": "增加关联",
|
||||
"Please select the records to be updated": "请选择要更新的记录",
|
||||
"Selector": "选择器",
|
||||
"Inner": "里面",
|
||||
"Search and select collection": "搜索并选择数据表",
|
||||
'Please fill in the iframe URL': '请填写嵌入的地址',
|
||||
|
||||
'Fix block': '固定区块'
|
||||
}
|
||||
|
@ -27,14 +27,24 @@ export const ActionBar = observer((props: any) => {
|
||||
}
|
||||
return (
|
||||
<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}
|
||||
>
|
||||
<div className={css`
|
||||
.ant-space:last-child {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||
<div
|
||||
className={css`
|
||||
.ant-space:last-child {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`}
|
||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}
|
||||
>
|
||||
<DndContext>
|
||||
<Space>
|
||||
{fieldSchema.mapProperties((schema, key) => {
|
||||
|
@ -7,6 +7,7 @@ import { useCollectionFilterOptions } from '../../../collection-manager/action-h
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { useFixedBlockDesignerSetting } from '../page';
|
||||
|
||||
export const KanbanDesigner = () => {
|
||||
const { name, title } = useCollection();
|
||||
@ -19,6 +20,8 @@ export const KanbanDesigner = () => {
|
||||
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const template = useSchemaTemplate();
|
||||
const fixedBlockDesignerSetting = useFixedBlockDesignerSetting();
|
||||
|
||||
return (
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
<SchemaSettings.BlockTitleItem />
|
||||
@ -52,6 +55,7 @@ export const KanbanDesigner = () => {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{fixedBlockDesignerSetting}
|
||||
<SchemaSettings.Divider />
|
||||
<SchemaSettings.Template componentName={'Kanban'} collectionName={name} resourceName={defaultResource} />
|
||||
<SchemaSettings.Divider />
|
||||
|
@ -1,5 +1,5 @@
|
||||
.react-kanban-board {
|
||||
margin-bottom: 24px;
|
||||
// margin-bottom: 24px;
|
||||
.nb-block-item {
|
||||
.ant-formily-item-control .ant-space-item{
|
||||
|
||||
|
@ -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;
|
@ -1,15 +1,17 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { FormDialog, FormLayout } from '@formily/antd';
|
||||
import { RecursionField, SchemaOptionsContext, useField, useFieldSchema } from '@formily/react';
|
||||
import { useMutationObserver } from 'ahooks';
|
||||
import { Button, PageHeader as AntdPageHeader, Spin, Tabs } from 'antd';
|
||||
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 { useHistory, useLocation } from 'react-router-dom';
|
||||
import { useDocumentTitle } from '../../../document-title';
|
||||
import { Icon } from '../../../icon';
|
||||
import { SchemaComponent, SchemaComponentOptions } from '../../core';
|
||||
import { useCompile, useDesignable } from '../../hooks';
|
||||
import FixedBlock from './FixedBlock';
|
||||
import { PageDesigner, PageTabDesigner } from './PageTabDesigner';
|
||||
|
||||
const designerCss = css`
|
||||
@ -130,10 +132,17 @@ export const Page = (props) => {
|
||||
// @ts-ignore
|
||||
return location?.query?.tab || Object.keys(fieldSchema.properties).shift();
|
||||
});
|
||||
|
||||
const [height, setHeight] = useState(0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={pageDesignerCss}>
|
||||
<PageDesigner title={fieldSchema.title || title} />
|
||||
<div className={pageDesignerCss}>
|
||||
<PageDesigner title={fieldSchema.title || title} />
|
||||
<div
|
||||
ref={(ref) => {
|
||||
setHeight(Math.floor(ref?.getBoundingClientRect().height || 0) + 1);
|
||||
}}
|
||||
>
|
||||
{!disablePageHeader && (
|
||||
<AntdPageHeader
|
||||
className={css`
|
||||
@ -234,32 +243,41 @@ export const Page = (props) => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div style={{ margin: 24 }}>
|
||||
{loading ? (
|
||||
<Spin />
|
||||
) : !disablePageHeader && enablePageTabs ? (
|
||||
<RecursionField
|
||||
schema={fieldSchema}
|
||||
onlyRenderProperties
|
||||
filterProperties={(s) => {
|
||||
return s.name === activeKey;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={css`
|
||||
.nb-grid:not(:last-child) {
|
||||
> .nb-schema-initializer-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<div style={{ margin: 24 }}>
|
||||
{loading ? (
|
||||
<Spin />
|
||||
) : (
|
||||
<FixedBlock
|
||||
height={
|
||||
// header 46 margin 48
|
||||
height + 46 + 48
|
||||
}
|
||||
>
|
||||
{!disablePageHeader && enablePageTabs ? (
|
||||
<RecursionField
|
||||
schema={fieldSchema}
|
||||
onlyRenderProperties
|
||||
filterProperties={(s) => {
|
||||
return s.name === activeKey;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={css`
|
||||
.nb-grid:not(:last-child) {
|
||||
> .nb-schema-initializer-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
)}
|
||||
</FixedBlock>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './Page';
|
||||
export * from './FixedBlock';
|
||||
|
@ -13,7 +13,7 @@ const useLabelFields = (collectionName?: any) => {
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
const targetFields = getCollectionFields(collectionName);
|
||||
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) => {
|
||||
return {
|
||||
value: field.name,
|
||||
@ -68,29 +68,58 @@ export const TableColumnDesigner = (props) => {
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
{
|
||||
intefaceCfg && intefaceCfg.sortable === true && (
|
||||
<SchemaSettings.SwitchItem
|
||||
title={t('Sortable')}
|
||||
checked={field.componentProps.sorter}
|
||||
onChange={(v) => {
|
||||
const schema: ISchema = {
|
||||
['x-uid']: columnSchema['x-uid'],
|
||||
};
|
||||
columnSchema['x-component-props'] = {
|
||||
...columnSchema['x-component-props'],
|
||||
sorter: v
|
||||
}
|
||||
schema['x-component-props'] = columnSchema['x-component-props'];
|
||||
field.componentProps.sorter = v;
|
||||
dn.emit('patch', {
|
||||
schema
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Column width')}
|
||||
schema={
|
||||
{
|
||||
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
|
||||
title={t('Sortable')}
|
||||
checked={field.componentProps.sorter}
|
||||
onChange={(v) => {
|
||||
const schema: ISchema = {
|
||||
['x-uid']: columnSchema['x-uid'],
|
||||
};
|
||||
columnSchema['x-component-props'] = {
|
||||
...columnSchema['x-component-props'],
|
||||
sorter: v,
|
||||
};
|
||||
schema['x-component-props'] = columnSchema['x-component-props'];
|
||||
field.componentProps.sorter = v;
|
||||
dn.emit('patch', {
|
||||
schema,
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{['linkTo', 'm2m', 'm2o', 'o2m', 'obo', 'oho'].includes(collectionField?.interface) && (
|
||||
<SchemaSettings.SelectItem
|
||||
title={t('Title field')}
|
||||
|
@ -4,12 +4,12 @@ import { css } from '@emotion/css';
|
||||
import { ArrayField, Field } from '@formily/core';
|
||||
import { ISchema, observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import { reaction } from '@formily/reactive';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { useEventListener, useMemoizedFn } from 'ahooks';
|
||||
import { Table as AntdTable, TableColumnProps } from 'antd';
|
||||
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 { DndContext } from '../..';
|
||||
import { DndContext, useDesignable } from '../..';
|
||||
import { RecordIndexProvider, RecordProvider, useSchemaInitializer } from '../../../';
|
||||
|
||||
const isColumnComponent = (schema: Schema) => {
|
||||
@ -24,6 +24,7 @@ const useTableColumns = () => {
|
||||
const start = Date.now();
|
||||
const field = useField<ArrayField>();
|
||||
const schema = useFieldSchema();
|
||||
const { designable } = useDesignable();
|
||||
const { exists, render } = useSchemaInitializer(schema['x-initializer']);
|
||||
const columns = schema
|
||||
.reduceProperties((buf, s) => {
|
||||
@ -38,13 +39,13 @@ const useTableColumns = () => {
|
||||
}
|
||||
}, []);
|
||||
const dataIndex = collectionFields?.length > 0 ? collectionFields[0].name : s.name;
|
||||
|
||||
return {
|
||||
title: <RecursionField name={s.name} schema={s} onlyRenderSelf />,
|
||||
dataIndex,
|
||||
key: s.name,
|
||||
sorter: s['x-component-props']?.['sorter'],
|
||||
// width: 300,
|
||||
width: 200,
|
||||
...s['x-component-props'],
|
||||
render: (v, record) => {
|
||||
const index = field.value?.indexOf(record);
|
||||
// console.log((Date.now() - start) / 1000);
|
||||
@ -65,6 +66,7 @@ const useTableColumns = () => {
|
||||
title: render(),
|
||||
dataIndex: '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 paginationProps = usePaginationProps(pagination1, pagination2);
|
||||
const requiredValidator = field.required || required;
|
||||
|
||||
useEffect(() => {
|
||||
field.setValidator((value) => {
|
||||
if (requiredValidator) {
|
||||
@ -360,10 +363,44 @@ export const Table: any = observer((props: any) => {
|
||||
},
|
||||
[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 (
|
||||
<div
|
||||
ref={mountedRef}
|
||||
className={css`
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
.ant-table-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
.ant-table {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
@ -372,6 +409,11 @@ export const Table: any = observer((props: any) => {
|
||||
>
|
||||
<SortableWrapper>
|
||||
<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}
|
||||
{...others}
|
||||
{...restProps}
|
||||
@ -380,8 +422,8 @@ export const Table: any = observer((props: any) => {
|
||||
onChange={(pagination, filters, sorter, extra) => {
|
||||
onTableChange?.(pagination, filters, sorter, extra);
|
||||
}}
|
||||
// tableLayout={'auto'}
|
||||
// scroll={{ x: 12 * 300 + 80 }}
|
||||
tableLayout={'auto'}
|
||||
scroll={scroll}
|
||||
columns={columns}
|
||||
dataSource={field?.value?.slice?.()}
|
||||
/>
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { ArrayItems } from '@formily/antd';
|
||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { ISchema, observer, useField, useFieldSchema } from '@formily/react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTableBlockContext } from '../../../block-provider';
|
||||
import { useCollection } from '../../../collection-manager';
|
||||
import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks';
|
||||
import { useRecord } from '../../../record-provider';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { useFixedBlockDesignerSetting } from '../page';
|
||||
|
||||
export const TableBlockDesigner = () => {
|
||||
const { name, title, sortable } = useCollection();
|
||||
@ -34,24 +36,29 @@ export const TableBlockDesigner = () => {
|
||||
});
|
||||
const template = useSchemaTemplate();
|
||||
const { dragSort } = field.decoratorProps;
|
||||
const fixedBlockDesignerSetting = useFixedBlockDesignerSetting();
|
||||
|
||||
return (
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
<SchemaSettings.BlockTitleItem />
|
||||
{sortable&& <SchemaSettings.SwitchItem
|
||||
title={t('Enable drag and drop sorting')}
|
||||
checked={field.decoratorProps.dragSort}
|
||||
onChange={(dragSort) => {
|
||||
field.decoratorProps.dragSort = dragSort;
|
||||
fieldSchema['x-decorator-props'].dragSort = dragSort;
|
||||
service.run({ ...service.params?.[0], sort: 'sort' });
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>}
|
||||
{sortable && (
|
||||
<SchemaSettings.SwitchItem
|
||||
title={t('Enable drag and drop sorting')}
|
||||
checked={field.decoratorProps.dragSort}
|
||||
onChange={(dragSort) => {
|
||||
field.decoratorProps.dragSort = dragSort;
|
||||
fieldSchema['x-decorator-props'].dragSort = dragSort;
|
||||
service.run({ ...service.params?.[0], sort: 'sort' });
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{fixedBlockDesignerSetting}
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Set the data scope')}
|
||||
schema={
|
||||
@ -176,7 +183,6 @@ export const TableBlockDesigner = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SchemaSettings.SelectItem
|
||||
title={t('Records per page')}
|
||||
value={field.decoratorProps?.params?.pageSize || 20}
|
||||
|
@ -1,11 +1,49 @@
|
||||
import { MenuOutlined } from '@ant-design/icons';
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { ISchema, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SchemaInitializer } from '../..';
|
||||
import { SchemaInitializer, SchemaSettings } from '../..';
|
||||
import { useAPIClient } from '../../api-client';
|
||||
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) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
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' }} />}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user