mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
feat: table performance (#3791)
* fix: table add useMemo and useCallback * fix: memo bug * fix: sub table bug * fix: form item performance * fix: settings center performance impove * fix: bug * fix: bug * fix: form first value change check performance * fix: revert first form change * fix: css move out component * fix: page change table should not render * fix: pre process merge bug * fix: assotion bug * fix: input and useDeppMemoized performance * fix: bug * fix: bug * fix: improve Action.tsx lazy show content * fix: remove Action performance imporve * fix: assocication read pretty not loading * fix: cssInJs imporve * fix: imporve kanban rerender * fix: remove useless CurrentAppInfoProvider in plugin * fix: divide the schema into several parts * fix: tabs.tsx and Page.tsx divide * fix: form-item imporve * fix: add OverrideSchemaComponentRefresher * fix: page and tabs bug * fix: workflow bug * fix: remove useDeepMemorized() * fix: e2e bug * fix: internal Tag and viewer * fix: collection field read pretty mode skip * fix: others performance * fix: revert collection field read pretty * fix: table column not render when value is null or undefined * fix: table and grid add view check * fix: kanban lazy render * fix: remove table useWhyDidYouUpdate * fix: table index skip loading * fix: card drag rerender loading * fix: e2e skip lazy render * fix: e2e bug * fix: action e2e bug * fix: grid and kanban card * fix: remove override refresher component * fix: unit test bug * fix: change schema component props name * fix: e2e and unit test bug * fix: e2e bug * fix: not lazy render when data length less 10 (T-3784) * chore: fix merge * chore: fix e2e * fix: drag bug (T-3807) * fix: repetitive refresh (T-3729) * fix: pre fix merge confict --------- Co-authored-by: Zeke Zhang <958414905@qq.com>
This commit is contained in:
parent
7b073727b1
commit
8a1345a5b8
@ -6,6 +6,7 @@
|
||||
"module": "es/index.mjs",
|
||||
"types": "es/index.d.ts",
|
||||
"dependencies": {
|
||||
"react-intersection-observer": "9.8.1",
|
||||
"@ahooksjs/use-url-state": "3.5.1",
|
||||
"@ant-design/cssinjs": "^1.11.1",
|
||||
"@ant-design/icons": "^5.1.4",
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Field } from '@formily/core';
|
||||
import { Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import React, { createContext, useContext, useEffect } from 'react';
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { omit } from 'lodash';
|
||||
import { useAPIClient, useRequest } from '../api-client';
|
||||
@ -253,28 +253,33 @@ export const ACLActionProvider = (props) => {
|
||||
|
||||
export const useACLFieldWhitelist = () => {
|
||||
const params = useContext(ACLActionParamsContext);
|
||||
const whitelist = []
|
||||
.concat(params?.whitelist || [])
|
||||
.concat(params?.fields || [])
|
||||
.concat(params?.appends || []);
|
||||
const whitelist = useMemo(() => {
|
||||
return []
|
||||
.concat(params?.whitelist || [])
|
||||
.concat(params?.fields || [])
|
||||
.concat(params?.appends || []);
|
||||
}, [params?.whitelist, params?.fields, params?.appends]);
|
||||
return {
|
||||
whitelist,
|
||||
schemaInWhitelist(fieldSchema: Schema, isSkip?) {
|
||||
if (isSkip) {
|
||||
return true;
|
||||
}
|
||||
if (whitelist.length === 0) {
|
||||
return true;
|
||||
}
|
||||
if (!fieldSchema) {
|
||||
return true;
|
||||
}
|
||||
if (!fieldSchema['x-collection-field']) {
|
||||
return true;
|
||||
}
|
||||
const [key1, key2] = fieldSchema['x-collection-field'].split('.');
|
||||
return whitelist?.includes(key2 || key1);
|
||||
},
|
||||
schemaInWhitelist: useCallback(
|
||||
(fieldSchema: Schema, isSkip?) => {
|
||||
if (isSkip) {
|
||||
return true;
|
||||
}
|
||||
if (whitelist.length === 0) {
|
||||
return true;
|
||||
}
|
||||
if (!fieldSchema) {
|
||||
return true;
|
||||
}
|
||||
if (!fieldSchema['x-collection-field']) {
|
||||
return true;
|
||||
}
|
||||
const [key1, key2] = fieldSchema['x-collection-field'].split('.');
|
||||
return whitelist?.includes(key2 || key1);
|
||||
},
|
||||
[whitelist],
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@ -290,7 +295,7 @@ export const ACLCollectionFieldProvider = (props) => {
|
||||
field.required = false;
|
||||
field.display = 'hidden';
|
||||
}
|
||||
}, [allowed]);
|
||||
}, [allowed, field]);
|
||||
|
||||
if (allowAll) {
|
||||
return <>{props.children}</>;
|
||||
|
@ -121,9 +121,9 @@ export class RouterManager {
|
||||
};
|
||||
|
||||
const ReactRouter = Routers[type];
|
||||
const routes = this.getRoutesTree();
|
||||
|
||||
const RenderRoutes = () => {
|
||||
const routes = this.getRoutesTree();
|
||||
const element = useRoutes(routes);
|
||||
return element;
|
||||
};
|
||||
|
@ -58,6 +58,17 @@ export function withInitializer<T>(C: ComponentType<T>) {
|
||||
[componentProps, props, style],
|
||||
);
|
||||
|
||||
const overlayClassName = useMemo(() => {
|
||||
return css`
|
||||
.ant-popover-inner {
|
||||
padding: ${`${token.paddingXXS}px 0`};
|
||||
.ant-menu-submenu-title {
|
||||
margin-block: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}, [token.paddingXXS]);
|
||||
|
||||
// designable 为 false 时,不渲染
|
||||
if (!designable && propsDesignable !== true) {
|
||||
return null;
|
||||
@ -79,14 +90,7 @@ export function withInitializer<T>(C: ComponentType<T>) {
|
||||
placement={'bottomLeft'}
|
||||
{...popoverProps}
|
||||
arrow={false}
|
||||
overlayClassName={css`
|
||||
.ant-popover-inner {
|
||||
padding: ${`${token.paddingXXS}px 0`};
|
||||
.ant-menu-submenu-title {
|
||||
margin-block: 0;
|
||||
}
|
||||
}
|
||||
`}
|
||||
overlayClassName={overlayClassName}
|
||||
open={visible}
|
||||
onOpenChange={setVisible}
|
||||
content={wrapSSR(
|
||||
|
@ -136,19 +136,13 @@ export const RenderChildrenWithAssociationFilter: React.FC<any> = (props) => {
|
||||
if (associationFilterSchema) {
|
||||
return (
|
||||
<Component {...field.componentProps}>
|
||||
<Row
|
||||
className={css`
|
||||
height: 100%;
|
||||
`}
|
||||
gutter={16}
|
||||
wrap={false}
|
||||
>
|
||||
<Row style={{ height: '100%' }} gutter={16} wrap={false}>
|
||||
<Col
|
||||
className={css`
|
||||
width: 200px;
|
||||
flex: 0 0 auto;
|
||||
`}
|
||||
style={props.associationFilterStyle}
|
||||
style={{
|
||||
...(props.associationFilterStyle || {}),
|
||||
width: 200,
|
||||
flex: '0 0 auto',
|
||||
}}
|
||||
>
|
||||
<RecursionField
|
||||
schema={fieldSchema}
|
||||
@ -157,17 +151,17 @@ export const RenderChildrenWithAssociationFilter: React.FC<any> = (props) => {
|
||||
/>
|
||||
</Col>
|
||||
<Col
|
||||
className={css`
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
`}
|
||||
style={{
|
||||
flex: '1 1 auto',
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`}
|
||||
style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<RecursionField
|
||||
schema={fieldSchema}
|
||||
|
@ -81,7 +81,11 @@ const InternalFormBlockProvider = (props) => {
|
||||
*/
|
||||
export const useFormBlockType = () => {
|
||||
const ctx = useFormBlockContext() || {};
|
||||
return { type: ctx.type } as { type: 'update' | 'create' };
|
||||
const res = useMemo(() => {
|
||||
return { type: ctx.type } as { type: 'update' | 'create' };
|
||||
}, [ctx.type]);
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const useIsDetailBlock = () => {
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { createForm } from '@formily/core';
|
||||
import { FormContext, useField, useFieldSchema } from '@formily/react';
|
||||
import React, { createContext, useContext, useMemo, useState } from 'react';
|
||||
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { useCollectionManager_deprecated } from '../collection-manager';
|
||||
import { FixedBlockWrapper, SchemaComponentOptions } from '../schema-component';
|
||||
import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider';
|
||||
import { useParsedFilter } from './hooks';
|
||||
import { useTableBlockParams } from '../modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps';
|
||||
import { useTableBlockParams } from '../modules/blocks/data-blocks/table';
|
||||
import { withDynamicSchemaProps } from '../application/hoc/withDynamicSchemaProps';
|
||||
|
||||
export const TableBlockContext = createContext<any>({});
|
||||
@ -41,14 +40,19 @@ const InternalTableBlockProvider = (props: Props) => {
|
||||
const field: any = useField();
|
||||
const { resource, service } = useBlockRequestContext();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { treeTable } = fieldSchema?.['x-decorator-props'] || {};
|
||||
const [expandFlag, setExpandFlag] = useState(fieldNames ? true : false);
|
||||
const allIncludesChildren = useMemo(() => {
|
||||
const { treeTable } = fieldSchema?.['x-decorator-props'] || {};
|
||||
const data = service?.data?.data;
|
||||
if (treeTable !== false) {
|
||||
const keys = getIdsWithChildren(service?.data?.data);
|
||||
return keys || [];
|
||||
const keys = getIdsWithChildren(data);
|
||||
return keys;
|
||||
}
|
||||
}, [service?.loading]);
|
||||
}, [service?.loading, fieldSchema]);
|
||||
|
||||
const setExpandFlagValue = useCallback(() => {
|
||||
setExpandFlag(!expandFlag);
|
||||
}, [expandFlag]);
|
||||
|
||||
return (
|
||||
<FixedBlockWrapper>
|
||||
@ -65,7 +69,7 @@ const InternalTableBlockProvider = (props: Props) => {
|
||||
expandFlag,
|
||||
childrenColumnName,
|
||||
allIncludesChildren,
|
||||
setExpandFlag: () => setExpandFlag(!expandFlag),
|
||||
setExpandFlag: setExpandFlagValue,
|
||||
}}
|
||||
>
|
||||
<RenderChildrenWithAssociationFilter {...props} />
|
||||
|
@ -34,21 +34,16 @@ export const CollectionHistoryProvider: React.FC = (props) => {
|
||||
|
||||
// console.log('location', location);
|
||||
|
||||
const service = useRequest<{
|
||||
data: any;
|
||||
}>(options, {
|
||||
manual: true,
|
||||
});
|
||||
|
||||
const isAdminPage = location.pathname.startsWith('/admin');
|
||||
const token = api.auth.getToken() || '';
|
||||
const { render } = useAppSpin();
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdminPage && token) {
|
||||
service.run();
|
||||
}
|
||||
}, [isAdminPage, token]);
|
||||
const service = useRequest<{
|
||||
data: any;
|
||||
}>(options, {
|
||||
refreshDeps: [isAdminPage, token],
|
||||
ready: !!(isAdminPage && token),
|
||||
});
|
||||
|
||||
// 刷新 collecionHistory
|
||||
const refreshCH = async () => {
|
||||
|
@ -348,6 +348,7 @@ export const AddFieldAction = (props) => {
|
||||
</Dropdown>
|
||||
<SchemaComponent
|
||||
schema={schema}
|
||||
distributed={false}
|
||||
components={{ ...components, ArrayTable }}
|
||||
scope={{
|
||||
getContainer,
|
||||
|
@ -236,6 +236,7 @@ export const EditFieldAction = (props) => {
|
||||
</a>
|
||||
<SchemaComponent
|
||||
schema={schema}
|
||||
distributed={false}
|
||||
components={{ ...components, ArrayTable }}
|
||||
scope={{
|
||||
getContainer,
|
||||
|
@ -2,7 +2,7 @@ import { Field } from '@formily/core';
|
||||
import { connect, useField, useFieldSchema } from '@formily/react';
|
||||
import { merge } from '@formily/shared';
|
||||
import { concat } from 'lodash';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useFormBlockContext } from '../../block-provider/FormBlockProvider';
|
||||
import { useCompile, useComponent } from '../../schema-component';
|
||||
import { useIsAllowToSetDefaultValue } from '../../schema-settings/hooks/useIsAllowToSetDefaultValue';
|
||||
@ -26,14 +26,17 @@ export const CollectionFieldInternalField: React.FC = (props: Props) => {
|
||||
const { isAllowToSetDefaultValue } = useIsAllowToSetDefaultValue();
|
||||
const uiSchema = useMemo(() => compile(uiSchemaOrigin), [JSON.stringify(uiSchemaOrigin)]);
|
||||
const Component = useComponent(component || uiSchema?.['x-component'] || 'Input');
|
||||
const setFieldProps = (key, value) => {
|
||||
field[key] = typeof field[key] === 'undefined' ? value : field[key];
|
||||
};
|
||||
const setRequired = () => {
|
||||
const setFieldProps = useCallback(
|
||||
(key, value) => {
|
||||
field[key] = typeof field[key] === 'undefined' ? value : field[key];
|
||||
},
|
||||
[field],
|
||||
);
|
||||
const setRequired = useCallback(() => {
|
||||
if (typeof fieldSchema['required'] === 'undefined') {
|
||||
field.required = !!uiSchema['required'];
|
||||
}
|
||||
};
|
||||
}, [fieldSchema, uiSchema]);
|
||||
const ctx = useFormBlockContext();
|
||||
|
||||
useEffect(() => {
|
||||
@ -71,7 +74,7 @@ export const CollectionFieldInternalField: React.FC = (props: Props) => {
|
||||
const originalProps = compile(uiSchema['x-component-props']) || {};
|
||||
const componentProps = merge(originalProps, field.componentProps || {});
|
||||
field.component = [Component, componentProps];
|
||||
}, [JSON.stringify(uiSchema)]);
|
||||
}, [uiSchema]);
|
||||
if (!uiSchema) {
|
||||
return null;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import { CollectionRecordProvider, CollectionRecord } from '../collection-record
|
||||
import { AllDataBlockProps, useDataBlockProps } from './DataBlockProvider';
|
||||
import { useDataBlockResource } from './DataBlockResourceProvider';
|
||||
import { useDataSourceHeaders } from '../utils';
|
||||
import _ from 'lodash';
|
||||
import { useDataLoadingMode } from '../../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem';
|
||||
import { useCollection, useCollectionManager } from '../collection';
|
||||
|
||||
@ -16,11 +17,8 @@ function useCurrentRequest<T>(options: Omit<AllDataBlockProps, 'type'>) {
|
||||
const dataLoadingMode = useDataLoadingMode();
|
||||
const resource = useDataBlockResource();
|
||||
const { action, params = {}, record, requestService, requestOptions } = options;
|
||||
if (params.filterByTk === undefined) {
|
||||
delete params.filterByTk;
|
||||
}
|
||||
const request = useRequest<T>(
|
||||
requestService
|
||||
const service = useMemo(() => {
|
||||
return requestService
|
||||
? requestService
|
||||
: (customParams) => {
|
||||
if (record) return Promise.resolve({ data: record });
|
||||
@ -29,13 +27,16 @@ function useCurrentRequest<T>(options: Omit<AllDataBlockProps, 'type'>) {
|
||||
`[nocobase]: The 'action' parameter is missing in the 'DataBlockRequestProvider' component`,
|
||||
);
|
||||
}
|
||||
return resource[action]({ ...params, ...customParams }).then((res) => res.data);
|
||||
},
|
||||
{
|
||||
...requestOptions,
|
||||
manual: true,
|
||||
},
|
||||
);
|
||||
const paramsValue = params.filterByTk === undefined ? _.omit(params, 'filterByTk') : params;
|
||||
|
||||
return resource[action]({ ...paramsValue, ...customParams }).then((res) => res.data);
|
||||
};
|
||||
}, [resource, action, params, record, requestService]);
|
||||
|
||||
const request = useRequest<T>(service, {
|
||||
...requestOptions,
|
||||
manual: true,
|
||||
});
|
||||
|
||||
// 因为修改 Schema 会导致 params 对象发生变化,所以这里使用 `DeepCompare`
|
||||
useDeepCompareEffect(() => {
|
||||
|
@ -6,7 +6,6 @@ import { CollectionFieldOptions_deprecated, useCollection_deprecated } from '../
|
||||
import { removeNullCondition } from '../schema-component';
|
||||
import { mergeFilter, useAssociatedFields } from './utils';
|
||||
import { useDataLoadingMode } from '../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem';
|
||||
import { GeneralField } from '@formily/core';
|
||||
|
||||
enum FILTER_OPERATOR {
|
||||
AND = '$and',
|
||||
@ -161,11 +160,13 @@ export const DataBlockCollector = ({
|
||||
export const useFilterBlock = () => {
|
||||
const ctx = React.useContext(FilterContext);
|
||||
// 有可能存在页面没有提供 FilterBlockProvider 的情况,比如内部使用的数据表管理页面
|
||||
const getDataBlocks = useCallback<() => DataBlock[]>(() => ctx?.dataBlocks || [], [ctx?.dataBlocks]);
|
||||
|
||||
if (!ctx) {
|
||||
return {
|
||||
inProvider: false,
|
||||
recordDataBlocks: () => {},
|
||||
getDataBlocks: () => [] as DataBlock[],
|
||||
getDataBlocks,
|
||||
removeDataBlock: () => {},
|
||||
};
|
||||
}
|
||||
@ -183,8 +184,10 @@ export const useFilterBlock = () => {
|
||||
// 由于 setDataBlocks 是异步操作,所以上面的 existingBlock 在判断时有可能用的是旧的 dataBlocks,所以下面还需要根据 uid 进行去重操作
|
||||
setDataBlocks((prev) => uniqBy([...prev, block], 'uid'));
|
||||
};
|
||||
const getDataBlocks = () => dataBlocks;
|
||||
const removeDataBlock = (uid: string) => {
|
||||
const blocks = dataBlocks.filter((item) => item.uid !== uid);
|
||||
if (blocks.length === dataBlocks.length) return;
|
||||
|
||||
setDataBlocks((prev) => prev.filter((item) => item.uid !== uid));
|
||||
};
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { expect, oneEmptyTableBlockBasedOnUsers, test } from '@nocobase/test/e2e
|
||||
test('actions', async ({ page, mockPage }) => {
|
||||
await mockPage(oneEmptyTableBlockBasedOnUsers).goto();
|
||||
await page.getByLabel('schema-initializer-ActionBar-table:configureActions-users').hover();
|
||||
//添加按钮
|
||||
// 添加按钮
|
||||
await page.getByRole('menuitem', { name: 'Add new' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Refresh' }).click();
|
||||
@ -24,9 +24,9 @@ test('actions', async ({ page, mockPage }) => {
|
||||
const addNew = await addNewBtn.boundingBox();
|
||||
const del = await delBtn.boundingBox();
|
||||
const refresh = await refreshBtn.boundingBox();
|
||||
const max = Math.max(addNew.x, refresh.x, del.x);
|
||||
//拖拽调整排序符合预期
|
||||
expect(addNew.x).toBe(max);
|
||||
|
||||
expect(addNew.x).toBeGreaterThan(refresh.x);
|
||||
expect(refresh.x).toBeGreaterThan(del.x);
|
||||
});
|
||||
|
||||
test('fields', async () => {});
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { ArrayField } from '@formily/core';
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import { useEffect } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useFilterBlock } from '../../../../../filter-provider/FilterProvider';
|
||||
import { mergeFilter } from '../../../../../filter-provider/utils';
|
||||
import { removeNullCondition } from '../../../../../schema-component';
|
||||
import { findFilterTargets } from '../../../../../block-provider/hooks';
|
||||
import { useTableBlockContext } from '../../../../../block-provider/TableBlockProvider';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
export const useTableBlockProps = () => {
|
||||
const field = useField<ArrayField>();
|
||||
@ -13,109 +14,135 @@ export const useTableBlockProps = () => {
|
||||
const ctx = useTableBlockContext();
|
||||
const globalSort = fieldSchema.parent?.['x-decorator-props']?.['params']?.['sort'];
|
||||
const { getDataBlocks } = useFilterBlock();
|
||||
|
||||
const isLoading = ctx?.service?.loading;
|
||||
const params = useMemo(() => ctx?.service?.params, [JSON.stringify(ctx?.service?.params)]);
|
||||
useEffect(() => {
|
||||
if (!ctx?.service?.loading) {
|
||||
field.value = [];
|
||||
field.value = ctx?.service?.data?.data;
|
||||
field?.setInitialValue(ctx?.service?.data?.data);
|
||||
if (!isLoading) {
|
||||
const serviceResponse = ctx?.service?.data;
|
||||
const data = serviceResponse?.data || [];
|
||||
const meta = serviceResponse?.meta || {};
|
||||
const selectedRowKeys = ctx?.field?.data?.selectedRowKeys;
|
||||
|
||||
if (!isEqual(field.value, data)) {
|
||||
field.value = data;
|
||||
field?.setInitialValue(data);
|
||||
}
|
||||
field.data = field.data || {};
|
||||
field.data.selectedRowKeys = ctx?.field?.data?.selectedRowKeys;
|
||||
|
||||
if (!isEqual(field.data.selectedRowKeys, selectedRowKeys)) {
|
||||
field.data.selectedRowKeys = selectedRowKeys;
|
||||
}
|
||||
|
||||
field.componentProps.pagination = field.componentProps.pagination || {};
|
||||
field.componentProps.pagination.pageSize = ctx?.service?.data?.meta?.pageSize;
|
||||
field.componentProps.pagination.total = ctx?.service?.data?.meta?.count;
|
||||
field.componentProps.pagination.current = ctx?.service?.data?.meta?.page;
|
||||
field.componentProps.pagination.pageSize = meta?.pageSize;
|
||||
field.componentProps.pagination.total = meta?.count;
|
||||
field.componentProps.pagination.current = meta?.page;
|
||||
}
|
||||
}, [ctx?.service?.data, ctx?.service?.loading]); // 这里如果依赖了 ctx?.field?.data?.selectedRowKeys 的话,会导致这个问题:
|
||||
}, [field, ctx?.service?.data, isLoading]);
|
||||
|
||||
return {
|
||||
childrenColumnName: ctx.childrenColumnName,
|
||||
loading: ctx?.service?.loading,
|
||||
showIndex: ctx.showIndex,
|
||||
dragSort: ctx.dragSort && ctx.dragSortBy,
|
||||
rowKey: ctx.rowKey || 'id',
|
||||
pagination:
|
||||
ctx?.params?.paginate !== false
|
||||
pagination: useMemo(() => {
|
||||
return params?.paginate !== false
|
||||
? {
|
||||
defaultCurrent: ctx?.params?.page || 1,
|
||||
defaultPageSize: ctx?.params?.pageSize,
|
||||
}
|
||||
: false,
|
||||
onRowSelectionChange(selectedRowKeys) {
|
||||
defaultCurrent: params?.page || 1,
|
||||
defaultPageSize: params?.pageSize,
|
||||
}
|
||||
: false;
|
||||
}, [params?.page, params?.pageSize, params?.paginate]),
|
||||
onRowSelectionChange: useCallback((selectedRowKeys) => {
|
||||
ctx.field.data = ctx?.field?.data || {};
|
||||
ctx.field.data.selectedRowKeys = selectedRowKeys;
|
||||
ctx?.field?.onRowSelect?.(selectedRowKeys);
|
||||
},
|
||||
async onRowDragEnd({ from, to }) {
|
||||
await ctx.resource.move({
|
||||
sourceId: from[ctx.rowKey || 'id'],
|
||||
targetId: to[ctx.rowKey || 'id'],
|
||||
sortField: ctx.dragSort && ctx.dragSortBy,
|
||||
});
|
||||
ctx.service.refresh();
|
||||
},
|
||||
onChange({ current, pageSize }, filters, sorter) {
|
||||
const sort = sorter.order ? (sorter.order === `ascend` ? [sorter.field] : [`-${sorter.field}`]) : globalSort;
|
||||
ctx.service.run({ ...ctx.service.params?.[0], page: current, pageSize, sort });
|
||||
},
|
||||
onClickRow(record, setSelectedRow, selectedRow) {
|
||||
const { targets, uid } = findFilterTargets(fieldSchema);
|
||||
const dataBlocks = getDataBlocks();
|
||||
}, []),
|
||||
onRowDragEnd: useCallback(
|
||||
async ({ from, to }) => {
|
||||
await ctx.resource.move({
|
||||
sourceId: from[ctx.rowKey || 'id'],
|
||||
targetId: to[ctx.rowKey || 'id'],
|
||||
sortField: ctx.dragSort && ctx.dragSortBy,
|
||||
});
|
||||
ctx.service.refresh();
|
||||
// ctx.resource
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
},
|
||||
[ctx.rowKey, ctx.dragSort, ctx.dragSortBy],
|
||||
),
|
||||
onChange: useCallback(
|
||||
({ current, pageSize }, filters, sorter) => {
|
||||
const sort = sorter.order ? (sorter.order === `ascend` ? [sorter.field] : [`-${sorter.field}`]) : globalSort;
|
||||
ctx.service.run({ ...params?.[0], page: current, pageSize, sort });
|
||||
// ctx.service
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
},
|
||||
[globalSort, params],
|
||||
),
|
||||
onClickRow: useCallback(
|
||||
(record, setSelectedRow, selectedRow) => {
|
||||
const { targets, uid } = findFilterTargets(fieldSchema);
|
||||
const dataBlocks = getDataBlocks();
|
||||
|
||||
// 如果是之前创建的区块是没有 x-filter-targets 属性的,所以这里需要判断一下避免报错
|
||||
if (!targets || !targets.some((target) => dataBlocks.some((dataBlock) => dataBlock.uid === target.uid))) {
|
||||
// 当用户已经点击过某一行,如果此时再把相连接的区块给删除的话,行的高亮状态就会一直保留。
|
||||
// 这里暂时没有什么比较好的方法,只是在用户再次点击的时候,把高亮状态给清除掉。
|
||||
setSelectedRow((prev) => (prev.length ? [] : prev));
|
||||
return;
|
||||
}
|
||||
|
||||
const value = [record[ctx.rowKey]];
|
||||
|
||||
dataBlocks.forEach((block) => {
|
||||
const target = targets.find((target) => target.uid === block.uid);
|
||||
if (!target) return;
|
||||
|
||||
const param = block.service.params?.[0] || {};
|
||||
// 保留原有的 filter
|
||||
const storedFilter = block.service.params?.[1]?.filters || {};
|
||||
|
||||
if (selectedRow.includes(record[ctx.rowKey])) {
|
||||
if (block.dataLoadingMode === 'manual') {
|
||||
return block.clearData();
|
||||
}
|
||||
delete storedFilter[uid];
|
||||
} else {
|
||||
storedFilter[uid] = {
|
||||
$and: [
|
||||
{
|
||||
[target.field || ctx.rowKey]: {
|
||||
[target.field ? '$in' : '$eq']: value,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
// 如果是之前创建的区块是没有 x-filter-targets 属性的,所以这里需要判断一下避免报错
|
||||
if (!targets || !targets.some((target) => dataBlocks.some((dataBlock) => dataBlock.uid === target.uid))) {
|
||||
// 当用户已经点击过某一行,如果此时再把相连接的区块给删除的话,行的高亮状态就会一直保留。
|
||||
// 这里暂时没有什么比较好的方法,只是在用户再次点击的时候,把高亮状态给清除掉。
|
||||
setSelectedRow((prev) => (prev.length ? [] : prev));
|
||||
return;
|
||||
}
|
||||
|
||||
const mergedFilter = mergeFilter([
|
||||
...Object.values(storedFilter).map((filter) => removeNullCondition(filter)),
|
||||
block.defaultFilter,
|
||||
]);
|
||||
const value = [record[ctx.rowKey]];
|
||||
|
||||
return block.doFilter(
|
||||
{
|
||||
...param,
|
||||
page: 1,
|
||||
filter: mergedFilter,
|
||||
},
|
||||
{ filters: storedFilter },
|
||||
);
|
||||
});
|
||||
dataBlocks.forEach((block) => {
|
||||
const target = targets.find((target) => target.uid === block.uid);
|
||||
if (!target) return;
|
||||
|
||||
// 更新表格的选中状态
|
||||
setSelectedRow((prev) => (prev?.includes(record[ctx.rowKey]) ? [] : [...value]));
|
||||
},
|
||||
onExpand(expanded, record) {
|
||||
const param = block.service.params?.[0] || {};
|
||||
// 保留原有的 filter
|
||||
const storedFilter = block.service.params?.[1]?.filters || {};
|
||||
|
||||
if (selectedRow.includes(record[ctx.rowKey])) {
|
||||
if (block.dataLoadingMode === 'manual') {
|
||||
return block.clearData();
|
||||
}
|
||||
delete storedFilter[uid];
|
||||
} else {
|
||||
storedFilter[uid] = {
|
||||
$and: [
|
||||
{
|
||||
[target.field || ctx.rowKey]: {
|
||||
[target.field ? '$in' : '$eq']: value,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const mergedFilter = mergeFilter([
|
||||
...Object.values(storedFilter).map((filter) => removeNullCondition(filter)),
|
||||
block.defaultFilter,
|
||||
]);
|
||||
|
||||
return block.doFilter(
|
||||
{
|
||||
...param,
|
||||
page: 1,
|
||||
filter: mergedFilter,
|
||||
},
|
||||
{ filters: storedFilter },
|
||||
);
|
||||
});
|
||||
|
||||
// 更新表格的选中状态
|
||||
setSelectedRow((prev) => (prev?.includes(record[ctx.rowKey]) ? [] : [...value]));
|
||||
},
|
||||
[ctx.rowKey, fieldSchema, getDataBlocks],
|
||||
),
|
||||
onExpand: useCallback((expanded, record) => {
|
||||
ctx?.field.onExpandClick?.(expanded, record);
|
||||
},
|
||||
}, []),
|
||||
};
|
||||
};
|
||||
|
@ -12,10 +12,10 @@ import { useDesignable, removeNullCondition } from '../../../../schema-component
|
||||
import {
|
||||
SchemaSettingsBlockTitleItem,
|
||||
SchemaSettingsSortField,
|
||||
SchemaSettingsDataScope,
|
||||
SchemaSettingsConnectDataBlocks,
|
||||
SchemaSettingsTemplate,
|
||||
} from '../../../../schema-settings';
|
||||
} from '../../../../schema-settings/SchemaSettings';
|
||||
import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope'
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ArrayItems } from '@formily/antd-v5';
|
||||
|
@ -7,8 +7,9 @@ import { useCollectionManager_deprecated } from '../../../../collection-manager'
|
||||
import { useDesignable } from '../../../../schema-component';
|
||||
import { useAssociationFieldContext } from '../../../../schema-component/antd/association-field/hooks';
|
||||
import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator';
|
||||
import { SchemaSettingsDefaultValue, isPatternDisabled } from '../../../../schema-settings';
|
||||
import { SchemaSettingsDefaultValue } from '../../../../schema-settings/SchemaSettingsDefaultValue';
|
||||
import { useFieldComponentName } from './utils';
|
||||
import { isPatternDisabled } from '../../../../schema-settings/isPatternDisabled';
|
||||
|
||||
export const tableColumnSettings = new SchemaSettings({
|
||||
name: 'fieldSettings:TableColumn',
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { ApiOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { Button, Card, Dropdown, Popover, Tooltip } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Dropdown, Tooltip } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useApp } from '../application';
|
||||
import { ActionContextProvider, useCompile } from '../schema-component';
|
||||
import { useCompile } from '../schema-component';
|
||||
import { useToken } from '../style';
|
||||
|
||||
export const PluginManagerLink = () => {
|
||||
@ -27,14 +26,22 @@ export const PluginManagerLink = () => {
|
||||
};
|
||||
|
||||
export const SettingsCenterDropdown = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const compile = useCompile();
|
||||
const { t } = useTranslation();
|
||||
const { token } = useToken();
|
||||
const navigate = useNavigate();
|
||||
const app = useApp();
|
||||
const settings = app.pluginSettingsManager.getList();
|
||||
const [open, setOpen] = useState(false);
|
||||
const settingItems = useMemo(() => {
|
||||
const settings = app.pluginSettingsManager.getList();
|
||||
return settings
|
||||
.filter((v) => v.isTopLevel !== false)
|
||||
.map((setting) => {
|
||||
return {
|
||||
key: setting.name,
|
||||
icon: setting.icon,
|
||||
label: <Link to={setting.path}>{compile(setting.title)}</Link>,
|
||||
};
|
||||
});
|
||||
}, [app, t]);
|
||||
return (
|
||||
<Dropdown
|
||||
menu={{
|
||||
@ -42,15 +49,7 @@ export const SettingsCenterDropdown = () => {
|
||||
maxHeight: '70vh',
|
||||
overflow: 'auto',
|
||||
},
|
||||
items: settings
|
||||
.filter((v) => v.isTopLevel !== false)
|
||||
.map((setting) => {
|
||||
return {
|
||||
key: setting.name,
|
||||
icon: setting.icon,
|
||||
label: <Link to={setting.path}>{compile(setting.title)}</Link>,
|
||||
};
|
||||
}),
|
||||
items: settingItems,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
@ -60,82 +59,4 @@ export const SettingsCenterDropdown = () => {
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
return (
|
||||
settings.length > 0 && (
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
setOpen(open);
|
||||
}}
|
||||
arrow={false}
|
||||
content={
|
||||
<div style={{ maxWidth: '21rem', overflow: 'auto', maxHeight: '50vh' }}>
|
||||
<Card
|
||||
bordered={false}
|
||||
className={css`
|
||||
box-shadow: none;
|
||||
`}
|
||||
style={{ boxShadow: 'none' }}
|
||||
>
|
||||
{settings
|
||||
.filter((v) => v.isTopLevel !== false)
|
||||
.map((setting) => (
|
||||
<Card.Grid
|
||||
style={{
|
||||
width: settings.length === 1 ? '100%' : settings.length === 2 ? '50%' : '33.33%',
|
||||
}}
|
||||
className={css`
|
||||
cursor: pointer;
|
||||
padding: 0 !important;
|
||||
box-shadow: none !important;
|
||||
&:hover {
|
||||
border-radius: ${token.borderRadius}px;
|
||||
background: rgba(0, 0, 0, 0.045);
|
||||
}
|
||||
`}
|
||||
key={setting.name}
|
||||
>
|
||||
<a
|
||||
role="button"
|
||||
aria-label={setting.name}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setOpen(false);
|
||||
navigate(setting.path);
|
||||
}}
|
||||
title={compile(setting.title)}
|
||||
style={{ display: 'block', color: 'inherit', minWidth: '4.5rem', padding: token.marginSM }}
|
||||
href={setting.path}
|
||||
>
|
||||
<div style={{ fontSize: '1.2rem', textAlign: 'center', marginBottom: '0.3rem' }}>
|
||||
{setting.icon || <SettingOutlined />}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
fontSize: token.fontSizeSM,
|
||||
}}
|
||||
>
|
||||
{compile(setting.title)}
|
||||
</div>
|
||||
</a>
|
||||
</Card.Grid>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
data-testid="plugin-settings-button"
|
||||
icon={<SettingOutlined style={{ color: token.colorTextHeaderMenu }} />}
|
||||
// title={t('All plugin settings')}
|
||||
/>
|
||||
</Popover>
|
||||
</ActionContextProvider>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { useCollection_deprecated } from '../collection-manager';
|
||||
import { CollectionRecordProvider } from '../data-source';
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { CollectionRecordProvider, useCollection } from '../data-source';
|
||||
import { useCurrentUserContext } from '../user';
|
||||
|
||||
export const RecordContext_deprecated = createContext({});
|
||||
@ -18,10 +17,14 @@ export const RecordProvider: React.FC<{
|
||||
collectionName?: string;
|
||||
}> = (props) => {
|
||||
const { record, children, parent, isNew } = props;
|
||||
const { name: __collectionName } = useCollection_deprecated();
|
||||
const value = { ...record };
|
||||
value['__parent'] = parent;
|
||||
value['__collectionName'] = __collectionName;
|
||||
const collection = useCollection();
|
||||
const value = useMemo(() => {
|
||||
const res = { ...record };
|
||||
res['__parent'] = parent;
|
||||
res['__collectionName'] = collection?.name;
|
||||
return res;
|
||||
}, [record, parent, collection?.name]);
|
||||
|
||||
return (
|
||||
<RecordContext_deprecated.Provider value={value}>
|
||||
<CollectionRecordProvider isNew={isNew} record={record} parentRecord={parent}>
|
||||
|
@ -204,7 +204,12 @@ const MenuEditor = (props) => {
|
||||
}
|
||||
return (
|
||||
<SchemaIdContext.Provider value={defaultSelectedUid}>
|
||||
<SchemaComponent memoized scope={{ useMenuProps, onSelect, sideMenuRef, defaultSelectedUid }} schema={schema} />
|
||||
<SchemaComponent
|
||||
distributed
|
||||
memoized
|
||||
scope={{ useMenuProps, onSelect, sideMenuRef, defaultSelectedUid }}
|
||||
schema={schema}
|
||||
/>
|
||||
</SchemaIdContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -275,66 +280,100 @@ const SetThemeOfHeaderSubmenu = ({ children }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const InternalAdminLayout = (props: any) => {
|
||||
const sideMenuRef = useRef<HTMLDivElement>();
|
||||
const result = useSystemSettings();
|
||||
// const { service } = useCollectionManager_deprecated();
|
||||
const AdminSideBar = ({ sideMenuRef }) => {
|
||||
const params = useParams<any>();
|
||||
if (!params.name) return null;
|
||||
return (
|
||||
<Layout.Sider
|
||||
className={css`
|
||||
height: 100%;
|
||||
/* position: fixed; */
|
||||
position: relative;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
z-index: 100;
|
||||
.ant-layout-sider-children {
|
||||
top: var(--nb-header-height);
|
||||
position: fixed;
|
||||
width: 200px;
|
||||
height: calc(100vh - var(--nb-header-height));
|
||||
}
|
||||
`}
|
||||
theme={'light'}
|
||||
ref={sideMenuRef}
|
||||
></Layout.Sider>
|
||||
);
|
||||
};
|
||||
|
||||
export const InternalAdminLayout = () => {
|
||||
const result = useSystemSettings();
|
||||
const { token } = useToken();
|
||||
const sideMenuRef = useRef<HTMLDivElement>();
|
||||
|
||||
const layoutHeaderCss = useMemo(() => {
|
||||
return css`
|
||||
.ant-menu.ant-menu-dark .ant-menu-item-selected,
|
||||
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,
|
||||
.ant-menu-submenu-horizontal.ant-menu-submenu-selected {
|
||||
background-color: ${token.colorBgHeaderMenuActive} !important;
|
||||
color: ${token.colorTextHeaderMenuActive} !important;
|
||||
}
|
||||
.ant-menu-submenu-horizontal.ant-menu-submenu-selected > .ant-menu-submenu-title {
|
||||
color: ${token.colorTextHeaderMenuActive} !important;
|
||||
}
|
||||
.ant-menu-dark.ant-menu-horizontal > .ant-menu-item:hover {
|
||||
background-color: ${token.colorBgHeaderMenuHover} !important;
|
||||
color: ${token.colorTextHeaderMenuHover} !important;
|
||||
}
|
||||
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: var(--nb-header-height);
|
||||
line-height: var(--nb-header-height);
|
||||
padding: 0;
|
||||
z-index: 100;
|
||||
background-color: ${token.colorBgHeader} !important;
|
||||
|
||||
.ant-menu {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-menu-item,
|
||||
.ant-menu-submenu-horizontal {
|
||||
color: ${token.colorTextHeaderMenu} !important;
|
||||
}
|
||||
`;
|
||||
}, [
|
||||
token.colorBgHeaderMenuActive,
|
||||
token.colorTextHeaderMenuActive,
|
||||
token.colorBgHeaderMenuHover,
|
||||
token.colorTextHeaderMenuHover,
|
||||
token.colorBgHeader,
|
||||
token.colorTextHeaderMenu,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<GlobalStyleForAdminLayout />
|
||||
<Layout.Header
|
||||
className={css`
|
||||
.ant-menu.ant-menu-dark .ant-menu-item-selected,
|
||||
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,
|
||||
.ant-menu-submenu-horizontal.ant-menu-submenu-selected {
|
||||
background-color: ${token.colorBgHeaderMenuActive} !important;
|
||||
color: ${token.colorTextHeaderMenuActive} !important;
|
||||
}
|
||||
.ant-menu-submenu-horizontal.ant-menu-submenu-selected > .ant-menu-submenu-title {
|
||||
color: ${token.colorTextHeaderMenuActive} !important;
|
||||
}
|
||||
.ant-menu-dark.ant-menu-horizontal > .ant-menu-item:hover {
|
||||
background-color: ${token.colorBgHeaderMenuHover} !important;
|
||||
color: ${token.colorTextHeaderMenuHover} !important;
|
||||
}
|
||||
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: var(--nb-header-height);
|
||||
line-height: var(--nb-header-height);
|
||||
padding: 0;
|
||||
z-index: 100;
|
||||
background-color: ${token.colorBgHeader} !important;
|
||||
|
||||
.ant-menu {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-menu-item,
|
||||
.ant-menu-submenu-horizontal {
|
||||
color: ${token.colorTextHeaderMenu} !important;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Layout.Header className={layoutHeaderCss}>
|
||||
<div
|
||||
className={css`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
`}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={css`
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
`}
|
||||
style={{
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
flex: '1 1 auto',
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={css`
|
||||
@ -390,27 +429,7 @@ export const InternalAdminLayout = (props: any) => {
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Header>
|
||||
{params.name && (
|
||||
<Layout.Sider
|
||||
className={css`
|
||||
height: 100%;
|
||||
/* position: fixed; */
|
||||
position: relative;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
z-index: 100;
|
||||
.ant-layout-sider-children {
|
||||
top: var(--nb-header-height);
|
||||
position: fixed;
|
||||
width: 200px;
|
||||
height: calc(100vh - var(--nb-header-height));
|
||||
}
|
||||
`}
|
||||
theme={'light'}
|
||||
ref={sideMenuRef}
|
||||
></Layout.Sider>
|
||||
)}
|
||||
<AdminSideBar sideMenuRef={sideMenuRef} />
|
||||
<Layout.Content
|
||||
className={css`
|
||||
display: flex;
|
||||
|
@ -3,7 +3,7 @@ import { isPortalInBody } from '@nocobase/utils/client';
|
||||
import { App, Button } from 'antd';
|
||||
import classnames from 'classnames';
|
||||
import { default as lodash } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StablePopover, useActionContext } from '../..';
|
||||
import { useDesignable } from '../../';
|
||||
@ -46,6 +46,7 @@ export const Action: ComposedAction = observer(
|
||||
actionCallback,
|
||||
/** 如果为 true 则说明该按钮是树表格的 Add child 按钮 */
|
||||
addChild,
|
||||
onMouseEnter,
|
||||
...others
|
||||
} = useProps(props);
|
||||
const aclCtx = useACLActionParamsContext();
|
||||
@ -65,17 +66,28 @@ export const Action: ComposedAction = observer(
|
||||
const openSize = fieldSchema?.['x-component-props']?.['openSize'];
|
||||
|
||||
const disabled = form.disabled || field.disabled || field.data?.disabled || propsDisabled;
|
||||
const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
|
||||
const linkageRules = useMemo(() => fieldSchema?.['x-linkage-rules'] || [], [fieldSchema?.['x-linkage-rules']]);
|
||||
const { designable } = useDesignable();
|
||||
const tarComponent = useComponent(component) || component;
|
||||
const { modal } = App.useApp();
|
||||
const variables = useVariables();
|
||||
const localVariables = useLocalVariables({ currentForm: { values: record } as any });
|
||||
const { getAriaLabel } = useGetAriaLabelOfAction(title);
|
||||
let actionTitle = title || compile(fieldSchema.title);
|
||||
actionTitle = lodash.isString(actionTitle) ? t(actionTitle) : actionTitle;
|
||||
const [btnHover, setBtnHover] = useState(popover);
|
||||
|
||||
useEffect(() => {
|
||||
if (popover) {
|
||||
setBtnHover(true);
|
||||
}
|
||||
}, [popover]);
|
||||
|
||||
const actionTitle = useMemo(() => {
|
||||
const res = title || compile(fieldSchema.title);
|
||||
return lodash.isString(res) ? t(res) : res;
|
||||
}, [title, fieldSchema.title, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!btnHover) return;
|
||||
field.stateOfLinkageRules = {};
|
||||
linkageRules
|
||||
.filter((k) => !k.disabled)
|
||||
@ -90,13 +102,14 @@ export const Action: ComposedAction = observer(
|
||||
});
|
||||
});
|
||||
});
|
||||
}, [field, linkageRules, localVariables, record, variables]);
|
||||
}, [btnHover, field, linkageRules, localVariables, record, variables]);
|
||||
|
||||
const handleButtonClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (isPortalInBody(e.target as Element)) {
|
||||
return;
|
||||
}
|
||||
setBtnHover(true);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@ -128,6 +141,13 @@ export const Action: ComposedAction = observer(
|
||||
};
|
||||
}, [designable, field?.data?.hidden, style]);
|
||||
|
||||
const handleMouseEnter = useCallback(
|
||||
(e) => {
|
||||
setBtnHover(true);
|
||||
onMouseEnter?.(e);
|
||||
},
|
||||
[onMouseEnter],
|
||||
);
|
||||
const renderButton = () => {
|
||||
if (!designable && (field?.data?.hidden || !aclCtx)) {
|
||||
return null;
|
||||
@ -138,6 +158,7 @@ export const Action: ComposedAction = observer(
|
||||
role="button"
|
||||
aria-label={getAriaLabel()}
|
||||
{...others}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
loading={field?.data?.loading}
|
||||
icon={icon ? <Icon type={icon} /> : null}
|
||||
disabled={disabled}
|
||||
@ -152,9 +173,16 @@ export const Action: ComposedAction = observer(
|
||||
</SortableItem>
|
||||
);
|
||||
};
|
||||
|
||||
const buttonElement = renderButton();
|
||||
|
||||
// if (!btnHover) {
|
||||
// return buttonElement;
|
||||
// }
|
||||
|
||||
const result = (
|
||||
<ActionContextProvider
|
||||
button={renderButton()}
|
||||
button={buttonElement}
|
||||
visible={visible}
|
||||
setVisible={setVisible}
|
||||
formValueChanged={formValueChanged}
|
||||
|
@ -8,9 +8,8 @@ import App4 from '../demos/demo4';
|
||||
describe('Action', () => {
|
||||
it('show the drawer when click the button', async () => {
|
||||
const { getByText } = render(<App1 />);
|
||||
|
||||
await userEvent.click(getByText('Open'));
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(getByText('Open'));
|
||||
// drawer
|
||||
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
|
||||
});
|
||||
@ -22,14 +21,14 @@ describe('Action', () => {
|
||||
expect(getByText('Hello')).toBeInTheDocument();
|
||||
|
||||
// close button
|
||||
await userEvent.click(getByText('Close'));
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(getByText('Close'));
|
||||
expect(document.querySelector('.ant-drawer')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// should also close when click the mask
|
||||
await userEvent.click(getByText('Open'));
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(getByText('Open'));
|
||||
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
|
||||
});
|
||||
await userEvent.click(document.querySelector('.ant-drawer-mask') as HTMLElement);
|
||||
@ -38,8 +37,8 @@ describe('Action', () => {
|
||||
});
|
||||
|
||||
// should also close when click the close icon
|
||||
await userEvent.click(getByText('Open'));
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(getByText('Open'));
|
||||
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
|
||||
});
|
||||
await userEvent.click(document.querySelector('.ant-drawer-close') as HTMLElement);
|
||||
@ -56,34 +55,28 @@ describe('Action', () => {
|
||||
expect(document.querySelector('.nb-action-page')).not.toBeInTheDocument();
|
||||
|
||||
// drawer
|
||||
await userEvent.click(getByText('Drawer'));
|
||||
await userEvent.click(getByText('Open'));
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(getByText('Drawer'));
|
||||
await userEvent.click(getByText('Open'));
|
||||
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
|
||||
expect(document.querySelector('.ant-modal')).not.toBeInTheDocument();
|
||||
expect(document.querySelector('.nb-action-page')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(getByText('Close'));
|
||||
|
||||
// modal
|
||||
await userEvent.click(getByText('Modal'));
|
||||
await userEvent.click(getByText('Open'));
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(getByText('Close'));
|
||||
await userEvent.click(getByText('Modal'));
|
||||
await userEvent.click(getByText('Open'));
|
||||
expect(document.querySelector('.ant-drawer')).not.toBeInTheDocument();
|
||||
expect(document.querySelector('.ant-modal')).toBeInTheDocument();
|
||||
expect(document.querySelector('.nb-action-page')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(getByText('Close'));
|
||||
|
||||
// page
|
||||
await userEvent.click(getByText('Page'));
|
||||
await userEvent.click(getByText('Open'));
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(getByText('Close'));
|
||||
// page
|
||||
await userEvent.click(getByText('Page'));
|
||||
await userEvent.click(getByText('Open'));
|
||||
expect(document.querySelector('.ant-drawer')).not.toBeInTheDocument();
|
||||
expect(document.querySelector('.ant-modal')).not.toBeInTheDocument();
|
||||
expect(document.querySelector('.nb-action-page')).toBeInTheDocument();
|
||||
@ -98,9 +91,8 @@ describe('Action', () => {
|
||||
describe('Action.Drawer without Action', () => {
|
||||
it('show the drawer when click the button', async () => {
|
||||
const { getByText } = render(<App2 />);
|
||||
|
||||
await userEvent.click(getByText('Open'));
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(getByText('Open'));
|
||||
// drawer
|
||||
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
|
||||
// mask
|
||||
@ -112,14 +104,14 @@ describe('Action.Drawer without Action', () => {
|
||||
});
|
||||
|
||||
// close button
|
||||
await userEvent.click(getByText('Close'));
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(getByText('Close'));
|
||||
expect(document.querySelector('.ant-drawer')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// should also close when click the mask
|
||||
await userEvent.click(getByText('Open'));
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(getByText('Open'));
|
||||
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
|
||||
});
|
||||
await userEvent.click(document.querySelector('.ant-drawer-mask') as HTMLElement);
|
||||
@ -128,9 +120,8 @@ describe('Action.Drawer without Action', () => {
|
||||
});
|
||||
|
||||
// should also close when click the close icon
|
||||
await userEvent.click(getByText('Open'));
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(getByText('Open'));
|
||||
expect(document.querySelector('.ant-drawer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
@ -7,21 +7,24 @@ export function useSetAriaLabelForDrawer(visible: boolean) {
|
||||
// 因 Drawer 设置 aria-label 无效,所以使用下面这种方式设置,方便 e2e 录制工具选中
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
const wrappers = [...document.querySelectorAll('.ant-drawer-wrapper-body')];
|
||||
const masks = [...document.querySelectorAll('.ant-drawer-mask')];
|
||||
// 如果存在多个 mask,最后一个 mask 为当前打开的 mask;wrapper 也是同理
|
||||
const currentMask = masks[masks.length - 1];
|
||||
const currentWrapper = wrappers[wrappers.length - 1];
|
||||
if (currentMask) {
|
||||
currentMask.setAttribute('role', 'button');
|
||||
currentMask.setAttribute('aria-label', getAriaLabel('mask'));
|
||||
}
|
||||
if (currentWrapper) {
|
||||
currentWrapper.setAttribute('role', 'button');
|
||||
// 都设置一下,让 e2e 录制工具自己选择
|
||||
currentWrapper.setAttribute('data-testid', getAriaLabel());
|
||||
currentWrapper.setAttribute('aria-label', getAriaLabel());
|
||||
}
|
||||
// 因为 Action 是点击后渲染内容,所以需要延迟一下
|
||||
setTimeout(() => {
|
||||
const wrappers = [...document.querySelectorAll('.ant-drawer-wrapper-body')];
|
||||
const masks = [...document.querySelectorAll('.ant-drawer-mask')];
|
||||
// 如果存在多个 mask,最后一个 mask 为当前打开的 mask;wrapper 也是同理
|
||||
const currentMask = masks[masks.length - 1];
|
||||
const currentWrapper = wrappers[wrappers.length - 1];
|
||||
if (currentMask) {
|
||||
currentMask.setAttribute('role', 'button');
|
||||
currentMask.setAttribute('aria-label', getAriaLabel('mask'));
|
||||
}
|
||||
if (currentWrapper) {
|
||||
currentWrapper.setAttribute('role', 'button');
|
||||
// 都设置一下,让 e2e 录制工具自己选择
|
||||
currentWrapper.setAttribute('data-testid', getAriaLabel());
|
||||
currentWrapper.setAttribute('aria-label', getAriaLabel());
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}, [getAriaLabel, visible]);
|
||||
}
|
||||
|
@ -1,25 +1,31 @@
|
||||
import { Field } from '@formily/core';
|
||||
import { observer, useField, useFieldSchema } from '@formily/react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useCollectionManager_deprecated } from '../../../collection-manager';
|
||||
import { AssociationFieldContext } from './context';
|
||||
import { markRecordAsNew } from '../../../data-source/collection-record/isNewRecord';
|
||||
import { useCollection, useCollectionManager } from '../../../data-source/collection';
|
||||
import { useSchemaComponentContext } from '../../hooks';
|
||||
|
||||
export const AssociationFieldProvider = observer(
|
||||
(props) => {
|
||||
const field = useField<Field>();
|
||||
const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated();
|
||||
const collection = useCollection();
|
||||
const dm = useCollectionManager();
|
||||
const fieldSchema = useFieldSchema();
|
||||
|
||||
// 这里有点奇怪,在 Table 切换显示的组件时,这个组件并不会触发重新渲染,所以增加这个 Hooks 让其重新渲染
|
||||
useSchemaComponentContext();
|
||||
|
||||
const allowMultiple = fieldSchema['x-component-props']?.multiple !== false;
|
||||
const allowDissociate = fieldSchema['x-component-props']?.allowDissociate !== false;
|
||||
|
||||
const collectionField = useMemo(
|
||||
() => getCollectionJoinField(fieldSchema['x-collection-field']),
|
||||
() => collection.getField(fieldSchema['x-collection-field']),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[fieldSchema['x-collection-field'], fieldSchema.name],
|
||||
);
|
||||
const isFileCollection = useMemo(
|
||||
() => getCollection(collectionField?.target)?.template === 'file',
|
||||
() => dm.getCollection(collectionField?.target)?.template === 'file',
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[fieldSchema['x-collection-field']],
|
||||
);
|
||||
@ -31,9 +37,13 @@ export const AssociationFieldProvider = observer(
|
||||
|
||||
const fieldValue = useMemo(() => JSON.stringify(field.value), [field.value]);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loading, setLoading] = useState(!field.readPretty);
|
||||
|
||||
useEffect(() => {
|
||||
if (field.readPretty) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
if (!collectionField) {
|
||||
setLoading(false);
|
||||
|
@ -7,6 +7,27 @@ import { useAssociationFieldContext, useInsertSchema } from './hooks';
|
||||
import { ACLCollectionProvider, useACLActionParamsContext } from '../../../acl';
|
||||
import schema from './schema';
|
||||
|
||||
const InternalNesterCss = css`
|
||||
& .ant-formily-item-layout-vertical {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.ant-card-body {
|
||||
padding: 15px 20px 5px;
|
||||
}
|
||||
.ant-divider-horizontal {
|
||||
margin: 10px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const InternalNesterCardCss = css`
|
||||
.ant-card-bordered {
|
||||
border: none;
|
||||
}
|
||||
.ant-card-body {
|
||||
padding: 0px 20px 20px 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const InternalNester = observer(
|
||||
() => {
|
||||
const field = useField();
|
||||
@ -23,29 +44,9 @@ export const InternalNester = observer(
|
||||
<ACLCollectionProvider actionPath={`${collectionField.target}:${actionName}`}>
|
||||
<FormLayout layout={'vertical'}>
|
||||
<div
|
||||
className={cx(
|
||||
css`
|
||||
& .ant-formily-item-layout-vertical {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.ant-card-body {
|
||||
padding: 15px 20px 5px;
|
||||
}
|
||||
.ant-divider-horizontal {
|
||||
margin: 10px 0;
|
||||
}
|
||||
`,
|
||||
{
|
||||
[css`
|
||||
.ant-card-body {
|
||||
padding: 0px 20px 20px 0px;
|
||||
}
|
||||
> .ant-card-bordered {
|
||||
border: none;
|
||||
}
|
||||
`]: showTitle === false,
|
||||
},
|
||||
)}
|
||||
className={cx(InternalNesterCss, {
|
||||
[InternalNesterCardCss]: showTitle === false,
|
||||
})}
|
||||
>
|
||||
<RecursionField
|
||||
onlyRenderProperties
|
||||
|
@ -11,6 +11,15 @@ import { InternalNester } from './InternalNester';
|
||||
import { ReadPrettyInternalViewer } from './InternalViewer';
|
||||
import { useAssociationFieldContext } from './hooks';
|
||||
|
||||
const InternaPopoverNesterContentCss = css`
|
||||
min-width: 600px;
|
||||
max-height: 440px;
|
||||
overflow: auto;
|
||||
.ant-card {
|
||||
border: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const InternaPopoverNester = observer(
|
||||
(props) => {
|
||||
const { options } = useAssociationFieldContext();
|
||||
@ -27,14 +36,7 @@ export const InternaPopoverNester = observer(
|
||||
<div
|
||||
ref={ref}
|
||||
style={{ minWidth: '600px', maxWidth: '800px', maxHeight: '440px', overflow: 'auto' }}
|
||||
className={css`
|
||||
min-width: 600px;
|
||||
max-height: 440px;
|
||||
overflow: auto;
|
||||
.ant-card {
|
||||
border: 0px;
|
||||
}
|
||||
`}
|
||||
className={InternaPopoverNesterContentCss}
|
||||
>
|
||||
<InternalNester {...nesterProps} />
|
||||
</div>
|
||||
@ -63,9 +65,9 @@ export const InternaPopoverNester = observer(
|
||||
>
|
||||
<span style={{ cursor: 'pointer', display: 'flex' }}>
|
||||
<div
|
||||
className={css`
|
||||
max-width: 95%;
|
||||
`}
|
||||
style={{
|
||||
maxWidth: '95%',
|
||||
}}
|
||||
>
|
||||
<ReadPrettyInternalViewer {...props} />
|
||||
</div>
|
||||
@ -77,15 +79,15 @@ export const InternaPopoverNester = observer(
|
||||
role="button"
|
||||
aria-label={getAriaLabel('mask')}
|
||||
onClick={() => setVisible(false)}
|
||||
className={css`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: transparent;
|
||||
z-index: 9999;
|
||||
`}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'transparent',
|
||||
zIndex: 9999,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ActionContextProvider>
|
||||
|
@ -45,6 +45,7 @@ export const ReadPrettyInternalTag: React.FC = observer(
|
||||
const { getCollection } = useCollectionManager_deprecated();
|
||||
const targetCollection = getCollection(collectionField?.target);
|
||||
const isTreeCollection = targetCollection?.template === 'tree';
|
||||
const [btnHover, setBtnHover] = useState(false);
|
||||
|
||||
const renderRecords = () =>
|
||||
toArr(props.value).map((record, index, arr) => {
|
||||
@ -65,7 +66,11 @@ export const ReadPrettyInternalTag: React.FC = observer(
|
||||
text
|
||||
) : enableLink !== false ? (
|
||||
<a
|
||||
onMouseEnter={() => {
|
||||
setBtnHover(true);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
setBtnHover(true);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (designable) {
|
||||
@ -87,6 +92,16 @@ export const ReadPrettyInternalTag: React.FC = observer(
|
||||
);
|
||||
});
|
||||
|
||||
const btnElement = (
|
||||
<EllipsisWithTooltip ellipsis={true} ref={ellipsisWithTooltipRef}>
|
||||
{renderRecords()}
|
||||
</EllipsisWithTooltip>
|
||||
);
|
||||
|
||||
if (enableLink === false || !btnHover) {
|
||||
return btnElement;
|
||||
}
|
||||
|
||||
const renderWithoutTableFieldResourceProvider = () => (
|
||||
<WithoutTableFieldResource.Provider value={true}>
|
||||
<FormProvider>
|
||||
@ -120,9 +135,7 @@ export const ReadPrettyInternalTag: React.FC = observer(
|
||||
<div>
|
||||
<BlockAssociationContext.Provider value={`${collectionField?.collectionName}.${collectionField?.name}`}>
|
||||
<CollectionProvider_deprecated name={collectionField?.target ?? collectionField?.targetCollection}>
|
||||
<EllipsisWithTooltip ellipsis={true} ref={ellipsisWithTooltipRef}>
|
||||
{renderRecords()}
|
||||
</EllipsisWithTooltip>
|
||||
{btnElement}
|
||||
<ActionContextProvider
|
||||
value={{ visible, setVisible, openMode: 'drawer', snapshot: collectionField?.interface === 'snapshot' }}
|
||||
>
|
||||
|
@ -47,6 +47,8 @@ export const ReadPrettyInternalViewer: React.FC = observer(
|
||||
const isTreeCollection = targetCollection?.template === 'tree';
|
||||
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
|
||||
const getLabelUiSchema = useLabelUiSchemaV2();
|
||||
const [btnHover, setBtnHover] = useState(false);
|
||||
|
||||
const renderRecords = () =>
|
||||
toArr(props.value).map((record, index, arr) => {
|
||||
const value = record?.[fieldNames?.label || 'label'];
|
||||
@ -70,7 +72,11 @@ export const ReadPrettyInternalViewer: React.FC = observer(
|
||||
text
|
||||
) : enableLink !== false ? (
|
||||
<a
|
||||
onMouseEnter={() => {
|
||||
setBtnHover(true);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
setBtnHover(true);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (designable) {
|
||||
@ -91,6 +97,16 @@ export const ReadPrettyInternalViewer: React.FC = observer(
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
const btnElement = (
|
||||
<EllipsisWithTooltip ellipsis={true} ref={ellipsisWithTooltipRef}>
|
||||
{renderRecords()}
|
||||
</EllipsisWithTooltip>
|
||||
);
|
||||
|
||||
if (enableLink === false || !btnHover) {
|
||||
return btnElement;
|
||||
}
|
||||
const renderWithoutTableFieldResourceProvider = () => (
|
||||
<WithoutTableFieldResource.Provider value={true}>
|
||||
<FormProvider>
|
||||
@ -124,9 +140,7 @@ export const ReadPrettyInternalViewer: React.FC = observer(
|
||||
<div>
|
||||
<BlockAssociationContext.Provider value={`${collectionField?.collectionName}.${collectionField?.name}`}>
|
||||
<CollectionProvider_deprecated name={collectionField?.target ?? collectionField?.targetCollection}>
|
||||
<EllipsisWithTooltip ellipsis={true} ref={ellipsisWithTooltipRef}>
|
||||
{renderRecords()}
|
||||
</EllipsisWithTooltip>
|
||||
{btnElement}
|
||||
<ActionContextProvider
|
||||
value={{
|
||||
visible,
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { APIClientProvider, AssociationSelect, FormProvider, SchemaComponent } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { mockAPIClient } from '../../../../testUtils';
|
||||
import { sleep } from '@nocobase/test/client';
|
||||
|
||||
const { apiClient, mockRequest } = mockAPIClient();
|
||||
mockRequest.onGet('/posts:list').reply(() => {
|
||||
mockRequest.onGet('/posts:list').reply(async () => {
|
||||
await sleep(100);
|
||||
return [
|
||||
200,
|
||||
{
|
||||
|
@ -1,75 +1,74 @@
|
||||
import { css } from '@emotion/css';
|
||||
import cls from 'classnames';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { SortableItem } from '../../common';
|
||||
import { useDesigner, useProps } from '../../hooks';
|
||||
import { useGetAriaLabelOfBlockItem } from './hooks/useGetAriaLabelOfBlockItem';
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
|
||||
|
||||
export const BlockItem: React.FC<any> = withDynamicSchemaProps((props) => {
|
||||
// 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
|
||||
const { className, children } = useProps(props);
|
||||
const blockItemCss = css`
|
||||
position: relative;
|
||||
&:hover {
|
||||
> .general-schema-designer {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&.nb-form-item:hover {
|
||||
> .general-schema-designer {
|
||||
background: var(--colorBgSettingsHover) !important;
|
||||
border: 0 !important;
|
||||
top: -5px !important;
|
||||
bottom: -5px !important;
|
||||
left: -5px !important;
|
||||
right: -5px !important;
|
||||
}
|
||||
}
|
||||
> .general-schema-designer {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: none;
|
||||
border: 2px solid var(--colorBorderSettingsHover);
|
||||
pointer-events: none;
|
||||
> .general-schema-designer-icons {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 2px;
|
||||
line-height: 16px;
|
||||
pointer-events: all;
|
||||
.ant-space-item {
|
||||
background-color: var(--colorSettings);
|
||||
color: #fff;
|
||||
line-height: 16px;
|
||||
width: 16px;
|
||||
padding-left: 1px;
|
||||
align-self: stretch;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Designer = useDesigner();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { getAriaLabel } = useGetAriaLabelOfBlockItem(props.name);
|
||||
export const BlockItem: React.FC<any> = withDynamicSchemaProps(
|
||||
(props) => {
|
||||
// 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
|
||||
const { className, children } = useProps(props);
|
||||
|
||||
return (
|
||||
<SortableItem
|
||||
role="button"
|
||||
aria-label={getAriaLabel()}
|
||||
className={cls(
|
||||
'nb-block-item',
|
||||
className,
|
||||
css`
|
||||
position: relative;
|
||||
&:hover {
|
||||
> .general-schema-designer {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&.nb-form-item:hover {
|
||||
> .general-schema-designer {
|
||||
background: var(--colorBgSettingsHover) !important;
|
||||
border: 0 !important;
|
||||
top: -5px !important;
|
||||
bottom: -5px !important;
|
||||
left: -5px !important;
|
||||
right: -5px !important;
|
||||
}
|
||||
}
|
||||
> .general-schema-designer {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: none;
|
||||
border: 2px solid var(--colorBorderSettingsHover);
|
||||
pointer-events: none;
|
||||
> .general-schema-designer-icons {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 2px;
|
||||
line-height: 16px;
|
||||
pointer-events: all;
|
||||
.ant-space-item {
|
||||
background-color: var(--colorSettings);
|
||||
color: #fff;
|
||||
line-height: 16px;
|
||||
width: 16px;
|
||||
padding-left: 1px;
|
||||
align-self: stretch;
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<Designer {...fieldSchema['x-toolbar-props']} />
|
||||
{children}
|
||||
</SortableItem>
|
||||
);
|
||||
});
|
||||
const Designer = useDesigner();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { getAriaLabel } = useGetAriaLabelOfBlockItem(props.name);
|
||||
|
||||
const label = useMemo(() => getAriaLabel(), [getAriaLabel]);
|
||||
|
||||
return (
|
||||
<SortableItem role="button" aria-label={label} className={cls('nb-block-item', className, blockItemCss)}>
|
||||
<Designer {...fieldSchema['x-toolbar-props']} />
|
||||
{children}
|
||||
</SortableItem>
|
||||
);
|
||||
},
|
||||
{ displayName: 'BlockItem' },
|
||||
);
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { Card } from 'antd';
|
||||
import { Card, Skeleton } from 'antd';
|
||||
import React from 'react';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { BlockItem } from '../block-item';
|
||||
import useStyles from './style';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
@ -18,11 +19,18 @@ export const CardItem = (props: Props) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const templateKey = fieldSchema?.['x-template-key'];
|
||||
const { wrapSSR, componentCls, hashId } = useStyles();
|
||||
const { ref, inView } = useInView({
|
||||
threshold: 1,
|
||||
initialInView: true,
|
||||
triggerOnce: true,
|
||||
skip: !!process.env.__E2E__,
|
||||
});
|
||||
|
||||
return wrapSSR(
|
||||
templateKey && !template ? null : (
|
||||
<BlockItem name={name} className={`${componentCls} ${hashId} noco-card-item`}>
|
||||
<Card className="card" bordered={false} {...restProps}>
|
||||
{props.children}
|
||||
<Card ref={ref} className="card" bordered={false} {...restProps}>
|
||||
{inView ? props.children : <Skeleton active paragraph={{ rows: 4 }} />}
|
||||
</Card>
|
||||
</BlockItem>
|
||||
),
|
||||
|
@ -4,7 +4,7 @@ import { isValid } from '@formily/shared';
|
||||
import { Checkbox as AntdCheckbox, Tag } from 'antd';
|
||||
import type { CheckboxGroupProps, CheckboxProps } from 'antd/es/checkbox';
|
||||
import uniq from 'lodash/uniq';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useCollectionField } from '../../../data-source/collection-field/CollectionFieldProvider';
|
||||
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
|
||||
|
||||
@ -36,8 +36,10 @@ export const Checkbox: ComposedCheckbox = connect(
|
||||
}),
|
||||
mapReadPretty(ReadPretty),
|
||||
);
|
||||
Checkbox.displayName = 'Checkbox';
|
||||
|
||||
Checkbox.ReadPretty = ReadPretty;
|
||||
Checkbox.ReadPretty.displayName = 'Checkbox.ReadPretty';
|
||||
|
||||
Checkbox.__ANT_CHECKBOX = true;
|
||||
|
||||
@ -52,20 +54,23 @@ Checkbox.Group = connect(
|
||||
}
|
||||
const field = useField<any>();
|
||||
const collectionField = useCollectionField();
|
||||
const dataSource = field.dataSource || collectionField?.uiSchema.enum || [];
|
||||
const value = uniq(field.value ? field.value : []);
|
||||
const tags = useMemo(() => {
|
||||
const dataSource = field.dataSource || collectionField?.uiSchema.enum || [];
|
||||
const value = uniq(field.value ? field.value : []);
|
||||
return dataSource.filter((option) => value.includes(option.value));
|
||||
}, [field.value]);
|
||||
|
||||
return (
|
||||
<EllipsisWithTooltip ellipsis={props.ellipsis}>
|
||||
{dataSource
|
||||
.filter((option) => value.includes(option.value))
|
||||
.map((option, key) => (
|
||||
<Tag key={key} color={option.color} icon={option.icon}>
|
||||
{option.label}
|
||||
</Tag>
|
||||
))}
|
||||
{tags.map((option, key) => (
|
||||
<Tag key={key} color={option.color} icon={option.icon}>
|
||||
{option.label}
|
||||
</Tag>
|
||||
))}
|
||||
</EllipsisWithTooltip>
|
||||
);
|
||||
}),
|
||||
);
|
||||
Checkbox.Group.displayName = 'Checkbox.Group';
|
||||
|
||||
export default Checkbox;
|
||||
|
@ -10,11 +10,12 @@ describe('Filter', () => {
|
||||
it('Filter & Action', async () => {
|
||||
render(<App3 />);
|
||||
|
||||
let tooltip;
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(screen.getByText(/open/i));
|
||||
tooltip = screen.getByRole('tooltip');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
});
|
||||
const tooltip = screen.getByRole('tooltip');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
|
||||
// 弹窗中显示的内容
|
||||
expect(within(tooltip).getByText(/name/i)).toBeInTheDocument();
|
||||
@ -78,9 +79,12 @@ describe('Filter', () => {
|
||||
it('FilterAction', async () => {
|
||||
render(<App5 />);
|
||||
|
||||
await waitFor(() => userEvent.click(screen.getByText(/filter/i)));
|
||||
const tooltip = screen.getByRole('tooltip');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
let tooltip;
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(screen.getByText(/filter/i));
|
||||
tooltip = screen.getByRole('tooltip');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// 弹窗中显示的内容
|
||||
expect(within(tooltip).getByText(/name/i)).toBeInTheDocument();
|
||||
|
@ -18,6 +18,20 @@ import useLazyLoadDisplayAssociationFieldsOfForm from './hooks/useLazyLoadDispla
|
||||
import useParseDefaultValue from './hooks/useParseDefaultValue';
|
||||
import { CollectionFieldProvider } from '../../../data-source';
|
||||
|
||||
Item.displayName = 'FormilyFormItem';
|
||||
|
||||
const formItemWrapCss = css`
|
||||
& .ant-space {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
`;
|
||||
|
||||
const formItemLabelCss = css`
|
||||
> .ant-formily-item-label {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FormItem: any = observer(
|
||||
(props: any) => {
|
||||
useEnsureOperatorsValid();
|
||||
@ -52,20 +66,9 @@ export const FormItem: any = observer(
|
||||
);
|
||||
}, [field.description]);
|
||||
const className = useMemo(() => {
|
||||
return cx(
|
||||
css`
|
||||
& .ant-space {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
`,
|
||||
{
|
||||
[css`
|
||||
> .ant-formily-item-label {
|
||||
display: none;
|
||||
}
|
||||
`]: showTitle === false,
|
||||
},
|
||||
);
|
||||
return cx(formItemWrapCss, {
|
||||
[formItemLabelCss]: showTitle === false,
|
||||
});
|
||||
}, [showTitle]);
|
||||
|
||||
return (
|
||||
|
@ -11,6 +11,7 @@ import { useVariables } from '../../../../variables';
|
||||
import { transformVariableValue } from '../../../../variables/utils/transformVariableValue';
|
||||
import { useSubFormValue } from '../../association-field/hooks';
|
||||
import { isDisplayField } from '../utils';
|
||||
import { untracked } from '@formily/reactive';
|
||||
|
||||
/**
|
||||
* 用于懒加载 Form 区块中只用于展示的关联字段的值
|
||||
@ -31,13 +32,13 @@ const useLazyLoadDisplayAssociationFieldsOfForm = () => {
|
||||
const { getAssociationAppends } = useAssociationNames();
|
||||
|
||||
const schemaName = fieldSchema.name.toString();
|
||||
const formValue = _.cloneDeep(isInSubForm || isInSubTable ? subFormValue : form.values);
|
||||
const collectionFieldRef = useRef(null);
|
||||
const sourceCollectionFieldRef = useRef(null);
|
||||
|
||||
// 是否已经预加载了数据(通过 appends 的形式)
|
||||
const hasPreloadData = useMemo(() => hasPreload(recordData, schemaName), [recordData, schemaName]);
|
||||
|
||||
const collectionFieldRef = useRef(null);
|
||||
const sourceCollectionFieldRef = useRef(null);
|
||||
|
||||
if (collectionFieldRef.current == null && isDisplayField(schemaName)) {
|
||||
collectionFieldRef.current = getCollectionJoinField(`${name}.${schemaName}`);
|
||||
}
|
||||
@ -45,6 +46,16 @@ const useLazyLoadDisplayAssociationFieldsOfForm = () => {
|
||||
sourceCollectionFieldRef.current = getCollectionJoinField(`${name}.${schemaName.split('.')[0]}`);
|
||||
}
|
||||
|
||||
const shouldNotLazyLoad = useMemo(() => {
|
||||
return (
|
||||
!isDisplayField(schemaName) || !variables || name === 'fields' || !collectionFieldRef.current || hasPreloadData
|
||||
);
|
||||
}, [schemaName, variables, name, collectionFieldRef.current, hasPreloadData]);
|
||||
|
||||
const formValue = shouldNotLazyLoad
|
||||
? {}
|
||||
: untracked(() => _.cloneDeep(isInSubForm || isInSubTable ? subFormValue : form.values));
|
||||
|
||||
const sourceKeyValue =
|
||||
isDisplayField(schemaName) && sourceCollectionFieldRef.current
|
||||
? _.get(formValue, `${schemaName.split('.')[0]}.${sourceCollectionFieldRef.current?.targetKey || 'id'}`)
|
||||
@ -53,13 +64,7 @@ const useLazyLoadDisplayAssociationFieldsOfForm = () => {
|
||||
useEffect(() => {
|
||||
// 如果 schemaName 中是以 `.` 分割的,说明是一个关联字段,需要去获取关联字段的值;
|
||||
// 在数据表管理页面,也存在 `a.b` 之类的 schema name,其 collectionName 为 fields,所以在这里排除一下 `name === 'fields'` 的情况
|
||||
if (
|
||||
!isDisplayField(schemaName) ||
|
||||
!variables ||
|
||||
name === 'fields' ||
|
||||
!collectionFieldRef.current ||
|
||||
hasPreloadData
|
||||
) {
|
||||
if (shouldNotLazyLoad) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -97,7 +102,7 @@ const useLazyLoadDisplayAssociationFieldsOfForm = () => {
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [sourceKeyValue]);
|
||||
}, [sourceKeyValue, shouldNotLazyLoad]);
|
||||
};
|
||||
|
||||
export default useLazyLoadDisplayAssociationFieldsOfForm;
|
||||
|
@ -195,6 +195,12 @@ const WithoutForm = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const formLayoutCss = css`
|
||||
.ant-formily-item-feedback-layout-loose {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Form: React.FC<FormProps> & {
|
||||
Designer?: any;
|
||||
FilterDesigner?: any;
|
||||
@ -210,14 +216,7 @@ export const Form: React.FC<FormProps> & {
|
||||
const formDisabled = disabled || field.disabled;
|
||||
return (
|
||||
<ConfigProvider componentDisabled={formDisabled}>
|
||||
<form
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
className={css`
|
||||
.ant-formily-item-feedback-layout-loose {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<form onSubmit={(e) => e.preventDefault()} className={formLayoutCss}>
|
||||
<Spin spinning={field.loading || false}>
|
||||
{form ? (
|
||||
<WithForm form={form} {...others} disabled={formDisabled} />
|
||||
|
@ -17,9 +17,9 @@ describe('FormV2', () => {
|
||||
});
|
||||
|
||||
await userEvent.type(input, '李四');
|
||||
await userEvent.click(submit);
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(screen.getByText('Submit'));
|
||||
// notification 的内容
|
||||
expect(screen.getByText(/\{"nickname":"李四"\}/i)).toBeInTheDocument();
|
||||
});
|
||||
|
@ -60,9 +60,10 @@ describe('Form', () => {
|
||||
it('Form & Drawer', async () => {
|
||||
render(<App1 />);
|
||||
|
||||
const openBtn = screen.getByText('Open');
|
||||
await userEvent.click(openBtn);
|
||||
expect(screen.getByText(/drawer title/i)).toBeInTheDocument();
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(screen.getByText('Open'));
|
||||
expect(screen.getByText(/drawer title/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('initialValue', async () => {
|
||||
@ -83,12 +84,13 @@ describe('Form', () => {
|
||||
it('initialValue of decorator', async () => {
|
||||
render(<App4 />);
|
||||
|
||||
const openBtn = screen.getByText('Open');
|
||||
await userEvent.click(openBtn);
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(screen.getByText('Open'));
|
||||
expect(screen.getByText(/drawer title/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const input = document.querySelector('.ant-input') as HTMLInputElement;
|
||||
|
||||
expect(screen.getByText(/drawer title/i)).toBeInTheDocument();
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveValue('aaa');
|
||||
expect(screen.getByText(/\{ "field1": "aaa" \}/i)).toBeInTheDocument();
|
||||
|
@ -16,29 +16,30 @@ const itemCss = css`
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
export const GridCardItem = withDynamicSchemaProps((props) => {
|
||||
const field = useField<ObjectField>();
|
||||
const parentRecordData = useCollectionParentRecordData();
|
||||
return (
|
||||
<Card
|
||||
role="button"
|
||||
aria-label="grid-card-item"
|
||||
className={css`
|
||||
height: 100%;
|
||||
> .ant-card-body {
|
||||
padding: 24px 24px 0px;
|
||||
height: 100%;
|
||||
}
|
||||
.nb-action-bar {
|
||||
padding: 5px 0;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className={itemCss}>
|
||||
<RecordProvider record={field.value} parent={parentRecordData}>
|
||||
{props.children}
|
||||
</RecordProvider>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
const gridCardCss = css`
|
||||
height: 100%;
|
||||
> .ant-card-body {
|
||||
padding: 24px 24px 0px;
|
||||
height: 100%;
|
||||
}
|
||||
.nb-action-bar {
|
||||
padding: 5px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const GridCardItem = withDynamicSchemaProps(
|
||||
(props) => {
|
||||
const field = useField<ObjectField>();
|
||||
const parentRecordData = useCollectionParentRecordData();
|
||||
return (
|
||||
<Card role="button" aria-label="grid-card-item" className={gridCardCss}>
|
||||
<div className={itemCss}>
|
||||
<RecordProvider record={field.value} parent={parentRecordData}>
|
||||
{props.children}
|
||||
</RecordProvider>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
},
|
||||
{ displayName: 'GridCardItem' },
|
||||
);
|
||||
|
@ -3,8 +3,8 @@ import { useDndContext, useDndMonitor, useDraggable, useDroppable } from '@dnd-k
|
||||
import { ISchema, RecursionField, Schema, observer, useField, useFieldSchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import cls from 'classnames';
|
||||
import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useDesignable, useFormBlockContext, useSchemaInitializerRender } from '../../../';
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { SchemaComponent, useDesignable, useFormBlockContext, useSchemaInitializerRender } from '../../../';
|
||||
import { useFormBlockType } from '../../../block-provider';
|
||||
import { DndContext } from '../../common/dnd-context';
|
||||
import { useToken } from '../__builtins__';
|
||||
@ -20,6 +20,9 @@ GridContext.displayName = 'GridContext';
|
||||
const breakRemoveOnGrid = (s: Schema) => s['x-component'] === 'Grid';
|
||||
const breakRemoveOnRow = (s: Schema) => s['x-component'] === 'Grid.Row';
|
||||
|
||||
const MemorizedRecursionField = React.memo(RecursionField);
|
||||
MemorizedRecursionField.displayName = 'MemorizedRecursionField';
|
||||
|
||||
const ColDivider = (props) => {
|
||||
const { token } = useToken();
|
||||
const dragIdRef = useRef<string | null>(null);
|
||||
@ -303,25 +306,38 @@ export const useGridRowContext = () => {
|
||||
|
||||
export const Grid: any = observer(
|
||||
(props: any) => {
|
||||
const { showDivider = true } = props;
|
||||
const { distributed, showDivider = true } = props;
|
||||
const gridRef = useRef(null);
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']);
|
||||
const InitializerComponent = (props) => render(props);
|
||||
const InitializerComponent = useCallback((props) => render(props), []);
|
||||
const addr = field.address.toString();
|
||||
const rows = useRowProperties();
|
||||
const { setPrintContent } = useFormBlockContext();
|
||||
const { wrapSSR, componentCls, hashId } = useStyles();
|
||||
|
||||
const distributedValue =
|
||||
distributed === undefined
|
||||
? fieldSchema?.parent['x-component'] === 'Page' || fieldSchema?.parent['x-component'] === 'Tabs.TabPane'
|
||||
: distributed;
|
||||
|
||||
useEffect(() => {
|
||||
gridRef.current && setPrintContent?.(gridRef.current);
|
||||
}, [gridRef.current]);
|
||||
|
||||
const gridContextValue = useMemo(() => {
|
||||
return {
|
||||
ref: gridRef,
|
||||
fieldSchema,
|
||||
renderSchemaInitializer: render,
|
||||
InitializerComponent,
|
||||
showDivider,
|
||||
};
|
||||
}, [fieldSchema, render, InitializerComponent, showDivider]);
|
||||
|
||||
return wrapSSR(
|
||||
<GridContext.Provider
|
||||
value={{ ref: gridRef, fieldSchema, renderSchemaInitializer: render, InitializerComponent, showDivider }}
|
||||
>
|
||||
<GridContext.Provider value={gridContextValue}>
|
||||
<div className={`nb-grid ${componentCls} ${hashId}`} style={{ position: 'relative' }} ref={gridRef}>
|
||||
<DndWrapper dndContext={props.dndContext}>
|
||||
{showDivider ? (
|
||||
@ -340,7 +356,11 @@ export const Grid: any = observer(
|
||||
{rows.map((schema, index) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<RecursionField name={schema.name} schema={schema} />
|
||||
{distributedValue ? (
|
||||
<SchemaComponent name={schema.name} schema={schema} distributed />
|
||||
) : (
|
||||
<MemorizedRecursionField name={schema.name} schema={schema} />
|
||||
)}
|
||||
{showDivider ? (
|
||||
<RowDivider
|
||||
rows={rows}
|
||||
@ -375,8 +395,25 @@ Grid.Row = observer(
|
||||
const { showDivider } = useGridContext();
|
||||
const { type } = useFormBlockType();
|
||||
|
||||
const ctxValue = useMemo(() => {
|
||||
return {
|
||||
schema: fieldSchema,
|
||||
cols,
|
||||
};
|
||||
}, [fieldSchema, cols]);
|
||||
|
||||
const mapProperties = useCallback(
|
||||
(schema) => {
|
||||
if (type === 'update') {
|
||||
schema.default = null;
|
||||
}
|
||||
return schema;
|
||||
},
|
||||
[type],
|
||||
);
|
||||
|
||||
return (
|
||||
<GridRowContext.Provider value={{ schema: fieldSchema, cols }}>
|
||||
<GridRowContext.Provider value={ctxValue}>
|
||||
<div
|
||||
className={cls('nb-grid-row', 'CardRow', {
|
||||
showDivider,
|
||||
@ -398,7 +435,7 @@ Grid.Row = observer(
|
||||
{cols.map((schema, index) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<RecursionField
|
||||
<MemorizedRecursionField
|
||||
name={schema.name}
|
||||
schema={schema}
|
||||
mapProperties={(schema) => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Popover } from 'antd';
|
||||
import React, { CSSProperties, forwardRef, useImperativeHandle, useRef, useState } from 'react';
|
||||
import React, { CSSProperties, forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
|
||||
const getContentWidth = (element) => {
|
||||
if (element) {
|
||||
@ -32,16 +32,40 @@ export const EllipsisWithTooltip = forwardRef((props: Partial<IEllipsisWithToolt
|
||||
setPopoverVisible: setVisible,
|
||||
};
|
||||
});
|
||||
if (!props.ellipsis) {
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
const { popoverContent } = props;
|
||||
|
||||
const isOverflowTooltip = () => {
|
||||
const isOverflowTooltip = useCallback(() => {
|
||||
if (!elRef.current) return false;
|
||||
const contentWidth = getContentWidth(elRef.current);
|
||||
const offsetWidth = elRef.current?.offsetWidth;
|
||||
return contentWidth > offsetWidth;
|
||||
};
|
||||
}, [elRef.current]);
|
||||
|
||||
const divContent = useMemo(
|
||||
() =>
|
||||
props.ellipsis ? (
|
||||
<div
|
||||
ref={elRef}
|
||||
style={{ ...ellipsisDefaultStyle }}
|
||||
onMouseEnter={(e) => {
|
||||
const el = e.target as any;
|
||||
const isShowTooltips = isOverflowTooltip();
|
||||
if (isShowTooltips) {
|
||||
setEllipsis(el.scrollWidth >= el.clientWidth);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
) : (
|
||||
props.children
|
||||
),
|
||||
[props.children, props.ellipsis],
|
||||
);
|
||||
|
||||
if (!props.ellipsis || !ellipsis) {
|
||||
return divContent;
|
||||
}
|
||||
const { popoverContent } = props;
|
||||
|
||||
return (
|
||||
<Popover
|
||||
@ -61,19 +85,7 @@ export const EllipsisWithTooltip = forwardRef((props: Partial<IEllipsisWithToolt
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
ref={elRef}
|
||||
style={{ ...ellipsisDefaultStyle }}
|
||||
onMouseEnter={(e) => {
|
||||
const el = e.target as any;
|
||||
const isShowTooltips = isOverflowTooltip();
|
||||
if (isShowTooltips) {
|
||||
setEllipsis(el.scrollWidth >= el.clientWidth);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
{divContent}
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
@ -46,4 +46,6 @@ export const Input: ComposedInput = Object.assign(
|
||||
},
|
||||
);
|
||||
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export default Input;
|
||||
|
@ -7,6 +7,11 @@ import { cx, css } from '@emotion/css';
|
||||
|
||||
export type JSONTextAreaProps = TextAreaProps & { value?: string; space?: number };
|
||||
|
||||
const jsonCss = css`
|
||||
font-size: 80%;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
`;
|
||||
|
||||
export const Json = React.forwardRef<typeof Input.TextArea, JSONTextAreaProps>(
|
||||
({ value, onChange, space = 2, ...props }: JSONTextAreaProps, ref: Ref<any>) => {
|
||||
const field = useField<Field>();
|
||||
@ -25,13 +30,7 @@ export const Json = React.forwardRef<typeof Input.TextArea, JSONTextAreaProps>(
|
||||
return (
|
||||
<Input.TextArea
|
||||
{...props}
|
||||
className={cx(
|
||||
css`
|
||||
font-size: 80%;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
`,
|
||||
props.className,
|
||||
)}
|
||||
className={cx(jsonCss, props.className)}
|
||||
ref={ref}
|
||||
value={text}
|
||||
onChange={(ev) => {
|
||||
|
@ -213,6 +213,7 @@ function PageContent(
|
||||
return (
|
||||
<FixedBlock key={schema.name} height={`calc(${height}px + 46px + ${token.marginLG}px * 2)`}>
|
||||
<SchemaComponent
|
||||
distributed
|
||||
schema={
|
||||
new Schema({
|
||||
properties: {
|
||||
@ -226,7 +227,9 @@ function PageContent(
|
||||
})
|
||||
) : (
|
||||
<FixedBlock height={`calc(${height}px + 46px + ${token.marginLG}px * 2)`}>
|
||||
<div className={`pageWithFixedBlockCss nb-page-content`}>{props.children}</div>
|
||||
<div className={`pageWithFixedBlockCss nb-page-content`}>
|
||||
<SchemaComponent schema={fieldSchema} distributed />
|
||||
</div>
|
||||
</FixedBlock>
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { APIClientProvider, FormProvider, RemoteSelect, SchemaComponent } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { mockAPIClient } from '../../../../testUtils';
|
||||
import { sleep } from '@nocobase/test/client';
|
||||
|
||||
const { apiClient, mockRequest } = mockAPIClient();
|
||||
mockRequest.onGet('/posts:list').reply(() => {
|
||||
mockRequest.onGet('/posts:list').reply(async () => {
|
||||
await sleep(100);
|
||||
return [
|
||||
200,
|
||||
{
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,19 +1,20 @@
|
||||
import { observer, RecursionField } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { useCollectionManager_deprecated } from '../../../../collection-manager';
|
||||
import { useRecord } from '../../../../record-provider';
|
||||
import { useCollection } from '../../../../data-source';
|
||||
export const ColumnFieldProvider = observer(
|
||||
(props: { schema: any; basePath: any; children: any }) => {
|
||||
const { schema, basePath } = props;
|
||||
const record = useRecord();
|
||||
const { getCollectionJoinField } = useCollectionManager_deprecated();
|
||||
const collection = useCollection();
|
||||
const fieldSchema = schema.reduceProperties((buf, s) => {
|
||||
if (s['x-component'] === 'CollectionField') {
|
||||
return s;
|
||||
}
|
||||
return buf;
|
||||
}, null);
|
||||
const collectionField = fieldSchema && getCollectionJoinField(fieldSchema['x-collection-field']);
|
||||
const collectionField = fieldSchema && collection.getField(fieldSchema['x-collection-field']);
|
||||
|
||||
if (
|
||||
fieldSchema &&
|
||||
record?.__collection &&
|
||||
|
@ -9,6 +9,7 @@ import { useDesigner } from '../../hooks/useDesigner';
|
||||
import { useTabsContext } from './context';
|
||||
import { TabsDesigner } from './Tabs.Designer';
|
||||
import { useSchemaInitializerRender } from '../../../application';
|
||||
import { SchemaComponent } from '../../core';
|
||||
|
||||
export const Tabs: any = observer(
|
||||
(props: TabsProps) => {
|
||||
@ -23,8 +24,8 @@ export const Tabs: any = observer(
|
||||
key,
|
||||
label: <RecursionField name={key} schema={schema} onlyRenderSelf />,
|
||||
children: (
|
||||
<PaneRoot {...(PaneRoot !== React.Fragment ? { active: key === contextProps.activeKey } : {})}>
|
||||
<RecursionField name={key} schema={schema} onlyRenderProperties />
|
||||
<PaneRoot key={key} {...(PaneRoot !== React.Fragment ? { active: key === contextProps.activeKey } : {})}>
|
||||
<SchemaComponent schema={schema} distributed />
|
||||
</PaneRoot>
|
||||
),
|
||||
};
|
||||
|
@ -5,10 +5,11 @@ import App1 from '../demos/demo1';
|
||||
describe('Tabs', () => {
|
||||
it('basic', async () => {
|
||||
render(<App1 />);
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(screen.getByText('Hello1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(screen.getByText('Tab2'));
|
||||
expect(screen.getByText('Hello2')).toBeInTheDocument();
|
||||
});
|
||||
|
@ -35,7 +35,7 @@ const schema: ISchema = {
|
||||
tab: 'Tab2',
|
||||
},
|
||||
properties: {
|
||||
aaa: {
|
||||
bbb: {
|
||||
'x-content': 'Hello2',
|
||||
},
|
||||
},
|
||||
|
@ -1,86 +1,92 @@
|
||||
import { DndContext as DndKitContext, DragEndEvent, DragOverlay, rectIntersection } from '@dnd-kit/core';
|
||||
import { Props } from '@dnd-kit/core/dist/components/DndContext/DndContext';
|
||||
import { observer } from '@formily/react';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAPIClient } from '../../../';
|
||||
import { createDesignable, useDesignable } from '../../hooks';
|
||||
|
||||
const useDragEnd = (props?: any) => {
|
||||
const useDragEnd = (onDragEnd) => {
|
||||
const { refresh } = useDesignable();
|
||||
const api = useAPIClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
const activeSchema = active?.data?.current?.schema;
|
||||
const overSchema = over?.data?.current?.schema;
|
||||
const insertAdjacent = over?.data?.current?.insertAdjacent;
|
||||
const breakRemoveOn = over?.data?.current?.breakRemoveOn;
|
||||
const wrapSchema = over?.data?.current?.wrapSchema;
|
||||
const onSuccess = over?.data?.current?.onSuccess;
|
||||
const removeParentsIfNoChildren = over?.data?.current?.removeParentsIfNoChildren ?? true;
|
||||
if (!activeSchema || !overSchema) {
|
||||
props?.onDragEnd?.(event);
|
||||
return;
|
||||
}
|
||||
return useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
const activeSchema = active?.data?.current?.schema;
|
||||
const overSchema = over?.data?.current?.schema;
|
||||
const insertAdjacent = over?.data?.current?.insertAdjacent;
|
||||
const breakRemoveOn = over?.data?.current?.breakRemoveOn;
|
||||
const wrapSchema = over?.data?.current?.wrapSchema;
|
||||
const onSuccess = over?.data?.current?.onSuccess;
|
||||
const removeParentsIfNoChildren = over?.data?.current?.removeParentsIfNoChildren ?? true;
|
||||
if (!activeSchema || !overSchema) {
|
||||
onDragEnd?.(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeSchema === overSchema) {
|
||||
props?.onDragEnd?.(event);
|
||||
return;
|
||||
}
|
||||
if (activeSchema === overSchema) {
|
||||
onDragEnd?.(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeSchema.parent === overSchema && insertAdjacent === 'beforeEnd') {
|
||||
props?.onDragEnd?.(event);
|
||||
return;
|
||||
}
|
||||
if (activeSchema.parent === overSchema && insertAdjacent === 'beforeEnd') {
|
||||
onDragEnd?.(event);
|
||||
return;
|
||||
}
|
||||
|
||||
const dn = createDesignable({
|
||||
t,
|
||||
api,
|
||||
refresh,
|
||||
current: overSchema,
|
||||
});
|
||||
|
||||
dn.loadAPIClientEvents();
|
||||
|
||||
if (activeSchema.parent === overSchema.parent) {
|
||||
dn.insertBeforeBeginOrAfterEnd(activeSchema);
|
||||
props?.onDragEnd?.(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (insertAdjacent) {
|
||||
dn.insertAdjacent(insertAdjacent, activeSchema, {
|
||||
wrap: wrapSchema,
|
||||
breakRemoveOn,
|
||||
removeParentsIfNoChildren,
|
||||
onSuccess,
|
||||
const dn = createDesignable({
|
||||
t,
|
||||
api,
|
||||
refresh,
|
||||
current: overSchema,
|
||||
});
|
||||
props?.onDragEnd?.(event);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
dn.loadAPIClientEvents();
|
||||
|
||||
if (activeSchema.parent === overSchema.parent) {
|
||||
dn.insertBeforeBeginOrAfterEnd(activeSchema);
|
||||
onDragEnd?.(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (insertAdjacent) {
|
||||
dn.insertAdjacent(insertAdjacent, activeSchema, {
|
||||
wrap: wrapSchema,
|
||||
breakRemoveOn,
|
||||
removeParentsIfNoChildren,
|
||||
onSuccess,
|
||||
});
|
||||
onDragEnd?.(event);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[onDragEnd],
|
||||
);
|
||||
};
|
||||
|
||||
export const DndContext = observer(
|
||||
(props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
const onDragStart = useCallback(
|
||||
(event) => {
|
||||
const { active } = event;
|
||||
const activeSchema = active?.data?.current?.schema;
|
||||
setVisible(!!activeSchema);
|
||||
if (props?.onDragStart) {
|
||||
props?.onDragStart?.(event);
|
||||
}
|
||||
},
|
||||
[props?.onDragStart],
|
||||
);
|
||||
|
||||
const onDragEnd = useDragEnd(props?.onDragEnd);
|
||||
|
||||
return (
|
||||
<DndKitContext
|
||||
collisionDetection={rectIntersection}
|
||||
{...props}
|
||||
onDragStart={(event) => {
|
||||
const { active } = event;
|
||||
const activeSchema = active?.data?.current?.schema;
|
||||
setVisible(!!activeSchema);
|
||||
if (props?.onDragStart) {
|
||||
props?.onDragStart?.(event);
|
||||
}
|
||||
}}
|
||||
onDragEnd={useDragEnd(props)}
|
||||
>
|
||||
<DndKitContext collisionDetection={rectIntersection} {...props} onDragStart={onDragStart} onDragEnd={onDragEnd}>
|
||||
<DragOverlay
|
||||
dropAnimation={{
|
||||
duration: 10,
|
||||
|
@ -2,6 +2,7 @@ import { IRecursionFieldProps, ISchemaFieldProps, RecursionField, Schema } from
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { SchemaComponentContext } from '../context';
|
||||
import { SchemaComponentOptions } from './SchemaComponentOptions';
|
||||
import { useUpdate } from 'ahooks';
|
||||
|
||||
type SchemaComponentOnChange = {
|
||||
onChange?: (s: Schema) => void;
|
||||
@ -26,16 +27,30 @@ const useMemoizedSchema = (schema) => {
|
||||
return useMemo(() => toSchema(schema), []);
|
||||
};
|
||||
|
||||
const RecursionSchemaComponent = (props: ISchemaFieldProps & SchemaComponentOnChange) => {
|
||||
const { components, scope, schema, ...others } = props;
|
||||
interface DistributedProps {
|
||||
/**
|
||||
* 是否和父级隔离刷新
|
||||
* @default false
|
||||
*/
|
||||
distributed?: boolean;
|
||||
}
|
||||
|
||||
const RecursionSchemaComponent = (props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => {
|
||||
const { components, scope, schema, distributed, ...others } = props;
|
||||
const ctx = useContext(SchemaComponentContext);
|
||||
const s = useMemo(() => toSchema(schema), [schema]);
|
||||
const refresh = useUpdate();
|
||||
|
||||
return (
|
||||
<SchemaComponentContext.Provider
|
||||
value={{
|
||||
...ctx,
|
||||
distributed: ctx.distributed == false ? false : distributed,
|
||||
refresh: () => {
|
||||
ctx.refresh?.();
|
||||
refresh();
|
||||
if (ctx.distributed === false || distributed === false) {
|
||||
ctx.refresh?.();
|
||||
}
|
||||
props.onChange?.(s);
|
||||
},
|
||||
}}
|
||||
@ -47,14 +62,15 @@ const RecursionSchemaComponent = (props: ISchemaFieldProps & SchemaComponentOnCh
|
||||
);
|
||||
};
|
||||
|
||||
const MemoizedSchemaComponent = (props: ISchemaFieldProps & SchemaComponentOnChange) => {
|
||||
const MemoizedSchemaComponent = (props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => {
|
||||
const { schema, ...others } = props;
|
||||
const s = useMemoizedSchema(schema);
|
||||
return <RecursionSchemaComponent {...others} schema={s} />;
|
||||
};
|
||||
|
||||
export const SchemaComponent = (
|
||||
props: (ISchemaFieldProps | IRecursionFieldProps) & { memoized?: boolean } & SchemaComponentOnChange,
|
||||
props: (ISchemaFieldProps | IRecursionFieldProps) & { memoized?: boolean } & SchemaComponentOnChange &
|
||||
DistributedProps,
|
||||
) => {
|
||||
const { memoized, ...others } = props;
|
||||
if (memoized) {
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { createForm } from '@formily/core';
|
||||
import { FormProvider, Schema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useUpdate } from 'ahooks';
|
||||
import { SchemaComponentContext } from '../context';
|
||||
import { ISchemaComponentProvider } from '../types';
|
||||
import { SchemaComponentOptions } from './SchemaComponentOptions';
|
||||
import { SchemaComponentOptions, useSchemaOptionsContext } from './SchemaComponentOptions';
|
||||
|
||||
const randomString = (prefix = '') => {
|
||||
return `${prefix}${uid()}`;
|
||||
@ -44,36 +45,47 @@ Schema.registerCompiler(Registry.compile);
|
||||
|
||||
export const SchemaComponentProvider: React.FC<ISchemaComponentProvider> = (props) => {
|
||||
const { designable, onDesignableChange, components, children } = props;
|
||||
const [uidValue, setUid] = useState(uid());
|
||||
const ctx = useContext(SchemaComponentContext);
|
||||
const ctxOptions = useSchemaOptionsContext();
|
||||
const refresh = useUpdate();
|
||||
const [formId, setFormId] = useState(uid());
|
||||
const form = useMemo(() => props.form || createForm(), [formId]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const scope = useMemo(() => {
|
||||
return { ...props.scope, t, randomString };
|
||||
}, [props.scope, t]);
|
||||
}, [props.scope, t, ctxOptions?.scope]);
|
||||
|
||||
const [active, setActive] = useState(designable);
|
||||
|
||||
const schemaComponentContextValue = useMemo(
|
||||
() => ({
|
||||
scope,
|
||||
components,
|
||||
reset: () => setFormId(uid()),
|
||||
refresh: () => {
|
||||
setUid(uid());
|
||||
},
|
||||
designable: typeof designable === 'boolean' ? designable : active,
|
||||
setDesignable: (value) => {
|
||||
if (typeof designable !== 'boolean') {
|
||||
setActive(value);
|
||||
}
|
||||
onDesignableChange?.(value);
|
||||
},
|
||||
}),
|
||||
[uidValue, scope, components, designable, active],
|
||||
);
|
||||
const designableValue = useMemo(() => {
|
||||
return typeof designable === 'boolean' ? designable : active;
|
||||
}, [designable, active, ctx.designable]);
|
||||
|
||||
const setDesignable = useMemo(() => {
|
||||
return (value) => {
|
||||
if (typeof designableValue !== 'boolean') {
|
||||
setActive(value);
|
||||
}
|
||||
onDesignableChange?.(value);
|
||||
};
|
||||
}, [designableValue, onDesignableChange]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setFormId(uid());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SchemaComponentContext.Provider value={schemaComponentContextValue}>
|
||||
<SchemaComponentContext.Provider
|
||||
value={{
|
||||
scope,
|
||||
components,
|
||||
reset,
|
||||
refresh,
|
||||
designable: designableValue,
|
||||
setDesignable,
|
||||
}}
|
||||
>
|
||||
<FormProvider form={form}>
|
||||
<SchemaComponentOptions inherit scope={scope} components={components}>
|
||||
{children}
|
||||
|
@ -1,29 +1,36 @@
|
||||
import { useEventListener } from 'ahooks';
|
||||
import { debounce } from 'lodash';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
export const useTableSize = () => {
|
||||
export const useTableSize = (enable?: boolean) => {
|
||||
const [height, setTableHeight] = useState(0);
|
||||
const [width, setTableWidth] = useState(0);
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const calcTableSize = useCallback(() => {
|
||||
if (!elementRef.current) return;
|
||||
const clientRect = elementRef.current.getBoundingClientRect();
|
||||
const tableHeight = Math.ceil(clientRect?.height || 0);
|
||||
const headerHeight = elementRef.current.querySelector('.ant-table-header')?.getBoundingClientRect().height || 0;
|
||||
const tableContentRect = elementRef.current.querySelector('.ant-table')?.getBoundingClientRect();
|
||||
if (!tableContentRect) return;
|
||||
const paginationRect = elementRef.current.querySelector('.ant-table-pagination')?.getBoundingClientRect();
|
||||
const paginationHeight = paginationRect
|
||||
? paginationRect.y - tableContentRect.height - tableContentRect.y + paginationRect.height
|
||||
: 0;
|
||||
setTableWidth(clientRect.width);
|
||||
setTableHeight(tableHeight - headerHeight - paginationHeight);
|
||||
}, []);
|
||||
const calcTableSize = useCallback(
|
||||
debounce(() => {
|
||||
if (!elementRef.current || !enable) return;
|
||||
const clientRect = elementRef.current.getBoundingClientRect();
|
||||
const tableHeight = Math.ceil(clientRect?.height || 0);
|
||||
const headerHeight = elementRef.current.querySelector('.ant-table-header')?.getBoundingClientRect().height || 0;
|
||||
const tableContentRect = elementRef.current.querySelector('.ant-table')?.getBoundingClientRect();
|
||||
if (!tableContentRect) return;
|
||||
const paginationRect = elementRef.current.querySelector('.ant-table-pagination')?.getBoundingClientRect();
|
||||
const paginationHeight = paginationRect
|
||||
? paginationRect.y - tableContentRect.height - tableContentRect.y + paginationRect.height
|
||||
: 0;
|
||||
setTableWidth(clientRect.width);
|
||||
setTableHeight(tableHeight - headerHeight - paginationHeight);
|
||||
}, 100),
|
||||
[enable],
|
||||
);
|
||||
|
||||
const tableSizeRefCallback: React.RefCallback<HTMLDivElement> = (ref) => {
|
||||
elementRef.current = ref && ref.children ? (ref.children[0] as HTMLDivElement) : null;
|
||||
calcTableSize();
|
||||
};
|
||||
const tableSizeRefCallback: React.RefCallback<HTMLDivElement> = useCallback(
|
||||
(ref) => {
|
||||
elementRef.current = ref && ref.children ? (ref.children[0] as HTMLDivElement) : null;
|
||||
calcTableSize();
|
||||
},
|
||||
[calcTableSize],
|
||||
);
|
||||
|
||||
useEventListener('resize', calcTableSize);
|
||||
|
||||
|
@ -10,6 +10,7 @@ export interface ISchemaComponentContext {
|
||||
designable?: boolean;
|
||||
setDesignable?: (value: boolean) => void;
|
||||
SchemaField?: React.FC<ISchemaFieldProps>;
|
||||
distributed?: boolean;
|
||||
}
|
||||
|
||||
export interface ISchemaComponentProvider {
|
||||
|
@ -1,12 +1,8 @@
|
||||
import { SchemaComponentOptions, CurrentAppInfoProvider } from '@nocobase/client';
|
||||
import { SchemaComponentOptions } from '@nocobase/client';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
export const DuplicatorProvider: FC = function (props) {
|
||||
return (
|
||||
<CurrentAppInfoProvider>
|
||||
<SchemaComponentOptions>{props.children}</SchemaComponentOptions>
|
||||
</CurrentAppInfoProvider>
|
||||
);
|
||||
return <SchemaComponentOptions>{props.children}</SchemaComponentOptions>;
|
||||
};
|
||||
|
||||
DuplicatorProvider.displayName = 'DuplicatorProvider';
|
||||
|
@ -23,20 +23,15 @@ export const ChartQueryMetadataProvider: React.FC = (props) => {
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const service = useRequest<{
|
||||
data: any;
|
||||
}>(options, {
|
||||
manual: true,
|
||||
});
|
||||
|
||||
const isAdminPage = location.pathname.startsWith('/admin');
|
||||
const token = api.auth.getToken() || '';
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdminPage && token) {
|
||||
service.run();
|
||||
}
|
||||
}, [isAdminPage, token]);
|
||||
const service = useRequest<{
|
||||
data: any;
|
||||
}>(options, {
|
||||
refreshDeps: [isAdminPage, token],
|
||||
ready: !!(isAdminPage && token),
|
||||
});
|
||||
|
||||
const refresh = async () => {
|
||||
const { data } = await api.request(options);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ActionBar, CurrentAppInfoProvider, Plugin, SchemaComponentOptions } from '@nocobase/client';
|
||||
import { ActionBar, Plugin, SchemaComponentOptions } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { GanttDesigner } from './Gantt.Designer';
|
||||
import { ganttSettings, oldGanttSettings } from './Gantt.Settings';
|
||||
@ -17,14 +17,12 @@ export { Gantt };
|
||||
|
||||
const GanttProvider = React.memo((props) => {
|
||||
return (
|
||||
<CurrentAppInfoProvider>
|
||||
<SchemaComponentOptions
|
||||
components={{ Gantt, GanttBlockInitializer, GanttBlockProvider }}
|
||||
scope={{ useGanttBlockProps }}
|
||||
>
|
||||
{props.children}
|
||||
</SchemaComponentOptions>
|
||||
</CurrentAppInfoProvider>
|
||||
<SchemaComponentOptions
|
||||
components={{ Gantt, GanttBlockInitializer, GanttBlockProvider }}
|
||||
scope={{ useGanttBlockProps }}
|
||||
>
|
||||
{props.children}
|
||||
</SchemaComponentOptions>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -14,6 +14,9 @@
|
||||
"@nocobase/server": "0.x",
|
||||
"@nocobase/test": "0.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-intersection-observer": "^9.8.1"
|
||||
},
|
||||
"gitHead": "d0b4efe4be55f8c79a98a331d99d9f8cf99021a1",
|
||||
"keywords": [
|
||||
"Blocks"
|
||||
|
@ -1,122 +1,124 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { FormLayout } from '@formily/antd-v5';
|
||||
import { observer, RecursionField, useFieldSchema } from '@formily/react';
|
||||
import {
|
||||
ActionContextProvider,
|
||||
DndContext,
|
||||
RecordProvider,
|
||||
SchemaComponentOptions,
|
||||
useCollectionParentRecordData,
|
||||
} from '@nocobase/client';
|
||||
import { ActionContextProvider, DndContext, RecordProvider, useCollectionParentRecordData } from '@nocobase/client';
|
||||
import { Card } from 'antd';
|
||||
import cls from 'classnames';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import React, { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { KanbanCardContext } from './context';
|
||||
|
||||
const cardCss = css`
|
||||
.ant-card-body {
|
||||
padding: 16px;
|
||||
}
|
||||
.nb-row-divider {
|
||||
height: 16px;
|
||||
margin-top: -16px;
|
||||
&:last-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.ant-description-input {
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ant-description-textarea {
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ant-formily-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.nb-grid-row:last-of-type {
|
||||
.nb-grid-col {
|
||||
.nb-form-item:last-of-type {
|
||||
.ant-formily-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-formily-item-control .ant-space-item: {
|
||||
whitespace: normal;
|
||||
wordbreak: break-all;
|
||||
wordwrap: break-word;
|
||||
}
|
||||
.ant-formily-item-label {
|
||||
color: #8c8c8c;
|
||||
fontweight: normal;
|
||||
}
|
||||
`;
|
||||
|
||||
const MemorizedRecursionField = React.memo(RecursionField);
|
||||
MemorizedRecursionField.displayName = 'MemorizedRecursionField';
|
||||
|
||||
export const KanbanCard: any = observer(
|
||||
(props: any) => {
|
||||
() => {
|
||||
const { setDisableCardDrag, cardViewerSchema, card, cardField, columnIndex, cardIndex } =
|
||||
useContext(KanbanCardContext);
|
||||
const parentRecordData = useCollectionParentRecordData();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const handleCardClick = useCallback((e: React.MouseEvent) => {
|
||||
const targetElement = e.target as Element; // 将事件目标转换为Element类型
|
||||
const currentTargetElement = e.currentTarget as Element;
|
||||
if (currentTargetElement.contains(targetElement)) {
|
||||
setVisible(true);
|
||||
e.stopPropagation();
|
||||
} else {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}, []);
|
||||
const cardStyle = useMemo(() => {
|
||||
return {
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onDragStart = useCallback(() => {
|
||||
setDisableCardDrag(true);
|
||||
}, []);
|
||||
const onDragEnd = useCallback(() => {
|
||||
setDisableCardDrag(false);
|
||||
}, []);
|
||||
|
||||
const actionContextValue = useMemo(() => {
|
||||
return {
|
||||
openMode: fieldSchema['x-component-props']?.['openMode'] || 'drawer',
|
||||
openSize: fieldSchema['x-component-props']?.['openSize'],
|
||||
visible,
|
||||
setVisible,
|
||||
};
|
||||
}, [fieldSchema, visible]);
|
||||
|
||||
const basePath = useMemo(
|
||||
() => cardField.address.concat(`${columnIndex}.cards.${cardIndex}`),
|
||||
[cardField, columnIndex, cardIndex],
|
||||
);
|
||||
const cardViewerBasePath = useMemo(
|
||||
() => cardField.address.concat(`${columnIndex}.cardViewer.${cardIndex}`),
|
||||
[cardField, columnIndex, cardIndex],
|
||||
);
|
||||
|
||||
return (
|
||||
<SchemaComponentOptions components={{}} scope={{}}>
|
||||
<Card
|
||||
onClick={(e) => {
|
||||
const targetElement = e.target as Element; // 将事件目标转换为Element类型
|
||||
const currentTargetElement = e.currentTarget as Element;
|
||||
if (currentTargetElement.contains(targetElement)) {
|
||||
setVisible(true);
|
||||
e.stopPropagation();
|
||||
} else {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
bordered={false}
|
||||
hoverable
|
||||
style={{ cursor: 'pointer', overflow: 'hidden' }}
|
||||
// bodyStyle={{ paddingBottom: 0 }}
|
||||
className={cls(css`
|
||||
.ant-card-body {
|
||||
padding: 16px;
|
||||
}
|
||||
.nb-row-divider {
|
||||
height: 16px;
|
||||
margin-top: -16px;
|
||||
&:last-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.ant-description-input {
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ant-description-textarea {
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ant-formily-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.nb-grid-row:last-of-type {
|
||||
.nb-grid-col {
|
||||
.nb-form-item:last-of-type {
|
||||
.ant-formily-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-formily-item-control .ant-space-item: {
|
||||
whitespace: normal;
|
||||
wordbreak: break-all;
|
||||
wordwrap: break-word;
|
||||
}
|
||||
.ant-formily-item-label {
|
||||
color: #8c8c8c;
|
||||
fontweight: normal;
|
||||
}
|
||||
`)}
|
||||
>
|
||||
<DndContext
|
||||
onDragStart={() => {
|
||||
setDisableCardDrag(true);
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
setDisableCardDrag(false);
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<Card onClick={handleCardClick} bordered={false} hoverable style={cardStyle} className={cardCss}>
|
||||
<DndContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
|
||||
<FormLayout layout={'vertical'}>
|
||||
<RecursionField
|
||||
basePath={cardField.address.concat(`${columnIndex}.cards.${cardIndex}`)}
|
||||
schema={fieldSchema}
|
||||
onlyRenderProperties
|
||||
/>
|
||||
<MemorizedRecursionField basePath={basePath} schema={fieldSchema} onlyRenderProperties />
|
||||
</FormLayout>
|
||||
</DndContext>
|
||||
</Card>
|
||||
{cardViewerSchema && (
|
||||
<ActionContextProvider
|
||||
value={{
|
||||
openMode: fieldSchema['x-component-props']?.['openMode'] || 'drawer',
|
||||
openSize: fieldSchema['x-component-props']?.['openSize'],
|
||||
visible,
|
||||
setVisible,
|
||||
}}
|
||||
>
|
||||
<ActionContextProvider value={actionContextValue}>
|
||||
<RecordProvider record={card} parent={parentRecordData}>
|
||||
<RecursionField
|
||||
basePath={cardField.address.concat(`${columnIndex}.cardViewer.${cardIndex}`)}
|
||||
schema={cardViewerSchema}
|
||||
onlyRenderProperties
|
||||
/>
|
||||
<MemorizedRecursionField basePath={cardViewerBasePath} schema={cardViewerSchema} onlyRenderProperties />
|
||||
</RecordProvider>
|
||||
</ActionContextProvider>
|
||||
)}
|
||||
</SchemaComponentOptions>
|
||||
</>
|
||||
);
|
||||
},
|
||||
{ displayName: 'KanbanCard' },
|
||||
|
@ -4,12 +4,14 @@ import {
|
||||
RecordProvider,
|
||||
SchemaComponentOptions,
|
||||
useCreateActionProps as useCAP,
|
||||
useCollection,
|
||||
useCollectionParentRecordData,
|
||||
useProps,
|
||||
withDynamicSchemaProps,
|
||||
} from '@nocobase/client';
|
||||
import { Spin, Tag } from 'antd';
|
||||
import React, { useContext, useMemo, useState } from 'react';
|
||||
import { Spin, Tag, Card, Skeleton } from 'antd';
|
||||
import React, { useCallback, useContext, useMemo, useRef, useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { Board } from './board';
|
||||
import { KanbanCardContext, KanbanColumnContext } from './context';
|
||||
import { useStyles } from './style';
|
||||
@ -57,105 +59,130 @@ export const toColumns = (groupField: any, dataSource: Array<any> = [], primaryK
|
||||
return Object.values(columns);
|
||||
};
|
||||
|
||||
const MemorizedRecursionField = React.memo(RecursionField);
|
||||
MemorizedRecursionField.displayName = 'MemorizedRecursionField';
|
||||
|
||||
export const Kanban: any = withDynamicSchemaProps(
|
||||
observer(
|
||||
(props: any) => {
|
||||
const { styles } = useStyles();
|
||||
observer((props: any) => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
// 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
|
||||
const { groupField, onCardDragEnd, dataSource, setDataSource, ...restProps } = useProps(props);
|
||||
// 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
|
||||
const { groupField, onCardDragEnd, dataSource, setDataSource, ...restProps } = useProps(props);
|
||||
|
||||
const parentRecordData = useCollectionParentRecordData();
|
||||
const field = useField<ArrayField>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const [disableCardDrag, setDisableCardDrag] = useState(false);
|
||||
const schemas = useMemo(
|
||||
() =>
|
||||
fieldSchema.reduceProperties(
|
||||
(buf, current) => {
|
||||
if (current['x-component'].endsWith('.Card')) {
|
||||
buf.card = current;
|
||||
} else if (current['x-component'].endsWith('.CardAdder')) {
|
||||
buf.cardAdder = current;
|
||||
} else if (current['x-component'].endsWith('.CardViewer')) {
|
||||
buf.cardViewer = current;
|
||||
}
|
||||
return buf;
|
||||
},
|
||||
{ card: null, cardAdder: null, cardViewer: null },
|
||||
),
|
||||
[],
|
||||
);
|
||||
const handleCardRemove = (card, column) => {
|
||||
const collection = useCollection();
|
||||
const primaryKey = collection.getPrimaryKey();
|
||||
const parentRecordData = useCollectionParentRecordData();
|
||||
const field = useField<ArrayField>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const [disableCardDrag, setDisableCardDrag] = useState(false);
|
||||
const schemas = useMemo(
|
||||
() =>
|
||||
fieldSchema.reduceProperties(
|
||||
(buf, current) => {
|
||||
if (current['x-component'].endsWith('.Card')) {
|
||||
buf.card = current;
|
||||
} else if (current['x-component'].endsWith('.CardAdder')) {
|
||||
buf.cardAdder = current;
|
||||
} else if (current['x-component'].endsWith('.CardViewer')) {
|
||||
buf.cardViewer = current;
|
||||
}
|
||||
return buf;
|
||||
},
|
||||
{ card: null, cardAdder: null, cardViewer: null },
|
||||
),
|
||||
[],
|
||||
);
|
||||
const handleCardRemove = useCallback(
|
||||
(card, column) => {
|
||||
const updatedBoard = Board.removeCard({ columns: field.value }, column, card);
|
||||
field.value = updatedBoard.columns;
|
||||
setDataSource(updatedBoard.columns);
|
||||
};
|
||||
const handleCardDragEnd = (card, fromColumn, toColumn) => {
|
||||
},
|
||||
[field],
|
||||
);
|
||||
const lastDraggedCard = useRef(null);
|
||||
const handleCardDragEnd = useCallback(
|
||||
(card, fromColumn, toColumn) => {
|
||||
lastDraggedCard.current = card[primaryKey];
|
||||
onCardDragEnd?.({ columns: field.value, groupField }, fromColumn, toColumn);
|
||||
const updatedBoard = Board.moveCard({ columns: field.value }, fromColumn, toColumn);
|
||||
field.value = updatedBoard.columns;
|
||||
setDataSource(updatedBoard.columns);
|
||||
};
|
||||
return (
|
||||
<Spin wrapperClassName={styles.nbKanban} spinning={field.loading || false}>
|
||||
<Board
|
||||
{...restProps}
|
||||
allowAddCard={!!schemas.cardAdder}
|
||||
disableColumnDrag
|
||||
cardAdderPosition={'bottom'}
|
||||
disableCardDrag={restProps.disableCardDrag || disableCardDrag}
|
||||
onCardRemove={handleCardRemove}
|
||||
onCardDragEnd={handleCardDragEnd}
|
||||
renderColumnHeader={({ title, color }) => (
|
||||
<div className={'react-kanban-column-header'}>
|
||||
<Tag color={color}>{title}</Tag>
|
||||
</div>
|
||||
)}
|
||||
renderCard={(card, { column, dragging }) => {
|
||||
const columnIndex = dataSource?.indexOf(column);
|
||||
const cardIndex = column?.cards?.indexOf(card);
|
||||
return (
|
||||
schemas.card && (
|
||||
<RecordProvider record={card} parent={parentRecordData}>
|
||||
<KanbanCardContext.Provider
|
||||
value={{
|
||||
setDisableCardDrag,
|
||||
cardViewerSchema: schemas.cardViewer,
|
||||
cardField: field,
|
||||
card,
|
||||
column,
|
||||
dragging,
|
||||
columnIndex,
|
||||
cardIndex,
|
||||
}}
|
||||
>
|
||||
<RecursionField name={schemas.card.name} schema={schemas.card} />
|
||||
</KanbanCardContext.Provider>
|
||||
</RecordProvider>
|
||||
)
|
||||
);
|
||||
}}
|
||||
renderCardAdder={({ column }) => {
|
||||
if (!schemas.cardAdder) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<KanbanColumnContext.Provider value={{ column, groupField }}>
|
||||
<SchemaComponentOptions scope={{ useCreateActionProps }}>
|
||||
<RecursionField name={schemas.cardAdder.name} schema={schemas.cardAdder} />
|
||||
</SchemaComponentOptions>
|
||||
</KanbanColumnContext.Provider>
|
||||
);
|
||||
}}
|
||||
>
|
||||
{{
|
||||
columns: dataSource || [],
|
||||
}}
|
||||
</Board>
|
||||
</Spin>
|
||||
);
|
||||
},
|
||||
{ displayName: 'Kanban' },
|
||||
),
|
||||
},
|
||||
[field],
|
||||
);
|
||||
|
||||
return (
|
||||
<Spin wrapperClassName={styles.nbKanban} spinning={field.loading || false}>
|
||||
<Board
|
||||
{...restProps}
|
||||
allowAddCard={!!schemas.cardAdder}
|
||||
disableColumnDrag
|
||||
cardAdderPosition={'bottom'}
|
||||
disableCardDrag={restProps.disableCardDrag || disableCardDrag}
|
||||
onCardRemove={handleCardRemove}
|
||||
onCardDragEnd={handleCardDragEnd}
|
||||
renderColumnHeader={({ title, color }) => (
|
||||
<div className={'react-kanban-column-header'}>
|
||||
<Tag color={color}>{title}</Tag>
|
||||
</div>
|
||||
)}
|
||||
renderCard={(card, { column, dragging }) => {
|
||||
const columnIndex = dataSource?.indexOf(column);
|
||||
const cardIndex = column?.cards?.indexOf(card);
|
||||
const { ref, inView } = useInView({
|
||||
threshold: 0.8,
|
||||
triggerOnce: true,
|
||||
initialInView: lastDraggedCard.current && lastDraggedCard.current === card[primaryKey],
|
||||
});
|
||||
return (
|
||||
schemas.card && (
|
||||
<RecordProvider record={card} parent={parentRecordData}>
|
||||
<KanbanCardContext.Provider
|
||||
value={{
|
||||
setDisableCardDrag,
|
||||
cardViewerSchema: schemas.cardViewer,
|
||||
cardField: field,
|
||||
card,
|
||||
column,
|
||||
dragging,
|
||||
columnIndex,
|
||||
cardIndex,
|
||||
}}
|
||||
>
|
||||
<div ref={ref}>
|
||||
{inView ? (
|
||||
<MemorizedRecursionField name={schemas.card.name} schema={schemas.card} />
|
||||
) : (
|
||||
<Card bordered={false}>
|
||||
<Skeleton active paragraph={{ rows: 4 }} />
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</KanbanCardContext.Provider>
|
||||
</RecordProvider>
|
||||
)
|
||||
);
|
||||
}}
|
||||
renderCardAdder={({ column }) => {
|
||||
if (!schemas.cardAdder) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<KanbanColumnContext.Provider value={{ column, groupField }}>
|
||||
<SchemaComponentOptions scope={{ useCreateActionProps }}>
|
||||
<MemorizedRecursionField name={schemas.cardAdder.name} schema={schemas.cardAdder} />
|
||||
</SchemaComponentOptions>
|
||||
</KanbanColumnContext.Provider>
|
||||
);
|
||||
}}
|
||||
>
|
||||
{{
|
||||
columns: dataSource || [],
|
||||
}}
|
||||
</Board>
|
||||
</Spin>
|
||||
);
|
||||
}),
|
||||
{ displayName: 'Kanban' },
|
||||
);
|
||||
|
@ -2,7 +2,7 @@ import { ArrayField } from '@formily/core';
|
||||
import { Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import { Spin } from 'antd';
|
||||
import uniq from 'lodash/uniq';
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||
import {
|
||||
useACLRoleContext,
|
||||
useCollection_deprecated,
|
||||
@ -10,7 +10,9 @@ import {
|
||||
FixedBlockWrapper,
|
||||
BlockProvider,
|
||||
useBlockRequestContext,
|
||||
useCollection,
|
||||
} from '@nocobase/client';
|
||||
import { isEqual } from 'lodash';
|
||||
import { toColumns } from './Kanban';
|
||||
|
||||
export const KanbanBlockContext = createContext<any>({});
|
||||
@ -138,20 +140,21 @@ export const useKanbanBlockProps = () => {
|
||||
const field = useField<ArrayField>();
|
||||
const ctx = useKanbanBlockContext();
|
||||
const [dataSource, setDataSource] = useState([]);
|
||||
const primaryKey = useCollection_deprecated().getPrimaryKey();
|
||||
const primaryKey = useCollection()?.getPrimaryKey();
|
||||
|
||||
useEffect(() => {
|
||||
if (!ctx?.service?.loading) {
|
||||
field.value = toColumns(ctx.groupField, ctx?.service?.data?.data, primaryKey);
|
||||
setDataSource(toColumns(ctx.groupField, ctx?.service?.data?.data, primaryKey));
|
||||
const data = toColumns(ctx.groupField, ctx?.service?.data?.data, primaryKey);
|
||||
if (isEqual(field.value, data)) {
|
||||
return;
|
||||
}
|
||||
// field.loading = ctx?.service?.loading;
|
||||
field.value = data;
|
||||
setDataSource(data);
|
||||
}, [ctx?.service?.loading]);
|
||||
return {
|
||||
setDataSource,
|
||||
dataSource,
|
||||
groupField: ctx.groupField,
|
||||
disableCardDrag: useDisableCardDrag(),
|
||||
async onCardDragEnd({ columns, groupField }, { fromColumnId, fromPosition }, { toColumnId, toPosition }) {
|
||||
|
||||
const disableCardDrag = useDisableCardDrag();
|
||||
|
||||
const onCardDragEnd = useCallback(
|
||||
async ({ columns, groupField }, { fromColumnId, fromPosition }, { toColumnId, toPosition }) => {
|
||||
const sourceColumn = columns.find((column) => column.id === fromColumnId);
|
||||
const destinationColumn = columns.find((column) => column.id === toColumnId);
|
||||
const sourceCard = sourceColumn?.cards?.[fromPosition];
|
||||
@ -169,5 +172,14 @@ export const useKanbanBlockProps = () => {
|
||||
}
|
||||
await ctx.resource.move(values);
|
||||
},
|
||||
[ctx?.sortField],
|
||||
);
|
||||
|
||||
return {
|
||||
setDataSource,
|
||||
dataSource,
|
||||
groupField: ctx.groupField,
|
||||
disableCardDrag,
|
||||
onCardDragEnd,
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Action, CurrentAppInfoProvider, Plugin, SchemaComponentOptions } from '@nocobase/client';
|
||||
import { Action, Plugin, SchemaComponentOptions } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { Kanban } from './Kanban';
|
||||
import { KanbanCard } from './Kanban.Card';
|
||||
@ -20,14 +20,12 @@ const KanbanV2 = Kanban;
|
||||
|
||||
const KanbanPluginProvider = React.memo((props) => {
|
||||
return (
|
||||
<CurrentAppInfoProvider>
|
||||
<SchemaComponentOptions
|
||||
components={{ Kanban, KanbanBlockProvider, KanbanV2, KanbanBlockInitializer }}
|
||||
scope={{ useKanbanBlockProps }}
|
||||
>
|
||||
{props.children}
|
||||
</SchemaComponentOptions>
|
||||
</CurrentAppInfoProvider>
|
||||
<SchemaComponentOptions
|
||||
components={{ Kanban, KanbanBlockProvider, KanbanV2, KanbanBlockInitializer }}
|
||||
scope={{ useKanbanBlockProps }}
|
||||
>
|
||||
{props.children}
|
||||
</SchemaComponentOptions>
|
||||
);
|
||||
});
|
||||
KanbanPluginProvider.displayName = 'KanbanPluginProvider';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CurrentAppInfoProvider, Plugin, SchemaComponentOptions } from '@nocobase/client';
|
||||
import { Plugin, SchemaComponentOptions } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { MapBlockOptions } from './block';
|
||||
import { mapActionInitializers, mapActionInitializers_deprecated } from './block/MapActionInitializers';
|
||||
@ -9,11 +9,9 @@ import { NAMESPACE, generateNTemplate } from './locale';
|
||||
import { useMapBlockProps } from './block/MapBlockProvider';
|
||||
const MapProvider = React.memo((props) => {
|
||||
return (
|
||||
<CurrentAppInfoProvider>
|
||||
<SchemaComponentOptions components={{ Map }}>
|
||||
<MapBlockOptions>{props.children}</MapBlockOptions>
|
||||
</SchemaComponentOptions>
|
||||
</CurrentAppInfoProvider>
|
||||
<SchemaComponentOptions components={{ Map }}>
|
||||
<MapBlockOptions>{props.children}</MapBlockOptions>
|
||||
</SchemaComponentOptions>
|
||||
);
|
||||
});
|
||||
MapProvider.displayName = 'MapProvider';
|
||||
|
@ -359,6 +359,7 @@ export function NodeDefaultView(props) {
|
||||
>
|
||||
<FormProvider form={form}>
|
||||
<SchemaComponent
|
||||
distributed={false}
|
||||
scope={{
|
||||
...instruction.scope,
|
||||
useFormProviderProps,
|
||||
|
@ -21808,6 +21808,11 @@ react-image-lightbox@^5.1.4:
|
||||
prop-types "^15.7.2"
|
||||
react-modal "^3.11.1"
|
||||
|
||||
react-intersection-observer@9.8.1, react-intersection-observer@^9.8.1:
|
||||
version "9.8.1"
|
||||
resolved "https://registry.npmmirror.com/react-intersection-observer/-/react-intersection-observer-9.8.1.tgz#9c3631c0c9acd624a2af1c192318752ea73b5d91"
|
||||
integrity sha512-QzOFdROX8D8MH3wE3OVKH0f3mLjKTtEN1VX/rkNuECCff+aKky0pIjulDhr3Ewqj5el/L+MhBkM3ef0Tbt+qUQ==
|
||||
|
||||
react-intl@^6.4.4:
|
||||
version "6.5.5"
|
||||
resolved "https://registry.npmmirror.com/react-intl/-/react-intl-6.5.5.tgz#d2de7bfd79718a7e3d8031e2599e94e0c8638377"
|
||||
|
Loading…
x
Reference in New Issue
Block a user