Merge branch 'next' into develop

This commit is contained in:
Zeke Zhang 2024-08-20 15:35:26 +08:00
commit f2f4cb1cac
21 changed files with 1112 additions and 97 deletions

View File

@ -71,5 +71,7 @@ export * from './modules/blocks/data-blocks/table-selector';
export * from './modules/blocks/index';
export * from './modules/blocks/useParentRecordCommon';
export { OpenModeProvider, useOpenModeContext } from './modules/popup/OpenModeProvider';
export { PopupContextProvider } from './modules/popup/PopupContextProvider';
export { usePopupUtils } from './modules/popup/usePopupUtils';
export { VariablePopupRecordProvider } from './modules/variable/variablesProvider/VariablePopupRecordProvider';

View File

@ -8,13 +8,13 @@
*/
import React from 'react';
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
import { usePopupUtils } from '../../../schema-component/antd/page/pagePopupUtils';
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
import { useOpenModeContext } from '../../popup/OpenModeProvider';
export const CreateChildInitializer = (props) => {
const { defaultOpenMode } = useOpenModeContext();
const { getPopupContext } = usePagePopup();
const { getPopupContext } = usePopupUtils();
const schema = {
type: 'void',
title: '{{ t("Add child") }}',

View File

@ -9,14 +9,14 @@
import React from 'react';
import { useSchemaInitializerItem } from '../../../application';
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
import { usePopupUtils } from '../../../schema-component/antd/page/pagePopupUtils';
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
import { useOpenModeContext } from '../../popup/OpenModeProvider';
export const CreateActionInitializer = () => {
const { defaultOpenMode } = useOpenModeContext();
const { getPopupContext } = usePagePopup();
const { getPopupContext } = usePopupUtils();
const schema = {
type: 'void',
'x-action': 'create',

View File

@ -9,14 +9,14 @@
import React from 'react';
import { useSchemaInitializerItem } from '../../../application';
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
import { usePopupUtils } from '../../../schema-component/antd/page/pagePopupUtils';
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
import { BlockInitializer } from '../../../schema-initializer/items';
import { useOpenModeContext } from '../../popup/OpenModeProvider';
export const PopupActionInitializer = (props) => {
const { defaultOpenMode } = useOpenModeContext();
const { getPopupContext } = usePagePopup();
const { getPopupContext } = usePopupUtils();
const schema = {
type: 'void',
title: '{{ t("Popup") }}',

View File

@ -8,14 +8,14 @@
*/
import React from 'react';
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
import { usePopupUtils } from '../../../schema-component/antd/page/pagePopupUtils';
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
import { useOpenModeContext } from '../../popup/OpenModeProvider';
export const UpdateActionInitializer = (props) => {
const { defaultOpenMode } = useOpenModeContext();
const { getPopupContext } = usePagePopup();
const { getPopupContext } = usePopupUtils();
const schema = {
type: 'void',
title: '{{ t("Edit") }}',

View File

@ -8,14 +8,14 @@
*/
import React from 'react';
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
import { usePopupUtils } from '../../../schema-component/antd/page/pagePopupUtils';
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
import { useOpenModeContext } from '../../popup/OpenModeProvider';
export const ViewActionInitializer = (props) => {
const { defaultOpenMode } = useOpenModeContext();
const { getPopupContext } = usePagePopup();
const { getPopupContext } = usePopupUtils();
const schema = {
type: 'void',
title: '{{ t("View") }}',

View File

@ -0,0 +1,49 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useFieldSchema } from '@formily/react';
import React, { useCallback, useContext, useState } from 'react';
import { ActionContextProvider } from '../../schema-component';
import { PopupVisibleProvider, PopupVisibleProviderContext } from '../../schema-component/antd/page/PagePopups';
/**
* provider the context for popup to work
* @param props
* @returns
*/
export const PopupContextProvider: React.FC = (props) => {
const [visible, setVisible] = useState(false);
const { visible: visibleWithURL, setVisible: setVisibleWithURL } = useContext(PopupVisibleProviderContext) || {
visible: false,
setVisible: () => {},
};
const fieldSchema = useFieldSchema();
const _setVisible = useCallback(
(value: boolean): void => {
setVisible?.(value);
setVisibleWithURL?.(value);
},
[setVisibleWithURL],
);
const openMode = fieldSchema['x-component-props']?.['openMode'] || 'drawer';
const openSize = fieldSchema['x-component-props']?.['openSize'];
return (
<PopupVisibleProvider visible={false}>
<ActionContextProvider
visible={visible || visibleWithURL}
setVisible={_setVisible}
openMode={openMode}
openSize={openSize}
>
{props.children}
</ActionContextProvider>
</PopupVisibleProvider>
);
};

View File

@ -0,0 +1,10 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
export { usePopupUtils } from '../../schema-component/antd/page/pagePopupUtils';

View File

@ -29,7 +29,7 @@ import { SortableItem } from '../../common';
import { useCompile, useComponent, useDesigner } from '../../hooks';
import { useProps } from '../../hooks/useProps';
import { PopupVisibleProvider } from '../page/PagePopups';
import { usePagePopup } from '../page/pagePopupUtils';
import { usePopupUtils } from '../page/pagePopupUtils';
import { usePopupSettings } from '../page/PopupSettingsProvider';
import ActionContainer from './Action.Container';
import { ActionDesigner } from './Action.Designer';
@ -77,7 +77,7 @@ export const Action: ComposedAction = withDynamicSchemaProps(
const aclCtx = useACLActionParamsContext();
const { wrapSSR, componentCls, hashId } = useStyles();
const { t } = useTranslation();
const { visibleWithURL, setVisibleWithURL } = usePagePopup();
const { visibleWithURL, setVisibleWithURL } = usePopupUtils();
const [visible, setVisible] = useState(false);
const [formValueChanged, setFormValueChanged] = useState(false);
const { setSubmitted: setParentSubmitted } = useActionContext();
@ -307,7 +307,7 @@ function RenderButton({
}) {
const { t } = useTranslation();
const { isPopupVisibleControlledByURL } = usePopupSettings();
const { openPopup } = usePagePopup();
const { openPopup } = usePopupUtils();
const handleButtonClick = useCallback(
(e: React.MouseEvent, checkPortal = true) => {

View File

@ -15,7 +15,7 @@ import { useCollectionManager_deprecated } from '../../../collection-manager';
import { useCollectionRecordData } from '../../../data-source/collection-record/CollectionRecordProvider';
import { useCompile } from '../../hooks';
import { useActionContext } from '../action';
import { usePagePopup } from '../page/pagePopupUtils';
import { usePopupUtils } from '../page/pagePopupUtils';
import { transformNestedData } from './InternalCascadeSelect';
import { ButtonListProps, ReadPrettyInternalViewer, isObject } from './InternalViewer';
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
@ -47,7 +47,7 @@ const ButtonTabList: React.FC<ButtonListProps> = (props) => {
const { getCollection } = useCollectionManager_deprecated();
const targetCollection = getCollection(collectionField?.target);
const isTreeCollection = targetCollection?.template === 'tree';
const { openPopup } = usePagePopup();
const { openPopup } = usePopupUtils();
const recordData = useCollectionRecordData();
const renderRecords = () =>

View File

@ -19,7 +19,7 @@ import { useCompile } from '../../hooks';
import { ActionContextProvider, useActionContext } from '../action';
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
import { PopupVisibleProvider } from '../page/PagePopups';
import { usePagePopup } from '../page/pagePopupUtils';
import { usePopupUtils } from '../page/pagePopupUtils';
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
import { transformNestedData } from './InternalCascadeSelect';
import schema from './schema';
@ -62,7 +62,7 @@ const ButtonLinkList: FC<ButtonListProps> = (props) => {
const isTreeCollection = targetCollection?.template === 'tree';
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
const getLabelUiSchema = useLabelUiSchemaV2();
const { openPopup } = usePagePopup();
const { openPopup } = usePopupUtils();
const recordData = useCollectionRecordData();
const renderRecords = () =>
@ -143,7 +143,7 @@ export const ReadPrettyInternalViewer: React.FC = observer(
const [visible, setVisible] = useState(false);
const { options: collectionField } = useAssociationFieldContext();
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
const { visibleWithURL, setVisibleWithURL } = usePagePopup();
const { visibleWithURL, setVisibleWithURL } = usePopupUtils();
const [btnHover, setBtnHover] = useState(!!visibleWithURL);
const { defaultOpenMode } = useOpenModeContext();

View File

@ -7,7 +7,8 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { ISchema } from '@formily/json-schema';
import { ISchema, Schema } from '@formily/json-schema';
import { useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared';
import { Result } from 'antd';
import _ from 'lodash';
@ -21,7 +22,7 @@ import { SchemaComponent } from '../../core';
import { TabsContextProvider } from '../tabs/context';
import { usePopupSettings } from './PopupSettingsProvider';
import { deleteRandomNestedSchemaKey, getRandomNestedSchemaKey } from './nestedSchemaKeyStorage';
import { PopupParams, getPopupParamsFromPath, getStoredPopupContext, usePagePopup } from './pagePopupUtils';
import { PopupParams, getPopupParamsFromPath, getStoredPopupContext, usePopupUtils } from './pagePopupUtils';
import {
PopupContext,
getPopupContextFromActionOrAssociationFieldSchema,
@ -86,7 +87,7 @@ const PopupParamsProvider: FC<Omit<PopupProps, 'hidden'>> = (props) => {
const PopupTabsPropsProvider: FC = ({ children }) => {
const { params } = useCurrentPopupContext();
const { changeTab } = usePagePopup();
const { changeTab } = usePopupUtils();
const onChange = useCallback(
(key: string) => {
changeTab(key);
@ -114,7 +115,7 @@ const PagePopupsItemProvider: FC<{
*/
currentLevel: number;
}> = ({ params, context, currentLevel, children }) => {
const { closePopup } = usePagePopup();
const { closePopup } = usePopupUtils();
const [visible, _setVisible] = useState(true);
const setVisible = (visible: boolean) => {
if (!visible) {
@ -183,7 +184,17 @@ const PagePopupsItemProvider: FC<{
* @param props
* @param parentSchema
*/
export const insertChildToParentSchema = (childSchema: ISchema, props: PopupProps, parentSchema: ISchema) => {
export const insertChildToParentSchema = ({
childSchema,
props,
parentSchema,
getPopupSchema = (currentSchema) => _.get(currentSchema.properties, Object.keys(currentSchema.properties)[0]),
}: {
childSchema: ISchema;
props: PopupProps;
parentSchema: ISchema;
getPopupSchema?: (currentSchema: ISchema) => ISchema;
}) => {
const { params, context, currentLevel } = props;
const componentSchema = {
@ -203,7 +214,7 @@ export const insertChildToParentSchema = (childSchema: ISchema, props: PopupProp
const nestedPopupKey = getRandomNestedSchemaKey(params.popupuid);
if (parentSchema.properties) {
const popupSchema = _.get(parentSchema.properties, Object.keys(parentSchema.properties)[0]);
const popupSchema = getPopupSchema(parentSchema);
if (_.isEmpty(_.get(popupSchema, `properties.${nestedPopupKey}`))) {
_.set(popupSchema, `properties.${nestedPopupKey}`, componentSchema);
}
@ -211,11 +222,13 @@ export const insertChildToParentSchema = (childSchema: ISchema, props: PopupProp
};
export const PagePopups = (props: { paramsList?: PopupParams[] }) => {
const fieldSchema = useFieldSchema();
const location = useLocation();
const popupParams = props.paramsList || getPopupParamsFromPath(getPopupPath(location));
const { requestSchema } = useRequestSchema();
const [rootSchema, setRootSchema] = useState<ISchema>(null);
const popupPropsRef = useRef<PopupProps[]>([]);
const { savePopupSchemaToSchema, getPopupSchemaFromSchema } = usePopupUtils();
useEffect(() => {
const run = async () => {
@ -223,13 +236,23 @@ export const PagePopups = (props: { paramsList?: PopupParams[] }) => {
(params) => getStoredPopupContext(params.popupuid)?.schema || requestSchema(params.popupuid),
);
const schemas = await Promise.all(waitList);
const clonedSchemas = schemas.map((schema) => {
const clonedSchemas = schemas.map((schema, index) => {
if (_.isEmpty(schema)) {
return get404Schema();
}
const result = _.cloneDeep(_.omit(schema, 'parent'));
const params = popupParams[index];
if (params.puid) {
const popupSchema = findSchemaByUid(params.puid, fieldSchema.root);
if (popupSchema) {
savePopupSchemaToSchema(_.omit(popupSchema, 'parent'), schema);
}
}
const result = _.cloneDeep(_.omit(schema, 'parent')) as Schema;
result['x-read-pretty'] = true;
return result;
});
popupPropsRef.current = clonedSchemas.map((schema, index, items) => {
@ -254,12 +277,22 @@ export const PagePopups = (props: { paramsList?: PopupParams[] }) => {
});
const rootSchema = clonedSchemas[0];
for (let i = 1; i < clonedSchemas.length; i++) {
insertChildToParentSchema(clonedSchemas[i], popupPropsRef.current[i], clonedSchemas[i - 1]);
insertChildToParentSchema({
childSchema: clonedSchemas[i],
props: popupPropsRef.current[i],
parentSchema: clonedSchemas[i - 1],
getPopupSchema: (currentSchema) => {
return (
getPopupSchemaFromSchema(currentSchema) ||
_.get(currentSchema.properties, Object.keys(currentSchema.properties)[0])
);
},
});
}
setRootSchema(rootSchema);
};
run();
}, [popupParams, requestSchema]);
}, [fieldSchema.root, getPopupSchemaFromSchema, popupParams, requestSchema, savePopupSchemaToSchema]);
const components = useMemo(() => ({ PagePopupsItemProvider }), []);
@ -406,3 +439,17 @@ function get404Schema() {
'x-read-pretty': true,
};
}
function findSchemaByUid(uid: string, rootSchema: Schema, resultRef: { value: Schema } = { value: null }) {
resultRef = resultRef || {
value: null,
};
rootSchema.mapProperties((schema) => {
if (schema['x-uid'] === uid) {
resultRef.value = schema;
} else {
findSchemaByUid(uid, schema, resultRef);
}
});
return resultRef.value;
}

View File

@ -41,7 +41,7 @@ describe('insertToPopupSchema', () => {
},
};
insertChildToParentSchema(childSchema, { params, context }, parentSchema);
insertChildToParentSchema({ childSchema, props: { params, context } as any, parentSchema });
expect(parentSchema).toEqual({
type: 'object',

View File

@ -37,6 +37,8 @@ export interface PopupParams {
tab?: string;
/** collection name */
collection?: string;
/** popup uid */
puid?: string;
}
export interface PopupContextStorage extends PopupContext {
@ -103,9 +105,11 @@ export const getPopupParamsFromPath = _.memoize((path: string) => {
});
export const getPopupPathFromParams = (params: PopupParams) => {
const { popupuid: popupUid, tab, filterbytk, sourceid, collection } = params;
const { popupuid: popupUid, tab, filterbytk, sourceid, collection, puid } = params;
const popupPath = [
popupUid,
puid && 'puid',
puid,
collection && 'collection',
collection,
filterbytk && 'filterbytk',
@ -123,7 +127,7 @@ export const getPopupPathFromParams = (params: PopupParams) => {
* Note: use this hook in a plugin is not recommended
* @returns
*/
export const usePagePopup = () => {
export const usePopupUtils = () => {
const navigate = useNavigateNoUpdate();
const location = useLocationNoUpdate();
const fieldSchema = useFieldSchema();
@ -153,16 +157,23 @@ export const usePagePopup = () => {
recordData,
sourceId,
collection: _collection,
puid,
}: {
/**
* this is the schema uid of the button that triggers the popup, while puid is the schema uid of the popup, they are different;
*/
popupUid: string;
recordData: Record<string, any>;
sourceId: string;
tabKey?: string;
collection?: string;
/** popup uid */
puid?: string;
}) => {
const filterByTK = cm.getFilterByTK(association || collection, recordData);
return getPopupPathFromParams({
popupuid: popupUid,
puid,
collection: _collection,
filterbytk: filterByTK,
sourceid: sourceId,
@ -187,11 +198,14 @@ export const usePagePopup = () => {
recordData,
parentRecordData,
collectionNameUsedInURL,
popupUidUsedInURL,
}: {
recordData?: Record<string, any>;
parentRecordData?: Record<string, any>;
/** if this value exists, it will be saved in the URL */
collectionNameUsedInURL?: string;
/** if this value exists, it will be saved in the URL */
popupUidUsedInURL?: string;
} = {}) => {
if (!isPopupVisibleControlledByURL()) {
return setVisibleFromAction?.(true);
@ -205,6 +219,7 @@ export const usePagePopup = () => {
recordData,
sourceId,
collection: collectionNameUsedInURL,
puid: popupUidUsedInURL,
});
let url = location.pathname;
if (_.last(url) === '/') {
@ -283,6 +298,14 @@ export const usePagePopup = () => {
[getNewPathname, navigate, popupParams?.popupuid, record?.data, location],
);
const savePopupSchemaToSchema = useCallback((popupSchema: ISchema, targetSchema: ISchema) => {
targetSchema['__popup'] = popupSchema;
}, []);
const getPopupSchemaFromSchema = useCallback((schema: ISchema) => {
return schema['__popup'];
}, []);
return {
/**
* used to open popup by changing the url
@ -292,10 +315,32 @@ export const usePagePopup = () => {
* used to close popup by changing the url
*/
closePopup,
savePopupSchemaToSchema,
getPopupSchemaFromSchema,
/**
* @deprecated
* TODO: remove this
*/
visibleWithURL: visible,
/**
* @deprecated
* TODO: remove this
*/
setVisibleWithURL: setVisible,
/**
* @deprecated
* TODO: remove this
*/
popupParams,
/**
* @deprecated
* TODO: remove this
*/
changeTab,
/**
* @deprecated
* TODO: remove this
*/
getPopupContext,
};
};

View File

@ -19,7 +19,7 @@ import { useTreeParentRecord } from '../../modules/blocks/data-blocks/table/Tree
import { useRecord } from '../../record-provider';
import { useCompile } from '../../schema-component';
import { linkageAction } from '../../schema-component/antd/action/utils';
import { usePagePopup } from '../../schema-component/antd/page/pagePopupUtils';
import { usePopupUtils } from '../../schema-component/antd/page/pagePopupUtils';
import { parseVariables } from '../../schema-component/common/utils/uitls';
import { useLocalVariables, useVariables } from '../../variables';
@ -70,7 +70,7 @@ const InternalCreateRecordAction = (props: any, ref) => {
const values = useRecord();
const variables = useVariables();
const localVariables = useLocalVariables({ currentForm: { values } as any });
const { openPopup } = usePagePopup();
const { openPopup } = usePopupUtils();
const treeRecordData = useTreeParentRecord();
const cm = useCollectionManager();

View File

@ -97,7 +97,7 @@ describe('action', () => {
expect(model.toJSON()).toMatchObject(matcher);
});
it.only('should be custom values', async () => {
it('should be custom values', async () => {
const Plugin = app.pm.get(PluginFileManagerServer) as PluginFileManagerServer;
const model = await Plugin.createFileRecord({
collectionName: 'attachments',
@ -117,6 +117,22 @@ describe('action', () => {
expect(model.toJSON()).toMatchObject(matcher);
});
it('should be upload file', async () => {
const Plugin = app.pm.get(PluginFileManagerServer) as PluginFileManagerServer;
const data = await Plugin.uploadFile({
filePath: path.resolve(__dirname, './files/text.txt'),
documentRoot: 'storage/backups/test',
});
const matcher = {
title: 'text',
extname: '.txt',
path: '',
meta: {},
storageId: 1,
};
expect(data).toMatchObject(matcher);
});
it('upload file should be ok', async () => {
const { body } = await agent.resource('attachments').create({
[FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt'),

View File

@ -36,6 +36,12 @@ export type FileRecordOptions = {
values?: any;
} & Transactionable;
export type UploadFileOptions = {
filePath: string;
storageName?: string;
documentRoot?: string;
};
export default class PluginFileManagerServer extends Plugin {
storageTypes = new Registry<IStorage>();
storagesCache = new Map<number, StorageModel>();
@ -46,15 +52,21 @@ export default class PluginFileManagerServer extends Plugin {
if (!collection) {
throw new Error(`collection does not exist`);
}
const storageRepository = this.db.getRepository('storages');
const collectionRepository = this.db.getRepository(collectionName);
const name = storageName || collection.options.storage;
const data = await this.uploadFile({ storageName: name, filePath });
return await collectionRepository.create({ values: { ...data, ...values }, transaction });
}
async uploadFile(options: UploadFileOptions) {
const { storageName, filePath, documentRoot } = options;
const storageRepository = this.db.getRepository('storages');
let storageInstance;
if (name) {
if (storageName) {
storageInstance = await storageRepository.findOne({
filter: {
name,
name: storageName,
},
});
}
@ -73,6 +85,10 @@ export default class PluginFileManagerServer extends Plugin {
throw new Error('[file-manager] no linked or default storage provided');
}
if (documentRoot) {
storageInstance.options['documentRoot'] = documentRoot;
}
const storageConfig = this.storageTypes.get(storageInstance.type);
if (!storageConfig) {
@ -97,8 +113,7 @@ export default class PluginFileManagerServer extends Plugin {
});
});
const data = getFileData({ app: this.app, file, storage: storageInstance, request: { body: {} } } as any);
return await collectionRepository.create({ values: { ...data, ...values }, transaction });
return getFileData({ app: this.app, file, storage: storageInstance, request: { body: {} } } as any);
}
async loadStorages(options?: { transaction: any }) {

View File

@ -9,19 +9,21 @@
import { css } from '@emotion/css';
import { FormLayout } from '@formily/antd-v5';
import { createForm } from '@formily/core';
import { observer, RecursionField, useFieldSchema } from '@formily/react';
import {
ActionContextProvider,
DndContext,
RecordProvider,
FormProvider,
PopupContextProvider,
useCollection,
useCollectionParentRecordData,
useCollectionRecordData,
usePopupUtils,
VariablePopupRecordProvider,
} from '@nocobase/client';
import { Schema } from '@nocobase/utils';
import { Card } from 'antd';
import React, { useCallback, useContext, useMemo, useState } from 'react';
import React, { useCallback, useContext, useMemo } from 'react';
import { KanbanCardContext } from './context';
import { useKanbanTranslation } from './locale';
const cardCss = css`
.ant-card-body {
@ -72,23 +74,27 @@ MemorizedRecursionField.displayName = 'MemorizedRecursionField';
export const KanbanCard: any = observer(
() => {
const { t } = useKanbanTranslation();
const collection = useCollection();
const { setDisableCardDrag, cardViewerSchema, card, cardField, columnIndex, cardIndex } =
useContext(KanbanCardContext);
const parentRecordData = useCollectionParentRecordData();
const { setDisableCardDrag } = useContext(KanbanCardContext) || {};
const fieldSchema = useFieldSchema();
const [visible, setVisible] = useState(false);
const handleCardClick = useCallback((e: React.MouseEvent) => {
const targetElement = e.target as Element; // 将事件目标转换为Element类型
const currentTargetElement = e.currentTarget as Element;
if (currentTargetElement.contains(targetElement)) {
setVisible(true);
e.stopPropagation();
} else {
e.stopPropagation();
}
}, []);
const { openPopup, getPopupSchemaFromSchema } = usePopupUtils();
const recordData = useCollectionRecordData();
const popupSchema = getPopupSchemaFromSchema(fieldSchema) || getPopupSchemaFromParent(fieldSchema);
const handleCardClick = useCallback(
(e: React.MouseEvent) => {
const targetElement = e.target as Element; // 将事件目标转换为Element类型
const currentTargetElement = e.currentTarget as Element;
if (currentTargetElement.contains(targetElement)) {
openPopup({
popupUidUsedInURL: popupSchema?.['x-uid'],
});
e.stopPropagation();
} else {
e.stopPropagation();
}
},
[openPopup, popupSchema],
);
const cardStyle = useMemo(() => {
return {
cursor: 'pointer',
@ -96,51 +102,70 @@ export const KanbanCard: any = observer(
};
}, []);
const form = useMemo(() => {
return createForm({
values: recordData,
});
}, [recordData]);
const onDragStart = useCallback(() => {
setDisableCardDrag(true);
}, []);
setDisableCardDrag?.(true);
}, [setDisableCardDrag]);
const onDragEnd = useCallback(() => {
setDisableCardDrag(false);
}, []);
setDisableCardDrag?.(false);
}, [setDisableCardDrag]);
const actionContextValue = useMemo(() => {
// if not wrapped, only Tab component's content will be rendered, Drawer component's content will not be rendered
const wrappedPopupSchema = useMemo(() => {
return {
openMode: fieldSchema['x-component-props']?.['openMode'] || 'drawer',
openSize: fieldSchema['x-component-props']?.['openSize'],
visible,
setVisible,
type: 'void',
properties: {
drawer: popupSchema,
},
};
}, [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],
);
}, [popupSchema]);
return (
<>
<Card onClick={handleCardClick} bordered={false} hoverable style={cardStyle} className={cardCss}>
<DndContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
<FormLayout layout={'vertical'}>
<MemorizedRecursionField basePath={basePath} schema={fieldSchema} onlyRenderProperties />
<FormProvider form={form}>
<MemorizedRecursionField schema={fieldSchema} onlyRenderProperties />
</FormProvider>
</FormLayout>
</DndContext>
</Card>
{cardViewerSchema && (
<ActionContextProvider value={actionContextValue}>
<RecordProvider record={card} parent={parentRecordData}>
<VariablePopupRecordProvider recordData={card} collection={collection}>
<MemorizedRecursionField basePath={cardViewerBasePath} schema={cardViewerSchema} onlyRenderProperties />
</VariablePopupRecordProvider>
</RecordProvider>
</ActionContextProvider>
)}
<PopupContextProvider>
<VariablePopupRecordProvider recordData={recordData} collection={collection}>
<MemorizedRecursionField schema={wrappedPopupSchema} />
</VariablePopupRecordProvider>
</PopupContextProvider>
</>
);
},
{ displayName: 'KanbanCard' },
);
function getPopupSchemaFromParent(fieldSchema: Schema) {
if (fieldSchema.parent?.properties?.cardViewer?.properties?.drawer) {
return fieldSchema.parent.properties.cardViewer.properties.drawer;
}
const cardSchema = findSchemaByUid(fieldSchema['x-uid'], fieldSchema.root);
return cardSchema.parent.properties.cardViewer.properties.drawer;
}
function findSchemaByUid(uid: string, rootSchema: Schema, resultRef: { value: Schema } = { value: null }) {
resultRef = resultRef || {
value: null,
};
rootSchema.mapProperties((schema) => {
if (schema['x-uid'] === uid) {
resultRef.value = schema;
} else {
findSchemaByUid(uid, schema, resultRef);
}
});
return resultRef.value;
}

View File

@ -136,7 +136,7 @@ export const Kanban: any = withDynamicSchemaProps(
<Tag color={color}>{title}</Tag>
</div>
)}
renderCard={(card, { column, dragging }) => {
renderCard={(card, { column }) => {
const columnIndex = dataSource?.indexOf(column);
const cardIndex = column?.cards?.indexOf(card);
const { ref, inView } = useInView({
@ -144,24 +144,18 @@ export const Kanban: any = withDynamicSchemaProps(
triggerOnce: true,
initialInView: lastDraggedCard.current && lastDraggedCard.current === card[primaryKey],
});
return (
schemas.card && (
<RecordProvider record={card} parent={parentRecordData}>
<KanbanCardContext.Provider
value={{
setDisableCardDrag,
cardViewerSchema: schemas.cardViewer,
cardField: field,
card,
column,
dragging,
columnIndex,
cardIndex,
}}
>
<div ref={ref}>
{inView ? (
<MemorizedRecursionField name={schemas.card.name} schema={schemas.card} />
<MemorizedRecursionField name={'card'} schema={fieldSchema.properties.card} />
) : (
<Card bordered={false}>
<Skeleton paragraph={{ rows: 4 }} />

View File

@ -0,0 +1,755 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
export const kanbanURL = {
collections: [
{
name: 'kanban',
fields: [
{
name: 'select',
interface: 'select',
uiSchema: {
enum: [
{
value: 'option1',
label: 'Option1',
color: 'red',
},
{
value: 'option2',
label: 'Option2',
color: 'green',
},
{
value: 'option3',
label: 'Option3',
color: 'blue',
},
],
type: 'string',
'x-component': 'Select',
title: 'Single select',
},
},
{
name: 'sort',
interface: 'sort',
scopeKey: 'select',
},
],
},
],
pageSchema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Page',
properties: {
eehnoq75dyp: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'page:addBlock',
properties: {
qy13k7twlgr: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-app-version': '1.3.0-alpha',
properties: {
z2zx32gyeno: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
'x-app-version': '1.3.0-alpha',
properties: {
'6h3p53u5ek7': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-acl-action': 'kanban:list',
'x-decorator': 'KanbanBlockProvider',
'x-decorator-props': {
collection: 'kanban',
dataSource: 'main',
action: 'list',
groupField: 'select',
sortField: 'sort',
params: {
paginate: false,
sort: ['sort'],
},
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:kanban',
'x-component': 'CardItem',
'x-app-version': '1.3.0-alpha',
properties: {
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-initializer': 'kanban:configureActions',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--nb-spacing)',
},
},
'x-app-version': '1.3.0-alpha',
'x-uid': 'zb0mo93az5z',
'x-async': false,
'x-index': 1,
},
ms2kyy843uc: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'array',
'x-component': 'Kanban',
'x-use-component-props': 'useKanbanBlockProps',
'x-app-version': '1.3.0-alpha',
properties: {
card: {
'x-uid': '1y1b9kliqui',
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-read-pretty': true,
'x-label-disabled': true,
'x-decorator': 'BlockItem',
'x-component': 'Kanban.Card',
'x-component-props': {
openMode: 'drawer',
},
'x-designer': 'Kanban.Card.Designer',
'x-app-version': '1.3.0-alpha',
'x-action-context': {
dataSource: 'main',
collection: 'kanban',
},
properties: {
grid: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-component-props': {
dndContext: false,
},
'x-app-version': '1.3.0-alpha',
properties: {
'8evl3dcvt86': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
properties: {
'36yop5rgpyi': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
select: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'kanban.select',
'x-component-props': {
style: {
width: '100%',
},
},
'x-read-pretty': true,
'x-uid': '8a20gaw8qdh',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'pzvcxvven6y',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'xd4gdx5j0qm',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'zx1rf8qfane',
'x-async': false,
'x-index': 1,
},
},
'x-async': false,
'x-index': 1,
},
cardViewer: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("View") }}',
'x-designer': 'Action.Designer',
'x-component': 'Kanban.CardViewer',
'x-action': 'view',
'x-component-props': {
openMode: 'drawer',
},
'x-app-version': '1.3.0-alpha',
properties: {
drawer: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("View record") }}',
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
'x-app-version': '1.3.0-alpha',
properties: {
tabs: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
'x-initializer': 'popup:addTab',
'x-app-version': '1.3.0-alpha',
properties: {
tab1: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{t("Details")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
'x-app-version': '1.3.0-alpha',
properties: {
grid: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'popup:common:addBlock',
'x-app-version': '1.3.0-alpha',
properties: {
'8bv7l5hiazu': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-app-version': '1.3.0-alpha',
properties: {
torszg5y5zd: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
'x-app-version': '1.3.0-alpha',
properties: {
d4p7lp86m03: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-acl-action': 'kanban:get',
'x-decorator': 'DetailsBlockProvider',
'x-use-decorator-props': 'useDetailsDecoratorProps',
'x-decorator-props': {
dataSource: 'main',
collection: 'kanban',
readPretty: true,
action: 'get',
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:details',
'x-component': 'CardItem',
'x-app-version': '1.3.0-alpha',
properties: {
'1oxl6q1ktkh': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Details',
'x-read-pretty': true,
'x-use-component-props': 'useDetailsProps',
'x-app-version': '1.3.0-alpha',
properties: {
r27t49cfoo5: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-initializer': 'details:configureActions',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 24,
},
},
'x-app-version': '1.3.0-alpha',
properties: {
uya6ulpeavl: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("Edit") }}',
'x-action': 'update',
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:edit',
'x-component': 'Action',
'x-component-props': {
openMode: 'drawer',
icon: 'EditOutlined',
type: 'primary',
},
'x-action-context': {
dataSource: 'main',
collection: 'kanban',
},
'x-decorator': 'ACLActionProvider',
'x-app-version': '1.3.0-alpha',
properties: {
drawer: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("Edit record") }}',
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
'x-app-version': '1.3.0-alpha',
properties: {
tabs: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
'x-initializer': 'popup:addTab',
'x-app-version': '1.3.0-alpha',
properties: {
tab1: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{t("Edit")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
'x-app-version': '1.3.0-alpha',
properties: {
grid: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer':
'popup:common:addBlock',
'x-app-version': '1.3.0-alpha',
properties: {
etzmlia85gh: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-app-version': '1.3.0-alpha',
properties: {
'6dn14wki0e5': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
'x-app-version':
'1.3.0-alpha',
properties: {
cruqx5ulnow: {
_isJSONSchemaObject:
true,
version: '2.0',
type: 'void',
'x-acl-action-props': {
skipScopeCheck: false,
},
'x-acl-action':
'kanban:update',
'x-decorator':
'FormBlockProvider',
'x-use-decorator-props':
'useEditFormBlockDecoratorProps',
'x-decorator-props': {
action: 'get',
dataSource: 'main',
collection: 'kanban',
},
'x-toolbar':
'BlockSchemaToolbar',
'x-settings':
'blockSettings:editForm',
'x-component':
'CardItem',
'x-app-version':
'1.3.0-alpha',
properties: {
y0q5ei7mma2: {
_isJSONSchemaObject:
true,
version: '2.0',
type: 'void',
'x-component':
'FormV2',
'x-use-component-props':
'useEditFormBlockProps',
'x-app-version':
'1.3.0-alpha',
properties: {
grid: {
_isJSONSchemaObject:
true,
version: '2.0',
type: 'void',
'x-component':
'Grid',
'x-initializer':
'form:configureFields',
'x-app-version':
'1.3.0-alpha',
properties: {
rjbvqpp7xmu: {
_isJSONSchemaObject:
true,
version:
'2.0',
type: 'void',
'x-component':
'Grid.Row',
'x-app-version':
'1.3.0-alpha',
properties:
{
cxiykvzwfv1:
{
_isJSONSchemaObject:
true,
version:
'2.0',
type: 'void',
'x-component':
'Grid.Col',
'x-app-version':
'1.3.0-alpha',
properties:
{
select:
{
_isJSONSchemaObject:
true,
version:
'2.0',
type: 'string',
'x-toolbar':
'FormItemSchemaToolbar',
'x-settings':
'fieldSettings:FormItem',
'x-component':
'CollectionField',
'x-decorator':
'FormItem',
'x-collection-field':
'kanban.select',
'x-component-props':
{
style:
{
width:
'100%',
},
},
'x-app-version':
'1.3.0-alpha',
'x-uid':
'eo2xh4adfaz',
'x-async':
false,
'x-index': 1,
},
},
'x-uid':
'walnc8ivgzp',
'x-async':
false,
'x-index': 1,
},
},
'x-uid':
'3san45vkk32',
'x-async':
false,
'x-index': 1,
},
},
'x-uid':
'opiixrhoo0z',
'x-async':
false,
'x-index': 1,
},
'5j12oi9pvf3': {
_isJSONSchemaObject:
true,
version: '2.0',
type: 'void',
'x-initializer':
'editForm:configureActions',
'x-component':
'ActionBar',
'x-component-props':
{
layout:
'one-column',
},
'x-app-version':
'1.3.0-alpha',
properties: {
m9e7qxjys7o: {
_isJSONSchemaObject:
true,
version:
'2.0',
title:
'{{ t("Submit") }}',
'x-action':
'submit',
'x-component':
'Action',
'x-use-component-props':
'useUpdateActionProps',
'x-toolbar':
'ActionSchemaToolbar',
'x-settings':
'actionSettings:updateSubmit',
'x-component-props':
{
type: 'primary',
htmlType:
'submit',
},
'x-action-settings':
{
triggerWorkflows:
[],
},
type: 'void',
'x-app-version':
'1.3.0-alpha',
'x-uid':
'5sc8g3215j4',
'x-async':
false,
'x-index': 1,
},
},
'x-uid':
'2v1zlgnh0lp',
'x-async':
false,
'x-index': 2,
},
},
'x-uid':
'u5viek4cakp',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'z38sbx5yocb',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'y0wdbxagcre',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'dn2spxvle3x',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'fsmyjuyc0mc',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'x27vohz57u1',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'uxbmlytgowr',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'p108r8s8pjh',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'detjnjixd9n',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'i9622c7n8nv',
'x-async': false,
'x-index': 1,
},
grid: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'details:configureFields',
'x-app-version': '1.3.0-alpha',
properties: {
cylu3xjywvu: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
'x-app-version': '1.3.0-alpha',
properties: {
z1eb85u2n4n: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
'x-app-version': '1.3.0-alpha',
properties: {
select: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'kanban.select',
'x-component-props': {
style: {
width: '100%',
},
},
'x-app-version': '1.3.0-alpha',
'x-uid': 'mfz4c6l4ft9',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'lt4bp9emb4z',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'ny6e5q4fhww',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'yfunnpaf97c',
'x-async': false,
'x-index': 2,
},
},
'x-uid': 'pegqhin3s88',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'wpafritzh4d',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '2vjqy76fds6',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'avlvzb0yl0s',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'm5x3kz75nm2',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '6wb8grmdenj',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'uybfp4br16z',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '4dwpi8dug9d',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'x0dusy43fhl',
'x-async': false,
'x-index': 2,
},
},
'x-uid': '6vpzr0nsxjs',
'x-async': false,
'x-index': 2,
},
},
'x-uid': 'f1eqli5bc07',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '4exwrzlac4i',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'edzxlnh6nx4',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 's95fpawrrle',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '57xhoz3sfzq',
'x-async': true,
'x-index': 1,
},
};

View File

@ -0,0 +1,57 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { expect, test } from '@nocobase/test/e2e';
import { kanbanURL } from './templates';
test.describe('kanban: open popup via URL', () => {
test('basic', async ({ page, mockPage, mockRecords }) => {
const nocoPage = await mockPage(kanbanURL).waitForInit();
await mockRecords('kanban', [{ select: 'option1' }, { select: 'option2' }, { select: 'option3' }]);
await nocoPage.goto();
// 1. click a card to open a popup
await page.getByLabel('block-item-CollectionField-').filter({ hasText: 'option1' }).click();
await expect(
page.getByTestId('drawer-Action.Container-kanban-View record').getByLabel('block-item-CollectionField-'),
).toHaveText('Single select:Option1');
// 2. then reload the page, the popup should be opened
await page.reload();
await expect(
page.getByTestId('drawer-Action.Container-kanban-View record').getByLabel('block-item-CollectionField-'),
).toHaveText('Single select:Option1');
// 3. click the `Edit` button to open another popup
await page.getByLabel('action-Action-Edit-update-').click();
await expect(
page.getByTestId('drawer-Action.Container-kanban-Edit record').getByLabel('block-item-CollectionField-'),
).toHaveText('Single select:Option1');
// 4. then reload the page, the second popup should be opened
await page.reload();
await expect(
page.getByTestId('drawer-Action.Container-kanban-Edit record').getByLabel('block-item-CollectionField-'),
).toHaveText('Single select:Option1');
// 5. change the `Option` then close, the previous popup's content should be updated
await page.getByTestId('select-single').click();
await page.getByRole('option', { name: 'Option2' }).click();
await page.getByLabel('action-Action-Submit-submit-').click();
await page.mouse.move(300, 0);
await expect(page.getByLabel('block-item-CollectionField-kanban-details-kanban.select-Single select')).toHaveText(
'Single select:Option2',
);
// close the first popup
// await page.locator('.ant-drawer-mask').click();
// await expect(page.getByLabel('block-item-CollectionField-').filter({ hasText: 'option2' })).toHaveCount(2);
});
});