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 { 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}

View File

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

View File

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

View File

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

View File

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

View File

@ -515,4 +515,5 @@ export default {
"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'
}

View File

@ -611,4 +611,6 @@ export default {
"Inner": "里面",
"Search and select collection": "搜索并选择数据表",
'Please fill in the iframe URL': '请填写嵌入的地址',
'Fix block': '固定区块'
}

View File

@ -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`
<div
className={css`
.ant-space:last-child {
margin-left: 8px;
}
`} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
`}
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}
>
<DndContext>
<Space>
{fieldSchema.mapProperties((schema, key) => {

View File

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

View File

@ -1,5 +1,5 @@
.react-kanban-board {
margin-bottom: 24px;
// margin-bottom: 24px;
.nb-block-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 { 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
ref={(ref) => {
setHeight(Math.floor(ref?.getBoundingClientRect().height || 0) + 1);
}}
>
{!disablePageHeader && (
<AntdPageHeader
className={css`
@ -234,10 +243,18 @@ export const Page = (props) => {
}
/>
)}
</div>
<div style={{ margin: 24 }}>
{loading ? (
<Spin />
) : !disablePageHeader && enablePageTabs ? (
) : (
<FixedBlock
height={
// header 46 margin 48
height + 46 + 48
}
>
{!disablePageHeader && enablePageTabs ? (
<RecursionField
schema={fieldSchema}
onlyRenderProperties
@ -258,8 +275,9 @@ export const Page = (props) => {
{props.children}
</div>
)}
</FixedBlock>
)}
</div>
</div>
</>
);
};

View File

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

View File

@ -68,8 +68,38 @@ export const TableColumnDesigner = (props) => {
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
title={t('Sortable')}
checked={field.componentProps.sorter}
@ -79,18 +109,17 @@ export const TableColumnDesigner = (props) => {
};
columnSchema['x-component-props'] = {
...columnSchema['x-component-props'],
sorter: v
}
sorter: v,
};
schema['x-component-props'] = columnSchema['x-component-props'];
field.componentProps.sorter = v;
dn.emit('patch', {
schema
schema,
});
dn.refresh();
}}
/>
)
}
)}
{['linkTo', 'm2m', 'm2o', 'o2m', 'obo', 'oho'].includes(collectionField?.interface) && (
<SchemaSettings.SelectItem
title={t('Title field')}

View File

@ -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?.()}
/>

View File

@ -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,10 +36,13 @@ 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
{sortable && (
<SchemaSettings.SwitchItem
title={t('Enable drag and drop sorting')}
checked={field.decoratorProps.dragSort}
onChange={(dragSort) => {
@ -51,7 +56,9 @@ export const TableBlockDesigner = () => {
},
});
}}
/>}
/>
)}
{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}

View File

@ -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' }} />}
/>