feat(kanban): add support for opening via URL (#5083)

* refactor: rename 'usePagePopup' to 'usePopupUtils'

* refactor: remove useless code

* refactor(kanban): reduce card dependency on context

* feat: finished

* test: add e2e test

* chore: fix e2e test
This commit is contained in:
Zeke Zhang 2024-08-20 15:33:09 +08:00 committed by GitHub
parent 0e5f8b5e12
commit 77b60ed16c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1075 additions and 91 deletions

View File

@ -71,5 +71,7 @@ export * from './modules/blocks/data-blocks/table-selector';
export * from './modules/blocks/index'; export * from './modules/blocks/index';
export * from './modules/blocks/useParentRecordCommon'; export * from './modules/blocks/useParentRecordCommon';
export { OpenModeProvider, useOpenModeContext } from './modules/popup/OpenModeProvider'; 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'; export { VariablePopupRecordProvider } from './modules/variable/variablesProvider/VariablePopupRecordProvider';

View File

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

View File

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

View File

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

View File

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

View File

@ -8,14 +8,14 @@
*/ */
import React from 'react'; 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 { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem'; import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
import { useOpenModeContext } from '../../popup/OpenModeProvider'; import { useOpenModeContext } from '../../popup/OpenModeProvider';
export const ViewActionInitializer = (props) => { export const ViewActionInitializer = (props) => {
const { defaultOpenMode } = useOpenModeContext(); const { defaultOpenMode } = useOpenModeContext();
const { getPopupContext } = usePagePopup(); const { getPopupContext } = usePopupUtils();
const schema = { const schema = {
type: 'void', type: 'void',
title: '{{ t("View") }}', 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 { useCompile, useComponent, useDesigner } from '../../hooks';
import { useProps } from '../../hooks/useProps'; import { useProps } from '../../hooks/useProps';
import { PopupVisibleProvider } from '../page/PagePopups'; import { PopupVisibleProvider } from '../page/PagePopups';
import { usePagePopup } from '../page/pagePopupUtils'; import { usePopupUtils } from '../page/pagePopupUtils';
import { usePopupSettings } from '../page/PopupSettingsProvider'; import { usePopupSettings } from '../page/PopupSettingsProvider';
import ActionContainer from './Action.Container'; import ActionContainer from './Action.Container';
import { ActionDesigner } from './Action.Designer'; import { ActionDesigner } from './Action.Designer';
@ -77,7 +77,7 @@ export const Action: ComposedAction = withDynamicSchemaProps(
const aclCtx = useACLActionParamsContext(); const aclCtx = useACLActionParamsContext();
const { wrapSSR, componentCls, hashId } = useStyles(); const { wrapSSR, componentCls, hashId } = useStyles();
const { t } = useTranslation(); const { t } = useTranslation();
const { visibleWithURL, setVisibleWithURL } = usePagePopup(); const { visibleWithURL, setVisibleWithURL } = usePopupUtils();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [formValueChanged, setFormValueChanged] = useState(false); const [formValueChanged, setFormValueChanged] = useState(false);
const { setSubmitted: setParentSubmitted } = useActionContext(); const { setSubmitted: setParentSubmitted } = useActionContext();
@ -307,7 +307,7 @@ function RenderButton({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { isPopupVisibleControlledByURL } = usePopupSettings(); const { isPopupVisibleControlledByURL } = usePopupSettings();
const { openPopup } = usePagePopup(); const { openPopup } = usePopupUtils();
const handleButtonClick = useCallback( const handleButtonClick = useCallback(
(e: React.MouseEvent, checkPortal = true) => { (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 { useCollectionRecordData } from '../../../data-source/collection-record/CollectionRecordProvider';
import { useCompile } from '../../hooks'; import { useCompile } from '../../hooks';
import { useActionContext } from '../action'; import { useActionContext } from '../action';
import { usePagePopup } from '../page/pagePopupUtils'; import { usePopupUtils } from '../page/pagePopupUtils';
import { transformNestedData } from './InternalCascadeSelect'; import { transformNestedData } from './InternalCascadeSelect';
import { ButtonListProps, ReadPrettyInternalViewer, isObject } from './InternalViewer'; import { ButtonListProps, ReadPrettyInternalViewer, isObject } from './InternalViewer';
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks'; import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
@ -47,7 +47,7 @@ const ButtonTabList: React.FC<ButtonListProps> = (props) => {
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 { openPopup } = usePagePopup(); const { openPopup } = usePopupUtils();
const recordData = useCollectionRecordData(); const recordData = useCollectionRecordData();
const renderRecords = () => const renderRecords = () =>

View File

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

View File

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

View File

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

View File

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

View File

@ -9,19 +9,21 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { FormLayout } from '@formily/antd-v5'; import { FormLayout } from '@formily/antd-v5';
import { createForm } from '@formily/core';
import { observer, RecursionField, useFieldSchema } from '@formily/react'; import { observer, RecursionField, useFieldSchema } from '@formily/react';
import { import {
ActionContextProvider,
DndContext, DndContext,
RecordProvider, FormProvider,
PopupContextProvider,
useCollection, useCollection,
useCollectionParentRecordData, useCollectionRecordData,
usePopupUtils,
VariablePopupRecordProvider, VariablePopupRecordProvider,
} from '@nocobase/client'; } from '@nocobase/client';
import { Schema } from '@nocobase/utils';
import { Card } from 'antd'; import { Card } from 'antd';
import React, { useCallback, useContext, useMemo, useState } from 'react'; import React, { useCallback, useContext, useMemo } from 'react';
import { KanbanCardContext } from './context'; import { KanbanCardContext } from './context';
import { useKanbanTranslation } from './locale';
const cardCss = css` const cardCss = css`
.ant-card-body { .ant-card-body {
@ -72,23 +74,27 @@ MemorizedRecursionField.displayName = 'MemorizedRecursionField';
export const KanbanCard: any = observer( export const KanbanCard: any = observer(
() => { () => {
const { t } = useKanbanTranslation();
const collection = useCollection(); const collection = useCollection();
const { setDisableCardDrag, cardViewerSchema, card, cardField, columnIndex, cardIndex } = const { setDisableCardDrag } = useContext(KanbanCardContext) || {};
useContext(KanbanCardContext);
const parentRecordData = useCollectionParentRecordData();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const [visible, setVisible] = useState(false); const { openPopup, getPopupSchemaFromSchema } = usePopupUtils();
const handleCardClick = useCallback((e: React.MouseEvent) => { const recordData = useCollectionRecordData();
const targetElement = e.target as Element; // 将事件目标转换为Element类型 const popupSchema = getPopupSchemaFromSchema(fieldSchema) || getPopupSchemaFromParent(fieldSchema);
const currentTargetElement = e.currentTarget as Element; const handleCardClick = useCallback(
if (currentTargetElement.contains(targetElement)) { (e: React.MouseEvent) => {
setVisible(true); const targetElement = e.target as Element; // 将事件目标转换为Element类型
e.stopPropagation(); const currentTargetElement = e.currentTarget as Element;
} else { if (currentTargetElement.contains(targetElement)) {
e.stopPropagation(); openPopup({
} popupUidUsedInURL: popupSchema?.['x-uid'],
}, []); });
e.stopPropagation();
} else {
e.stopPropagation();
}
},
[openPopup, popupSchema],
);
const cardStyle = useMemo(() => { const cardStyle = useMemo(() => {
return { return {
cursor: 'pointer', cursor: 'pointer',
@ -96,51 +102,70 @@ export const KanbanCard: any = observer(
}; };
}, []); }, []);
const form = useMemo(() => {
return createForm({
values: recordData,
});
}, [recordData]);
const onDragStart = useCallback(() => { const onDragStart = useCallback(() => {
setDisableCardDrag(true); setDisableCardDrag?.(true);
}, []); }, [setDisableCardDrag]);
const onDragEnd = useCallback(() => { 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 { return {
openMode: fieldSchema['x-component-props']?.['openMode'] || 'drawer', type: 'void',
openSize: fieldSchema['x-component-props']?.['openSize'], properties: {
visible, drawer: popupSchema,
setVisible, },
}; };
}, [fieldSchema, visible]); }, [popupSchema]);
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 (
<> <>
<Card onClick={handleCardClick} bordered={false} hoverable style={cardStyle} className={cardCss}> <Card onClick={handleCardClick} bordered={false} hoverable style={cardStyle} className={cardCss}>
<DndContext onDragStart={onDragStart} onDragEnd={onDragEnd}> <DndContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
<FormLayout layout={'vertical'}> <FormLayout layout={'vertical'}>
<MemorizedRecursionField basePath={basePath} schema={fieldSchema} onlyRenderProperties /> <FormProvider form={form}>
<MemorizedRecursionField schema={fieldSchema} onlyRenderProperties />
</FormProvider>
</FormLayout> </FormLayout>
</DndContext> </DndContext>
</Card> </Card>
{cardViewerSchema && ( <PopupContextProvider>
<ActionContextProvider value={actionContextValue}> <VariablePopupRecordProvider recordData={recordData} collection={collection}>
<RecordProvider record={card} parent={parentRecordData}> <MemorizedRecursionField schema={wrappedPopupSchema} />
<VariablePopupRecordProvider recordData={card} collection={collection}> </VariablePopupRecordProvider>
<MemorizedRecursionField basePath={cardViewerBasePath} schema={cardViewerSchema} onlyRenderProperties /> </PopupContextProvider>
</VariablePopupRecordProvider>
</RecordProvider>
</ActionContextProvider>
)}
</> </>
); );
}, },
{ displayName: 'KanbanCard' }, { 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> <Tag color={color}>{title}</Tag>
</div> </div>
)} )}
renderCard={(card, { column, dragging }) => { renderCard={(card, { column }) => {
const columnIndex = dataSource?.indexOf(column); const columnIndex = dataSource?.indexOf(column);
const cardIndex = column?.cards?.indexOf(card); const cardIndex = column?.cards?.indexOf(card);
const { ref, inView } = useInView({ const { ref, inView } = useInView({
@ -144,24 +144,18 @@ export const Kanban: any = withDynamicSchemaProps(
triggerOnce: true, triggerOnce: true,
initialInView: lastDraggedCard.current && lastDraggedCard.current === card[primaryKey], initialInView: lastDraggedCard.current && lastDraggedCard.current === card[primaryKey],
}); });
return ( return (
schemas.card && ( schemas.card && (
<RecordProvider record={card} parent={parentRecordData}> <RecordProvider record={card} parent={parentRecordData}>
<KanbanCardContext.Provider <KanbanCardContext.Provider
value={{ value={{
setDisableCardDrag, setDisableCardDrag,
cardViewerSchema: schemas.cardViewer,
cardField: field,
card,
column,
dragging,
columnIndex,
cardIndex,
}} }}
> >
<div ref={ref}> <div ref={ref}>
{inView ? ( {inView ? (
<MemorizedRecursionField name={schemas.card.name} schema={schemas.card} /> <MemorizedRecursionField name={'card'} schema={fieldSchema.properties.card} />
) : ( ) : (
<Card bordered={false}> <Card bordered={false}>
<Skeleton paragraph={{ rows: 4 }} /> <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);
});
});