From c731abf82c104ed49b8ed3c4bbbc698d28db04a8 Mon Sep 17 00:00:00 2001 From: Dunqing Date: Tue, 20 Dec 2022 21:43:27 +0800 Subject: [PATCH] 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 --- .../block-provider/KanbanBlockProvider.tsx | 6 +- .../src/block-provider/TableBlockProvider.tsx | 2 + packages/core/client/src/board/Board.tsx | 6 +- packages/core/client/src/board/Column.tsx | 36 ++++- packages/core/client/src/board/style.less | 3 +- packages/core/client/src/locale/en_US.ts | 29 ++-- packages/core/client/src/locale/zh_CN.ts | 36 ++--- .../antd/action/ActionBar.tsx | 22 ++- .../antd/kanban/Kanban.Designer.tsx | 4 + .../schema-component/antd/kanban/index.less | 2 +- .../schema-component/antd/page/FixedBlock.tsx | 128 ++++++++++++++++++ .../src/schema-component/antd/page/Page.tsx | 78 +++++++---- .../src/schema-component/antd/page/index.ts | 1 + .../antd/table-v2/Table.Column.Designer.tsx | 79 +++++++---- .../schema-component/antd/table-v2/Table.tsx | 56 +++++++- .../antd/table-v2/TableBlockDesigner.tsx | 42 +++--- .../buttons/TableActionColumnInitializers.tsx | 50 ++++++- 17 files changed, 451 insertions(+), 129 deletions(-) create mode 100644 packages/core/client/src/schema-component/antd/page/FixedBlock.tsx diff --git a/packages/core/client/src/block-provider/KanbanBlockProvider.tsx b/packages/core/client/src/block-provider/KanbanBlockProvider.tsx index 17947fd52..2b2970d75 100644 --- a/packages/core/client/src/block-provider/KanbanBlockProvider.tsx +++ b/packages/core/client/src/block-provider/KanbanBlockProvider.tsx @@ -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(); + 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} @@ -93,7 +97,7 @@ const useAssociationNames = (collection) => { if (gridSchema) { recursiveProperties(gridSchema, 'CollectionField', associationFields, appends); } - + return uniq(appends); }; diff --git a/packages/core/client/src/block-provider/TableBlockProvider.tsx b/packages/core/client/src/block-provider/TableBlockProvider.tsx index 1442d0261..40c54b407 100644 --- a/packages/core/client/src/block-provider/TableBlockProvider.tsx +++ b/packages/core/client/src/block-provider/TableBlockProvider.tsx @@ -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({}); @@ -14,6 +15,7 @@ const InternalTableBlockProvider = (props) => { // if (service.loading) { // return ; // } + useFixedSchema(); return (
); +const Columns = forwardRef((props, ref: any) => ( +
+)); 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; diff --git a/packages/core/client/src/board/Column.tsx b/packages/core/client/src/board/Column.tsx index 89d38c760..ea5f8fd71 100644 --- a/packages/core/client/src/board/Column.tsx +++ b/packages/core/client/src/board/Column.tsx @@ -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) => ( -
-)); +const ColumnEmptyPlaceholder = forwardRef( + ( + props: { + children: React.ReactNode; + style?: React.CSSProperties; + }, + ref: any, + ) => { + return ( +
+ ); + }, +); const DroppableColumn = withDroppable(ColumnEmptyPlaceholder); @@ -23,6 +34,8 @@ function Column({ allowAddCard, cardAdderPosition = 'top', }) { + const { fixedBlock } = useKanbanBlockContext(); + const [headerHeight, setHeaderHeight] = useState(0); return ( {(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}`} > -
{renderColumnHeader(children)}
+
setHeaderHeight(Math.ceil(ref?.getBoundingClientRect().height || 0)) : null} + {...columnProvided.dragHandleProps} + > + {renderColumnHeader(children)} +
{cardAdderPosition === 'top' && allowAddCard && renderCardAdder({ column: children, onConfirm: onCardNew })} {children?.cards?.length ? ( -
+
{children.cards.map((card, index) => ( { } return (
-
+
{fieldSchema.mapProperties((schema, key) => { diff --git a/packages/core/client/src/schema-component/antd/kanban/Kanban.Designer.tsx b/packages/core/client/src/schema-component/antd/kanban/Kanban.Designer.tsx index 2a5f2291c..8a6904d45 100644 --- a/packages/core/client/src/schema-component/antd/kanban/Kanban.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/kanban/Kanban.Designer.tsx @@ -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 ( @@ -52,6 +55,7 @@ export const KanbanDesigner = () => { }); }} /> + {fixedBlockDesignerSetting} diff --git a/packages/core/client/src/schema-component/antd/kanban/index.less b/packages/core/client/src/schema-component/antd/kanban/index.less index 3f5859a43..5c8ef8bce 100644 --- a/packages/core/client/src/schema-component/antd/kanban/index.less +++ b/packages/core/client/src/schema-component/antd/kanban/index.less @@ -1,5 +1,5 @@ .react-kanban-board { - margin-bottom: 24px; + // margin-bottom: 24px; .nb-block-item { .ant-formily-item-control .ant-space-item{ diff --git a/packages/core/client/src/schema-component/antd/page/FixedBlock.tsx b/packages/core/client/src/schema-component/antd/page/FixedBlock.tsx new file mode 100644 index 000000000..8a1b853bf --- /dev/null +++ b/packages/core/client/src/schema-component/antd/page/FixedBlock.tsx @@ -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 ( + { + 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 = (props) => { + const { height } = props; + const [fixedSchema, setFixedSchema] = useState(); + const schema = useMemo(() => { + if (!fixedSchema || fixedSchema['x-decorator-props']?.fixedBlock !== true) return; + return fixedSchema.parent; + }, [fixedSchema, fixedSchema?.['x-decorator-props']['fixedBlock']]); + + return ( + + {schema ? ( +
+ +
+ ) : ( + props.children + )} +
+ ); +}; + +export default FixedBlock; diff --git a/packages/core/client/src/schema-component/antd/page/Page.tsx b/packages/core/client/src/schema-component/antd/page/Page.tsx index 49da7fff9..03bb7ea99 100644 --- a/packages/core/client/src/schema-component/antd/page/Page.tsx +++ b/packages/core/client/src/schema-component/antd/page/Page.tsx @@ -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 ( - <> -
- +
+ +
{ + setHeight(Math.floor(ref?.getBoundingClientRect().height || 0) + 1); + }} + > {!disablePageHeader && ( { } /> )} -
- {loading ? ( - - ) : !disablePageHeader && enablePageTabs ? ( - { - return s.name === activeKey; - }} - /> - ) : ( -
.nb-schema-initializer-button { - display: none; - } - } - `} - > - {props.children} -
- )} -
- +
+ {loading ? ( + + ) : ( + + {!disablePageHeader && enablePageTabs ? ( + { + return s.name === activeKey; + }} + /> + ) : ( +
.nb-schema-initializer-button { + display: none; + } + } + `} + > + {props.children} +
+ )} +
+ )} +
+
); }; diff --git a/packages/core/client/src/schema-component/antd/page/index.ts b/packages/core/client/src/schema-component/antd/page/index.ts index d9925d752..8e2a9a439 100644 --- a/packages/core/client/src/schema-component/antd/page/index.ts +++ b/packages/core/client/src/schema-component/antd/page/index.ts @@ -1 +1,2 @@ export * from './Page'; +export * from './FixedBlock'; diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx index f152c2180..dd8d6aa0a 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx @@ -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, @@ -33,7 +33,7 @@ export const TableColumnDesigner = (props) => { fieldSchema?.['x-component-props']?.['fieldNames'] || uiSchema?.['x-component-props']?.['fieldNames']; const options = useLabelFields(collectionField?.target); const intefaceCfg = getInterface(collectionField?.interface); - + return ( { dn.refresh(); }} /> - { - intefaceCfg && intefaceCfg.sortable === true && ( - { - 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(); - }} - /> - ) - } + { + 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 && ( + { + 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) && ( { @@ -24,6 +24,7 @@ const useTableColumns = () => { const start = Date.now(); const field = useField(); 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: , 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 ? () =>
: 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(); + const calcTableSize = () => { + if (!elementRef.current) return; + const clientRect = elementRef.current?.getBoundingClientRect(); + setTableHeight(Math.ceil(clientRect?.height || 0)); + }; + useEventListener('resize', calcTableSize); + + const mountedRef: RefCallback = (ref) => { + elementRef.current = ref; + calcTableSize(); + }; return (
{ > { + 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?.()} /> diff --git a/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx b/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx index 4c0aef8f7..dce7a8c51 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx @@ -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 ( - {sortable&& { - 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 && ( + { + 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} { }} /> )} - { + const { t } = useTranslation(); + const { dn } = useDesignable(); + const fieldSchema = useFieldSchema(); + return ( + { + 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={} />