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