diff --git a/packages/core/client/src/acl/ACLProvider.tsx b/packages/core/client/src/acl/ACLProvider.tsx index e5eeda36c..798ba8194 100644 --- a/packages/core/client/src/acl/ACLProvider.tsx +++ b/packages/core/client/src/acl/ACLProvider.tsx @@ -45,7 +45,6 @@ const getRouteUrl = (props) => { }; export const ACLRolesCheckProvider = (props) => { - const route = getRouteUrl(props.children.props); const { setDesignable } = useDesignable(); const { render } = useAppSpin(); const api = useAPIClient(); diff --git a/packages/core/client/src/application/CustomRouterContextProvider.tsx b/packages/core/client/src/application/CustomRouterContextProvider.tsx new file mode 100644 index 000000000..5ae2ca45f --- /dev/null +++ b/packages/core/client/src/application/CustomRouterContextProvider.tsx @@ -0,0 +1,89 @@ +/** + * 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 React, { FC, useEffect } from 'react'; +import { Location, NavigateFunction, NavigateOptions, useLocation, useNavigate } from 'react-router-dom'; + +const NavigateNoUpdateContext = React.createContext(null); +const LocationNoUpdateContext = React.createContext(null); +export const LocationSearchContext = React.createContext(''); + +/** + * When the URL changes, components that use `useNavigate` will re-render. + * This provider provides a `navigateNoUpdate` method that can avoid re-rendering. + * + * see: https://github.com/remix-run/react-router/issues/7634 + * @param param0 + * @returns + */ +const NavigateNoUpdateProvider: FC = ({ children }) => { + const navigate = useNavigate(); + const navigateRef = React.useRef(navigate); + navigateRef.current = navigate; + + const navigateNoUpdate = React.useCallback((to: string, options?: NavigateOptions) => { + navigateRef.current(to, options); + }, []); + + return ( + + {children} + + ); +}; + +/** + * When the URL changes, components that use `useLocation` will re-render. + * This provider provides a `useLocationNoUpdate` method that can avoid re-rendering. + **/ +const LocationNoUpdateProvider: FC = ({ children }) => { + const location = useLocation(); + const locationRef = React.useRef({}); + + useEffect(() => { + Object.assign(locationRef.current, location); + }, [location]); + + return {children}; +}; + +const LocationSearchProvider: FC = ({ children }) => { + const location = useLocation(); + return {children}; +}; + +/** + * use `useNavigateNoUpdate` to avoid components that use `useNavigateNoUpdate` re-rendering. + * @returns + */ +export const useNavigateNoUpdate = () => { + return React.useContext(NavigateNoUpdateContext); +}; + +/** + * use `useLocationNoUpdate` to avoid components that use `useLocationNoUpdate` re-rendering. + * @returns + */ +export const useLocationNoUpdate = () => { + return React.useContext(LocationNoUpdateContext); +}; + +export const useLocationSearch = () => { + return React.useContext(LocationSearchContext); +}; + +export const CustomRouterContextProvider: FC = ({ children }) => { + return ( + + + {children} + + + ); +}; diff --git a/packages/core/client/src/application/RouterManager.tsx b/packages/core/client/src/application/RouterManager.tsx index fbe1a334f..2468bf513 100644 --- a/packages/core/client/src/application/RouterManager.tsx +++ b/packages/core/client/src/application/RouterManager.tsx @@ -20,6 +20,7 @@ import { useRoutes, } from 'react-router-dom'; import { Application } from './Application'; +import { CustomRouterContextProvider } from './CustomRouterContextProvider'; import { BlankComponent, RouterContextCleaner } from './components'; export interface BrowserRouterOptions extends Omit { @@ -143,10 +144,12 @@ export class RouterManager { return ( - - - {children} - + + + + {children} + + ); diff --git a/packages/core/client/src/application/hooks/useAppSpin.ts b/packages/core/client/src/application/hooks/useAppSpin.ts index 19d81ae0c..71d2a4e3c 100644 --- a/packages/core/client/src/application/hooks/useAppSpin.ts +++ b/packages/core/client/src/application/hooks/useAppSpin.ts @@ -8,12 +8,16 @@ */ import { Spin } from 'antd'; -import React from 'react'; +import React, { useCallback } from 'react'; import { useApp } from './useApp'; export const useAppSpin = () => { const app = useApp(); + const renderSpin = useCallback( + () => (app?.renderComponent ? app?.renderComponent?.('AppSpin') : React.createElement(Spin)), + [app], + ); return { - render: () => (app?.renderComponent ? app?.renderComponent?.('AppSpin') : React.createElement(Spin)), + render: renderSpin, }; }; diff --git a/packages/core/client/src/application/index.ts b/packages/core/client/src/application/index.ts index 63a51be8b..12adce186 100644 --- a/packages/core/client/src/application/index.ts +++ b/packages/core/client/src/application/index.ts @@ -8,6 +8,7 @@ */ export * from './Application'; +export * from './CustomRouterContextProvider'; export * from './Plugin'; export * from './PluginSettingsManager'; export * from './RouterManager'; @@ -19,5 +20,8 @@ export * from './schema-initializer'; export * from './schema-settings'; export * from './schema-settings/context/SchemaSettingItemContext'; export * from './schema-settings/hooks/useSchemaSettingsRender'; +export * from './schema-settings/utils/createModalSettingsItem'; +export * from './schema-settings/utils/createSelectSettingsItem'; +export * from './schema-settings/utils/createSwitchSettingsItem'; export * from './schema-toolbar'; export * from './utils'; diff --git a/packages/core/client/src/application/schema-settings/index.ts b/packages/core/client/src/application/schema-settings/index.ts index f0e510f4e..2b155a229 100644 --- a/packages/core/client/src/application/schema-settings/index.ts +++ b/packages/core/client/src/application/schema-settings/index.ts @@ -12,4 +12,3 @@ export * from './SchemaSettingsManager'; export * from './components'; export * from './context/SchemaSettingItemContext'; export * from './types'; -export * from './utils'; diff --git a/packages/core/client/src/application/schema-settings/utils/index.ts b/packages/core/client/src/application/schema-settings/utils/index.ts deleted file mode 100644 index 4e6b4f85a..000000000 --- a/packages/core/client/src/application/schema-settings/utils/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * 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 * from './createSelectSettingsItem'; -export * from './createSwitchSettingsItem'; -export * from './createModalSettingsItem'; diff --git a/packages/core/client/src/block-provider/BlockProvider.tsx b/packages/core/client/src/block-provider/BlockProvider.tsx index ecb1c38e4..6575a2063 100644 --- a/packages/core/client/src/block-provider/BlockProvider.tsx +++ b/packages/core/client/src/block-provider/BlockProvider.tsx @@ -20,6 +20,7 @@ import { WithoutTableFieldResource, useCollectionParentRecord, useCollectionRecord, + useCollectionRecordData, useDataBlockProps, useDataBlockRequest, useDataBlockResource, @@ -35,6 +36,7 @@ import { import { DataBlockCollector } from '../filter-provider/FilterProvider'; import { useSourceId } from '../modules/blocks/useSourceId'; import { RecordProvider, useRecordIndex } from '../record-provider'; +import { usePagePopup } from '../schema-component/antd/page/pagePopupUtils'; import { useAssociationNames } from './hooks'; import { useDataBlockParentRecord } from './hooks/useDataBlockParentRecord'; @@ -293,11 +295,17 @@ export const useBlockAssociationContext = () => { export const useFilterByTk = () => { const { resource, __parent } = useBlockRequestContext(); const recordIndex = useRecordIndex(); - const record = useRecord(); + const recordData = useCollectionRecordData(); const collection = useCollection_deprecated(); const { getCollectionField } = useCollectionManager_deprecated(); const assoc = useBlockAssociationContext(); const withoutTableFieldResource = useContext(WithoutTableFieldResource); + const { popupParams } = usePagePopup(); + + if (popupParams?.filterbytk) { + return popupParams.filterbytk; + } + if (!withoutTableFieldResource) { if (resource instanceof TableFieldResource || __parent?.block === 'TableField') { return recordIndex; @@ -306,9 +314,9 @@ export const useFilterByTk = () => { if (assoc) { const association = getCollectionField(assoc); - return record?.[association.targetKey || 'id']; + return recordData?.[association.targetKey || 'id']; } - return record?.[collection.filterTargetKey || 'id']; + return recordData?.[collection.filterTargetKey || 'id']; }; /** diff --git a/packages/core/client/src/block-provider/FormBlockProvider.tsx b/packages/core/client/src/block-provider/FormBlockProvider.tsx index 4f995aea3..c3e72f798 100644 --- a/packages/core/client/src/block-provider/FormBlockProvider.tsx +++ b/packages/core/client/src/block-provider/FormBlockProvider.tsx @@ -154,10 +154,8 @@ export const useFormBlockContext = () => { export const useFormBlockProps = () => { const ctx = useFormBlockContext(); const treeParentRecord = useTreeParentRecord(); - const { fieldSchema } = useActionContext(); - const addChild = fieldSchema?.['x-component-props']?.addChild; useEffect(() => { - if (addChild) { + if (treeParentRecord) { ctx.form?.query('parent').take((field) => { field.disabled = true; field.value = treeParentRecord; diff --git a/packages/core/client/src/block-provider/TableBlockProvider.tsx b/packages/core/client/src/block-provider/TableBlockProvider.tsx index cf5296619..b96b8666d 100644 --- a/packages/core/client/src/block-provider/TableBlockProvider.tsx +++ b/packages/core/client/src/block-provider/TableBlockProvider.tsx @@ -159,7 +159,7 @@ export const TableBlockProvider = withDynamicSchemaProps((props) => { } params['tree'] = true; } else { - const f = collection.fields.find((f) => f.treeChildren); + const f = collection?.fields.find((f) => f.treeChildren); if (f) { childrenColumnName = f.name; } diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index a8d814f73..732167bf5 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -19,7 +19,6 @@ import omit from 'lodash/omit'; import qs from 'qs'; import { ChangeEvent, useCallback, useContext, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; import { useReactToPrint } from 'react-to-print'; import { AssociationFilter, @@ -30,6 +29,7 @@ import { useTableBlockContext, } from '../..'; import { useAPIClient, useRequest } from '../../api-client'; +import { useNavigateNoUpdate } from '../../application/CustomRouterContextProvider'; import { useFormBlockContext } from '../../block-provider/FormBlockProvider'; import { useCollectionManager_deprecated, useCollection_deprecated } from '../../collection-manager'; import { DataBlock, useFilterBlock } from '../../filter-provider/FilterProvider'; @@ -206,7 +206,7 @@ export const useCreateActionProps = () => { const form = useForm(); const { field, resource } = useBlockRequestContext(); const { setVisible, setSubmitted, setFormValueChanged } = useActionContext(); - const navigate = useNavigate(); + const navigate = useNavigateNoUpdate(); const actionSchema = useFieldSchema(); const actionField = useField(); const compile = useCompile(); @@ -548,7 +548,7 @@ export const useCustomizeUpdateActionProps = () => { const { resource, __parent, service } = useBlockRequestContext(); const filterByTk = useFilterByTk(); const actionSchema = useFieldSchema(); - const navigate = useNavigate(); + const navigate = useNavigateNoUpdate(); const compile = useCompile(); const form = useForm(); const { modal } = App.useApp(); @@ -643,7 +643,7 @@ export const useCustomizeBulkUpdateActionProps = () => { const { rowKey } = tableBlockContext; const selectedRecordKeys = tableBlockContext.field?.data?.selectedRowKeys ?? expressionScope?.selectedRecordKeys ?? {}; - const navigate = useNavigate(); + const navigate = useNavigateNoUpdate(); const compile = useCompile(); const { t } = useTranslation(); const actionField = useField(); @@ -754,7 +754,7 @@ export const useCustomizeBulkUpdateActionProps = () => { export const useCustomizeRequestActionProps = () => { const apiClient = useAPIClient(); - const navigate = useNavigate(); + const navigate = useNavigateNoUpdate(); const filterByTk = useFilterByTk(); const actionSchema = useFieldSchema(); const compile = useCompile(); @@ -850,7 +850,7 @@ export const useUpdateActionProps = () => { const { field, resource, __parent } = useBlockRequestContext(); const { setVisible, setSubmitted, setFormValueChanged } = useActionContext(); const actionSchema = useFieldSchema(); - const navigate = useNavigate(); + const navigate = useNavigateNoUpdate(); const { fields, getField, name } = useCollection_deprecated(); const compile = useCompile(); const actionField = useField(); @@ -1530,7 +1530,7 @@ export function appendQueryStringToUrl(url: string, queryString: string) { } export function useLinkActionProps() { - const navigate = useNavigate(); + const navigate = useNavigateNoUpdate(); const fieldSchema = useFieldSchema(); const { t } = useTranslation(); const url = fieldSchema?.['x-component-props']?.['url']; diff --git a/packages/core/client/src/block-provider/hooks/useParsedFilter.ts b/packages/core/client/src/block-provider/hooks/useParsedFilter.ts index 9351a20c2..2c30832d7 100644 --- a/packages/core/client/src/block-provider/hooks/useParsedFilter.ts +++ b/packages/core/client/src/block-provider/hooks/useParsedFilter.ts @@ -73,7 +73,7 @@ export function useParsedFilter({ filterOption }: { filterOption: any }) { equals: _.isEqual, }, ); - }, [JSON.stringify(filterOption)]); + }, [JSON.stringify(filterOption), parseFilter, findVariable]); return { /** 数据范围的筛选参数 */ diff --git a/packages/core/client/src/collection-manager/CollectionHistoryProvider.tsx b/packages/core/client/src/collection-manager/CollectionHistoryProvider.tsx index f30cff945..225607c66 100644 --- a/packages/core/client/src/collection-manager/CollectionHistoryProvider.tsx +++ b/packages/core/client/src/collection-manager/CollectionHistoryProvider.tsx @@ -7,7 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import React, { createContext, useContext, useEffect } from 'react'; +import React, { createContext, useCallback, useContext, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; import { useAPIClient, useRequest } from '../api-client'; import { useAppSpin } from '../application/hooks/useAppSpin'; @@ -23,26 +23,23 @@ const CollectionHistoryContext = createContext({ }); CollectionHistoryContext.displayName = 'CollectionHistoryContext'; +const options = { + resource: 'collectionsHistory', + action: 'list', + params: { + paginate: false, + appends: ['fields'], + filter: { + // inherit: false, + }, + sort: ['sort'], + }, +}; + export const CollectionHistoryProvider: React.FC = (props) => { const api = useAPIClient(); - - const options = { - resource: 'collectionsHistory', - action: 'list', - params: { - paginate: false, - appends: ['fields'], - filter: { - // inherit: false, - }, - sort: ['sort'], - }, - }; - const location = useLocation(); - // console.log('location', location); - const isAdminPage = location.pathname.startsWith('/admin'); const token = api.auth.getToken() || ''; const { render } = useAppSpin(); @@ -55,26 +52,24 @@ export const CollectionHistoryProvider: React.FC = (props) => { }); // 刷新 collecionHistory - const refreshCH = async () => { + const refreshCH = useCallback(async () => { const { data } = await api.request(options); service.mutate(data); return data?.data || []; - }; + }, [service]); + + const value = useMemo(() => { + return { + historyCollections: service.data?.data, + refreshCH, + }; + }, [refreshCH, service.data?.data]); if (service.loading) { return render(); } - return ( - - {props.children} - - ); + return {props.children}; }; export const useHistoryCollectionsByNames = (collectionNames: string[]) => { diff --git a/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx b/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx index 10a258c00..611c24123 100644 --- a/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx +++ b/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx @@ -7,15 +7,15 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { useAPIClient, useRequest } from '../api-client'; -import { CollectionManagerSchemaComponentProvider } from './CollectionManagerSchemaComponentProvider'; -import { CollectionCategroriesContext } from './context'; -import { CollectionManagerOptions } from './types'; +import { useAppSpin } from '../application/hooks/useAppSpin'; import { CollectionManagerProvider } from '../data-source/collection/CollectionManagerProvider'; import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider'; import { useCollectionHistory } from './CollectionHistoryProvider'; -import { useAppSpin } from '../application/hooks/useAppSpin'; +import { CollectionManagerSchemaComponentProvider } from './CollectionManagerSchemaComponentProvider'; +import { CollectionCategroriesContext } from './context'; +import { CollectionManagerOptions } from './types'; /** * @deprecated use `CollectionManagerProvider` instead @@ -28,18 +28,19 @@ export const CollectionManagerProvider_deprecated: React.FC { const api = useAPIClient(); const dm = useDataSourceManager(); const { refreshCH } = useCollectionHistory(); - const coptions = { - url: 'collectionCategories:list', - params: { - paginate: false, - sort: ['sort'], - }, - }; const service = useRequest<{ data: any; }>(() => { @@ -50,17 +51,18 @@ export const RemoteCollectionManagerProvider = (props: any) => { }>(coptions); const { render } = useAppSpin(); + const refreshCategory = useCallback(async () => { + const { data } = await api.request(coptions); + result.mutate(data); + return data?.data || []; + }, [result]); + if (service.loading) { return render(); } - const refreshCategory = async () => { - const { data } = await api.request(coptions); - result.mutate(data); - return data?.data || []; - }; return ( - + ); diff --git a/packages/core/client/src/collection-manager/CollectionManagerSchemaComponentProvider.tsx b/packages/core/client/src/collection-manager/CollectionManagerSchemaComponentProvider.tsx index e84514677..d95300d06 100644 --- a/packages/core/client/src/collection-manager/CollectionManagerSchemaComponentProvider.tsx +++ b/packages/core/client/src/collection-manager/CollectionManagerSchemaComponentProvider.tsx @@ -9,21 +9,22 @@ import React from 'react'; import { SchemaComponentOptions } from '..'; -import { CollectionProvider_deprecated, ResourceActionProvider, useDataSourceFromRAC } from '.'; +import { CollectionProvider_deprecated } from './CollectionProvider_deprecated'; +import { ResourceActionProvider, useDataSourceFromRAC } from './ResourceActionProvider'; import * as hooks from './action-hooks'; -import { DataSourceProvider_deprecated, ds, SubFieldDataSourceProvider_deprecated } from './sub-table'; +import { DataSourceProvider_deprecated, SubFieldDataSourceProvider_deprecated, ds } from './sub-table'; + +const scope = { cm: { ...hooks, useDataSourceFromRAC }, ds }; +const components = { + SubFieldDataSourceProvider_deprecated, + DataSourceProvider_deprecated, + CollectionProvider_deprecated, + ResourceActionProvider, +}; export const CollectionManagerSchemaComponentProvider: React.FC = (props) => { return ( - + {props.children} ); diff --git a/packages/core/client/src/collection-manager/hooks/useCollectionManager_deprecated.ts b/packages/core/client/src/collection-manager/hooks/useCollectionManager_deprecated.ts index 5f8240269..a63268269 100644 --- a/packages/core/client/src/collection-manager/hooks/useCollectionManager_deprecated.ts +++ b/packages/core/client/src/collection-manager/hooks/useCollectionManager_deprecated.ts @@ -7,17 +7,17 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import { uid } from '@formily/shared'; import { CascaderProps } from 'antd'; import _ from 'lodash'; import { useCallback, useMemo, useState } from 'react'; -import { useCompile, useSchemaComponentContext } from '../../schema-component'; -import { CollectionFieldOptions_deprecated, CollectionOptions } from '../types'; -import { InheritanceCollectionMixin } from '../mixins/InheritanceCollectionMixin'; -import { uid } from '@formily/shared'; +import { useCollectionManager } from '../../data-source/collection/CollectionManagerProvider'; +import { DEFAULT_DATA_SOURCE_KEY } from '../../data-source/data-source/DataSourceManager'; import { useDataSourceManager } from '../../data-source/data-source/DataSourceManagerProvider'; import { useDataSource } from '../../data-source/data-source/DataSourceProvider'; -import { DEFAULT_DATA_SOURCE_KEY } from '../../data-source/data-source/DataSourceManager'; -import { useCollectionManager } from '../../data-source/collection/CollectionManagerProvider'; +import { useCompile, useSchemaComponentContext } from '../../schema-component'; +import { InheritanceCollectionMixin } from '../mixins/InheritanceCollectionMixin'; +import { CollectionFieldOptions_deprecated, CollectionOptions } from '../types'; /** * @deprecated use `useCollectionManager` instead @@ -273,9 +273,12 @@ export const useCollectionManager_deprecated = (dataSourceName?: string) => { ); // 是否可以作为标题字段 - const isTitleField = (field) => { - return getInterface(field.interface)?.titleUsable; - }; + const isTitleField = useCallback( + (field) => { + return getInterface(field.interface)?.titleUsable; + }, + [getInterface], + ); const getParentCollectionFields = useCallback( (parentCollection, currentCollection, customDataSource?: string) => { diff --git a/packages/core/client/src/data-source/data-block/DataBlockProvider.tsx b/packages/core/client/src/data-source/data-block/DataBlockProvider.tsx index dd8e6bcab..b590e855c 100644 --- a/packages/core/client/src/data-source/data-block/DataBlockProvider.tsx +++ b/packages/core/client/src/data-source/data-block/DataBlockProvider.tsx @@ -7,7 +7,6 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { useFieldSchema } from '@formily/react'; import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react'; import { ACLCollectionProvider } from '../../acl/ACLProvider'; diff --git a/packages/core/client/src/data-source/data-block/DataBlockRequestProvider.tsx b/packages/core/client/src/data-source/data-block/DataBlockRequestProvider.tsx index 20c652047..70ac5d442 100644 --- a/packages/core/client/src/data-source/data-block/DataBlockRequestProvider.tsx +++ b/packages/core/client/src/data-source/data-block/DataBlockRequestProvider.tsx @@ -38,7 +38,7 @@ function useCurrentRequest(options: Omit) { } const paramsValue = params.filterByTk === undefined ? _.omit(params, 'filterByTk') : params; - return resource[action]({ ...paramsValue, ...customParams }).then((res) => res.data); + return resource[action]?.({ ...paramsValue, ...customParams }).then((res) => res.data); }; }, [resource, action, JSON.stringify(params), JSON.stringify(record), requestService]); @@ -129,7 +129,16 @@ export const BlockRequestProvider: FC = ({ children }) => { }); const memoizedParentRecord = useMemo(() => { - return parentRequest.data?.data && new CollectionRecord({ isNew: false, data: parentRequest.data?.data }); + return ( + parentRequest.data?.data && + new CollectionRecord({ + isNew: false, + data: + parentRequest.data?.data instanceof CollectionRecord + ? parentRequest.data?.data.data + : parentRequest.data?.data, + }) + ); }, [parentRequest.data?.data]); return ( @@ -137,13 +146,13 @@ export const BlockRequestProvider: FC = ({ children }) => { {action !== 'list' ? ( {children} ) : ( - + {children} )} diff --git a/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx b/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx index fd8753acf..6625dc927 100644 --- a/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx +++ b/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx @@ -40,7 +40,7 @@ export const DataBlockResourceProvider: FC<{ children?: ReactNode }> = ({ childr const resource = useMemo(() => { if (association) { - return api.resource(association, sourceIdValue, headers); + return api.resource(association, sourceIdValue, headers, !sourceIdValue); } return api.resource(collectionName, undefined, headers); }, [api, association, collection, sourceIdValue, headers]); diff --git a/packages/core/client/src/filter-provider/FilterProvider.tsx b/packages/core/client/src/filter-provider/FilterProvider.tsx index 443dade67..81193a289 100644 --- a/packages/core/client/src/filter-provider/FilterProvider.tsx +++ b/packages/core/client/src/filter-provider/FilterProvider.tsx @@ -40,7 +40,7 @@ export interface ForeignKeyField { type Collection = ReturnType; export interface DataBlock { - /** 唯一标识符,schema 中的 name 值 */ + /** 唯一标识符,schema 中的 x-uid 值 */ uid: string; /** 用户自行设置的区块名称 */ title?: string; diff --git a/packages/core/client/src/modules/actions/__e2e__/action.schemaSetting.test.ts b/packages/core/client/src/modules/actions/__e2e__/action.schemaSetting.test.ts index b70a658ba..d4c8bee00 100644 --- a/packages/core/client/src/modules/actions/__e2e__/action.schemaSetting.test.ts +++ b/packages/core/client/src/modules/actions/__e2e__/action.schemaSetting.test.ts @@ -8,7 +8,8 @@ */ import { expect, test } from '@nocobase/test/e2e'; -import { OneTableWithDelete } from './templates'; +import { OneTableWithDelete, shouldRefreshBlockDataAfterMultiplePopupsClosed } from './templates'; + test.describe('action settings', () => { test('refresh data on action', async ({ page, mockPage, mockRecords }) => { await mockPage(OneTableWithDelete).goto(); @@ -44,4 +45,24 @@ test.describe('action settings', () => { await page.waitForTimeout(500); expect(requestMade).toBeFalsy(); }); + + test('should refresh block data after multiple popups closed', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(shouldRefreshBlockDataAfterMultiplePopupsClosed).waitForInit(); + await mockRecord('users', { username: 'Test', roles: [{ title: 'Test role' }] }); + await nocoPage.goto(); + + await page.getByLabel('action-Action.Link-Edit-update-users-table-1').click(); + await page.getByTestId('drawer-Action.Container-users-Edit record').getByLabel('action-Action.Link-Edit-').click(); + await page.getByLabel('block-item-CollectionField-').getByRole('textbox').fill('abc123'); + await page.getByLabel('action-Action-Submit-submit-').click(); + + // the first popup + await expect( + page.getByTestId('drawer-Action.Container-users-Edit record').getByRole('button', { name: 'abc123' }), + ).toBeVisible(); + + // close the first popup + await page.getByLabel('drawer-Action.Container-users-Edit record-mask').click(); + await expect(page.getByLabel('block-item-CardItem-users-').getByRole('button', { name: 'abc123' })).toBeVisible(); + }); }); diff --git a/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts b/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts index 78fdb62e8..b53c141b5 100644 --- a/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts +++ b/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts @@ -8,7 +8,7 @@ */ import { expect, test } from '@nocobase/test/e2e'; -import { oneEmptyTableWithUsers } from './templates'; +import { PopupAndSubPageWithParams, oneEmptyTableWithUsers } from './templates'; test.describe('Link', () => { test('basic', async ({ page, mockPage, mockRecords }) => { @@ -84,4 +84,30 @@ test.describe('Link', () => { await expect(page.getByRole('button', { name: 'nocobase', exact: true })).toBeVisible(); await expect(page.getByRole('button', { name: users[1].username, exact: true })).toBeVisible(); }); + + test('popup and sub page with search params', async ({ page, mockPage, mockRecords }) => { + const nocoPage = mockPage(PopupAndSubPageWithParams); + const url = await nocoPage.getUrl(); + await page.goto(url + '?name=abc'); + + // open popup with drawer mode + await page.getByLabel('action-Action.Link-View-view-').click(); + await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('abc'); + + await page.reload(); + await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('abc'); + + await page.goBack(); + await page.getByLabel('action-Action.Link-View-view-').hover(); + await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:view-users').hover(); + await page.getByRole('menuitem', { name: 'Open mode Drawer' }).click(); + await page.getByRole('option', { name: 'Page' }).click(); + + // open sub page with page mode + await page.getByLabel('action-Action.Link-View-view-').click(); + await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('abc'); + + await page.reload(); + await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('abc'); + }); }); diff --git a/packages/core/client/src/modules/actions/__e2e__/link/templates.ts b/packages/core/client/src/modules/actions/__e2e__/link/templates.ts index 13885789f..3dfc2698d 100644 --- a/packages/core/client/src/modules/actions/__e2e__/link/templates.ts +++ b/packages/core/client/src/modules/actions/__e2e__/link/templates.ts @@ -222,3 +222,368 @@ export const oneEmptyTableWithUsers = { 'x-index': 1, }, }; +export const PopupAndSubPageWithParams = { + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + properties: { + b1atmxyqbff: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: { + rkebzj98nvq: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.11-alpha', + properties: { + kxz1o673sem: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.11-alpha', + properties: { + t79t7o2scw5: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'users', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.2.11-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.2.11-alpha', + 'x-uid': 'm3lmb6s616o', + 'x-async': false, + 'x-index': 1, + }, + sghz67ot4pa: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.2.11-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.2.11-alpha', + properties: { + ebq18cea3uo: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.2.11-alpha', + properties: { + c14cpxkc4ke: { + 'x-uid': 'ech5j6ogus0', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("View") }}', + 'x-action': 'view', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:view', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'drawer', + }, + 'x-action-context': { + dataSource: 'main', + collection: 'users', + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("View record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Details")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:common:addBlock', + properties: { + z1h7wnh6s60: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.11-alpha', + properties: { + bxzkcif7vu9: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.11-alpha', + properties: { + e2pjbwaou03: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-acl-action': 'users:create', + 'x-decorator': 'FormBlockProvider', + 'x-use-decorator-props': + 'useCreateFormBlockDecoratorProps', + 'x-decorator-props': { + dataSource: 'main', + collection: 'users', + isCusomeizeCreate: true, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:createForm', + 'x-component': 'CardItem', + 'x-app-version': '1.2.11-alpha', + properties: { + j86fmzva2n0: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useCreateFormBlockProps', + 'x-app-version': '1.2.11-alpha', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + 'x-app-version': '1.2.11-alpha', + properties: { + s1cu9qfbyx6: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.11-alpha', + properties: { + rmck9ducuw7: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.11-alpha', + properties: { + nickname: { + 'x-uid': '22a0rhtgo1x', + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': + 'FormItemSchemaToolbar', + 'x-settings': + 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': + 'users.nickname', + 'x-component-props': {}, + 'x-app-version': '1.2.11-alpha', + default: + '{{$nURLSearchParams.name}}', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'eudbw9veltu', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'jdt8bqzqi2s', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '5hq25kdeet9', + 'x-async': false, + 'x-index': 1, + }, + kke3cg70wha: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'createForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + }, + 'x-app-version': '1.2.11-alpha', + 'x-uid': 'usbcj3rxwdq', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'xk49vnplykb', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'xz0fzo8aiwd', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'c2z7nkp1er8', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '817scrzocq2', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'o5aa5p4qdfy', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'cyys2ghczmx', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'pte28pwapoh', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'nnc6fz5v38k', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'nba9caa8pc1', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '14vqyxwyyty', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'tu8n9op1z9r', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'v2gig2bsyex', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '9r7wta3gbml', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'zj3df6jm0dh', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'fz1jj9rlu87', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'de0k4foh9cc', + 'x-async': true, + 'x-index': 1, + }, +}; diff --git a/packages/core/client/src/modules/actions/__e2e__/templates.ts b/packages/core/client/src/modules/actions/__e2e__/templates.ts index d945d69de..d0255ad84 100644 --- a/packages/core/client/src/modules/actions/__e2e__/templates.ts +++ b/packages/core/client/src/modules/actions/__e2e__/templates.ts @@ -196,3 +196,780 @@ export const OneTableWithDelete = { 'x-index': 1, }, }; +export const shouldRefreshBlockDataAfterMultiplePopupsClosed = { + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + properties: { + tyx0aioxlr1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: { + '0i5raczm0ne': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.11-alpha', + properties: { + zjk1te2zpix: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.11-alpha', + properties: { + k7eyfysw52n: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'users', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.2.11-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.2.11-alpha', + 'x-uid': 'fkgbia9i3kt', + 'x-async': false, + 'x-index': 1, + }, + pn8pb7x81kh: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.2.11-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.2.11-alpha', + properties: { + g3flunks833: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.2.11-alpha', + properties: { + noihi5pm44q: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Edit") }}', + 'x-action': 'update', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:edit', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'drawer', + icon: 'EditOutlined', + }, + 'x-action-context': { + dataSource: 'main', + collection: 'users', + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Edit record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Edit")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:common:addBlock', + properties: { + suv1o9u1ti1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.11-alpha', + properties: { + oqzxplx2qap: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.11-alpha', + properties: { + uxrg8z6qz3w: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users.roles:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + association: 'users.roles', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'name', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.2.11-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.2.11-alpha', + 'x-uid': 'nzb7njucb1g', + 'x-async': false, + 'x-index': 1, + }, + prcxluma72r: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.2.11-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.2.11-alpha', + properties: { + z3n08qgeo8y: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.2.11-alpha', + properties: { + pla7f5mwirx: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Edit") }}', + 'x-action': 'update', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:edit', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'drawer', + icon: 'EditOutlined', + }, + 'x-action-context': { + dataSource: 'main', + association: 'users.roles', + sourceId: 1, + parentPopupRecord: { + collection: 'users', + filterByTk: 1, + }, + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Edit record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Edit")}}', + 'x-component': + 'Tabs.TabPane', + 'x-designer': + 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': + 'popup:common:addBlock', + properties: { + k6k23ljfmwv: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': + 'Grid.Row', + 'x-app-version': + '1.2.11-alpha', + properties: { + ddkwboqqe19: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': + 'Grid.Col', + 'x-app-version': + '1.2.11-alpha', + properties: { + '33h5pkdwmt5': + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-acl-action-props': + { + skipScopeCheck: + false, + }, + 'x-acl-action': + 'users.roles:update', + 'x-decorator': + 'FormBlockProvider', + 'x-use-decorator-props': + 'useEditFormBlockDecoratorProps', + 'x-decorator-props': + { + action: + 'get', + dataSource: + 'main', + association: + 'users.roles', + }, + 'x-toolbar': + 'BlockSchemaToolbar', + 'x-settings': + 'blockSettings:editForm', + 'x-component': + 'CardItem', + 'x-is-current': + true, + 'x-app-version': + '1.2.11-alpha', + properties: + { + '7t3234fb1yu': + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'FormV2', + 'x-use-component-props': + 'useEditFormBlockProps', + 'x-app-version': + '1.2.11-alpha', + properties: + { + grid: { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid', + 'x-initializer': + 'form:configureFields', + 'x-app-version': + '1.2.11-alpha', + properties: + { + xizv324ooej: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid.Row', + 'x-app-version': + '1.2.11-alpha', + properties: + { + gjzwqen45hw: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid.Col', + 'x-app-version': + '1.2.11-alpha', + properties: + { + title: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'string', + 'x-toolbar': + 'FormItemSchemaToolbar', + 'x-settings': + 'fieldSettings:FormItem', + 'x-component': + 'CollectionField', + 'x-decorator': + 'FormItem', + 'x-collection-field': + 'roles.title', + 'x-component-props': + {}, + 'x-app-version': + '1.2.11-alpha', + 'x-uid': + 'i1q5apprfo3', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'cyrc9goozvc', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + '1yonjmmam2w', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'yzom6z8f3t2', + 'x-async': + false, + 'x-index': 1, + }, + u0l41h24oqc: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-initializer': + 'editForm:configureActions', + 'x-component': + 'ActionBar', + 'x-component-props': + { + layout: + 'one-column', + }, + 'x-app-version': + '1.2.11-alpha', + properties: + { + se7ggsy8wv0: + { + _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.2.11-alpha', + 'x-uid': + '38c98jagvqk', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + '4s6eki715ub', + 'x-async': + false, + 'x-index': 2, + }, + }, + 'x-uid': + 'xnnwdz56xzc', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'zvr9ezaqhn8', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'l3xw7vpnnmi', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'mydjjzt5wjb', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '1a12yaxkvlh', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '28wuyvfuzxa', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '739bd4xxp5r', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'sc3bt4kz43j', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'bxmc5k79nhw', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '4hz3jkt2wfo', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'x6x0ygv9b0s', + 'x-async': false, + 'x-index': 1, + }, + lyn5o91qf9v: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + 'x-app-version': '1.2.11-alpha', + properties: { + title: { + _isJSONSchemaObject: true, + version: '2.0', + 'x-collection-field': 'roles.title', + 'x-component': 'CollectionField', + 'x-component-props': { + ellipsis: true, + }, + 'x-read-pretty': true, + 'x-decorator': null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-app-version': '1.2.11-alpha', + 'x-uid': 'a47ajmlmdq4', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'v3uq1qkh0f1', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'qxusos5qytp', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'skdhbz3b5wb', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '9keek6gthp6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'n6k49p6zvw5', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'sod8zbrugmq', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'lt3vak0nmab', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'g1ee1hpkd4p', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'p4enijxueeg', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'bxd64x3df56', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '0y4f0f650h9', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '2qk92ft951c', + 'x-async': false, + 'x-index': 1, + }, + fv22e8igzgz: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + 'x-app-version': '1.2.11-alpha', + properties: { + roles: { + 'x-uid': 'zkenfmew3f7', + _isJSONSchemaObject: true, + version: '2.0', + 'x-collection-field': 'users.roles', + 'x-component': 'CollectionField', + 'x-component-props': { + fieldNames: { + label: 'title', + value: 'name', + }, + ellipsis: true, + size: 'small', + }, + 'x-read-pretty': true, + 'x-decorator': null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-app-version': '1.2.11-alpha', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '2fmv2b2n8it', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'n48nmuoljua', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'oiu3t5hup6s', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'kc32hfy3m57', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'aayu9ix6qh6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'vutamnve682', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'da87s7d7xs0', + 'x-async': true, + 'x-index': 1, + }, +}; diff --git a/packages/core/client/src/modules/actions/add-child/CreateChildInitializer.tsx b/packages/core/client/src/modules/actions/add-child/CreateChildInitializer.tsx index 604e7df2e..c42d3a2fc 100644 --- a/packages/core/client/src/modules/actions/add-child/CreateChildInitializer.tsx +++ b/packages/core/client/src/modules/actions/add-child/CreateChildInitializer.tsx @@ -8,8 +8,11 @@ */ import React from 'react'; +import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils'; +import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField'; import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem'; export const CreateChildInitializer = (props) => { + const { getPopupContext } = usePagePopup(); const schema = { type: 'void', title: '{{ t("Add child") }}', @@ -63,6 +66,7 @@ export const CreateChildInitializer = (props) => { }, }, }, + [CONTEXT_SCHEMA_KEY]: getPopupContext(), }; return ; }; diff --git a/packages/core/client/src/modules/actions/add-new/CreateActionInitializer.tsx b/packages/core/client/src/modules/actions/add-new/CreateActionInitializer.tsx index 2e3f1b44e..d3b1f2109 100644 --- a/packages/core/client/src/modules/actions/add-new/CreateActionInitializer.tsx +++ b/packages/core/client/src/modules/actions/add-new/CreateActionInitializer.tsx @@ -9,9 +9,12 @@ import React from 'react'; import { useSchemaInitializerItem } from '../../../application'; +import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils'; +import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField'; import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem'; export const CreateActionInitializer = () => { + const { getPopupContext } = usePagePopup(); const schema = { type: 'void', 'x-action': 'create', @@ -65,6 +68,7 @@ export const CreateActionInitializer = () => { }, }, }, + [CONTEXT_SCHEMA_KEY]: getPopupContext(), }; const itemConfig = useSchemaInitializerItem(); return ; diff --git a/packages/core/client/src/modules/actions/view-edit-popup/PopupActionInitializer.tsx b/packages/core/client/src/modules/actions/view-edit-popup/PopupActionInitializer.tsx index e72b28b9b..2c930cf55 100644 --- a/packages/core/client/src/modules/actions/view-edit-popup/PopupActionInitializer.tsx +++ b/packages/core/client/src/modules/actions/view-edit-popup/PopupActionInitializer.tsx @@ -9,9 +9,12 @@ import React from 'react'; import { useSchemaInitializerItem } from '../../../application'; +import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils'; +import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField'; import { BlockInitializer } from '../../../schema-initializer/items'; export const PopupActionInitializer = (props) => { + const { getPopupContext } = usePagePopup(); const schema = { type: 'void', title: '{{ t("Popup") }}', @@ -58,6 +61,7 @@ export const PopupActionInitializer = (props) => { }, }, }, + [CONTEXT_SCHEMA_KEY]: getPopupContext(), }; const itemConfig = useSchemaInitializerItem(); diff --git a/packages/core/client/src/modules/actions/view-edit-popup/UpdateActionInitializer.tsx b/packages/core/client/src/modules/actions/view-edit-popup/UpdateActionInitializer.tsx index 195240ff3..094db370b 100644 --- a/packages/core/client/src/modules/actions/view-edit-popup/UpdateActionInitializer.tsx +++ b/packages/core/client/src/modules/actions/view-edit-popup/UpdateActionInitializer.tsx @@ -8,9 +8,12 @@ */ import React from 'react'; +import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils'; +import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField'; import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem'; export const UpdateActionInitializer = (props) => { + const { getPopupContext } = usePagePopup(); const schema = { type: 'void', title: '{{ t("Edit") }}', @@ -57,6 +60,7 @@ export const UpdateActionInitializer = (props) => { }, }, }, + [CONTEXT_SCHEMA_KEY]: getPopupContext(), }; return ; }; diff --git a/packages/core/client/src/modules/actions/view-edit-popup/ViewActionInitializer.tsx b/packages/core/client/src/modules/actions/view-edit-popup/ViewActionInitializer.tsx index b86028e2b..e24f7120f 100644 --- a/packages/core/client/src/modules/actions/view-edit-popup/ViewActionInitializer.tsx +++ b/packages/core/client/src/modules/actions/view-edit-popup/ViewActionInitializer.tsx @@ -8,9 +8,12 @@ */ import React from 'react'; +import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils'; +import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField'; import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem'; export const ViewActionInitializer = (props) => { + const { getPopupContext } = usePagePopup(); const schema = { type: 'void', title: '{{ t("View") }}', @@ -56,6 +59,7 @@ export const ViewActionInitializer = (props) => { }, }, }, + [CONTEXT_SCHEMA_KEY]: getPopupContext(), }; return ; }; diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts index 284fe0135..f031df748 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/__e2e__/schemaInitializer.test.ts @@ -37,6 +37,7 @@ test.describe('where single data details block can be added', () => { await nocoPage.goto(); // 1.打开弹窗 + await page.getByRole('button', { name: '2', exact: true }).getByText('2').hover(); await page.getByRole('button', { name: '2', exact: true }).getByText('2').click(); // 2.通过 Current record 创建一个详情区块 @@ -121,7 +122,7 @@ test.describe('configure actions', () => { // create delete ------------------------------------------------------------------------------------ await createAction(page, 'Delete'); - await expect(page.getByLabel('action-Action-Delete-destroy-general-details-')).toBeVisible(); + await expect(page.getByLabel('action-Action-Delete-destroy-general-details')).toBeVisible(); // create print await createAction(page, 'Print'); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/associationForm.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/associationForm.test.ts index 073ac924c..658385a48 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/associationForm.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/associationForm.test.ts @@ -48,13 +48,13 @@ test.describe('association form block', () => { test('association table block add new ', async ({ page, mockPage, mockRecord }) => { await mockPage(T3979).goto(); await mockRecord('general'); - await expect(await page.getByLabel('block-item-CardItem-general-')).toBeVisible(); + await expect(page.getByLabel('block-item-CardItem-general-')).toBeVisible(); // 1. 打开关系字段弹窗 await page.getByLabel('block-item-CardItem-general-').locator('a').click(); await page.getByLabel('block-item-CardItem-roles-').click(); // 2. 提交后,Table 会显示新增的数据 - await page.getByLabel('action-Action-Add new-create-roles-table-').click(); + await page.getByLabel('action-Action-Add new-create-roles-table').click(); // 3. 区块数据表为关系字段的区块 await page @@ -64,6 +64,6 @@ test.describe('association form block', () => { await page.getByRole('menuitem', { name: 'form Form' }).hover(); await page.getByRole('menuitem', { name: 'Current collection' }).click(); - await expect(await page.getByLabel('block-item-CardItem-roles-form')).toBeVisible(); + await expect(page.getByLabel('block-item-CardItem-roles-form')).toBeVisible(); }); }); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings.test.ts index a91dc590b..949d3997a 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings.test.ts @@ -17,7 +17,15 @@ import { test, } from '@nocobase/test/e2e'; import { oneEmptyTableWithUsers } from '../../../details-multi/__e2e__/templatesOfBug'; -import { T2174, T3871, oneFormAndOneTableWithUsers, oneTableWithNestPopups } from './templatesOfBug'; +import { + T2174, + T3871, + currentPopupRecordInPopupThatOpenedByAssociationField, + oneFormAndOneTableWithUsers, + oneTableWithNestPopups, + parentPopupRecordInSubPageTheFirstLevelIsASubpageAndTheSecondLevelIsAPopup, + parentPopupRecordInSubPageTheFirstLevelIsASubpageAndTheSecondLevelIsASubpageToo, +} from './templatesOfBug'; test.describe('set default value', () => { test('basic fields', async ({ page, mockPage }) => { @@ -203,7 +211,6 @@ test.describe('set default value', () => { // https://nocobase.height.app/T-4028/description // 刷新页面后,默认值应该依然存在 await page.reload(); - await page.getByRole('button', { name: 'Add new' }).click(); await page .getByTestId('drawer-Action.Container-general-Add record') .getByRole('button', { name: 'Add new' }) @@ -363,11 +370,51 @@ test.describe('set default value', () => { ).toBeVisible(); }); + test('Current popup record in popup that opened by association field', async ({ mockPage, page }) => { + await mockPage(currentPopupRecordInPopupThatOpenedByAssociationField).goto(); + + await page.getByText('Member').click(); + + // Current popup record in the first popup + await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Member'); + + // Current popup record and Parent popup record in the second popup + await page.getByLabel('action-Action-Edit-update-').click(); + await expect( + page + .getByTestId('drawer-Action.Container-roles-Edit record') + .getByLabel('block-item-CollectionField-users-form-users.nickname-Current popup record') + .getByRole('textbox'), + ).toHaveValue('Member'); + await expect( + page.getByLabel('block-item-CollectionField-users-form-users.username-Parent popup record').getByRole('textbox'), + ).toHaveValue('Member'); + }); + test('Parent popup record', async ({ page, mockPage }) => { await mockPage(oneTableWithNestPopups).goto(); // 1. 表单字段默认值中使用 `Parent popup record` await page.getByLabel('action-Action.Link-View').click(); + + // 在第一级弹窗中,不应该包含 Parent popup record 变量 + await page + .getByTestId('drawer-Action.Container-users-View record') + .getByLabel('block-item-CardItem-users-') + .hover(); + await page + .getByTestId('drawer-Action.Container-users-View record') + .getByLabel('designer-schema-settings-CardItem-blockSettings:table-users') + .hover(); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await page.getByText('Add condition', { exact: true }).click(); + await page.getByLabel('variable-button').click(); + await expect(page.getByRole('menuitemcheckbox', { name: 'Current popup record right' })).toBeVisible(); + await expect(page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' })).not.toBeVisible(); + + // 关闭数据范围设置弹窗 + await page.getByRole('button', { name: 'Close', exact: true }).click(); + await page.getByLabel('action-Action.Link-View in popup').click(); await page.getByLabel('schema-initializer-Grid-popup').nth(1).hover(); await page.getByRole('menuitem', { name: 'form Form (Add new) right' }).hover(); @@ -462,11 +509,273 @@ test.describe('set default value', () => { await page.getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-users-users.').hover(); await page.getByRole('menuitem', { name: 'Set default value' }).click(); await page.getByLabel('variable-button').click(); + await expect(page.getByRole('menuitemcheckbox', { name: 'Current popup record right' })).not.toBeVisible(); await page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); await page.getByRole('button', { name: 'OK', exact: true }).click(); await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin'); }); + + test('Parent popup record in sub page. The first level is a subpage, and the second level is a popup', async ({ + page, + mockPage, + }) => { + await mockPage(parentPopupRecordInSubPageTheFirstLevelIsASubpageAndTheSecondLevelIsAPopup).goto(); + + // 1. 表单字段默认值中使用 `Parent popup record` + await page.getByLabel('action-Action.Link-View').click(); + + // 在第一级弹窗中,不应该包含 Parent popup record 变量 + await page.getByLabel('block-item-CardItem-users-').hover(); + await page.getByLabel('designer-schema-settings-CardItem-blockSettings:table-users').hover(); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await page.getByText('Add condition', { exact: true }).click(); + await page.getByLabel('variable-button').click(); + await expect(page.getByRole('menuitemcheckbox', { name: 'Current popup record right' })).toBeVisible(); + await expect(page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' })).not.toBeVisible(); + + // 关闭数据范围设置弹窗 + await page.getByRole('button', { name: 'Close', exact: true }).click(); + + await page.getByLabel('action-Action.Link-View in popup').click(); + await page.getByLabel('schema-initializer-Grid-popup').nth(1).hover(); + await page.getByRole('menuitem', { name: 'form Form (Add new) right' }).hover(); + await page.getByRole('menuitem', { name: 'Other records right' }).hover(); + await page.getByRole('menuitem', { name: 'Users' }).click(); + await page.mouse.move(300, 0); + await page.getByLabel('schema-initializer-Grid-form:').hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + await page.getByLabel('block-item-CollectionField-').hover(); + await page.getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-users-users.').hover(); + await page.getByRole('menuitem', { name: 'Set default value' }).click(); + await page.getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin'); + + await page.reload(); + await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin'); + + // 2. 表单联动规则中使用 `Parent popup record` + // 创建 Username 字段 + await page.getByLabel('schema-initializer-Grid-form:').hover(); + await page.getByRole('menuitem', { name: 'Username' }).click(); + // 设置联动规则 + await page.getByLabel('block-item-CardItem-users-form').hover(); + await page.getByLabel('designer-schema-settings-CardItem-blockSettings:createForm-users').hover(); + await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); + await page.mouse.move(300, 0); + await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); + await page.getByText('Add property').click(); + await page.getByTestId('select-linkage-property-field').click(); + await page.getByTitle('Username').click(); + await page.getByTestId('select-linkage-action-field').click(); + await page.getByRole('option', { name: 'Value', exact: true }).click(); + await page.getByTestId('select-linkage-value-type').click(); + await page.getByTitle('Expression').click(); + await page.getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Username' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + // 需正确显示变量的值 + await expect( + page.getByLabel('block-item-CollectionField-users-form-users.username-Username').getByRole('textbox'), + ).toHaveValue('nocobase'); + + await page.reload(); + await expect( + page.getByLabel('block-item-CollectionField-users-form-users.username-Username').getByRole('textbox'), + ).toHaveValue('nocobase'); + + // 3. Table 数据选择器中使用 `Parent popup record` + // 创建 Table 区块 + await page.getByLabel('schema-initializer-Grid-popup').nth(1).hover(); + await page.getByRole('menuitem', { name: 'table Table right' }).hover(); + await page.getByRole('menuitem', { name: 'Other records right' }).hover(); + await page.getByRole('menuitem', { name: 'Users' }).click(); + await page.mouse.move(300, 0); + // 显示 Nickname 字段 + await page + .getByTestId('drawer-Action.Container-users-View record') + .getByLabel('schema-initializer-TableV2-') + .hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + await page.mouse.move(300, 0); + // 设置数据范围(使用 `Parent popup record` 变量) + await page + .getByTestId('drawer-Action.Container-users-View record') + .getByLabel('block-item-CardItem-users-table') + .hover(); + await page + .getByTestId('drawer-Action.Container-users-View record') + .getByLabel('designer-schema-settings-CardItem-blockSettings:table-users') + .hover(); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await page.getByText('Add condition', { exact: true }).click(); + await page.getByTestId('select-filter-field').click(); + await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); + await page.getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + // 数据需显示正确 + await expect( + page + .getByTestId('drawer-Action.Container-users-View record') + .getByLabel('block-item-CardItem-users-table') + .getByRole('button', { name: 'Super Admin' }), + ).toBeVisible(); + + await page.reload(); + await expect( + page + .getByTestId('drawer-Action.Container-users-View record') + .getByLabel('block-item-CardItem-users-table') + .getByRole('button', { name: 'Super Admin' }), + ).toBeVisible(); + + // 4. 退出二级弹窗,在第一级弹窗中点击 Add new 按钮 + await page.goBack(); + await page.getByLabel('action-Action-Add new-create-').click(); + + // 5. 在新增表单中使用 `Parent popup record` + await page.getByLabel('block-item-CollectionField-').hover(); + await page.getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-users-users.').hover(); + await page.getByRole('menuitem', { name: 'Set default value' }).click(); + await page.getByLabel('variable-button').click(); + await expect(page.getByRole('menuitemcheckbox', { name: 'Current popup record right' })).not.toBeVisible(); + await page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin'); + + await page.reload(); + await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin'); + }); + + test('Parent popup record in sub page. The first level is a subpage, and the second level is a subpage too', async ({ + page, + mockPage, + }) => { + await mockPage(parentPopupRecordInSubPageTheFirstLevelIsASubpageAndTheSecondLevelIsASubpageToo).goto(); + + // 1. 表单字段默认值中使用 `Parent popup record` + await page.getByLabel('action-Action.Link-View').click(); + + // 在第一级弹窗中,不应该包含 Parent popup record 变量 + await page.getByLabel('block-item-CardItem-users-').hover(); + await page.getByLabel('designer-schema-settings-CardItem-blockSettings:table-users').hover(); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await page.getByText('Add condition', { exact: true }).click(); + await page.getByLabel('variable-button').click(); + await expect(page.getByRole('menuitemcheckbox', { name: 'Current popup record right' })).toBeVisible(); + await expect(page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' })).not.toBeVisible(); + + // 关闭数据范围设置弹窗 + await page.getByRole('button', { name: 'Close', exact: true }).click(); + + await page.getByLabel('action-Action.Link-View in popup').click(); + await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.getByRole('menuitem', { name: 'form Form (Add new) right' }).hover(); + await page.getByRole('menuitem', { name: 'Other records right' }).hover(); + await page.getByRole('menuitem', { name: 'Users' }).click(); + await page.mouse.move(300, 0); + await page.getByLabel('schema-initializer-Grid-form:').hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + await page.getByLabel('block-item-CollectionField-').hover(); + await page.getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-users-users.').hover(); + await page.getByRole('menuitem', { name: 'Set default value' }).click(); + await page.getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin'); + + await page.reload(); + await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin'); + + // 2. 表单联动规则中使用 `Parent popup record` + // 创建 Username 字段 + await page.getByLabel('schema-initializer-Grid-form:').hover(); + await page.getByRole('menuitem', { name: 'Username' }).click(); + // 设置联动规则 + await page.getByLabel('block-item-CardItem-users-form').hover(); + await page.getByLabel('designer-schema-settings-CardItem-blockSettings:createForm-users').hover(); + await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); + await page.mouse.move(300, 0); + await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); + await page.getByText('Add property').click(); + await page.getByTestId('select-linkage-property-field').click(); + await page.getByTitle('Username').click(); + await page.getByTestId('select-linkage-action-field').click(); + await page.getByRole('option', { name: 'Value', exact: true }).click(); + await page.getByTestId('select-linkage-value-type').click(); + await page.getByTitle('Expression').click(); + await page.getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Username' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + // 需正确显示变量的值 + await expect( + page.getByLabel('block-item-CollectionField-users-form-users.username-Username').getByRole('textbox'), + ).toHaveValue('nocobase'); + + await page.reload(); + await expect( + page.getByLabel('block-item-CollectionField-users-form-users.username-Username').getByRole('textbox'), + ).toHaveValue('nocobase'); + + // 3. Table 数据选择器中使用 `Parent popup record` + // 创建 Table 区块 + await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.getByRole('menuitem', { name: 'table Table right' }).hover(); + await page.getByRole('menuitem', { name: 'Other records right' }).hover(); + await page.getByRole('menuitem', { name: 'Users' }).click(); + await page.mouse.move(300, 0); + // 显示 Nickname 字段 + await page.getByLabel('schema-initializer-TableV2-').hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + await page.mouse.move(300, 0); + // 设置数据范围(使用 `Parent popup record` 变量) + await page.getByLabel('block-item-CardItem-users-table').hover(); + await page.getByLabel('designer-schema-settings-CardItem-blockSettings:table-users').hover(); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await page.getByText('Add condition', { exact: true }).click(); + await page.getByTestId('select-filter-field').click(); + await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); + await page.getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + // 数据需显示正确 + await expect( + page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'Super Admin' }), + ).toBeVisible(); + + await page.reload(); + await expect( + page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'Super Admin' }), + ).toBeVisible(); + + // 4. 退出二级弹窗,在第一级弹窗中点击 Add new 按钮 + await page.goBack(); + await page.getByLabel('action-Action-Add new-create-').click(); + + // 5. 在新增表单中使用 `Parent popup record` + await page.getByLabel('block-item-CollectionField-').hover(); + await page.getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-users-users.').hover(); + await page.getByRole('menuitem', { name: 'Set default value' }).click(); + await page.getByLabel('variable-button').click(); + await expect(page.getByRole('menuitemcheckbox', { name: 'Current popup record right' })).not.toBeVisible(); + await page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin'); + + await page.reload(); + await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin'); + }); }); test.describe('actions schema settings', () => { diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings1.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings1.test.ts index cc8ce6cae..42fa98da6 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings1.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings1.test.ts @@ -48,7 +48,6 @@ test.describe('creation form block schema settings', () => { // 刷新页面后,显示的应该依然是上次设置的值 await page.reload(); - await page.getByRole('button', { name: 'Add new' }).click(); await runExpect(); }); @@ -68,7 +67,6 @@ test.describe('creation form block schema settings', () => { // 刷新页面 await page.reload(); - await page.getByRole('button', { name: 'Add new' }).click(); await page.getByLabel('block-item-CardItem-general-form').hover(); await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); await expect(page.getByRole('menuitem', { name: 'Save as block template' })).not.toBeVisible(); @@ -82,7 +80,6 @@ test.describe('creation form block schema settings', () => { // 刷新页面 await page.reload(); - await page.getByRole('button', { name: 'Add new' }).click(); await page.getByLabel('block-item-CardItem-general-form').hover(); await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); await expect(page.getByRole('menuitem', { name: 'Save as block template' })).toBeVisible(); @@ -115,7 +112,6 @@ test.describe('creation form block schema settings', () => { // 刷新页面后,区块依然是被删除状态 await page.reload(); - await page.getByRole('button', { name: 'Add new' }).click(); await expect(page.getByLabel('block-item-CardItem-general-form')).not.toBeVisible(); }); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/templatesOfBug.ts index a32120b71..83180241c 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/templatesOfBug.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/templatesOfBug.ts @@ -10875,3 +10875,2299 @@ export const oneTableWithNestPopups = { 'x-async': true, }, }; +export const parentPopupRecordInSubPageTheFirstLevelIsASubpageAndTheSecondLevelIsASubpageToo = { + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + 'x-index': 1, + properties: { + zvj8cbqvt05: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + 'x-index': 1, + properties: { + j0zzcf3k2vc: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '0.21.0-alpha.6', + 'x-index': 1, + properties: { + pn0pgjxjlz2: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '0.21.0-alpha.6', + 'x-index': 1, + properties: { + o9zor60xpvi: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'users', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '0.21.0-alpha.6', + 'x-index': 1, + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '0.21.0-alpha.6', + 'x-index': 1, + 'x-uid': 'ufwwzuvh0ii', + 'x-async': false, + }, + '3rr559rnt6k': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '0.21.0-alpha.6', + 'x-index': 2, + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-designer': 'TableV2.ActionColumnDesigner', + 'x-initializer': 'table:configureItemActions', + 'x-app-version': '0.21.0-alpha.6', + 'x-index': 1, + properties: { + cf7dj1iffh3: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '0.21.0-alpha.6', + 'x-index': 1, + properties: { + oealnj6s2rw: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: 'View record', + 'x-action': 'view', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:view', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'page', + danger: false, + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + 'x-index': 1, + 'x-action-context': { + dataSource: 'main', + collection: 'users', + }, + 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-index': 1, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + 'x-index': 1, + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Details")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + 'x-index': 1, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:common:addBlock', + 'x-index': 1, + properties: { + o3az953k64s: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + '09c7dcvslhi': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + xv84k1hns7x: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'users', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + h6qhcp7es4z: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-action': 'create', + 'x-acl-action': 'create', + title: "{{t('Add new')}}", + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:addNew', + 'x-component': 'Action', + 'x-decorator': 'ACLActionProvider', + 'x-component-props': { + openMode: 'drawer', + type: 'primary', + component: 'CreateRecordAction', + icon: 'PlusOutlined', + }, + 'x-align': 'right', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Add record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + 'x-initializer-props': { + gridInitializer: + 'popup:addNew:addBlock', + }, + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Add new")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': + 'popup:addNew:addBlock', + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + '84oe9zbk4i3': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': + '1.2.7-alpha', + 'x-index': 1, + properties: { + g1ye1zczjy0: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': + 'Grid.Col', + 'x-app-version': + '1.2.7-alpha', + 'x-index': 1, + properties: { + '9boifjys3jn': { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-acl-action-props': + { + skipScopeCheck: + true, + }, + 'x-acl-action': + 'users:create', + 'x-decorator': + 'FormBlockProvider', + 'x-use-decorator-props': + 'useCreateFormBlockDecoratorProps', + 'x-decorator-props': + { + dataSource: + 'main', + collection: + 'users', + }, + 'x-toolbar': + 'BlockSchemaToolbar', + 'x-settings': + 'blockSettings:createForm', + 'x-component': + 'CardItem', + 'x-app-version': + '1.2.7-alpha', + 'x-index': 1, + properties: { + rmsurarncfw: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': + 'FormV2', + 'x-use-component-props': + 'useCreateFormBlockProps', + 'x-app-version': + '1.2.7-alpha', + 'x-index': 1, + properties: { + grid: { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid', + 'x-initializer': + 'form:configureFields', + 'x-app-version': + '1.2.7-alpha', + 'x-index': 1, + properties: + { + akmulvncf99: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid.Row', + 'x-app-version': + '1.2.7-alpha', + 'x-index': 1, + properties: + { + nmmv2oujq4y: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid.Col', + 'x-app-version': + '1.2.7-alpha', + 'x-index': 1, + properties: + { + nickname: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'string', + 'x-toolbar': + 'FormItemSchemaToolbar', + 'x-settings': + 'fieldSettings:FormItem', + 'x-component': + 'CollectionField', + 'x-decorator': + 'FormItem', + 'x-collection-field': + 'users.nickname', + 'x-component-props': + {}, + 'x-app-version': + '1.2.7-alpha', + 'x-index': 1, + 'x-uid': + '3wdw5ur6zbj', + 'x-async': + false, + }, + }, + 'x-uid': + '0etm4dgenpw', + 'x-async': + false, + }, + }, + 'x-uid': + 'rgi68bc4uvr', + 'x-async': + false, + }, + }, + 'x-uid': + 'ns36964nznx', + 'x-async': + false, + }, + '6b7iqambrl7': + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-initializer': + 'createForm:configureActions', + 'x-component': + 'ActionBar', + 'x-component-props': + { + layout: + 'one-column', + style: + { + marginTop: + 'var(--nb-spacing)', + }, + }, + 'x-app-version': + '1.2.7-alpha', + 'x-index': 2, + 'x-uid': + 'r2uf2sjeus1', + 'x-async': + false, + }, + }, + 'x-uid': + 'tjed9hp0w6t', + 'x-async': + false, + }, + }, + 'x-uid': + '380wbgkhahs', + 'x-async': false, + }, + }, + 'x-uid': 'h5niq5h5tn2', + 'x-async': false, + }, + }, + 'x-uid': 'f76pufhckas', + 'x-async': false, + }, + }, + 'x-uid': '2dd396w43br', + 'x-async': false, + }, + }, + 'x-uid': '87g6mq3ys2y', + 'x-async': false, + }, + }, + 'x-uid': 'spx9ipa7o9d', + 'x-async': false, + }, + }, + 'x-uid': 'ne2nrb2lisk', + 'x-async': false, + }, + }, + 'x-uid': 'tyiqn1q8qdc', + 'x-async': false, + }, + }, + 'x-uid': 'cdw5o18gg4r', + 'x-async': false, + }, + gjshpj169w3: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.2.7-alpha', + 'x-index': 2, + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + '6s1te9svg45': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + v2uyliqmu3s: { + 'x-uid': 'nufuf2do0vb', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: 'View in popup', + 'x-action': 'view', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:view', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'page', + iconColor: '#1677FF', + danger: false, + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + 'x-index': 1, + 'x-action-context': { + dataSource: 'main', + collection: 'users', + }, + 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-index': 1, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + 'x-index': 1, + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Details")}}', + 'x-component': + 'Tabs.TabPane', + 'x-designer': + 'Tabs.Designer', + 'x-component-props': {}, + 'x-index': 1, + properties: { + grid: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': + 'popup:common:addBlock', + 'x-index': 1, + 'x-uid': 'tfkctokyxye', + 'x-async': false, + }, + }, + 'x-uid': 'h45bcm99kga', + 'x-async': false, + }, + }, + 'x-uid': 'a6u2hu5hm4q', + 'x-async': false, + }, + }, + 'x-uid': 'zs94ks6dasi', + 'x-async': false, + }, + }, + 'x-async': false, + }, + }, + 'x-uid': 'n2mwxmac09n', + 'x-async': false, + }, + }, + 'x-uid': '2vb4ytmwm5e', + 'x-async': false, + }, + }, + 'x-uid': 'j0sj1zrdy6m', + 'x-async': false, + }, + }, + 'x-uid': 'rimxpmmoeuh', + 'x-async': false, + }, + }, + 'x-uid': '7sf6sglzrj1', + 'x-async': false, + }, + }, + 'x-uid': '50hkamulao2', + 'x-async': false, + }, + }, + 'x-uid': '6sbty7usxsx', + 'x-async': false, + }, + }, + 'x-uid': '5r2023moo61', + 'x-async': false, + }, + }, + 'x-uid': 'fjnaekdahgs', + 'x-async': false, + }, + }, + 'x-uid': '8dgjhobpef4', + 'x-async': false, + }, + }, + 'x-uid': 'qy2ht7pnx61', + 'x-async': false, + }, + }, + 'x-uid': 'vlat9iyiv2s', + 'x-async': false, + }, + }, + 'x-uid': '1mea5zyj1le', + 'x-async': false, + }, + }, + 'x-uid': 'gqvwr17tjut', + 'x-async': false, + }, + }, + 'x-uid': 'x3hjuulm454', + 'x-async': false, + }, + }, + 'x-uid': 'jxgc0f8lxq0', + 'x-async': false, + }, + }, + 'x-uid': '2w29ujc9nsk', + 'x-async': false, + }, + }, + 'x-uid': 'usm2zydty2c', + 'x-async': false, + }, + }, + 'x-uid': 'ds4yojdxmoj', + 'x-async': true, + }, +}; +export const parentPopupRecordInSubPageTheFirstLevelIsASubpageAndTheSecondLevelIsAPopup = { + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + 'x-index': 1, + properties: { + zvj8cbqvt05: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + 'x-index': 1, + properties: { + j0zzcf3k2vc: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '0.21.0-alpha.6', + 'x-index': 1, + properties: { + pn0pgjxjlz2: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '0.21.0-alpha.6', + 'x-index': 1, + properties: { + o9zor60xpvi: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'users', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '0.21.0-alpha.6', + 'x-index': 1, + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '0.21.0-alpha.6', + 'x-index': 1, + 'x-uid': 'oz266esv0u9', + 'x-async': false, + }, + '3rr559rnt6k': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '0.21.0-alpha.6', + 'x-index': 2, + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-designer': 'TableV2.ActionColumnDesigner', + 'x-initializer': 'table:configureItemActions', + 'x-app-version': '0.21.0-alpha.6', + 'x-index': 1, + properties: { + cf7dj1iffh3: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '0.21.0-alpha.6', + 'x-index': 1, + properties: { + oealnj6s2rw: { + 'x-uid': 'cclcsigumvo', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: 'View record', + 'x-action': 'view', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:view', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'page', + danger: false, + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + 'x-index': 1, + 'x-action-context': { + dataSource: 'main', + collection: 'users', + }, + 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-index': 1, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + 'x-index': 1, + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Details")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + 'x-index': 1, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:common:addBlock', + 'x-index': 1, + properties: { + o3az953k64s: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + '09c7dcvslhi': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + xv84k1hns7x: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'users', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + h6qhcp7es4z: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-action': 'create', + 'x-acl-action': 'create', + title: "{{t('Add new')}}", + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:addNew', + 'x-component': 'Action', + 'x-decorator': 'ACLActionProvider', + 'x-component-props': { + openMode: 'drawer', + type: 'primary', + component: 'CreateRecordAction', + icon: 'PlusOutlined', + }, + 'x-align': 'right', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Add record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + 'x-initializer-props': { + gridInitializer: + 'popup:addNew:addBlock', + }, + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Add new")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': + 'popup:addNew:addBlock', + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + '84oe9zbk4i3': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': + '1.2.7-alpha', + 'x-index': 1, + properties: { + g1ye1zczjy0: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': + 'Grid.Col', + 'x-app-version': + '1.2.7-alpha', + 'x-index': 1, + properties: { + '9boifjys3jn': { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-acl-action-props': + { + skipScopeCheck: + true, + }, + 'x-acl-action': + 'users:create', + 'x-decorator': + 'FormBlockProvider', + 'x-use-decorator-props': + 'useCreateFormBlockDecoratorProps', + 'x-decorator-props': + { + dataSource: + 'main', + collection: + 'users', + }, + 'x-toolbar': + 'BlockSchemaToolbar', + 'x-settings': + 'blockSettings:createForm', + 'x-component': + 'CardItem', + 'x-app-version': + '1.2.7-alpha', + 'x-index': 1, + properties: { + rmsurarncfw: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': + 'FormV2', + 'x-use-component-props': + 'useCreateFormBlockProps', + 'x-app-version': + '1.2.7-alpha', + 'x-index': 1, + properties: { + grid: { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid', + 'x-initializer': + 'form:configureFields', + 'x-app-version': + '1.2.7-alpha', + 'x-index': 1, + properties: + { + akmulvncf99: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid.Row', + 'x-app-version': + '1.2.7-alpha', + 'x-index': 1, + properties: + { + nmmv2oujq4y: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid.Col', + 'x-app-version': + '1.2.7-alpha', + 'x-index': 1, + properties: + { + nickname: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'string', + 'x-toolbar': + 'FormItemSchemaToolbar', + 'x-settings': + 'fieldSettings:FormItem', + 'x-component': + 'CollectionField', + 'x-decorator': + 'FormItem', + 'x-collection-field': + 'users.nickname', + 'x-component-props': + {}, + 'x-app-version': + '1.2.7-alpha', + 'x-index': 1, + 'x-uid': + 'qwihrljz4ig', + 'x-async': + false, + }, + }, + 'x-uid': + 'cw7ujbu7kvb', + 'x-async': + false, + }, + }, + 'x-uid': + 'craxy98y2zx', + 'x-async': + false, + }, + }, + 'x-uid': + 'zgchm9fihtk', + 'x-async': + false, + }, + '6b7iqambrl7': + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-initializer': + 'createForm:configureActions', + 'x-component': + 'ActionBar', + 'x-component-props': + { + layout: + 'one-column', + style: + { + marginTop: + 'var(--nb-spacing)', + }, + }, + 'x-app-version': + '1.2.7-alpha', + 'x-index': 2, + 'x-uid': + 'fvzf6110ke8', + 'x-async': + false, + }, + }, + 'x-uid': + '83w04j8tqut', + 'x-async': + false, + }, + }, + 'x-uid': + 'dk69v1igksh', + 'x-async': false, + }, + }, + 'x-uid': '31mvbq2kmar', + 'x-async': false, + }, + }, + 'x-uid': '9h75xux5p9l', + 'x-async': false, + }, + }, + 'x-uid': 'y28yooyjmsd', + 'x-async': false, + }, + }, + 'x-uid': 'ux1anvsf7cl', + 'x-async': false, + }, + }, + 'x-uid': 'alki9oi1nr1', + 'x-async': false, + }, + }, + 'x-uid': 'wki8c7a8rwj', + 'x-async': false, + }, + }, + 'x-uid': 'iobdqxecbbx', + 'x-async': false, + }, + }, + 'x-uid': '0johya9mys9', + 'x-async': false, + }, + gjshpj169w3: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.2.7-alpha', + 'x-index': 2, + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + '6s1te9svg45': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.2.7-alpha', + 'x-index': 1, + properties: { + v2uyliqmu3s: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: 'View in popup', + 'x-action': 'view', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:view', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'drawer', + iconColor: '#1677FF', + danger: false, + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + 'x-index': 1, + 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-index': 1, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + 'x-index': 1, + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Details")}}', + 'x-component': + 'Tabs.TabPane', + 'x-designer': + 'Tabs.Designer', + 'x-component-props': {}, + 'x-index': 1, + properties: { + grid: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': + 'popup:common:addBlock', + 'x-index': 1, + 'x-uid': '8noqw3h04eq', + 'x-async': false, + }, + }, + 'x-uid': 'ylw2eo9rt8e', + 'x-async': false, + }, + }, + 'x-uid': 'bvperanp4fp', + 'x-async': false, + }, + }, + 'x-uid': 'viw4us41at0', + 'x-async': false, + }, + }, + 'x-uid': 'vlp312vjopp', + 'x-async': false, + }, + }, + 'x-uid': 'yzq33vwz0tg', + 'x-async': false, + }, + }, + 'x-uid': '7e3t95j7ts5', + 'x-async': false, + }, + }, + 'x-uid': '2n0gerupo20', + 'x-async': false, + }, + }, + 'x-uid': 'tqik6e5h7k4', + 'x-async': false, + }, + }, + 'x-uid': 'mrl8hno9wqu', + 'x-async': false, + }, + }, + 'x-uid': 'vjh64ro97uh', + 'x-async': false, + }, + }, + 'x-uid': 'hbavbxd32qk', + 'x-async': false, + }, + }, + 'x-uid': 'xd58013ua2y', + 'x-async': false, + }, + }, + 'x-uid': 'h5vyd61upnk', + 'x-async': false, + }, + }, + 'x-uid': 'txy45vnnj48', + 'x-async': false, + }, + }, + 'x-async': false, + }, + }, + 'x-uid': 'jheggi5va13', + 'x-async': false, + }, + }, + 'x-uid': 'g6d0rjs4tnm', + 'x-async': false, + }, + }, + 'x-uid': 'wzoo17xq36h', + 'x-async': false, + }, + }, + 'x-uid': 'x7wly65os5m', + 'x-async': false, + }, + }, + 'x-uid': 'lnx96j64sjz', + 'x-async': false, + }, + }, + 'x-uid': 'qa35qwbuh0o', + 'x-async': false, + }, + }, + 'x-uid': 'jq4pfrlja72', + 'x-async': false, + }, + }, + 'x-uid': '43za15mtl55', + 'x-async': true, + }, +}; +export const currentPopupRecordInPopupThatOpenedByAssociationField = { + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + properties: { + '7v3jq0c7ci8': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: { + sg5huq1g79r: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.11-alpha', + properties: { + xjdcefk975q: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.11-alpha', + properties: { + kjak3j6azmi: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'users', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.2.11-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.2.11-alpha', + 'x-uid': 'jhmatcdkyge', + 'x-async': false, + 'x-index': 1, + }, + tahnpoowd47: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.2.11-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.2.11-alpha', + properties: { + d2rkmk6o37z: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.2.11-alpha', + 'x-uid': '9vkwc13th9o', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ya1toa6tq7u', + 'x-async': false, + 'x-index': 1, + }, + pk40atz013x: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + 'x-app-version': '1.2.11-alpha', + properties: { + roles: { + 'x-uid': 'tbo1jb8yh9l', + _isJSONSchemaObject: true, + version: '2.0', + 'x-collection-field': 'users.roles', + 'x-component': 'CollectionField', + 'x-component-props': { + fieldNames: { + label: 'title', + value: 'name', + }, + ellipsis: true, + size: 'small', + }, + 'x-read-pretty': true, + 'x-decorator': null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-app-version': '1.2.11-alpha', + 'x-action-context': { + dataSource: 'main', + association: 'users.roles', + sourceId: 1, + }, + properties: { + '9qgmmodwbrm': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("View record") }}', + 'x-component': 'AssociationField.Viewer', + 'x-component-props': { + className: 'nb-action-popup', + }, + 'x-index': 1, + 'x-app-version': '1.2.11-alpha', + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + 'x-app-version': '1.2.11-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.2.11-alpha', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:common:addBlock', + 'x-app-version': '1.2.11-alpha', + properties: { + hlui5td2mzx: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.11-alpha', + properties: { + qslzbc2i4g7: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.11-alpha', + properties: { + qmkisnspbcn: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action': 'users.roles:get', + 'x-decorator': 'DetailsBlockProvider', + 'x-use-decorator-props': 'useDetailsDecoratorProps', + 'x-decorator-props': { + dataSource: 'main', + association: 'users.roles', + readPretty: true, + action: 'get', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:details', + 'x-component': 'CardItem', + 'x-is-current': true, + 'x-app-version': '1.2.11-alpha', + properties: { + h7bydxoz048: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Details', + 'x-read-pretty': true, + 'x-use-component-props': 'useDetailsProps', + 'x-app-version': '1.2.11-alpha', + properties: { + '5p2nc8tujzi': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'details:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 24, + }, + }, + 'x-app-version': '1.2.11-alpha', + properties: { + eonkpnfwuje: { + _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', + association: 'users.roles', + sourceId: 1, + }, + 'x-decorator': 'ACLActionProvider', + 'x-app-version': '1.2.11-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.2.11-alpha', + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + 'x-app-version': '1.2.11-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.2.11-alpha', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': + 'popup:common:addBlock', + 'x-app-version': '1.2.11-alpha', + properties: { + vpuoragajl4: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': + '1.2.11-alpha', + properties: { + dw0bbrtfwam: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': + 'Grid.Col', + 'x-app-version': + '1.2.11-alpha', + properties: { + '93be63wygk1': { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-acl-action-props': + { + skipScopeCheck: + true, + }, + 'x-acl-action': + 'users:create', + 'x-decorator': + 'FormBlockProvider', + 'x-use-decorator-props': + 'useCreateFormBlockDecoratorProps', + 'x-decorator-props': + { + dataSource: + 'main', + collection: + 'users', + isCusomeizeCreate: + true, + }, + 'x-toolbar': + 'BlockSchemaToolbar', + 'x-settings': + 'blockSettings:createForm', + 'x-component': + 'CardItem', + 'x-app-version': + '1.2.11-alpha', + properties: { + rgnp5z1guas: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': + 'FormV2', + 'x-use-component-props': + 'useCreateFormBlockProps', + 'x-app-version': + '1.2.11-alpha', + properties: { + grid: { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid', + 'x-initializer': + 'form:configureFields', + 'x-app-version': + '1.2.11-alpha', + properties: + { + edldb7vjp9h: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid.Row', + 'x-app-version': + '1.2.11-alpha', + properties: + { + '8b29nba6lwu': + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid.Col', + 'x-app-version': + '1.2.11-alpha', + properties: + { + nickname: + { + 'x-uid': + '3efbg7ev85y', + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'string', + 'x-toolbar': + 'FormItemSchemaToolbar', + 'x-settings': + 'fieldSettings:FormItem', + 'x-component': + 'CollectionField', + 'x-decorator': + 'FormItem', + 'x-collection-field': + 'users.nickname', + 'x-component-props': + {}, + 'x-app-version': + '1.2.11-alpha', + default: + '{{$nPopupRecord.title}}', + title: + 'Current popup record', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'vt5glvexxmy', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + '2nu8acu05bq', + 'x-async': + false, + 'x-index': 1, + }, + elank4j6ejl: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid.Row', + 'x-app-version': + '1.2.11-alpha', + properties: + { + '0vd4xfrz4ym': + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid.Col', + 'x-app-version': + '1.2.11-alpha', + properties: + { + username: + { + 'x-uid': + '5wca4h5qm30', + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'string', + 'x-toolbar': + 'FormItemSchemaToolbar', + 'x-settings': + 'fieldSettings:FormItem', + 'x-component': + 'CollectionField', + 'x-decorator': + 'FormItem', + 'x-collection-field': + 'users.username', + 'x-component-props': + {}, + 'x-app-version': + '1.2.11-alpha', + default: + '{{$nParentPopupRecord.title}}', + title: + 'Parent popup record', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 't014ef85vps', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'ey8m2ww2i22', + 'x-async': + false, + 'x-index': 2, + }, + }, + 'x-uid': + 'fbkdzitehma', + 'x-async': + false, + 'x-index': 1, + }, + hzwm7i7osp5: { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-initializer': + 'createForm:configureActions', + 'x-component': + 'ActionBar', + 'x-component-props': + { + layout: + 'one-column', + }, + 'x-app-version': + '1.2.11-alpha', + 'x-uid': + 'txhv0ztp8df', + 'x-async': + false, + 'x-index': 2, + }, + }, + 'x-uid': + 'y0n8wniikrm', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'g3xrmvnx1f2', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'esr4dmnh247', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '23ja6mm6cjn', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'qt4i6s5dbm6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'll1fml22y3k', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'jzdmyzykcjp', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ydcosha2ywu', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '613dykptvzt', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'mxawqyonxcm', + '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.2.11-alpha', + 'x-uid': 'xdsgygv5ddd', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'ctz5vwsyqxl', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'zdmrlz13iv4', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ykywzr06b0d', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'vlb6idas0u0', + 'x-async': false, + 'x-index': 1, + }, + sqob98vfvn5: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.11-alpha', + properties: { + '0tqerdtlph4': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.11-alpha', + properties: { + whrb78tisi9: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-acl-action': 'users:create', + 'x-decorator': 'FormBlockProvider', + 'x-use-decorator-props': 'useCreateFormBlockDecoratorProps', + 'x-decorator-props': { + dataSource: 'main', + collection: 'users', + isCusomeizeCreate: true, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:createForm', + 'x-component': 'CardItem', + 'x-app-version': '1.2.11-alpha', + properties: { + okwa9isvg20: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useCreateFormBlockProps', + 'x-app-version': '1.2.11-alpha', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + 'x-app-version': '1.2.11-alpha', + properties: { + w0z5w1ravav: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.11-alpha', + properties: { + hdxme12nbvn: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.11-alpha', + properties: { + nickname: { + 'x-uid': 'arbmzko8ucm', + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'users.nickname', + 'x-component-props': {}, + 'x-app-version': '1.2.11-alpha', + default: '{{$nPopupRecord.title}}', + title: 'Current popup record', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '5aa1sejg87n', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'zjxkw7sj3ju', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'xnytyxmxxhb', + 'x-async': false, + 'x-index': 1, + }, + td0a76ltry9: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'createForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + }, + 'x-app-version': '1.2.11-alpha', + 'x-uid': 'tiidq1ieutt', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '5e54dj8hxyi', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'huc0cu06cor', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '6lng7hegc6i', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '6otvy2i4idn', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '6lveg9s090p', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '0wiln39l5au', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'r9unvop3xz7', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'frj9wy0ykp9', + 'x-async': false, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'u7230hzmgaq', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '64o8pdgvvto', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'ckllg5qkcv6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'eskjoxwqfd2', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'r1j05mcyuld', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '5pw8082pcqr', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'k7jako03z9s', + 'x-async': true, + 'x-index': 1, + }, +}; diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts index ae1672481..d824e98d9 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts @@ -36,6 +36,7 @@ test.describe('where edit form block can be added', () => { await nocoPage.goto(); // 1.打开弹窗 + await page.getByRole('button', { name: '2', exact: true }).getByText('2').hover(); await page.getByRole('button', { name: '2', exact: true }).getByText('2').click(); // 2.创建一个编辑表单区块 diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaSettings.test.ts index 3a92720b3..9e0a66747 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaSettings.test.ts @@ -50,7 +50,6 @@ test.describe('edit form block schema settings', () => { // 刷新页面后,显示的应该依然是上次设置的值 await page.reload(); - await page.getByText('Edit', { exact: true }).click(); await runExpect(); }); @@ -81,7 +80,6 @@ test.describe('edit form block schema settings', () => { // 刷新页面后,设置的值应该依然存在 await page.reload(); - await page.getByText('Edit', { exact: true }).click(); await clickOption(page, 'Linkage rules'); await runExpect(); }); @@ -104,7 +102,6 @@ test.describe('edit form block schema settings', () => { // 刷新页面 await page.reload(); - await page.getByText('Edit', { exact: true }).click(); await page.getByLabel('block-item-CardItem-general-form').hover(); await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); await expect(page.getByRole('menuitem', { name: 'Save as block template' })).not.toBeVisible(); @@ -117,7 +114,6 @@ test.describe('edit form block schema settings', () => { // 刷新页面 await page.reload(); - await page.getByText('Edit', { exact: true }).click(); await page.getByLabel('block-item-CardItem-general-form').hover(); await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); await expect(page.getByRole('menuitem', { name: 'Save as block template' })).toBeVisible(); @@ -152,30 +148,28 @@ test.describe('edit form block schema settings', () => { // 刷新页面后,区块依然是被删除状态 await page.reload(); - await page.getByText('Edit', { exact: true }).click(); await expect(page.getByLabel('block-item-CardItem-general-form')).not.toBeVisible(); }); // https://nocobase.height.app/T-3825 test('Unsaved changes warning display', async ({ page, mockPage, mockRecord }) => { await mockPage(T3825).goto(); await mockRecord('general', { number: 9, formula: 10 }); - await expect(await page.getByLabel('block-item-CardItem-general-')).toBeVisible(); + await expect(page.getByLabel('block-item-CardItem-general-')).toBeVisible(); //没有改动时不显示提示 - await page.getByLabel('action-Action.Link-Edit-').click(); + await page.getByLabel('action-Action.Link-Edit record-update-general-table-').click(); await page.getByLabel('drawer-Action.Container-general-Edit record-mask').click(); - await expect(await page.getByLabel('action-Action-Add new-create-')).toBeVisible(); + await expect(page.getByLabel('action-Action-Add new-create-')).toBeVisible(); //有改动时显示提示 - await page.getByLabel('action-Action.Link-Edit-').click(); + await page.getByLabel('action-Action.Link-Edit record-update-general-table-').click(); await page.getByRole('spinbutton').fill(''); await page.getByRole('spinbutton').fill('10'); await expect( - await page + page .getByLabel('block-item-CollectionField-general-form-general.formula-formula') - .locator('.nb-read-pretty-input-number') - .innerText(), - ).toBe('11'); + .locator('.nb-read-pretty-input-number'), + ).toHaveText('11'); await page.getByLabel('drawer-Action.Container-general-Edit record-mask').click(); - await expect(await page.getByText('Unsaved changes')).toBeVisible(); + await expect(page.getByText('Unsaved changes')).toBeVisible(); }); }); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/templatesOfBug.ts index 65bfee817..b41e4fe94 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/templatesOfBug.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/templatesOfBug.ts @@ -426,7 +426,7 @@ export const T3825: PageConfig = { _isJSONSchemaObject: true, version: '2.0', type: 'void', - title: '{{ t("Edit") }}', + title: '{{ t("Edit record") }}', 'x-action': 'update', 'x-toolbar': 'ActionSchemaToolbar', 'x-settings': 'actionSettings:edit', diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/TableActionColumnInitializers.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/TableActionColumnInitializers.tsx index 609960c42..ab94c2edb 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/TableActionColumnInitializers.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/TableActionColumnInitializers.tsx @@ -16,7 +16,6 @@ import { useAPIClient } from '../../../../api-client'; import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer'; import { SchemaInitializerActionModal } from '../../../../application/schema-initializer/components/SchemaInitializerActionModal'; import { SchemaInitializerItem } from '../../../../application/schema-initializer/components/SchemaInitializerItem'; -import { useSchemaInitializer } from '../../../../application/schema-initializer/context'; import { useCollection_deprecated } from '../../../../collection-manager'; import { SelectWithTitle } from '../../../../common/SelectWithTitle'; import { useDataBlockProps } from '../../../../data-source'; diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts index 2e6e266bb..e1b2437b4 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts @@ -17,7 +17,13 @@ import { oneTableBlockWithAddNewAndViewAndEditAndBasicFields, test, } from '@nocobase/test/e2e'; -import { T3843, oneTableWithColumnFixed, oneTableWithUpdateRecord } from './templatesOfBug'; +import { + T3843, + oneTableWithColumnFixed, + oneTableWithUpdateRecord, + testingOfOpenModeForAddChild, + testingWithPageMode, +} from './templatesOfBug'; const addSomeCustomActions = async (page: Page) => { // 先删除掉之前的 actions @@ -50,7 +56,7 @@ test.describe('actions schema settings', () => { await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-general' }).hover(); }; - test('supported options', async ({ page, mockPage, mockRecord }) => { + test('supported options', async ({ page, mockPage }) => { await mockPage(oneEmptyTableBlockWithActions).goto(); await expectSettingsMenu({ @@ -60,7 +66,7 @@ test.describe('actions schema settings', () => { }); }); - test('edit button', async ({ page, mockPage, mockRecord }) => { + test('edit button', async ({ page, mockPage }) => { await mockPage(oneEmptyTableBlockWithActions).goto(); await showMenu(page); @@ -72,7 +78,7 @@ test.describe('actions schema settings', () => { await expect(page.getByRole('button', { name: '1234' })).toBeVisible(); }); - test('open mode', async ({ page, mockPage, mockRecord }) => { + test('open mode', async ({ page, mockPage }) => { await mockPage(oneEmptyTableBlockWithActions).goto(); await showMenu(page); @@ -85,9 +91,45 @@ test.describe('actions schema settings', () => { await page.getByRole('button', { name: 'Add new' }).click(); await expect(page.getByTestId('modal-Action.Container-general-Add record')).toBeVisible(); + await page.getByLabel('modal-Action.Container-general-Add record-mask').click(); + + // 切换为 page + await page.getByLabel('action-Action-Add new-create-').hover(); + await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-general' }).hover(); + await page.getByRole('menuitem', { name: 'Open mode Dialog' }).click(); + await page.getByRole('option', { name: 'Page' }).click(); + + // 点击按钮后会跳转到一个页面 + await page.getByLabel('action-Action-Add new-create-').click(); + expect(page.url()).toContain('/subpages/'); + + // 配置出一个表单 + await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.getByRole('menuitem', { name: 'form Form right' }).hover(); + await page.getByRole('menuitem', { name: 'Current collection' }).click(); + + await page.getByLabel('schema-initializer-Grid-form:').hover(); + await page.getByRole('menuitem', { name: 'Single select' }).click(); + await page.mouse.move(300, 0); + + await page.getByLabel('schema-initializer-ActionBar-').hover(); + await page.getByRole('menuitem', { name: 'Submit' }).click(); + + // 创建一条数据后返回,列表中应该有这条数据 + await page.getByTestId('select-single').click(); + await page.getByRole('option', { name: 'option3' }).click(); + + await page.getByLabel('action-Action-Submit-submit-').click(); + + await page.goBack(); + + await page.getByLabel('schema-initializer-TableV2-').hover(); + await page.getByRole('menuitem', { name: 'Single select' }).click(); + await page.mouse.move(300, 0); + await expect(page.getByRole('button', { name: 'option3' })).toHaveCount(1); }); - test('popup size', async ({ page, mockPage, mockRecord }) => { + test('popup size', async ({ page, mockPage }) => { await mockPage(oneEmptyTableBlockWithActions).goto(); await showMenu(page); @@ -116,7 +158,7 @@ test.describe('actions schema settings', () => { expect(drawerWidth2).toBeGreaterThanOrEqual(800); }); - test('delete', async ({ page, mockPage, mockRecord }) => { + test('delete', async ({ page, mockPage }) => { await mockPage(oneEmptyTableBlockWithActions).goto(); await showMenu(page); @@ -149,7 +191,7 @@ test.describe('actions schema settings', () => { await page.getByLabel('designer-schema-settings-Filter.Action-Filter.Action.Designer-general').hover(); }; - test('supported options', async ({ page, mockPage, mockRecord }) => { + test('supported options', async ({ page, mockPage }) => { await mockPage(oneEmptyTableBlockWithActions).goto(); await expectSettingsMenu({ @@ -316,6 +358,390 @@ test.describe('actions schema settings', () => { '', ); }); + + test('open mode: page', async ({ page, mockPage }) => { + await mockPage(testingWithPageMode).goto(); + + // 打开弹窗 + await page.getByLabel('action-Action.Link-View').click(); + + // 详情区块 + await expect( + page + .getByLabel('block-item-CardItem-users-details') + .getByLabel('block-item-CollectionField-users-details-users.nickname-Nickname'), + ).toHaveText('Nickname:Super Admin'); + + // 关系区块 + await expect( + page + .getByLabel('block-item-CardItem-roles-details') + .getByLabel('block-item-CollectionField-roles-details-roles.name-Role UID'), + ).toHaveText('Role UID:admin'); + + // 使用变量 `Current role` 设置默认值 + await expect(page.getByLabel('block-item-CardItem-roles-form').getByRole('textbox')).toHaveValue('root'); + + // 使用变量 `Current popup record` 设置默认值 + await expect(page.getByLabel('block-item-CardItem-users-form').getByRole('textbox')).toHaveValue('Super Admin'); + + // ----------------------------------------------------------------------------------------------- + + // 打开嵌套弹窗 + await page.getByLabel('action-Action.Link-View role-view-roles-table-admin').click(); + + // 详情区块 + await expect(page.getByLabel('block-item-CollectionField-roles-details-roles.title-Role name')).toHaveText( + 'Role name:Admin', + ); + + // 使用变量 `Parent popup record` 设置数据范围 + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-roles-table') + .getByRole('button', { name: 'Admin', exact: true }), + ).toBeVisible(); + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-roles-table') + .getByRole('button', { name: 'Member', exact: true }), + ).toBeVisible(); + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-roles-table') + .getByRole('button', { name: 'Root', exact: true }), + ).toBeVisible(); + + // 使用变量 `Current popup record` 和 `Parent popup record` 设置默认值 + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-users-form') + .getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname') + .getByRole('textbox'), + ).toHaveValue('admin'); + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-users-form') + .getByLabel('block-item-CollectionField-users-form-users.username-Username') + .getByRole('textbox'), + ).toHaveValue('nocobase'); + + // ----------------------------------------------------------------------------------- + + // 关闭嵌套弹窗后,再点击不同的行打开嵌套弹窗,应该显示不同的数据 + await page.getByLabel('drawer-Action.Container-roles-View record-mask').click(); + await page.getByLabel('action-Action.Link-View role-view-roles-table-member').click(); + + // 详情区块 + await expect(page.getByLabel('block-item-CollectionField-roles-details-roles.title-Role name')).toHaveText( + 'Role name:Member', + ); + + // 使用变量 `Parent popup record` 设置数据范围 + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-roles-table') + .getByRole('button', { name: 'Admin', exact: true }), + ).toBeVisible(); + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-roles-table') + .getByRole('button', { name: 'Member', exact: true }), + ).toBeVisible(); + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-roles-table') + .getByRole('button', { name: 'Root', exact: true }), + ).toBeVisible(); + + // 使用变量 `Current popup record` 和 `Parent popup record` 设置默认值 + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-users-form') + .getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname') + .getByRole('textbox'), + ).toHaveValue('member'); + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-users-form') + .getByLabel('block-item-CollectionField-users-form-users.username-Username') + .getByRole('textbox'), + ).toHaveValue('nocobase'); + + // ----------------------------------------------------------------------------------- + + // 刷新页面后,弹窗中的内容不变 + await page.reload(); + + // 详情区块 + await expect(page.getByLabel('block-item-CollectionField-roles-details-roles.title-Role name')).toHaveText( + 'Role name:Member', + ); + + // 使用变量 `Parent popup record` 设置数据范围 + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-roles-table') + .getByRole('button', { name: 'Admin', exact: true }), + ).toBeVisible(); + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-roles-table') + .getByRole('button', { name: 'Member', exact: true }), + ).toBeVisible(); + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-roles-table') + .getByRole('button', { name: 'Root', exact: true }), + ).toBeVisible(); + + // 使用变量 `Current popup record` 和 `Parent popup record` 设置默认值 + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-users-form') + .getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname') + .getByRole('textbox'), + ).toHaveValue('member'); + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-users-form') + .getByLabel('block-item-CollectionField-users-form-users.username-Username') + .getByRole('textbox'), + ).toHaveValue('nocobase'); + + // ----------------------------------------------------------------------------------- + + // 关闭弹窗,然后将 Open mode 调整为 page + await page.getByLabel('drawer-Action.Container-roles-View record-mask').click(); + await page.locator('.ant-drawer-mask').click(); + + await page.getByLabel('action-Action.Link-View').hover(); + await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:view-users').hover(); + await page.getByRole('menuitem', { name: 'Open mode Drawer' }).click(); + await page.getByRole('option', { name: 'Page' }).click(); + + // 跳转到子页面后,其内容应该和弹窗中的内容一致 + await page.getByLabel('action-Action.Link-View').click(); + expect(page.url()).toContain('/subpages'); + + // 详情区块 + await expect( + page + .getByLabel('block-item-CardItem-users-details') + .getByLabel('block-item-CollectionField-users-details-users.nickname-Nickname'), + ).toHaveText('Nickname:Super Admin'); + + // 关系区块 + await expect( + page + .getByLabel('block-item-CardItem-roles-details') + .getByLabel('block-item-CollectionField-roles-details-roles.name-Role UID'), + ).toHaveText('Role UID:admin'); + + // 使用变量 `Current role` 设置默认值 + await expect(page.getByLabel('block-item-CardItem-roles-form').getByRole('textbox')).toHaveValue('root'); + + // 使用变量 `Current popup record` 设置默认值 + await expect(page.getByLabel('block-item-CardItem-users-form').getByRole('textbox')).toHaveValue('Super Admin'); + + // --------------------------------------------------------------------------------------------------------------- + + // 打开子页面中的弹窗 + await page.getByLabel('action-Action.Link-View role-view-roles-table-member').click(); + + // 详情区块 + await expect(page.getByLabel('block-item-CollectionField-roles-details-roles.title-Role name')).toHaveText( + 'Role name:Member', + ); + + // 使用变量 `Parent popup record` 设置数据范围 + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-roles-table') + .getByRole('button', { name: 'Admin', exact: true }), + ).toBeVisible(); + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-roles-table') + .getByRole('button', { name: 'Member', exact: true }), + ).toBeVisible(); + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-roles-table') + .getByRole('button', { name: 'Root', exact: true }), + ).toBeVisible(); + + // 使用变量 `Current popup record` 和 `Parent popup record` 设置默认值 + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-users-form') + .getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname') + .getByRole('textbox'), + ).toHaveValue('member'); + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-users-form') + .getByLabel('block-item-CollectionField-users-form-users.username-Username') + .getByRole('textbox'), + ).toHaveValue('nocobase'); + + // ----------------------------------------------------------------------------------- + + // 关闭弹窗后,重新选择一条不同的数据打开,应该显示不同的数据 + await page.getByLabel('drawer-Action.Container-roles-View record-mask').click(); + await page.getByLabel('action-Action.Link-View role-view-roles-table-admin').click(); + + // 详情区块 + await expect(page.getByLabel('block-item-CollectionField-roles-details-roles.title-Role name')).toHaveText( + 'Role name:Admin', + ); + + // 使用变量 `Parent popup record` 设置数据范围 + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-roles-table') + .getByRole('button', { name: 'Admin', exact: true }), + ).toBeVisible(); + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-roles-table') + .getByRole('button', { name: 'Member', exact: true }), + ).toBeVisible(); + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-roles-table') + .getByRole('button', { name: 'Root', exact: true }), + ).toBeVisible(); + + // 使用变量 `Current popup record` 和 `Parent popup record` 设置默认值 + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-users-form') + .getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname') + .getByRole('textbox'), + ).toHaveValue('admin'); + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-users-form') + .getByLabel('block-item-CollectionField-users-form-users.username-Username') + .getByRole('textbox'), + ).toHaveValue('nocobase'); + + // ----------------------------------------------------------------------------------- + + // 刷新页面后,弹窗中的内容不变 + await page.reload(); + + // 详情区块 + await expect(page.getByLabel('block-item-CollectionField-roles-details-roles.title-Role name')).toHaveText( + 'Role name:Admin', + ); + + // 使用变量 `Parent popup record` 设置数据范围 + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-roles-table') + .getByRole('button', { name: 'Admin', exact: true }), + ).toBeVisible(); + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-roles-table') + .getByRole('button', { name: 'Member', exact: true }), + ).toBeVisible(); + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-roles-table') + .getByRole('button', { name: 'Root', exact: true }), + ).toBeVisible(); + + // 使用变量 `Current popup record` 和 `Parent popup record` 设置默认值 + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-users-form') + .getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname') + .getByRole('textbox'), + ).toHaveValue('admin'); + await expect( + page + .getByTestId('drawer-Action.Container-roles-View record') + .getByLabel('block-item-CardItem-users-form') + .getByLabel('block-item-CollectionField-users-form-users.username-Username') + .getByRole('textbox'), + ).toHaveValue('nocobase'); + + // ----------------------------------------------------------------------------------- + + // 关闭弹窗后,将子页面中的 Open mode 调整为 page + await page.goBack(); + await page.getByLabel('action-Action.Link-View role-view-roles-table-admin').hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:view-roles' }) + .hover(); + await page.getByRole('menuitem', { name: 'Open mode Drawer' }).click(); + await page.getByRole('option', { name: 'Page' }).click(); + + // 点击按钮跳转到子页面 + await page.getByLabel('action-Action.Link-View role-view-roles-table-admin').click(); + + // 详情区块 + await expect(page.getByLabel('block-item-CollectionField-roles-details-roles.title-Role name')).toHaveText( + 'Role name:Admin', + ); + + // 使用变量 `Parent popup record` 设置数据范围 + await expect( + page.getByLabel('block-item-CardItem-roles-table').getByRole('button', { name: 'Admin', exact: true }), + ).toBeVisible(); + await expect( + page.getByLabel('block-item-CardItem-roles-table').getByRole('button', { name: 'Member', exact: true }), + ).toBeVisible(); + await expect( + page.getByLabel('block-item-CardItem-roles-table').getByRole('button', { name: 'Root', exact: true }), + ).toBeVisible(); + + // 使用变量 `Current popup record` 和 `Parent popup record` 设置默认值 + await expect( + page + .getByLabel('block-item-CardItem-users-form') + .getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname') + .getByRole('textbox'), + ).toHaveValue('admin'); + await expect( + page + .getByLabel('block-item-CardItem-users-form') + .getByLabel('block-item-CollectionField-users-form-users.username-Username') + .getByRole('textbox'), + ).toHaveValue('nocobase'); + }); }); test.describe('popup', () => { @@ -459,7 +885,7 @@ test.describe('actions schema settings', () => { await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:addChild-tree').hover(); }; - test('supported options', async ({ page, mockPage, mockRecord }) => { + test('supported options', async ({ page, mockPage }) => { const nocoPage = await mockPage(oneEmptyTableWithTreeCollection).waitForInit(); await nocoPage.goto(); await page.getByLabel('block-item-CardItem-treeCollection-table').hover(); @@ -511,6 +937,35 @@ test.describe('actions schema settings', () => { .getByText('1', { exact: true }), ).toBeVisible(); }); + + test('open mode', async ({ page, mockPage }) => { + const nocoPage = await mockPage(testingOfOpenModeForAddChild).waitForInit(); + await nocoPage.goto(); + + // add a record + await page.getByLabel('action-Action-Add new-create-').click(); + await page.getByLabel('action-Action-Submit-submit-').click(); + + // open popup with drawer mode + await page.getByLabel('action-Action.Link-Add child-').click(); + await expect(page.getByTestId('select-object-single')).toHaveText('1'); + + await page.reload(); + await expect(page.getByTestId('select-object-single')).toHaveText('1'); + + await page.goBack(); + await page.getByLabel('action-Action.Link-Add child-').hover(); + await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:addChild-treeCollection').hover(); + await page.getByRole('menuitem', { name: 'Open mode Drawer' }).click(); + await page.getByRole('option', { name: 'Page' }).click(); + + // open popup with page mode + await page.getByLabel('action-Action.Link-Add child-').click(); + await expect(page.getByTestId('select-object-single')).toHaveText('1'); + + await page.reload(); + await expect(page.getByTestId('select-object-single')).toHaveText('1'); + }); }); test.describe('add record', () => { @@ -519,7 +974,7 @@ test.describe('actions schema settings', () => { await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-general' }).hover(); }; - test('supported options', async ({ page, mockPage, mockRecord }) => { + test('supported options', async ({ page, mockPage }) => { await mockPage(oneEmptyTableBlockWithActions).goto(); await expectSettingsMenu({ diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/templatesOfBug.ts index cbddd24da..c445d536a 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/templatesOfBug.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/templatesOfBug.ts @@ -7,7 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { PageConfig } from '@nocobase/test/e2e'; +import { PageConfig, tree } from '@nocobase/test/e2e'; export const T2183 = { pageSchema: { @@ -3830,3 +3830,2530 @@ export const T4334 = { }, }, }; +export const testingWithPageMode = { + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + properties: { + '4hkzwf7xiwx': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: { + '8876jkmbc0j': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.8-alpha', + properties: { + '8hg0vhy0tz0': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.8-alpha', + properties: { + ejbnvzcacpb: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'users', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.2.8-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.2.8-alpha', + 'x-uid': 'hb16vet3e9h', + 'x-async': false, + 'x-index': 1, + }, + b1fkx7l3lqk: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.2.8-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.2.8-alpha', + properties: { + '9d3q7flbesh': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.2.8-alpha', + properties: { + md90hk6dgga: { + 'x-uid': 'ud03p691p2i', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: 'View record', + 'x-action': 'view', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:view', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'drawer', + iconColor: '#1677FF', + danger: false, + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("View record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Details")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:common:addBlock', + properties: { + sl3cha2cvm9: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.8-alpha', + properties: { + efcxs5deb11: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.8-alpha', + properties: { + cq3c4x58uv8: { + 'x-uid': '48ndgvjusx0', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action': 'users:get', + 'x-decorator': 'DetailsBlockProvider', + 'x-use-decorator-props': 'useDetailsDecoratorProps', + 'x-decorator-props': { + dataSource: 'main', + collection: 'users', + readPretty: true, + action: 'get', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:details', + 'x-component': 'CardItem', + 'x-app-version': '1.2.8-alpha', + 'x-component-props': { + title: 'Details', + }, + properties: { + qzudlhn0oci: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Details', + 'x-read-pretty': true, + 'x-use-component-props': 'useDetailsProps', + 'x-app-version': '1.2.8-alpha', + properties: { + hhuvprt9qkx: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'details:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 24, + }, + }, + 'x-app-version': '1.2.8-alpha', + 'x-uid': 't532hppuhhd', + '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.2.8-alpha', + properties: { + rcqh876sdp3: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.8-alpha', + properties: { + '399yhxj2n3k': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.8-alpha', + properties: { + nickname: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': + 'FormItemSchemaToolbar', + 'x-settings': + 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': + 'users.nickname', + 'x-component-props': {}, + 'x-app-version': '1.2.8-alpha', + 'x-uid': '0jxfoc7hndh', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '4qljzi4ndyv', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ne81cyfsbjb', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'hk6i6c12h3i', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'ztg6637y1ne', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '9adrj3pcevi', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'cl10ahmo9by', + 'x-async': false, + 'x-index': 1, + }, + ldd6v9tyucb: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.8-alpha', + properties: { + pstw5adsq5o: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.8-alpha', + properties: { + zz234wka6vz: { + 'x-uid': 'te1we7isnhz', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action': 'users.roles:view', + 'x-decorator': 'DetailsBlockProvider', + 'x-use-decorator-props': + 'useDetailsWithPaginationDecoratorProps', + 'x-decorator-props': { + dataSource: 'main', + association: 'users.roles', + readPretty: true, + action: 'list', + params: { + pageSize: 1, + }, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:detailsWithPagination', + 'x-component': 'CardItem', + 'x-app-version': '1.2.8-alpha', + 'x-component-props': { + title: 'Association block', + }, + properties: { + l9udri1zsv7: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Details', + 'x-read-pretty': true, + 'x-use-component-props': + 'useDetailsWithPaginationProps', + 'x-app-version': '1.2.8-alpha', + properties: { + grvze9sdawq: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'details:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 24, + }, + }, + 'x-app-version': '1.2.8-alpha', + 'x-uid': 'wk0gfg70k0c', + '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.2.8-alpha', + properties: { + v8bvtnoja3x: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.8-alpha', + properties: { + e2vli230fql: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.8-alpha', + properties: { + name: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': + 'FormItemSchemaToolbar', + 'x-settings': + 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'roles.name', + 'x-component-props': {}, + 'x-app-version': '1.2.8-alpha', + 'x-uid': 'leeamsjfgqn', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'usgby9v925w', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '69c6wchsh5b', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'cfbf2ty6rhz', + 'x-async': false, + 'x-index': 2, + }, + pagination: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Pagination', + 'x-use-component-props': + 'useDetailsPaginationProps', + 'x-app-version': '1.2.8-alpha', + 'x-uid': 'ap394y0ezco', + 'x-async': false, + 'x-index': 3, + }, + }, + 'x-uid': 'g7bo1djalz6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'p6pjlaeabar', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'vt1zgkhdrl8', + 'x-async': false, + 'x-index': 2, + }, + row_e36fkny8odp: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-index': 3, + properties: { + qmc56j0ey82: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + xkykwl13upy: { + 'x-uid': 'witajgm9436', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-settings': 'blockSettings:markdown', + 'x-decorator': 'CardItem', + 'x-decorator-props': { + name: 'markdown', + }, + 'x-component': 'Markdown.Void', + 'x-editable': false, + 'x-component-props': { + content: + '--- \n\nThe following blocks all use variables.', + }, + 'x-app-version': '1.2.8-alpha', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'u9slxyqyjva', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'c15ckwf9bfh', + 'x-async': false, + }, + '7ek9blpx7sw': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.8-alpha', + properties: { + s3djmoa1kdm: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.8-alpha', + properties: { + t4yomnfik7v: { + 'x-uid': '2ntepquubp8', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-acl-action': 'users.roles:create', + 'x-decorator': 'FormBlockProvider', + 'x-use-decorator-props': + 'useCreateFormBlockDecoratorProps', + 'x-decorator-props': { + dataSource: 'main', + association: 'users.roles', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:createForm', + 'x-component': 'CardItem', + 'x-app-version': '1.2.8-alpha', + 'x-component-props': { + title: 'Variable: Current role', + }, + properties: { + gpegyxxlg9s: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useCreateFormBlockProps', + 'x-app-version': '1.2.8-alpha', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + 'x-app-version': '1.2.8-alpha', + properties: { + '8rcj0xfttuu': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.8-alpha', + properties: { + '0v4kqye0ibq': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.8-alpha', + properties: { + title: { + 'x-uid': 'u2s2tzdej9l', + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': + 'FormItemSchemaToolbar', + 'x-settings': + 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'roles.title', + 'x-component-props': {}, + 'x-app-version': '1.2.8-alpha', + default: '{{$nRole}}', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'jjw69unhi73', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'dos9kkrtudw', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '87bd6es8q0u', + 'x-async': false, + 'x-index': 1, + }, + '27pckgxgpfj': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'createForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + style: { + marginTop: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.2.8-alpha', + 'x-uid': 'dkfshq47o8i', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '4gz0unx7rya', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'wr8env9e30j', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'mcmyeg3147y', + 'x-async': false, + 'x-index': 4, + }, + rjt3znbrf3q: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.8-alpha', + properties: { + '4uxjny23hm5': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.8-alpha', + properties: { + e93x4mhkui9: { + 'x-uid': '1ppi52hawn4', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-acl-action': 'users:create', + 'x-decorator': 'FormBlockProvider', + 'x-use-decorator-props': + 'useCreateFormBlockDecoratorProps', + 'x-decorator-props': { + dataSource: 'main', + collection: 'users', + isCusomeizeCreate: true, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:createForm', + 'x-component': 'CardItem', + 'x-app-version': '1.2.8-alpha', + 'x-component-props': { + title: 'Variable: Current popup record', + }, + properties: { + szglhz28rug: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useCreateFormBlockProps', + 'x-app-version': '1.2.8-alpha', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + 'x-app-version': '1.2.8-alpha', + properties: { + j6yhv8x9v6m: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.8-alpha', + properties: { + '0vb82er0zyq': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.8-alpha', + properties: { + nickname: { + 'x-uid': '35o8hx997k2', + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': + 'FormItemSchemaToolbar', + 'x-settings': + 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': + 'users.nickname', + 'x-component-props': {}, + 'x-app-version': '1.2.8-alpha', + default: + '{{$nPopupRecord.nickname}}', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'lywj2998nly', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'lin5pq4bpex', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'p6x3vd3fhez', + 'x-async': false, + 'x-index': 1, + }, + dvj11mlfnah: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'createForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + style: { + marginTop: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.2.8-alpha', + 'x-uid': 'gma9mwzv4dr', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'l5a4ychqmrs', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '9168tscxqr0', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '610enru817y', + 'x-async': false, + 'x-index': 5, + }, + '6vt3eg1fdkf': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.8-alpha', + properties: { + ukqk042qbtz: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.8-alpha', + 'x-uid': 'ejjhbc28t3p', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'q3j2vgv8gfg', + 'x-async': false, + 'x-index': 6, + }, + h7pk2zbtoe2: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.8-alpha', + properties: { + '9cnceetg9e3': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.8-alpha', + properties: { + xnp9sk5bp8n: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users.roles:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + association: 'users.roles', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'name', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.2.8-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.2.8-alpha', + 'x-uid': '1q67rp6unjp', + 'x-async': false, + 'x-index': 1, + }, + msso4r8x4u3: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.2.8-alpha', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.2.8-alpha', + properties: { + vrn0nxktd4u: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.2.8-alpha', + properties: { + kpqrl0kgpyb: { + 'x-uid': 'obkauwgfgq7', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: 'View role', + 'x-action': 'view', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:view', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'drawer', + iconColor: '#1677FF', + danger: false, + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("View record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Details")}}', + 'x-component': + 'Tabs.TabPane', + 'x-designer': + 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': + 'popup:common:addBlock', + properties: { + heliwtqu3eq: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': + 'Grid.Row', + 'x-app-version': + '1.2.8-alpha', + properties: { + jbt07chbalw: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': + 'Grid.Col', + 'x-app-version': + '1.2.8-alpha', + properties: { + i437g06welg: { + 'x-uid': + 'edf63ztrbxq', + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-acl-action': + 'users.roles:get', + 'x-decorator': + 'DetailsBlockProvider', + 'x-use-decorator-props': + 'useDetailsDecoratorProps', + 'x-decorator-props': + { + dataSource: + 'main', + association: + 'users.roles', + readPretty: + true, + action: + 'get', + }, + 'x-toolbar': + 'BlockSchemaToolbar', + 'x-settings': + 'blockSettings:details', + 'x-component': + 'CardItem', + 'x-is-current': + true, + 'x-app-version': + '1.2.8-alpha', + 'x-component-props': + { + title: + 'Details', + }, + properties: + { + u0gd3xb9lu3: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Details', + 'x-read-pretty': + true, + 'x-use-component-props': + 'useDetailsProps', + 'x-app-version': + '1.2.8-alpha', + properties: + { + b8zdned1saz: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-initializer': + 'details:configureActions', + 'x-component': + 'ActionBar', + 'x-component-props': + { + style: + { + marginBottom: 24, + }, + }, + 'x-app-version': + '1.2.8-alpha', + 'x-uid': + 'plxpukolwn4', + '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.2.8-alpha', + properties: + { + mz536stgwkv: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid.Row', + 'x-app-version': + '1.2.8-alpha', + properties: + { + '8yutqqodl7q': + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid.Col', + 'x-app-version': + '1.2.8-alpha', + properties: + { + title: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'string', + 'x-toolbar': + 'FormItemSchemaToolbar', + 'x-settings': + 'fieldSettings:FormItem', + 'x-component': + 'CollectionField', + 'x-decorator': + 'FormItem', + 'x-collection-field': + 'roles.title', + 'x-component-props': + {}, + 'x-app-version': + '1.2.8-alpha', + 'x-uid': + 'oll0umtp7tn', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + '75ns4lofnq7', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'tytsnjz1iji', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'q9wcv2kt2n2', + 'x-async': + false, + 'x-index': 2, + }, + }, + 'x-uid': + 'qyzoxdhemtt', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + '1v4p33jtwx0', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'i8qn6tn2etg', + 'x-async': false, + 'x-index': 1, + }, + f0ugu63rh9c: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': + 'Grid.Row', + 'x-app-version': + '1.2.8-alpha', + properties: { + skrtxdw11vg: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': + 'Grid.Col', + 'x-app-version': + '1.2.8-alpha', + properties: { + b6wpu86agsn: { + 'x-uid': + 'fkcab93zwgk', + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-decorator': + 'TableBlockProvider', + 'x-acl-action': + 'roles:list', + 'x-use-decorator-props': + 'useTableBlockDecoratorProps', + 'x-decorator-props': + { + collection: + 'roles', + dataSource: + 'main', + action: + 'list', + params: + { + pageSize: 20, + filter: + { + $and: [ + { + name: { + $includes: + '{{$nParentPopupRecord.roles.name}}', + }, + }, + ], + }, + }, + rowKey: + 'name', + showIndex: + true, + dragSort: + false, + }, + 'x-toolbar': + 'BlockSchemaToolbar', + 'x-settings': + 'blockSettings:table', + 'x-component': + 'CardItem', + 'x-filter-targets': + [], + 'x-app-version': + '1.2.8-alpha', + 'x-component-props': + { + title: + "Use 'Parent popup recor' in data scope", + }, + properties: + { + actions: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-initializer': + 'table:configureActions', + 'x-component': + 'ActionBar', + 'x-component-props': + { + style: + { + marginBottom: + 'var(--nb-spacing)', + }, + }, + 'x-app-version': + '1.2.8-alpha', + 'x-uid': + 'n45qyt4l7p8', + 'x-async': + false, + 'x-index': 1, + }, + pld27iksvjt: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'array', + 'x-initializer': + 'table:configureColumns', + 'x-component': + 'TableV2', + 'x-use-component-props': + 'useTableBlockProps', + 'x-component-props': + { + rowKey: + 'id', + rowSelection: + { + type: 'checkbox', + }, + }, + 'x-app-version': + '1.2.8-alpha', + properties: + { + actions: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + title: + '{{ t("Actions") }}', + 'x-action-column': + 'actions', + 'x-decorator': + 'TableV2.Column.ActionBar', + 'x-component': + 'TableV2.Column', + 'x-toolbar': + 'TableColumnSchemaToolbar', + 'x-initializer': + 'table:configureItemActions', + 'x-settings': + 'fieldSettings:TableColumn', + 'x-toolbar-props': + { + initializer: + 'table:configureItemActions', + }, + 'x-app-version': + '1.2.8-alpha', + properties: + { + pgr25kbm5zw: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-decorator': + 'DndContext', + 'x-component': + 'Space', + 'x-component-props': + { + split: + '|', + }, + 'x-app-version': + '1.2.8-alpha', + 'x-uid': + 'f0cusyi0qg3', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'vg540gtmup2', + 'x-async': + false, + 'x-index': 1, + }, + '9kuhit9axbf': + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-decorator': + 'TableV2.Column.Decorator', + 'x-toolbar': + 'TableColumnSchemaToolbar', + 'x-settings': + 'fieldSettings:TableColumn', + 'x-component': + 'TableV2.Column', + 'x-app-version': + '1.2.8-alpha', + properties: + { + title: + { + _isJSONSchemaObject: + true, + version: + '2.0', + 'x-collection-field': + 'roles.title', + 'x-component': + 'CollectionField', + 'x-component-props': + { + ellipsis: + true, + }, + 'x-read-pretty': + true, + 'x-decorator': + null, + 'x-decorator-props': + { + labelStyle: + { + display: + 'none', + }, + }, + 'x-app-version': + '1.2.8-alpha', + 'x-uid': + 'w8tgvckzkgc', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'bj9txs37q50', + 'x-async': + false, + 'x-index': 2, + }, + }, + 'x-uid': + 'cfpfvp6acu3', + 'x-async': + false, + 'x-index': 2, + }, + }, + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'zxcdynph3qu', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + '0obzkof2agt', + 'x-async': false, + 'x-index': 2, + }, + '9zm7fz6qs11': { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': + 'Grid.Row', + 'x-app-version': + '1.2.8-alpha', + properties: { + xn7xjfcea4f: { + _isJSONSchemaObject: + true, + version: '2.0', + type: 'void', + 'x-component': + 'Grid.Col', + 'x-app-version': + '1.2.8-alpha', + properties: { + h8iybxqo68a: { + 'x-uid': + 'ybylt4pirzc', + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-acl-action-props': + { + skipScopeCheck: + true, + }, + 'x-acl-action': + 'users:create', + 'x-decorator': + 'FormBlockProvider', + 'x-use-decorator-props': + 'useCreateFormBlockDecoratorProps', + 'x-decorator-props': + { + dataSource: + 'main', + collection: + 'users', + isCusomeizeCreate: + true, + }, + 'x-toolbar': + 'BlockSchemaToolbar', + 'x-settings': + 'blockSettings:createForm', + 'x-component': + 'CardItem', + 'x-app-version': + '1.2.8-alpha', + 'x-component-props': + { + title: + "Use 'Current popup record' and 'Parent popup record' in linkage rules", + }, + properties: + { + v515rcx3gq9: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'FormV2', + 'x-use-component-props': + 'useCreateFormBlockProps', + 'x-app-version': + '1.2.8-alpha', + properties: + { + grid: { + 'x-uid': + 'rcl4n8ekgkc', + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid', + 'x-initializer': + 'form:configureFields', + 'x-app-version': + '1.2.8-alpha', + 'x-linkage-rules': + [ + { + condition: + { + $and: [], + }, + actions: + [ + { + targetFields: + [ + 'nickname', + ], + operator: + 'value', + value: + { + mode: 'express', + value: + '{{$nPopupRecord.name}}', + result: + '{{$nPopupRecord.name}}', + }, + }, + { + targetFields: + [ + 'username', + ], + operator: + 'value', + value: + { + mode: 'express', + value: + '{{$nParentPopupRecord.username}}', + result: + '{{$nParentPopupRecord.username}}', + }, + }, + ], + }, + ], + properties: + { + mwpb4y1xvwc: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid.Row', + 'x-app-version': + '1.2.8-alpha', + properties: + { + lznos2z04u7: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid.Col', + 'x-app-version': + '1.2.8-alpha', + properties: + { + nickname: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'string', + 'x-toolbar': + 'FormItemSchemaToolbar', + 'x-settings': + 'fieldSettings:FormItem', + 'x-component': + 'CollectionField', + 'x-decorator': + 'FormItem', + 'x-collection-field': + 'users.nickname', + 'x-component-props': + {}, + 'x-app-version': + '1.2.8-alpha', + 'x-uid': + 'i6av2tkto6l', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + '6cu6oi5rwle', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'jzdbzph6mk7', + 'x-async': + false, + 'x-index': 1, + }, + avpc9774lpj: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid.Row', + 'x-app-version': + '1.2.8-alpha', + properties: + { + '48k53nm8op9': + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-component': + 'Grid.Col', + 'x-app-version': + '1.2.8-alpha', + properties: + { + username: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'string', + 'x-toolbar': + 'FormItemSchemaToolbar', + 'x-settings': + 'fieldSettings:FormItem', + 'x-component': + 'CollectionField', + 'x-decorator': + 'FormItem', + 'x-collection-field': + 'users.username', + 'x-component-props': + {}, + 'x-app-version': + '1.2.8-alpha', + 'x-uid': + 'gaunuwoypii', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'kq6lpqn7sl4', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'j8a0hpc8q38', + 'x-async': + false, + 'x-index': 2, + }, + }, + 'x-async': + false, + 'x-index': 1, + }, + ck43uehl6sb: + { + _isJSONSchemaObject: + true, + version: + '2.0', + type: 'void', + 'x-initializer': + 'createForm:configureActions', + 'x-component': + 'ActionBar', + 'x-component-props': + { + layout: + 'one-column', + style: + { + marginTop: + 'var(--nb-spacing)', + }, + }, + 'x-app-version': + '1.2.8-alpha', + 'x-uid': + '8ypx0en4tuh', + 'x-async': + false, + 'x-index': 2, + }, + }, + 'x-uid': + 'euep38x5ww3', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + '5tm9p7yp7yw', + 'x-async': + false, + 'x-index': 1, + }, + }, + 'x-uid': + 'pgkloesa9q5', + 'x-async': false, + 'x-index': 3, + }, + }, + 'x-uid': 'zo0dy988kb8', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '9onrofp42n0', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '4ry5hqrw816', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '23ivl7e4cwd', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'hyx52cjis9q', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'y3oz74k0sde', + 'x-async': false, + 'x-index': 1, + }, + u62b74oeoa3: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + 'x-app-version': '1.2.8-alpha', + properties: { + name: { + _isJSONSchemaObject: true, + version: '2.0', + 'x-collection-field': 'roles.name', + 'x-component': 'CollectionField', + 'x-component-props': { + ellipsis: true, + }, + 'x-read-pretty': true, + 'x-decorator': null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-app-version': '1.2.8-alpha', + 'x-uid': 'j62s7fv2rz1', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'vkh30z3hub8', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'o5aa8m8sv9y', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '1c0ppas5bkq', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ch4po0mfhpc', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'qzou4f5uq00', + 'x-async': false, + 'x-index': 7, + }, + }, + 'x-uid': 'y3m6fzw3vxm', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'lzrgy21qagb', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '2d9k3mjqq5o', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'wqzj6suh29r', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '5ls6p9ujrlt', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ko8gsxd2z04', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'zds12bomc53', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '0dya4p8vlqd', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ts6uc46oogs', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'owznkqli9c2', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'mhh5qraws7m', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '0cdb9slpkvt', + 'x-async': true, + 'x-index': 1, + }, +}; +export const testingOfOpenModeForAddChild = { + collections: tree, + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + 'x-index': 1, + properties: { + euelf5zhr9q: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + 'x-index': 1, + properties: { + tt9ipsm1wb7: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-index': 1, + properties: { + '84prsw2twys': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-index': 1, + properties: { + xfm61n5c4ml: { + 'x-uid': 'niqn7muvetp', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'treeCollection:list', + 'x-decorator-props': { + collection: 'treeCollection', + resource: 'treeCollection', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + disableTemplate: false, + treeTable: true, + }, + 'x-designer': 'TableBlockDesigner', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-index': 1, + 'x-async': false, + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-index': 1, + properties: { + '14rxh3ytw3c': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-action': 'create', + 'x-acl-action': 'create', + title: "{{t('Add new')}}", + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:addNew', + 'x-component': 'Action', + 'x-decorator': 'ACLActionProvider', + 'x-component-props': { + openMode: 'drawer', + type: 'primary', + component: 'CreateRecordAction', + icon: 'PlusOutlined', + }, + 'x-action-context': { + dataSource: 'main', + collection: 'treeCollection', + }, + 'x-align': 'right', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-app-version': '1.2.11-alpha', + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Add record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + 'x-app-version': '1.2.11-alpha', + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + 'x-initializer-props': { + gridInitializer: 'popup:addNew:addBlock', + }, + 'x-app-version': '1.2.11-alpha', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Add new")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + 'x-app-version': '1.2.11-alpha', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:addNew:addBlock', + 'x-app-version': '1.2.11-alpha', + properties: { + '722yyrv7m98': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.11-alpha', + properties: { + '7vy82xvhbzv': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.11-alpha', + properties: { + ojcfptdk0c0: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-acl-action': 'treeCollection:create', + 'x-decorator': 'FormBlockProvider', + 'x-use-decorator-props': 'useCreateFormBlockDecoratorProps', + 'x-decorator-props': { + dataSource: 'main', + collection: 'treeCollection', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:createForm', + 'x-component': 'CardItem', + 'x-app-version': '1.2.11-alpha', + properties: { + etmgtwb4474: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useCreateFormBlockProps', + 'x-app-version': '1.2.11-alpha', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + 'x-app-version': '1.2.11-alpha', + 'x-uid': 'qyfkgumnv90', + 'x-async': false, + 'x-index': 1, + }, + zrbty8toodw: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'createForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + }, + 'x-app-version': '1.2.11-alpha', + properties: { + kpw3o1spzj1: { + _isJSONSchemaObject: true, + version: '2.0', + title: '{{ t("Submit") }}', + 'x-action': 'submit', + 'x-component': 'Action', + 'x-use-component-props': 'useCreateActionProps', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:createSubmit', + 'x-component-props': { + type: 'primary', + htmlType: 'submit', + }, + 'x-action-settings': { + triggerWorkflows: [], + }, + type: 'void', + 'x-app-version': '1.2.11-alpha', + 'x-uid': 'nrk908wqbts', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'igr9k3h7t7k', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'ma5rlhv9dum', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '0wnvhgf9n8d', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'e35bul8clzk', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'kq2ogpqu4br', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'fhwssrh6vew', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'k1qre6axckx', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '8zoopsn9t1c', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'v0b2ve58b9n', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '0oy33akilo9', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'moycl7oeq4d', + 'x-async': false, + }, + hls18lynxej: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + useProps: '{{ useTableBlockProps }}', + }, + 'x-index': 2, + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-designer': 'TableV2.ActionColumnDesigner', + 'x-initializer': 'table:configureItemActions', + 'x-index': 1, + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-index': 1, + properties: { + gc1mzfhtmdl: { + 'x-uid': '6vxfbkfht31', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Add child") }}', + 'x-action': 'create', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:addChild', + 'x-component': 'Action.Link', + 'x-visible': '{{treeTable}}', + 'x-component-props': { + openMode: 'drawer', + type: 'link', + addChild: true, + style: { + height: 'auto', + lineHeight: 'normal', + }, + component: 'CreateRecordAction', + }, + 'x-action-context': { + dataSource: 'main', + collection: 'treeCollection', + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Add record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + 'x-initializer-props': { + gridInitializer: 'popup:addNew:addBlock', + }, + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Add new")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:addNew:addBlock', + properties: { + qbw17tsgocv: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.11-alpha', + properties: { + '9bpqpmy2l8c': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.11-alpha', + properties: { + yf0mn75iodr: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-acl-action': 'treeCollection:create', + 'x-decorator': 'FormBlockProvider', + 'x-use-decorator-props': + 'useCreateFormBlockDecoratorProps', + 'x-decorator-props': { + dataSource: 'main', + collection: 'treeCollection', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:createForm', + 'x-component': 'CardItem', + 'x-app-version': '1.2.11-alpha', + properties: { + vnb4zilj1s6: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useCreateFormBlockProps', + 'x-app-version': '1.2.11-alpha', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + 'x-app-version': '1.2.11-alpha', + properties: { + '5bepa3nxiwi': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.11-alpha', + properties: { + '65hydfrna86': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.11-alpha', + properties: { + parent: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': + 'FormItemSchemaToolbar', + 'x-settings': + 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': + 'treeCollection.parent', + 'x-component-props': { + fieldNames: { + label: 'id', + value: 'id', + }, + }, + 'x-app-version': '1.2.11-alpha', + 'x-uid': 'x0pqhzn0dfu', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'g1131ruon9u', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 't690n5j1if6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'nuf1hbm4uqf', + 'x-async': false, + 'x-index': 1, + }, + vyza1c3iq0t: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'createForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + }, + 'x-app-version': '1.2.11-alpha', + properties: { + zsf1ou7k1y1: { + _isJSONSchemaObject: true, + version: '2.0', + title: '{{ t("Submit") }}', + 'x-action': 'submit', + 'x-component': 'Action', + 'x-use-component-props': + 'useCreateActionProps', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:createSubmit', + 'x-component-props': { + type: 'primary', + htmlType: 'submit', + }, + 'x-action-settings': { + triggerWorkflows: [], + }, + type: 'void', + 'x-app-version': '1.2.11-alpha', + 'x-uid': 'a2t0bdb0c23', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ddakb17iwlm', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'vjd5wufu5l2', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ro5s0kuk1kq', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '683qh1ldrxs', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '31v1l2j2tfy', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '1nd9ovqqned', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '58hcwivcxbj', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ahgw09yxsac', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '08m47skufh8', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'umteghuduih', + 'x-async': false, + }, + }, + 'x-uid': 'epu2ma0vdqt', + 'x-async': false, + }, + }, + 'x-uid': 'sv212245quv', + 'x-async': false, + }, + }, + }, + }, + 'x-uid': '4rxdrz9ssqx', + 'x-async': false, + }, + }, + 'x-uid': 'ectsxs7va4s', + 'x-async': false, + }, + }, + 'x-uid': 'lvmtg2dpaob', + 'x-async': false, + }, + }, + 'x-uid': '3fgd1qvr2x0', + 'x-async': true, + }, +}; diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx index f5c7e3b8e..b74e0d1c6 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/tableBlockSettings.tsx @@ -11,6 +11,7 @@ import { useField, useFieldSchema } from '@formily/react'; import { useTranslation } from 'react-i18next'; import { useAPIClient } from '../../../../api-client'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { createSwitchSettingsItem } from '../../../../application/schema-settings/utils/createSwitchSettingsItem'; import { useTableBlockContext } from '../../../../block-provider'; import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../../collection-manager'; import { FilterBlockType } from '../../../../filter-provider/utils'; @@ -20,10 +21,9 @@ import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/Schema import { SchemaSettingsConnectDataBlocks } from '../../../../schema-settings/SchemaSettingsConnectDataBlocks'; import { SchemaSettingsSortField } from '../../../../schema-settings/SchemaSettingsSortField'; import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate'; -import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem'; import { setDefaultSortingRulesSchemaSettingsItem } from '../../../../schema-settings/setDefaultSortingRulesSchemaSettingsItem'; import { setTheDataScopeSchemaSettingsItem } from '../../../../schema-settings/setTheDataScopeSchemaSettingsItem'; -import { createSwitchSettingsItem } from '../../../../application/schema-settings/utils'; +import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem'; export const tableBlockSettings = new SchemaSettings({ name: 'blockSettings:table', diff --git a/packages/core/client/src/modules/blocks/filter-blocks/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/filter-blocks/__e2e__/schemaInitializer.test.ts index 3bdfba6db..92ad32b63 100644 --- a/packages/core/client/src/modules/blocks/filter-blocks/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/filter-blocks/__e2e__/schemaInitializer.test.ts @@ -171,7 +171,7 @@ test.describe('where filter block can be added', () => { await connectToOtherBlock('Users #o1nq'); await page.getByLabel('block-item-CardItem-users-filter-form').getByRole('textbox').fill(usersRecords[0].nickname); - await page.getByLabel('action-Action-Filter-submit-users-filter-form-').click({ position: { x: 10, y: 10 } }); + await page.getByLabel('action-Action-Filter-submit-users-filter-form').click({ position: { x: 10, y: 10 } }); await page.waitForLoadState('networkidle'); for (const record of usersRecords) { await expect(page.getByLabel('block-item-CardItem-users-details').getByText(record.nickname)).toBeVisible({ diff --git a/packages/core/client/src/modules/page/__e2e__/router.test.ts b/packages/core/client/src/modules/page/__e2e__/router.test.ts new file mode 100644 index 000000000..d9aa85995 --- /dev/null +++ b/packages/core/client/src/modules/page/__e2e__/router.test.ts @@ -0,0 +1,53 @@ +/** + * 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 { pageTabRouting } from './templatesOfBug'; + +test.describe('router', () => { + test('page tabs', async ({ page, mockPage }) => { + const nocoPage = await mockPage(pageTabRouting).waitForInit(); + const pageUrl = await nocoPage.getUrl(); + + // 1. 旧版的 URL + await page.goto(`${pageUrl}/?tab=bbch3c9b5jl`); + await expect(page.getByText('This is tab2.')).toBeVisible(); + + // 2. 点击 tab1 应该跳转到 tab1,并使用新版 URL + await page.getByText('tab1').click(); + await expect(page.getByText('This is tab1.')).toBeVisible(); + expect(page.url()).toMatch(new RegExp(`${pageUrl}/tabs/u4earq3d9go`)); + + // 3. 点击 tab2 应该跳转到 tab2,并使用新版 URL + await page.getByText('tab2').click(); + await expect(page.getByText('This is tab2.')).toBeVisible(); + expect(page.url()).toMatch(new RegExp(`${pageUrl}/tabs/bbch3c9b5jl`)); + + // 4. 使用不带 tab 参数的 URL,应该默认显示第一个 tab + await nocoPage.goto(); + await expect(page.getByText('This is tab1.')).toBeVisible(); + expect(page.url()).toMatch(new RegExp(pageUrl)); + }); + + test('side menu should not hide when go back from settings page', async ({ page, mockPage }) => { + await mockPage({ + type: 'group', + }).goto(); + + // 最初是有侧边菜单的 + await expect(page.getByTestId('schema-initializer-Menu-side')).toBeVisible(); + + // 跳转到插件设置页面后再返回,侧边菜单应该还在 + await page.getByTestId('plugin-settings-button').hover(); + await page.getByRole('link', { name: 'API keys' }).click(); + await expect(page.getByText('API keys').first()).toBeVisible(); + await page.goBack(); + await expect(page.getByTestId('schema-initializer-Menu-side')).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/modules/page/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/page/__e2e__/schemaSettings.test.ts index 88d4a6d50..82dbfc4fa 100644 --- a/packages/core/client/src/modules/page/__e2e__/schemaSettings.test.ts +++ b/packages/core/client/src/modules/page/__e2e__/schemaSettings.test.ts @@ -71,7 +71,7 @@ test.describe('page schema settings', () => { }); test.describe('tabs schema settings', () => { - async function showSettings(page: Page) { + async function showSettingsOfTab(page: Page) { await page.getByText('Unnamed').hover(); await page.getByRole('tab').getByLabel('designer-schema-settings-Page').hover(); } @@ -87,7 +87,7 @@ test.describe('tabs schema settings', () => { await mockPage().goto(); await enablePageTabs(page); - await showSettings(page); + await showSettingsOfTab(page); await page.getByRole('menuitem', { name: 'Edit', exact: true }).click(); await page.getByLabel('block-item-Input-Tab name').getByRole('textbox').click(); await page.getByLabel('block-item-Input-Tab name').getByRole('textbox').fill('new name of page tab'); @@ -103,7 +103,7 @@ test.describe('tabs schema settings', () => { await mockPage().goto(); await enablePageTabs(page); - await showSettings(page); + await showSettingsOfTab(page); await page.getByRole('menuitem', { name: 'Delete', exact: true }).click(); await page.getByRole('button', { name: 'OK', exact: true }).click(); diff --git a/packages/core/client/src/modules/page/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/page/__e2e__/templatesOfBug.ts index d3436f53a..cfe9db615 100644 --- a/packages/core/client/src/modules/page/__e2e__/templatesOfBug.ts +++ b/packages/core/client/src/modules/page/__e2e__/templatesOfBug.ts @@ -7,3 +7,135 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +export const pageTabRouting = { + pageSchema: { + 'x-uid': 'i7o68vu98u8', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + 'x-app-version': '1.2.3-alpha', + 'x-component-props': { + enablePageTabs: true, + }, + properties: { + u4earq3d9go: { + 'x-uid': 'xbx6zg90ij2', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + 'x-app-version': '1.2.3-alpha', + title: 'tab1', + properties: { + nq41b5sodws: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.3-alpha', + properties: { + mhiac303xfd: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.3-alpha', + properties: { + feimcvzb7rs: { + 'x-uid': 'maurznopx6g', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-settings': 'blockSettings:markdown', + 'x-decorator': 'CardItem', + 'x-decorator-props': { + name: 'markdown', + }, + 'x-component': 'Markdown.Void', + 'x-editable': false, + 'x-component-props': { + content: 'This is tab1.', + }, + 'x-app-version': '1.2.3-alpha', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'im4ufr0vu12', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'nt3axo5te3r', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + bbch3c9b5jl: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: 'tab2', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + 'x-app-version': '1.2.3-alpha', + properties: { + '8veis5l6j8x': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.2.3-alpha', + properties: { + ayw9hqnz30u: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.2.3-alpha', + properties: { + njodwctznc4: { + 'x-uid': 'ox614ghc544', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-settings': 'blockSettings:markdown', + 'x-decorator': 'CardItem', + 'x-decorator-props': { + name: 'markdown', + }, + 'x-component': 'Markdown.Void', + 'x-editable': false, + 'x-component-props': { + content: 'This is tab2.', + }, + 'x-app-version': '1.2.3-alpha', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ca5bt5ygrkp', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ranzuquuert', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'qhjdmy9nk6q', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-async': true, + 'x-index': 1, + }, + keepUid: true, +}; diff --git a/packages/core/client/src/modules/popup/__e2e__/schemaInitializer1.test.ts b/packages/core/client/src/modules/popup/__e2e__/schemaInitializer1.test.ts index 58e5f3560..5047887a2 100644 --- a/packages/core/client/src/modules/popup/__e2e__/schemaInitializer1.test.ts +++ b/packages/core/client/src/modules/popup/__e2e__/schemaInitializer1.test.ts @@ -27,7 +27,7 @@ test.describe('where to open a popup and what can be added to it', () => { await page.getByLabel('schema-initializer-Tabs-').click(); await page.getByRole('textbox').click(); await page.getByRole('textbox').fill('test123'); - await page.getByLabel('action-Action-Submit-general-table').click(); + await page.getByLabel('action-Action-Submit-general').click(); await expect(page.getByText('test123')).toBeVisible(); @@ -54,7 +54,7 @@ test.describe('where to open a popup and what can be added to it', () => { await page.getByLabel('schema-initializer-Tabs-').click(); await page.getByRole('textbox').click(); await page.getByRole('textbox').fill('test7'); - await page.getByLabel('action-Action-Submit-general-table').click(); + await page.getByLabel('action-Action-Submit-general').click(); await expect(page.getByText('test7')).toBeVisible(); @@ -83,7 +83,7 @@ test.describe('where to open a popup and what can be added to it', () => { await page.getByLabel('schema-initializer-Tabs-').click(); await page.getByRole('textbox').click(); await page.getByRole('textbox').fill('test8'); - await page.getByLabel('action-Action-Submit-general-table-0').click(); + await page.getByLabel('action-Action-Submit-general').click(); await expect(page.getByText('test8')).toBeVisible(); @@ -140,7 +140,7 @@ test.describe('where to open a popup and what can be added to it', () => { await page.getByLabel('schema-initializer-Tabs-').click(); await page.getByRole('textbox').click(); await page.getByRole('textbox').fill('test1'); - await page.getByLabel('action-Action-Submit-general-table').click(); + await page.getByLabel('action-Action-Submit-general').click(); await expect(page.getByText('test1')).toBeVisible(); @@ -171,7 +171,73 @@ test.describe('where to open a popup and what can be added to it', () => { await page.getByLabel('schema-initializer-Tabs-').click(); await page.getByRole('textbox').click(); await page.getByRole('textbox').fill('test8'); - await page.getByLabel('action-Action-Submit-general-details').click(); + await page.getByLabel('action-Action-Submit-general').click(); + + await expect(page.getByText('test8')).toBeVisible(); + + // add blocks + await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover(); + await page.getByRole('menuitem', { name: 'Details' }).hover(); + await page.getByRole('menuitem', { name: 'Current record' }).click(); + await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover(); + await page.getByRole('menuitem', { name: 'form Form (Edit)' }).first().click(); + await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover(); + await page.getByRole('menuitem', { name: 'Markdown' }).click(); + await page.mouse.move(300, 0); + + await expect(page.getByLabel('block-item-CardItem-general-details')).toBeVisible(); + await expect(page.getByLabel('block-item-CardItem-general-form')).toBeVisible(); + await expect(page.getByLabel('block-item-Markdown.Void-')).toBeVisible(); + + // add relationship blocks + // 下拉列表中,可选择以下区块进行创建 + await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover(); + await expect(page.getByRole('menuitem', { name: 'table Details right' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'form Form (Edit)' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'form Form (Add new) right' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'form Form (Add new) right' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'table Table right' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'ordered-list List right' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'ordered-list Grid Card right' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Calendar' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Gantt' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Kanban' })).toBeVisible(); + + await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover(); + await page.getByRole('menuitem', { name: 'Details' }).hover(); + await page.getByRole('menuitem', { name: 'Associated records' }).hover(); + await page.getByRole('menuitem', { name: 'Many to one' }).click(); + await page.mouse.move(300, 0); + await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible(); + + await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover(); + await page.getByRole('menuitem', { name: 'table Table right' }).hover(); + await page.getByRole('menuitem', { name: 'Associated records' }).hover(); + await page.getByRole('menuitem', { name: 'One to many' }).click(); + await page.mouse.move(300, 0); + await expect(page.getByLabel('block-item-CardItem-users-table')).toBeVisible(); + // 屏幕上没有显示错误提示 + await expect(page.locator('.ant-notification-notice').first()).toBeHidden({ timeout: 1000 }); + }); + + test('association link after reload page', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneDetailBlockWithM2oFieldToGeneral).waitForInit(); + const record = await mockRecord('targetToGeneral'); + await nocoPage.goto(); + + // open dialog + await page + .getByLabel('block-item-CollectionField-targetToGeneral-details-targetToGeneral.toGeneral-toGeneral') + .getByText(record.id, { exact: true }) + .click(); + + await page.reload(); + + // add a tab + await page.getByLabel('schema-initializer-Tabs-').click(); + await page.getByRole('textbox').click(); + await page.getByRole('textbox').fill('test8'); + await page.getByLabel('action-Action-Submit-general').click(); await expect(page.getByText('test8')).toBeVisible(); diff --git a/packages/core/client/src/modules/popup/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/popup/__e2e__/schemaSettings.test.ts index 91d60fd63..7da9ed5c4 100644 --- a/packages/core/client/src/modules/popup/__e2e__/schemaSettings.test.ts +++ b/packages/core/client/src/modules/popup/__e2e__/schemaSettings.test.ts @@ -31,6 +31,9 @@ test.describe('tabs schema settings', () => { await page.goto(commonPageUrl); await page.getByRole('button', { name: 'Add new' }).click(); + // There will be a prompt at the top, wait for it to disappear + await page.waitForTimeout(1000); + await showSettings(page); await page.getByRole('menuitem', { name: 'Edit', exact: true }).click(); await page.mouse.move(300, 0); @@ -48,6 +51,9 @@ test.describe('tabs schema settings', () => { await page.goto(commonPageUrl); await page.getByRole('button', { name: 'Add new' }).click(); + // There will be a prompt at the top, wait for it to disappear + await page.waitForTimeout(1000); + await showSettings(page); await page.getByRole('menuitem', { name: 'Delete', exact: true }).click(); await page.getByRole('button', { name: 'OK', exact: true }).click(); diff --git a/packages/core/client/src/nocobase-buildin-plugin/index.tsx b/packages/core/client/src/nocobase-buildin-plugin/index.tsx index f89f1797b..e975cd987 100644 --- a/packages/core/client/src/nocobase-buildin-plugin/index.tsx +++ b/packages/core/client/src/nocobase-buildin-plugin/index.tsx @@ -24,8 +24,10 @@ import { RemoteDocumentTitlePlugin } from '../document-title'; import { PinnedListPlugin } from '../plugin-manager'; import { PMPlugin } from '../pm'; import { AdminLayoutPlugin, RouteSchemaComponent } from '../route-switch'; -import { AntdSchemaComponentPlugin, SchemaComponentPlugin } from '../schema-component'; +import { AntdSchemaComponentPlugin, PageTabs, SchemaComponentPlugin } from '../schema-component'; import { ErrorFallback } from '../schema-component/antd/error-fallback'; +import { PagePopups } from '../schema-component/antd/page/PagePopups'; +import { SubPage } from '../schema-component/antd/page/SubPages'; import { AssociationFilterPlugin, SchemaInitializerPlugin } from '../schema-initializer'; import { SchemaSettingsPlugin } from '../schema-settings'; import { BlockTemplateDetails, BlockTemplatePage } from '../schema-templates'; @@ -302,6 +304,22 @@ export class NocoBaseBuildInPlugin extends Plugin { path: '/admin/:name', Component: 'AdminDynamicPage', }); + this.router.add('admin.page.tab', { + path: '/admin/:name/tabs/:tabUid', + Component: PageTabs as any, + }); + this.router.add('admin.page.popup', { + path: '/admin/:name/popups/*', + Component: PagePopups, + }); + this.router.add('admin.page.tab.popup', { + path: '/admin/:name/tabs/:tabUid/popups/*', + Component: PagePopups, + }); + this.router.add('admin.subPage', { + path: '/admin/subpages/*', + Component: SubPage, + }); } addComponents() { diff --git a/packages/core/client/src/pm/PluginSetting.tsx b/packages/core/client/src/pm/PluginSetting.tsx index 7712b9d4f..69bab956b 100644 --- a/packages/core/client/src/pm/PluginSetting.tsx +++ b/packages/core/client/src/pm/PluginSetting.tsx @@ -10,12 +10,11 @@ import { PageHeader } from '@ant-design/pro-layout'; import { css } from '@emotion/css'; import { Layout, Menu, Result } from 'antd'; -import _, { get } from 'lodash'; import React, { createContext, useCallback, useMemo } from 'react'; import { Navigate, Outlet, useLocation, useNavigate, useParams } from 'react-router-dom'; -import { useStyles } from './style'; import { ADMIN_SETTINGS_PATH, PluginSettingsPageType, useApp } from '../application'; import { useCompile } from '../schema-component'; +import { useStyles } from './style'; export const SettingsCenterContext = createContext({}); SettingsCenterContext.displayName = 'SettingsCenterContext'; diff --git a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx index a8654507f..203cc3e7d 100644 --- a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx +++ b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx @@ -11,8 +11,8 @@ import { css } from '@emotion/css'; import { useSessionStorageState } from 'ahooks'; import { App, ConfigProvider, Divider, Layout } from 'antd'; import { createGlobalStyle } from 'antd-style'; -import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { Link, Outlet, useLocation, useMatch, useNavigate, useParams } from 'react-router-dom'; +import React, { FC, createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { Link, Outlet, useMatch, useParams } from 'react-router-dom'; import { ACLRolesCheckProvider, CurrentAppInfoProvider, @@ -33,11 +33,12 @@ import { useSystemSettings, useToken, } from '../../../'; +import { useLocationNoUpdate, useNavigateNoUpdate } from '../../../application/CustomRouterContextProvider'; import { Plugin } from '../../../application/Plugin'; import { useAppSpin } from '../../../application/hooks/useAppSpin'; +import { useMenuTranslation } from '../../../schema-component/antd/menu/locale'; import { Help } from '../../../user/Help'; import { VariablesProvider } from '../../../variables'; -import { useMenuTranslation } from '../../../schema-component/antd/menu/locale'; const filterByACL = (schema, options) => { const { allowAll, allowMenuItemIds = [] } = options; @@ -76,13 +77,13 @@ const useMenuProps = () => { const MenuEditor = (props) => { const { notification } = App.useApp(); - const [hasNotice, setHasNotice] = useSessionStorageState('plugin-notice', { defaultValue: false }); + const [, setHasNotice] = useSessionStorageState('plugin-notice', { defaultValue: false }); const { t } = useMenuTranslation(); const { setTitle: _setTitle } = useDocumentTitle(); const setTitle = useCallback((title) => _setTitle(t(title)), []); - const navigate = useNavigate(); + const navigate = useNavigateNoUpdate(); const params = useParams(); - const location = useLocation(); + const location = useLocationNoUpdate(); const isMatchAdmin = useMatch('/admin'); const isMatchAdminName = useMatch('/admin/:name'); const defaultSelectedUid = params.name; @@ -91,12 +92,12 @@ const MenuEditor = (props) => { const ctx = useACLRoleContext(); const [current, setCurrent] = useState(null); - const onSelect = ({ item }) => { + const onSelect = useCallback(({ item }) => { const schema = item.props.schema; setTitle(schema.title); setCurrent(schema); navigate(`/admin/${schema['x-uid']}`); - }; + }, []); const { render } = useAppSpin(); const adminSchemaUid = useAdminSchemaUid(); const { data, loading } = useRequest<{ @@ -154,7 +155,7 @@ const MenuEditor = (props) => { sideMenuRef.current.style.display = 'block'; } } - }, [data?.data, params.name, sideMenuRef]); + }, [data?.data, params.name, sideMenuRef, location?.pathname]); const schema = useMemo(() => { const s = filterByACL(data?.data, ctx); @@ -211,17 +212,16 @@ const MenuEditor = (props) => { }, ); + const scope = useMemo(() => { + return { useMenuProps, onSelect, sideMenuRef, defaultSelectedUid }; + }, []); + if (loading) { return render(); } return ( - + ); }; @@ -292,35 +292,35 @@ const SetThemeOfHeaderSubmenu = ({ children }) => { ); }; +const sideClass = css` + height: 100%; + /* position: fixed; */ + position: relative; + left: 0; + top: 0; + background: rgba(0, 0, 0, 0); + z-index: 100; + .ant-layout-sider-children { + top: var(--nb-header-height); + position: fixed; + width: 200px; + height: calc(100vh - var(--nb-header-height)); + } +`; + +const InternalAdminSideBar: FC<{ pageUid: string; sideMenuRef: any }> = memo((props) => { + if (!props.pageUid) return null; + return ; +}); +InternalAdminSideBar.displayName = 'InternalAdminSideBar'; + const AdminSideBar = ({ sideMenuRef }) => { const params = useParams(); - if (!params.name) return null; - return ( - - ); + return ; }; export const AdminDynamicPage = () => { - const params = useParams<{ name?: string }>(); - return ; + return ; }; export const InternalAdminLayout = () => { diff --git a/packages/core/client/src/route-switch/antd/route-schema-component/index.tsx b/packages/core/client/src/route-switch/antd/route-schema-component/index.tsx index 2c89f9e96..00bbc31ff 100644 --- a/packages/core/client/src/route-switch/antd/route-schema-component/index.tsx +++ b/packages/core/client/src/route-switch/antd/route-schema-component/index.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { useParams } from 'react-router-dom'; import { RemoteSchemaComponent } from '../../../'; -export function RouteSchemaComponent(props: any) { - const params = useParams(); +export function RouteSchemaComponent() { + const params = useParams(); return ; } diff --git a/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx b/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx index 1cf8c900d..c8131ed50 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx @@ -11,13 +11,12 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea import { Drawer } from 'antd'; import classNames from 'classnames'; import React from 'react'; -import { ActionDrawerProps, OpenSize } from './types'; +import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; +import { ErrorFallback } from '../error-fallback'; import { useStyles } from './Action.Drawer.style'; import { useActionContext } from './hooks'; import { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer'; -import { ComposedActionDrawer } from './types'; -import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; -import { ErrorFallback } from '../error-fallback'; +import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types'; const DrawerErrorFallback: React.FC = (props) => { const { visible, setVisible } = useActionContext(); diff --git a/packages/core/client/src/schema-component/antd/action/Action.tsx b/packages/core/client/src/schema-component/antd/action/Action.tsx index 2f47549d9..2b6b19b08 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.tsx @@ -11,7 +11,7 @@ import { observer, RecursionField, useField, useFieldSchema, useForm } from '@fo import { isPortalInBody } from '@nocobase/utils/client'; import { App, Button } from 'antd'; import classnames from 'classnames'; -import { default as lodash } from 'lodash'; +import _, { default as lodash } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { useTranslation } from 'react-i18next'; @@ -28,6 +28,10 @@ import { useLocalVariables, useVariables } from '../../../variables'; 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 { usePopupSettings } from '../page/PopupSettingsProvider'; +import { useNavigateTOSubPage } from '../page/SubPages'; import ActionContainer from './Action.Container'; import { ActionDesigner } from './Action.Designer'; import { ActionDrawer } from './Action.Drawer'; @@ -36,11 +40,16 @@ import { ActionModal } from './Action.Modal'; import { ActionPage } from './Action.Page'; import useStyles from './Action.style'; import { ActionContextProvider } from './context'; -import { useA } from './hooks'; import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction'; import { ActionProps, ComposedAction } from './types'; import { linkageAction, setInitialActionState } from './utils'; +const useA = () => { + return { + async run() {}, + }; +}; + const handleError = (err) => console.log(err); export const Action: ComposedAction = withDynamicSchemaProps( @@ -48,7 +57,6 @@ export const Action: ComposedAction = withDynamicSchemaProps( const { popover, confirm, - openMode: om, containerRefKey, component, useAction = useA, @@ -70,11 +78,13 @@ export const Action: ComposedAction = withDynamicSchemaProps( const aclCtx = useACLActionParamsContext(); const { wrapSSR, componentCls, hashId } = useStyles(); const { t } = useTranslation(); + const { visibleWithURL, setVisibleWithURL } = usePagePopup(); const [visible, setVisible] = useState(false); const [formValueChanged, setFormValueChanged] = useState(false); + const { setSubmitted: setParentSubmitted } = useActionContext(); const Designer = useDesigner(); const field = useField(); - const { run, element, disabled: disableAction } = useAction(actionCallback); + const { run, element, disabled: disableAction } = _.isFunction(useAction) ? useAction(actionCallback) : ({} as any); const fieldSchema = useFieldSchema(); const compile = useCompile(); const form = useForm(); @@ -120,47 +130,12 @@ export const Action: ComposedAction = withDynamicSchemaProps( }); }, [field, linkageRules, localVariables, variables]); - const handleButtonClick = useCallback( - (e: React.MouseEvent, checkPortal = true) => { - if (checkPortal && isPortalInBody(e.target as Element)) { - return; - } - e.preventDefault(); - e.stopPropagation(); - - if (!disabled && aclCtx) { - const onOk = () => { - if (onClick) { - onClick(e, () => { - if (refreshDataBlockRequest !== false) { - service?.refresh?.(); - } - }); - } else { - setVisible(true); - run(); - } - }; - if (confirm?.content) { - modal.confirm({ - title: t(confirm.title, { title: actionTitle }), - content: t(confirm.content, { title: actionTitle }), - onOk, - }); - } else { - onOk(); - } - } - }, - [confirm, disabled, modal, onClick, run], - ); - const buttonStyle = useMemo(() => { return { ...style, opacity: designable && (field?.data?.hidden || !aclCtx) && 0.1, }; - }, [designable, field?.data?.hidden, style]); + }, [aclCtx, designable, field?.data?.hidden, style]); const handleMouseEnter = useCallback( (e) => { @@ -168,55 +143,66 @@ export const Action: ComposedAction = withDynamicSchemaProps( }, [onMouseEnter], ); - const renderButton = () => { - if (!designable && (field?.data?.hidden || !aclCtx)) { - return null; - } - return ( - : icon} - disabled={disabled} - style={buttonStyle} - onClick={handleButtonClick} - component={tarComponent || Button} - className={classnames(componentCls, hashId, className, 'nb-action')} - type={(props as any).type === 'danger' ? undefined : props.type} - > - {actionTitle} - - - ); + const buttonProps = { + designable, + field, + aclCtx, + actionTitle, + icon, + loading, + disabled, + buttonStyle, + handleMouseEnter, + tarComponent, + designerProps, + componentCls, + hashId, + className, + others, + getAriaLabel, + type: props.type, + Designer, + openMode, + onClick, + refreshDataBlockRequest, + service, + fieldSchema, + setVisible, + run, + confirm, + modal, }; - const buttonElement = renderButton(); + const buttonElement = RenderButton(buttonProps); // if (!btnHover) { // return buttonElement; // } const result = ( - - {popover && } - {!popover && renderButton()} - {!popover && props.children} - {element} - + + { + setVisible?.(value); + setVisibleWithURL?.(value); + }} + formValueChanged={formValueChanged} + setFormValueChanged={setFormValueChanged} + openMode={openMode} + openSize={openSize} + containerRefKey={containerRefKey} + fieldSchema={fieldSchema} + setSubmitted={setParentSubmitted} + > + {popover && } + {!popover && } + {!popover && props.children} + {element} + + ); // fix https://nocobase.height.app/T-3235/description @@ -284,3 +270,129 @@ Action.Container = ActionContainer; Action.Page = ActionPage; export default Action; + +// TODO: Plugin-related code should not exist in the core. It would be better to implement it by modifying the schema, but it would cause incompatibility. +function isBulkEditAction(fieldSchema) { + return fieldSchema['x-action'] === 'customize:bulkEdit'; +} + +function RenderButton({ + designable, + field, + aclCtx, + actionTitle, + icon, + loading, + disabled, + buttonStyle, + handleMouseEnter, + tarComponent, + designerProps, + componentCls, + hashId, + className, + others, + getAriaLabel, + type, + Designer, + openMode, + onClick, + refreshDataBlockRequest, + service, + fieldSchema, + setVisible, + run, + confirm, + modal, +}) { + const { t } = useTranslation(); + const { navigateToSubPage } = useNavigateTOSubPage(); + const { isPopupVisibleControlledByURL } = usePopupSettings(); + const { openPopup } = usePagePopup(); + + const handleButtonClick = useCallback( + (e: React.MouseEvent, checkPortal = true) => { + if (checkPortal && isPortalInBody(e.target as Element)) { + return; + } + e.preventDefault(); + e.stopPropagation(); + + if (!disabled && aclCtx) { + const onOk = () => { + if (openMode === 'page') { + return navigateToSubPage(); + } + if (onClick) { + onClick(e, () => { + if (refreshDataBlockRequest !== false) { + service?.refresh?.(); + } + }); + } else if (isBulkEditAction(fieldSchema) || !isPopupVisibleControlledByURL) { + setVisible(true); + run?.(); + } else { + if ( + ['view', 'update', 'create', 'customize:popup'].includes(fieldSchema['x-action']) && + fieldSchema['x-uid'] + ) { + openPopup(); + } else { + setVisible(true); + run?.(); + } + } + }; + if (confirm?.content) { + modal.confirm({ + title: t(confirm.title, { title: actionTitle }), + content: t(confirm.content, { title: actionTitle }), + onOk, + }); + } else { + onOk(); + } + } + }, + [ + aclCtx, + actionTitle, + confirm?.content, + confirm?.title, + disabled, + modal, + onClick, + openPopup, + refreshDataBlockRequest, + run, + service, + setVisible, + t, + ], + ); + + if (!designable && (field?.data?.hidden || !aclCtx)) { + return null; + } + + return ( + : icon} + disabled={disabled} + style={buttonStyle} + onClick={handleButtonClick} + component={tarComponent || Button} + className={classnames(componentCls, hashId, className, 'nb-action')} + type={type === 'danger' ? undefined : type} + > + {actionTitle} + + + ); +} diff --git a/packages/core/client/src/schema-component/antd/action/__tests__/action.test.tsx b/packages/core/client/src/schema-component/antd/action/__tests__/action.test.tsx index c02e8c599..937a92a93 100644 --- a/packages/core/client/src/schema-component/antd/action/__tests__/action.test.tsx +++ b/packages/core/client/src/schema-component/antd/action/__tests__/action.test.tsx @@ -82,7 +82,6 @@ describe('Action', () => { expect(document.querySelector('.nb-action-page')).not.toBeInTheDocument(); }); await waitFor(async () => { - await userEvent.click(getByText('Close')); // page await userEvent.click(getByText('Page')); await userEvent.click(getByText('Open')); diff --git a/packages/core/client/src/schema-component/antd/action/context.tsx b/packages/core/client/src/schema-component/antd/action/context.tsx index 589943526..895701f86 100644 --- a/packages/core/client/src/schema-component/antd/action/context.tsx +++ b/packages/core/client/src/schema-component/antd/action/context.tsx @@ -8,7 +8,6 @@ */ import React, { createContext, useEffect, useRef, useState } from 'react'; -import { useActionContext } from './hooks'; import { useDataBlockRequest } from '../../../data-source'; import { ActionContextProps } from './types'; @@ -17,11 +16,10 @@ ActionContext.displayName = 'ActionContext'; export const ActionContextProvider: React.FC = (props) => { const [submitted, setSubmitted] = useState(false); //是否有提交记录 - const contextProps = useActionContext(); const { visible } = { ...props, ...props.value } || {}; const isFirstRender = useRef(true); // 使用ref跟踪是否为首次渲染 const service = useDataBlockRequest(); - const { setSubmitted: setParentSubmitted } = { ...props, ...props.value, ...contextProps }; + const { setSubmitted: setParentSubmitted } = { ...props, ...props.value }; useEffect(() => { if (visible !== undefined) { if (isFirstRender.current) { @@ -39,7 +37,7 @@ export const ActionContextProvider: React.FC + {props.children} ); diff --git a/packages/core/client/src/schema-component/antd/action/hooks.ts b/packages/core/client/src/schema-component/antd/action/hooks.ts index ef22498f3..4992f3a33 100644 --- a/packages/core/client/src/schema-component/antd/action/hooks.ts +++ b/packages/core/client/src/schema-component/antd/action/hooks.ts @@ -14,12 +14,6 @@ import { useTranslation } from 'react-i18next'; import { useIsDetailBlock } from '../../../block-provider/FormBlockProvider'; import { ActionContext } from './context'; -export const useA = () => { - return { - async run() {}, - }; -}; - export const useActionContext = () => { const ctx = useContext(ActionContext); const { t } = useTranslation(); diff --git a/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx b/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx index ec21079de..3cd2709ee 100644 --- a/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx @@ -10,7 +10,7 @@ import { Field } from '@formily/core'; import { observer, useField, useFieldSchema } from '@formily/react'; import React, { useEffect, useMemo, useState } from 'react'; -import { useCollection, useCollectionManager } from '../../../data-source/collection'; +import { useCollectionManager } from '../../../data-source/collection'; import { markRecordAsNew } from '../../../data-source/collection-record/isNewRecord'; import { useSchemaComponentContext } from '../../hooks'; import { AssociationFieldContext } from './context'; @@ -18,8 +18,7 @@ import { AssociationFieldContext } from './context'; export const AssociationFieldProvider = observer( (props) => { const field = useField(); - const collection = useCollection(); - const dm = useCollectionManager(); + const cm = useCollectionManager(); const fieldSchema = useFieldSchema(); // 这里有点奇怪,在 Table 切换显示的组件时,这个组件并不会触发重新渲染,所以增加这个 Hooks 让其重新渲染 @@ -29,12 +28,12 @@ export const AssociationFieldProvider = observer( const allowDissociate = fieldSchema['x-component-props']?.allowDissociate !== false; const collectionField = useMemo( - () => collection.getField(fieldSchema['x-collection-field']), + () => cm.getCollectionField(fieldSchema['x-collection-field']), // eslint-disable-next-line react-hooks/exhaustive-deps [fieldSchema['x-collection-field'], fieldSchema.name], ); const isFileCollection = useMemo( - () => dm.getCollection(collectionField?.target)?.template === 'file', + () => cm.getCollection(collectionField?.target)?.template === 'file', // eslint-disable-next-line react-hooks/exhaustive-deps [fieldSchema['x-collection-field']], ); diff --git a/packages/core/client/src/schema-component/antd/association-field/InternalTag.tsx b/packages/core/client/src/schema-component/antd/association-field/InternalTag.tsx index 843e21e89..c27fa099e 100644 --- a/packages/core/client/src/schema-component/antd/association-field/InternalTag.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/InternalTag.tsx @@ -7,22 +7,20 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { observer, RecursionField, useField, useFieldSchema } from '@formily/react'; +import { observer, useFieldSchema } from '@formily/react'; import { toArr } from '@formily/shared'; -import React, { Fragment, useRef, useState } from 'react'; +import React, { Fragment, useRef } from 'react'; import { useDesignable } from '../../'; -import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider'; -import { CollectionProvider_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; -import { RecordProvider, useRecord } from '../../../record-provider'; -import { FormProvider } from '../../core'; +import { useCollectionManager_deprecated } from '../../../collection-manager'; +import { useCollectionRecordData } from '../../../data-source/collection-record/CollectionRecordProvider'; import { useCompile } from '../../hooks'; -import { ActionContextProvider, useActionContext } from '../action'; -import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip'; +import { useActionContext } from '../action'; +import { usePagePopup } from '../page/pagePopupUtils'; +import { transformNestedData } from './InternalCascadeSelect'; +import { ButtonListProps, ReadPrettyInternalViewer, isObject } from './InternalViewer'; import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks'; import schema from './schema'; import { getTabFormatValue, useLabelUiSchema } from './util'; -import { transformNestedData } from './InternalCascadeSelect'; -import { isObject } from './InternalViewer'; interface IEllipsisWithTooltipRef { setPopoverVisible: (boolean) => void; @@ -34,126 +32,77 @@ const toValue = (value, placeholder) => { } return value; }; + +const ButtonTabList: React.FC = (props) => { + const fieldSchema = useFieldSchema(); + const { enableLink, tagColorField } = fieldSchema['x-component-props']; + const fieldNames = useFieldNames({ fieldNames: props.fieldNames }); + const insertViewer = useInsertSchema('Viewer'); + const { options: collectionField } = useAssociationFieldContext(); + const compile = useCompile(); + const { designable } = useDesignable(); + const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label'); + const { snapshot } = useActionContext(); + const ellipsisWithTooltipRef = useRef(); + const { getCollection } = useCollectionManager_deprecated(); + const targetCollection = getCollection(collectionField?.target); + const isTreeCollection = targetCollection?.template === 'tree'; + const { openPopup } = usePagePopup(); + const recordData = useCollectionRecordData(); + + const renderRecords = () => + toArr(props.value).map((record, index, arr) => { + const value = record?.[fieldNames?.label || 'label']; + const label = isTreeCollection + ? transformNestedData(record) + .map((o) => o?.[fieldNames?.label || 'label']) + .join(' / ') + : isObject(value) + ? JSON.stringify(value) + : value; + const val = toValue(compile(label), 'N/A'); + const text = getTabFormatValue(compile(labelUiSchema), val, record[tagColorField]); + return ( + + + {snapshot ? ( + text + ) : enableLink !== false ? ( + { + props.setBtnHover(true); + }} + onClick={(e) => { + props.setBtnHover(true); + e.stopPropagation(); + e.preventDefault(); + if (designable) { + insertViewer(schema.Viewer); + } + openPopup({ + recordData: record, + parentRecordData: recordData, + }); + ellipsisWithTooltipRef?.current?.setPopoverVisible(false); + }} + > + {text} + + ) : ( + text + )} + + {index < arr.length - 1 ? , : null} + + ); + }); + + return <>{renderRecords()}; +}; + export const ReadPrettyInternalTag: React.FC = observer( (props: any) => { - const fieldSchema = useFieldSchema(); - const recordCtx = useRecord(); - const { enableLink, tagColorField } = fieldSchema['x-component-props']; - // value 做了转换,但 props.value 和原来 useField().value 的值不一致 - const field = useField(); - const fieldNames = useFieldNames(props); - const [visible, setVisible] = useState(false); - const insertViewer = useInsertSchema('Viewer'); - const { options: collectionField } = useAssociationFieldContext(); - const [record, setRecord] = useState({}); - const compile = useCompile(); - const { designable } = useDesignable(); - const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label'); - const { snapshot } = useActionContext(); - const ellipsisWithTooltipRef = useRef(); - const { getCollection } = useCollectionManager_deprecated(); - const targetCollection = getCollection(collectionField?.target); - const isTreeCollection = targetCollection?.template === 'tree'; - const [btnHover, setBtnHover] = useState(false); - - const renderRecords = () => - toArr(props.value).map((record, index, arr) => { - const value = record?.[fieldNames?.label || 'label']; - const label = isTreeCollection - ? transformNestedData(record) - .map((o) => o?.[fieldNames?.label || 'label']) - .join(' / ') - : isObject(value) - ? JSON.stringify(value) - : value; - const val = toValue(compile(label), 'N/A'); - const text = getTabFormatValue(compile(labelUiSchema), val, record[tagColorField]); - return ( - - - {snapshot ? ( - text - ) : enableLink !== false ? ( - { - setBtnHover(true); - }} - onClick={(e) => { - setBtnHover(true); - e.stopPropagation(); - e.preventDefault(); - if (designable) { - insertViewer(schema.Viewer); - } - setVisible(true); - setRecord(record); - ellipsisWithTooltipRef?.current?.setPopoverVisible(false); - }} - > - {text} - - ) : ( - text - )} - - {index < arr.length - 1 ? , : null} - - ); - }); - - const btnElement = ( - - {renderRecords()} - - ); - - if (enableLink === false || !btnHover) { - return btnElement; - } - - const renderWithoutTableFieldResourceProvider = () => ( - - - { - return s['x-component'] === 'AssociationField.Viewer'; - }} - /> - - - ); - - const renderRecordProvider = () => { - const collectionFieldNames = fieldSchema?.['x-collection-field']?.split('.'); - - return collectionFieldNames && collectionFieldNames.length > 2 ? ( - - {renderWithoutTableFieldResourceProvider()} - - ) : ( - - {renderWithoutTableFieldResourceProvider()} - - ); - }; - - return ( -
- - - {btnElement} - - {renderRecordProvider()} - - - -
- ); + return ; }, { displayName: 'ReadPrettyInternalTag' }, ); diff --git a/packages/core/client/src/schema-component/antd/association-field/InternalViewer.tsx b/packages/core/client/src/schema-component/antd/association-field/InternalViewer.tsx index 0f59c893c..f33f49690 100644 --- a/packages/core/client/src/schema-component/antd/association-field/InternalViewer.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/InternalViewer.tsx @@ -9,18 +9,16 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { toArr } from '@formily/shared'; -import React, { Fragment, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import React, { FC, Fragment, useRef, useState } from 'react'; import { useDesignable } from '../../'; -import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider'; -import { CollectionProvider_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; -import { Collection } from '../../../data-source'; +import { WithoutTableFieldResource } from '../../../block-provider'; +import { useCollectionManager, useCollectionRecordData } from '../../../data-source'; import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider'; -import { RecordProvider, useRecord } from '../../../record-provider'; -import { FormProvider } from '../../core'; 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 { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks'; import { transformNestedData } from './InternalCascadeSelect'; import schema from './schema'; @@ -39,137 +37,153 @@ const toValue = (value, placeholder) => { export function isObject(value) { return typeof value === 'object' && value !== null; } + +export interface ButtonListProps { + value: any; + setBtnHover: any; + fieldNames?: { + label: string; + value: string; + }; +} + +const ButtonLinkList: FC = (props) => { + const fieldSchema = useFieldSchema(); + const cm = useCollectionManager(); + const { enableLink } = fieldSchema['x-component-props'] || {}; + const fieldNames = useFieldNames({ fieldNames: props.fieldNames }); + const insertViewer = useInsertSchema('Viewer'); + const { options: collectionField } = useAssociationFieldContext(); + const compile = useCompile(); + const { designable } = useDesignable(); + const { snapshot } = useActionContext(); + const targetCollection = cm.getCollection(collectionField?.target); + const isTreeCollection = targetCollection?.template === 'tree'; + const ellipsisWithTooltipRef = useRef(); + const getLabelUiSchema = useLabelUiSchemaV2(); + const { openPopup } = usePagePopup(); + const recordData = useCollectionRecordData(); + + const renderRecords = () => + toArr(props.value).map((record, index, arr) => { + const value = record?.[fieldNames?.label || 'label']; + const label = isTreeCollection + ? transformNestedData(record) + .map((o) => o?.[fieldNames?.label || 'label']) + .join(' / ') + : isObject(value) + ? JSON.stringify(value) + : value; + const val = toValue(compile(label), 'N/A'); + const labelUiSchema = getLabelUiSchema( + record?.__collection || collectionField?.target, + fieldNames?.label || 'label', + ); + const text = getLabelFormatValue(compile(labelUiSchema), val, true); + return ( + + + {snapshot ? ( + text + ) : enableLink !== false ? ( + { + props.setBtnHover(true); + }} + onClick={(e) => { + props.setBtnHover(true); + e.stopPropagation(); + e.preventDefault(); + if (designable) { + insertViewer(schema.Viewer); + } + openPopup({ + recordData: record, + parentRecordData: recordData, + }); + ellipsisWithTooltipRef?.current?.setPopoverVisible(false); + }} + > + {text} + + ) : ( + text + )} + + {index < arr.length - 1 ? , : null} + + ); + }); + + return <>{renderRecords()}; +}; + +interface ReadPrettyInternalViewerProps { + ButtonList: FC; + value: any; + fieldNames?: { + label: string; + value: string; + }; +} + export const ReadPrettyInternalViewer: React.FC = observer( - (props: any) => { + (props: ReadPrettyInternalViewerProps) => { + const { value, ButtonList = ButtonLinkList } = props; const fieldSchema = useFieldSchema(); - const recordCtx = useRecord(); - const { getCollection } = useCollectionManager_deprecated(); const { enableLink } = fieldSchema['x-component-props'] || {}; // value 做了转换,但 props.value 和原来 useField().value 的值不一致 const field = useField(); - const fieldNames = useFieldNames(props); const [visible, setVisible] = useState(false); - const insertViewer = useInsertSchema('Viewer'); const { options: collectionField } = useAssociationFieldContext(); - const [record, setRecord] = useState({}); - const compile = useCompile(); - const { designable } = useDesignable(); - const { snapshot } = useActionContext(); - const targetCollection = getCollection(collectionField?.target); - const isTreeCollection = targetCollection?.template === 'tree'; const ellipsisWithTooltipRef = useRef(); - const getLabelUiSchema = useLabelUiSchemaV2(); - const [btnHover, setBtnHover] = useState(false); - const { t } = useTranslation(); - - const renderRecords = () => - toArr(props.value).map((record, index, arr) => { - const value = record?.[fieldNames?.label || 'label']; - const label = isTreeCollection - ? transformNestedData(record) - .map((o) => o?.[fieldNames?.label || 'label']) - .join(' / ') - : isObject(value) - ? JSON.stringify(value) - : value; - const val = toValue(compile(label), 'N/A'); - const labelUiSchema = getLabelUiSchema( - record?.__collection || collectionField?.target, - fieldNames?.label || 'label', - ); - const text = getLabelFormatValue(compile(labelUiSchema), val, true); - return ( - - - {snapshot ? ( - text - ) : enableLink !== false ? ( - { - setBtnHover(true); - }} - onClick={(e) => { - setBtnHover(true); - e.stopPropagation(); - e.preventDefault(); - if (designable) { - insertViewer(schema.Viewer); - } - setVisible(true); - setRecord(record); - ellipsisWithTooltipRef?.current?.setPopoverVisible(false); - }} - > - {text} - - ) : ( - text - )} - - {index < arr.length - 1 ? , : null} - - ); - }); + const { visibleWithURL, setVisibleWithURL } = usePagePopup(); + const [btnHover, setBtnHover] = useState(!!visibleWithURL); const btnElement = ( - {renderRecords()} + ); if (enableLink === false || !btnHover) { return btnElement; } + const renderWithoutTableFieldResourceProvider = () => ( - + // The recordData here is only provided when the popup is opened, not the current row record + - - { - return s['x-component'] === 'AssociationField.Viewer'; - }} - /> - + { + return s['x-component'] === 'AssociationField.Viewer'; + }} + /> ); - const renderRecordProvider = () => { - const collectionFieldNames = fieldSchema?.['x-collection-field']?.split('.'); - - return collectionFieldNames && collectionFieldNames.length > 2 ? ( - - {renderWithoutTableFieldResourceProvider()} - - ) : ( - - {renderWithoutTableFieldResourceProvider()} - - ); - }; - return ( -
- - - {btnElement} - - {renderRecordProvider()} - - - -
+ + { + setVisible?.(value); + setVisibleWithURL?.(value); + }, + openMode: 'drawer', + snapshot: collectionField?.interface === 'snapshot', + fieldSchema: fieldSchema, + }} + > + {btnElement} + {renderWithoutTableFieldResourceProvider()} + + ); }, { displayName: 'ReadPrettyInternalViewer' }, diff --git a/packages/core/client/src/schema-component/antd/association-field/hooks.ts b/packages/core/client/src/schema-component/antd/association-field/hooks.ts index df34a781f..1638736ff 100644 --- a/packages/core/client/src/schema-component/antd/association-field/hooks.ts +++ b/packages/core/client/src/schema-component/antd/association-field/hooks.ts @@ -185,7 +185,14 @@ export default function useServiceOptions(props) { }, [collectionField?.target, action, filter, service]); } -export const useFieldNames = (props) => { +export const useFieldNames = ( + props: { + fieldNames?: { + label: string; + value: string; + }; + } = {}, +) => { const fieldSchema = useFieldSchema(); const fieldNames = fieldSchema['x-component-props']?.['field']?.['uiSchema']?.['x-component-props']?.['fieldNames'] || diff --git a/packages/core/client/src/schema-component/antd/error-fallback/ErrorFallback.tsx b/packages/core/client/src/schema-component/antd/error-fallback/ErrorFallback.tsx index 17a398620..7e56d3762 100644 --- a/packages/core/client/src/schema-component/antd/error-fallback/ErrorFallback.tsx +++ b/packages/core/client/src/schema-component/antd/error-fallback/ErrorFallback.tsx @@ -7,19 +7,19 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import { useFieldSchema } from '@formily/react'; import { Button, Result, Typography } from 'antd'; import React, { FC } from 'react'; import { FallbackProps } from 'react-error-boundary'; import { Trans, useTranslation } from 'react-i18next'; -import { ErrorFallbackModal } from './ErrorFallbackModal'; import { useAPIClient } from '../../../api-client'; -import { useFieldSchema } from '@formily/react'; -import { useLocation } from 'react-router-dom'; +import { useLocationNoUpdate } from '../../../application'; +import { ErrorFallbackModal } from './ErrorFallbackModal'; const { Paragraph, Text, Link } = Typography; export const useDownloadLogs = (error: any, data: Record = {}) => { - const location = useLocation(); + const location = useLocationNoUpdate(); const [loading, setLoading] = React.useState(false); const api = useAPIClient(); return { diff --git a/packages/core/client/src/schema-component/antd/grid-card/GridCard.Decorator.tsx b/packages/core/client/src/schema-component/antd/grid-card/GridCard.Decorator.tsx index 4210c0071..07341cf97 100644 --- a/packages/core/client/src/schema-component/antd/grid-card/GridCard.Decorator.tsx +++ b/packages/core/client/src/schema-component/antd/grid-card/GridCard.Decorator.tsx @@ -32,7 +32,8 @@ const InternalGridCardBlockProvider = (props) => { useEffect(() => { if (!service?.loading) { - form.setValuesIn(field.address.concat('list').toString(), service?.data?.data); + // @ts-ignore + form.fields[field.address.concat('list').toString()]?.setValue(service?.data?.data); } }, [field.address, form, service?.data?.data, service?.loading]); diff --git a/packages/core/client/src/schema-component/antd/list/List.Decorator.tsx b/packages/core/client/src/schema-component/antd/list/List.Decorator.tsx index 1cef59b51..176921d78 100644 --- a/packages/core/client/src/schema-component/antd/list/List.Decorator.tsx +++ b/packages/core/client/src/schema-component/antd/list/List.Decorator.tsx @@ -32,7 +32,10 @@ const InternalListBlockProvider = (props) => { useEffect(() => { if (!service?.loading) { - form.setValuesIn(field.address.concat('list').toString(), service?.data?.data); + form.query(/\.list$/).forEach((field) => { + // @ts-ignore + field.setValue?.(service?.data?.data); + }); } }, [field.address, form, service?.data?.data, service?.loading]); diff --git a/packages/core/client/src/schema-component/antd/list/List.tsx b/packages/core/client/src/schema-component/antd/list/List.tsx index 55976c78a..f33ec4d66 100644 --- a/packages/core/client/src/schema-component/antd/list/List.tsx +++ b/packages/core/client/src/schema-component/antd/list/List.tsx @@ -7,7 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { cx, css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import { ArrayField } from '@formily/core'; import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react'; import { List as AntdList, PaginationProps, theme } from 'antd'; diff --git a/packages/core/client/src/schema-component/antd/menu/Menu.Designer.tsx b/packages/core/client/src/schema-component/antd/menu/Menu.Designer.tsx index a67d45d06..986869aee 100644 --- a/packages/core/client/src/schema-component/antd/menu/Menu.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/menu/Menu.Designer.tsx @@ -10,7 +10,7 @@ import { TreeSelect } from '@formily/antd-v5'; import { Field, onFieldChange } from '@formily/core'; import { ISchema, Schema, useField, useFieldSchema } from '@formily/react'; -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { findByUid } from '.'; import { createDesignable, useCompile } from '../..'; @@ -210,6 +210,8 @@ const InsertMenuItems = (props) => { ); }; +const components = { TreeSelect }; + export const MenuDesigner = () => { const field = useField(); const fieldSchema = useFieldSchema(); @@ -218,48 +220,58 @@ export const MenuDesigner = () => { const { t } = useTranslation(); const menuSchema = findMenuSchema(fieldSchema); const compile = useCompile(); - const onSelect = compile(menuSchema?.['x-component-props']?.['onSelect']); - const items = toItems(menuSchema?.properties); - const effects = (form) => { - onFieldChange('target', (field: Field) => { - const [, component] = field?.value?.split?.('||') || []; - field.query('position').take((f: Field) => { - f.dataSource = - component === 'Menu.SubMenu' - ? [ - { label: t('Before'), value: 'beforeBegin' }, - { label: t('After'), value: 'afterEnd' }, - { label: t('Inner'), value: 'beforeEnd' }, - ] - : [ - { label: t('Before'), value: 'beforeBegin' }, - { label: t('After'), value: 'afterEnd' }, - ]; + const onSelect = useMemo( + () => compile(menuSchema?.['x-component-props']?.['onSelect']), + [menuSchema?.['x-component-props']?.['onSelect']], + ); + const items = useMemo(() => toItems(menuSchema?.properties), [menuSchema?.properties]); + const effects = useCallback( + (form) => { + onFieldChange('target', (field: Field) => { + const [, component] = field?.value?.split?.('||') || []; + field.query('position').take((f: Field) => { + f.dataSource = + component === 'Menu.SubMenu' + ? [ + { label: t('Before'), value: 'beforeBegin' }, + { label: t('After'), value: 'afterEnd' }, + { label: t('Inner'), value: 'beforeEnd' }, + ] + : [ + { label: t('Before'), value: 'beforeBegin' }, + { label: t('After'), value: 'afterEnd' }, + ]; + }); }); - }); - }; - const schema = { - type: 'object', - title: t('Edit menu item'), - properties: { - title: { - title: t('Menu item title'), - required: true, - 'x-decorator': 'FormItem', - 'x-component': 'Input', - 'x-component-props': {}, - }, - icon: { - title: t('Menu item icon'), - 'x-component': 'IconPicker', - 'x-decorator': 'FormItem', - }, }, - }; - const initialValues = { - title: field.title, - icon: field.componentProps.icon, - }; + [t], + ); + const schema = useMemo(() => { + return { + type: 'object', + title: t('Edit menu item'), + properties: { + title: { + title: t('Menu item title'), + required: true, + 'x-decorator': 'FormItem', + 'x-component': 'Input', + 'x-component-props': {}, + }, + icon: { + title: t('Menu item icon'), + 'x-component': 'IconPicker', + 'x-decorator': 'FormItem', + }, + }, + }; + }, [t]); + const initialValues = useMemo(() => { + return { + title: field.title, + icon: field.componentProps.icon, + }; + }, [field]); if (fieldSchema['x-component'] === 'Menu.URL') { schema.properties['href'] = { title: t('Link'), @@ -268,6 +280,90 @@ export const MenuDesigner = () => { }; initialValues['href'] = field.componentProps.href; } + const onEditSubmit: (values: any) => void = useCallback( + ({ title, icon, href }) => { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + 'x-server-hooks': [ + { + type: 'onSelfSave', + method: 'extractTextToLocale', + }, + ], + }; + if (title) { + fieldSchema.title = title; + field.title = title; + schema['title'] = title; + refresh(); + } + field.componentProps.icon = icon; + field.componentProps.href = href; + schema['x-component-props'] = { icon, href }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['icon'] = icon; + fieldSchema['x-component-props']['href'] = href; + onSelect?.({ item: { props: { schema: fieldSchema } } }); + dn.emit('patch', { + schema, + }); + }, + [fieldSchema, field, dn, refresh, onSelect], + ); + + const modalSchema = useMemo(() => { + return { + type: 'object', + title: t('Move to'), + properties: { + target: { + title: t('Target'), + enum: items, + required: true, + 'x-decorator': 'FormItem', + 'x-component': 'TreeSelect', + 'x-component-props': {}, + }, + position: { + title: t('Position'), + required: true, + enum: [ + { label: t('Before'), value: 'beforeBegin' }, + { label: t('After'), value: 'afterEnd' }, + ], + default: 'afterEnd', + 'x-component': 'Radio.Group', + 'x-decorator': 'FormItem', + }, + }, + } as ISchema; + }, [items, t]); + + const onMoveToSubmit: (values: any) => void = useCallback( + ({ target, position }) => { + const [uid] = target?.split?.('||') || []; + if (!uid) { + return; + } + const current = findByUid(menuSchema, uid); + const dn = createDesignable({ + t, + api, + refresh, + current, + }); + dn.loadAPIClientEvents(); + dn.insertAdjacent(position, fieldSchema); + }, + [fieldSchema, menuSchema, t, api, refresh], + ); + + const removeConfirmTitle = useMemo(() => { + return { + title: t('Delete menu item'), + }; + }, [t]); + return ( { eventKey="edit" schema={schema as ISchema} initialValues={initialValues} - onSubmit={({ title, icon, href }) => { - const schema = { - ['x-uid']: fieldSchema['x-uid'], - 'x-server-hooks': [ - { - type: 'onSelfSave', - method: 'extractTextToLocale', - }, - ], - }; - if (title) { - fieldSchema.title = title; - field.title = title; - schema['title'] = title; - refresh(); - } - field.componentProps.icon = icon; - field.componentProps.href = href; - schema['x-component-props'] = { icon, href }; - fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; - fieldSchema['x-component-props']['icon'] = icon; - fieldSchema['x-component-props']['href'] = href; - onSelect?.({ item: { props: { schema: fieldSchema } } }); - dn.emit('patch', { - schema, - }); - }} + onSubmit={onEditSubmit} /> { - const [uid] = target?.split?.('||') || []; - if (!uid) { - return; - } - const current = findByUid(menuSchema, uid); - const dn = createDesignable({ - t, - api, - refresh, - current, - }); - dn.loadAPIClientEvents(); - dn.insertAdjacent(position, fieldSchema); - }} + schema={modalSchema} + onSubmit={onMoveToSubmit} /> - + ); }; diff --git a/packages/core/client/src/schema-component/antd/menu/Menu.tsx b/packages/core/client/src/schema-component/antd/menu/Menu.tsx index 67bd23b2c..29b1f08bc 100644 --- a/packages/core/client/src/schema-component/antd/menu/Menu.tsx +++ b/packages/core/client/src/schema-component/antd/menu/Menu.tsx @@ -20,7 +20,7 @@ import { import { uid } from '@formily/shared'; import { error } from '@nocobase/utils/client'; import { Menu as AntdMenu, MenuProps } from 'antd'; -import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { createDesignable, DndContext, SortableItem, useDesignable, useDesigner } from '../..'; @@ -229,47 +229,52 @@ const HeaderMenu = ({ return result; }, [children, designable]); + const handleSelect = useCallback( + (info: any) => { + const s = schema.properties?.[info.key]; + + if (!s) { + return; + } + + if (mode === 'mix') { + if (s['x-component'] !== 'Menu.SubMenu') { + onSelect?.(info); + } else { + const menuItemSchema = findMenuItem(s); + if (!menuItemSchema) { + return onSelect?.(info); + } + // TODO + setLoading(true); + const keys = findKeysByUid(schema, menuItemSchema['x-uid']); + setDefaultSelectedKeys(keys); + setTimeout(() => { + setLoading(false); + }, 100); + onSelect?.({ + key: menuItemSchema.name, + item: { + props: { + schema: menuItemSchema, + }, + }, + }); + } + } else { + onSelect?.(info); + } + }, + [schema, mode, onSelect, setLoading, setDefaultSelectedKeys], + ); + return ( <> { - const s = schema.properties?.[info.key]; - - if (!s) { - return; - } - - if (mode === 'mix') { - if (s['x-component'] !== 'Menu.SubMenu') { - onSelect?.(info); - } else { - const menuItemSchema = findMenuItem(s); - if (!menuItemSchema) { - return onSelect?.(info); - } - // TODO - setLoading(true); - const keys = findKeysByUid(schema, menuItemSchema['x-uid']); - setDefaultSelectedKeys(keys); - setTimeout(() => { - setLoading(false); - }, 100); - onSelect?.({ - key: menuItemSchema.name, - item: { - props: { - schema: menuItemSchema, - }, - }, - }); - } - } else { - onSelect?.(info); - } - }} + onSelect={handleSelect} mode={mode === 'mix' ? 'horizontal' : mode} defaultOpenKeys={defaultOpenKeys} defaultSelectedKeys={defaultSelectedKeys} @@ -347,12 +352,8 @@ const SideMenu = ({ mode={'inline'} openKeys={openKeys} selectedKeys={selectedKeys} - onSelect={(info) => { - onSelect?.(info); - }} - onOpenChange={(openKeys) => { - setOpenKeys(openKeys); - }} + onSelect={onSelect} + onOpenChange={setOpenKeys} className={sideMenuClass} items={items as MenuProps['items']} /> @@ -496,6 +497,14 @@ export const Menu: ComposedMenu = observer( { displayName: 'Menu' }, ); +const menuItemTitleStyle = { + overflow: 'hidden', + textOverflow: 'ellipsis', + display: 'inline-block', + width: '100%', + verticalAlign: 'middle', +}; + Menu.Item = observer( (props) => { const { t } = useMenuTranslation(); @@ -521,17 +530,7 @@ Menu.Item = observer( removeParentsIfNoChildren={false} > - - {t(field.title)} - + {t(field.title)} diff --git a/packages/core/client/src/schema-component/antd/page/style.ts b/packages/core/client/src/schema-component/antd/page/Page.style.ts similarity index 98% rename from packages/core/client/src/schema-component/antd/page/style.ts rename to packages/core/client/src/schema-component/antd/page/Page.style.ts index 308243abf..dda910166 100644 --- a/packages/core/client/src/schema-component/antd/page/style.ts +++ b/packages/core/client/src/schema-component/antd/page/Page.style.ts @@ -7,7 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { genStyleHook } from './../__builtins__'; +import { genStyleHook } from '../__builtins__'; export const useStyles = genStyleHook('nb-page', (token) => { const { componentCls } = token; diff --git a/packages/core/client/src/schema-component/antd/page/Page.tsx b/packages/core/client/src/schema-component/antd/page/Page.tsx index 52f60ae40..5292be922 100644 --- a/packages/core/client/src/schema-component/antd/page/Page.tsx +++ b/packages/core/client/src/schema-component/antd/page/Page.tsx @@ -13,13 +13,14 @@ import { FormLayout } from '@formily/antd-v5'; import { Schema, SchemaOptionsContext, useFieldSchema } from '@formily/react'; import { Button, Tabs } from 'antd'; import classNames from 'classnames'; -import React, { useContext, useEffect, useMemo, useState } from 'react'; +import React, { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { useTranslation } from 'react-i18next'; -import { useSearchParams } from 'react-router-dom'; +import { Outlet, useOutletContext, useParams, useSearchParams } from 'react-router-dom'; import { FormDialog } from '..'; import { useStyles as useAClStyles } from '../../../acl/style'; import { useRequest } from '../../../api-client'; +import { useNavigateNoUpdate } from '../../../application/CustomRouterContextProvider'; import { useAppSpin } from '../../../application/hooks/useAppSpin'; import { useDocumentTitle } from '../../../document-title'; import { useGlobalTheme } from '../../../global-theme'; @@ -32,8 +33,8 @@ import { useCompile, useDesignable } from '../../hooks'; import { useToken } from '../__builtins__'; import { ErrorFallback } from '../error-fallback'; import FixedBlock from './FixedBlock'; +import { useStyles } from './Page.style'; import { PageDesigner, PageTabDesigner } from './PageTabDesigner'; -import { useStyles } from './style'; export const Page = (props) => { const { children, ...others } = props; @@ -44,6 +45,7 @@ export const Page = (props) => { const dn = useDesignable(); const { theme } = useGlobalTheme(); const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer(); + const { tabUid, name: pageUid } = useParams(); // react18 tab 动画会卡顿,所以第一个 tab 时,动画禁用,后面的 tab 才启用 const [hasMounted, setHasMounted] = useState(false); @@ -62,11 +64,13 @@ export const Page = (props) => { const enablePageTabs = fieldSchema['x-component-props']?.enablePageTabs; const hidePageTitle = fieldSchema['x-component-props']?.hidePageTitle; const options = useContext(SchemaOptionsContext); - const [searchParams, setSearchParams] = useSearchParams(); + const navigate = useNavigateNoUpdate(); + const [searchParams] = useSearchParams(); const [loading, setLoading] = useState(false); const activeKey = useMemo( - () => searchParams.get('tab') || Object.keys(fieldSchema.properties || {}).shift(), - [fieldSchema.properties, searchParams], + // 处理 searchParams 是为了兼容旧版的 tab 参数 + () => tabUid || searchParams.get('tab') || Object.keys(fieldSchema.properties || {}).shift(), + [fieldSchema.properties, searchParams, tabUid], ); const [height, setHeight] = useState(0); const { wrapSSR, hashId, componentCls } = useStyles(); @@ -87,9 +91,115 @@ export const Page = (props) => { }, ); - const handleErrors = (error) => { + const handleErrors = useCallback((error) => { console.error(error); - }; + }, []); + + const footer = useMemo(() => { + return enablePageTabs ? ( + + { + setLoading(true); + navigate(`/admin/${pageUid}/tabs/${activeKey}`); + setTimeout(() => { + setLoading(false); + }, 50); + }} + tabBarExtraContent={ + dn.designable && ( + + ) + } + items={fieldSchema.mapProperties((schema) => { + return { + label: ( + + {schema['x-icon'] && } + {schema.title || t('Unnamed')} + + + ), + key: schema.name as string, + }; + })} + /> + + ) : null; + }, [ + hasMounted, + activeKey, + fieldSchema, + dn.designable, + options.scope, + options.components, + pageUid, + fieldSchema.mapProperties((schema) => schema.title || t('Unnamed')).join(), + enablePageTabs, + ]); return wrapSSR(
@@ -106,155 +216,94 @@ export const Page = (props) => { // 如果标题为空的时候会导致 PageHeader 不渲染,所以这里设置一个空白字符,然后再设置高度为 0 title={pageHeaderTitle || ' '} {...others} - footer={ - enablePageTabs && ( - - { - setLoading(true); - setSearchParams([['tab', activeKey]]); - setTimeout(() => { - setLoading(false); - }, 50); - }} - tabBarExtraContent={ - dn.designable && ( - - ) - } - items={fieldSchema.mapProperties((schema) => { - return { - label: ( - - {schema['x-icon'] && } - {schema.title || t('Unnamed')} - - - ), - key: schema.name as string, - }; - })} - /> - - ) - } + footer={footer} /> )}
- {PageContent(loading, disablePageHeader, enablePageTabs, fieldSchema, activeKey, height, props)} + {tabUid ? ( + // used to match the rout with name "admin.page.tab" + + ) : ( + <> + + {/* Used to match the route with name "admin.page.popup" */} + + + )}
, ); }; +export const PageTabs = () => { + const { loading, disablePageHeader, enablePageTabs, fieldSchema, height, tabUid } = useOutletContext(); + return ( + <> + + {/* used to match the route with name "admin.page.tab.popup" */} + + + ); +}; + Page.displayName = 'Page'; -function PageContent( - loading: boolean, - disablePageHeader: any, - enablePageTabs: any, - fieldSchema: Schema, - activeKey: string, - height: number, - props: any, -): React.ReactNode { - const { token } = useToken(); - const { render } = useAppSpin(); +const PageContent = memo( + ({ + loading, + disablePageHeader, + enablePageTabs, + fieldSchema, + activeKey, + height, + }: { + loading: boolean; + disablePageHeader: any; + enablePageTabs: any; + fieldSchema: Schema; + activeKey: string; + height: number; + }) => { + const { token } = useToken(); + const { render } = useAppSpin(); - if (loading) { - return render(); - } + if (loading) { + return render(); + } - return !disablePageHeader && enablePageTabs ? ( - fieldSchema.mapProperties((schema) => { - if (schema.name !== activeKey) return null; + return ( + <> + {!disablePageHeader && enablePageTabs ? ( + fieldSchema.mapProperties((schema) => { + if (schema.name !== activeKey) return null; - return ( - - - - ); - }) - ) : ( - -
- -
-
- ); -} + return ( + + + + ); + }) + ) : ( + +
+ +
+
+ )} + + ); + }, +); +PageContent.displayName = 'PageContent'; diff --git a/packages/core/client/src/schema-component/antd/page/PagePopups.tsx b/packages/core/client/src/schema-component/antd/page/PagePopups.tsx new file mode 100644 index 000000000..825affa36 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/page/PagePopups.tsx @@ -0,0 +1,245 @@ +/** + * 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 { ISchema } from '@formily/json-schema'; +import _ from 'lodash'; +import { FC, default as React, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Location, useLocation } from 'react-router-dom'; +import { useAPIClient } from '../../../api-client'; +import { DataBlockProvider } from '../../../data-source/data-block/DataBlockProvider'; +import { BlockRequestContext } from '../../../data-source/data-block/DataBlockRequestProvider'; +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 { + PopupContext, + getPopupContextFromActionOrAssociationFieldSchema, +} from './usePopupContextInActionOrAssociationField'; + +interface PopupsVisibleProviderProps { + visible: boolean; + setVisible?: (value: boolean) => void; +} + +interface PopupProps { + params: PopupParams; + context: PopupContext; +} + +export const PopupVisibleProviderContext = React.createContext(null); +export const PopupParamsProviderContext = React.createContext(null); +PopupVisibleProviderContext.displayName = 'PopupVisibleProviderContext'; +PopupParamsProviderContext.displayName = 'PopupParamsProviderContext'; + +export const usePopupContextAndParams = () => { + const context = React.useContext(PopupParamsProviderContext); + return (context || {}) as PopupProps; +}; + +/** + * The difference between this component and ActionContextProvider is that + * this component is only used to control the popups in the PagePopupsItem component (excluding the nested popups within it). + * @param param0 + * @returns + */ +export const PopupVisibleProvider: FC = ({ children, visible, setVisible }) => { + return ( + + {children} + + ); +}; + +const PopupParamsProvider: FC = (props) => { + const value = useMemo(() => { + return { params: props.params, context: props.context }; + }, [props.params, props.context]); + return {props.children}; +}; + +const PopupTabsPropsProvider: FC<{ params: PopupParams }> = ({ children, params }) => { + const { changeTab } = usePagePopup(); + const onTabClick = useCallback( + (key: string) => { + changeTab(key); + }, + [changeTab], + ); + const { isPopupVisibleControlledByURL } = usePopupSettings(); + + if (!isPopupVisibleControlledByURL) { + return <>{children}; + } + + return ( + + {children} + + ); +}; + +const PagePopupsItemProvider: FC<{ params: PopupParams; context: PopupContext }> = ({ params, context, children }) => { + const { closePopup } = usePagePopup(); + const [visible, _setVisible] = useState(true); + const setVisible = (visible: boolean) => { + if (!visible) { + _setVisible(false); + + if (process.env.__E2E__) { + setTimeout(() => { + closePopup(); + // Deleting here ensures that the next time the same popup is opened, it will generate another random key. + deleteRandomNestedSchemaKey(params.popupuid); + }); + return; + } + + // Leave some time to refresh the block data + setTimeout(() => { + closePopup(); + // Deleting here ensures that the next time the same popup is opened, it will generate another random key. + deleteRandomNestedSchemaKey(params.popupuid); + }, 300); + } + }; + const storedContext = { ...getStoredPopupContext(params.popupuid) }; + + if (!context) { + context = storedContext; + } + + return ( + + + + {/* Pass the service of the block where the button is located down, to refresh the block's data when the popup is closed */} + + +
{children}
+
+
+
+
+
+ ); +}; + +/** + * insert childSchema to parentSchema to render the nested popups + * @param childSchema + * @param props + * @param parentSchema + */ +export const insertChildToParentSchema = (childSchema: ISchema, props: PopupProps, parentSchema: ISchema) => { + const { params, context } = props; + + const componentSchema = { + type: 'void', + 'x-component': 'PagePopupsItemProvider', + 'x-component-props': { + params, + context, + }, + properties: { + popupAction: childSchema, + }, + }; + + // If we don't use a random name, it will cause the component's parameters not to be updated when reopening the popup + const nestedPopupKey = getRandomNestedSchemaKey(params.popupuid); + + if (parentSchema.properties) { + const popupSchema = _.get(parentSchema.properties, Object.keys(parentSchema.properties)[0]); + if (_.isEmpty(_.get(popupSchema, `properties.${nestedPopupKey}`))) { + _.set(popupSchema, `properties.${nestedPopupKey}`, componentSchema); + } + } +}; + +export const PagePopups = (props: { paramsList?: PopupParams[] }) => { + const location = useLocation(); + const popupParams = props.paramsList || getPopupParamsFromPath(getPopupPath(location)); + const { requestSchema } = useRequestSchema(); + const [rootSchema, setRootSchema] = useState(null); + const popupPropsRef = useRef([]); + + useEffect(() => { + const run = async () => { + const waitList = popupParams.map( + (params) => getStoredPopupContext(params.popupuid)?.schema || requestSchema(params.popupuid), + ); + const schemas = await Promise.all(waitList); + const clonedSchemas = schemas.map((schema) => { + const result = _.cloneDeep(_.omit(schema, 'parent')); + result['x-read-pretty'] = true; + return result; + }); + popupPropsRef.current = clonedSchemas.map((schema, index) => { + const schemaContext = getPopupContextFromActionOrAssociationFieldSchema(schema); + return { + params: popupParams[index], + context: schemaContext, + }; + }); + const rootSchema = clonedSchemas[0]; + for (let i = 1; i < clonedSchemas.length; i++) { + insertChildToParentSchema(clonedSchemas[i], popupPropsRef.current[i], clonedSchemas[i - 1]); + } + setRootSchema(rootSchema); + }; + run(); + }, [popupParams, requestSchema]); + + const components = useMemo(() => ({ PagePopupsItemProvider }), []); + + if (!rootSchema) { + return null; + } + + return ( + + ; + + ); +}; + +export const useRequestSchema = () => { + const api = useAPIClient(); + + const requestSchema = useCallback(async (uid: string) => { + const data = await api.request({ + url: `/uiSchemas:getJsonSchema/${uid}`, + }); + return data.data?.data as ISchema; + }, []); + + return { requestSchema }; +}; + +/** + * The reason why we don't use the decoded data returned by useParams here is because we need the raw values. + * @param location + * @returns + */ +export const getPopupPath = (location: Location) => { + const [, ...popupsPath] = location.pathname.split('/popups/'); + return popupsPath.join('/popups/'); +}; diff --git a/packages/core/client/src/schema-component/antd/page/PopupSettingsProvider.tsx b/packages/core/client/src/schema-component/antd/page/PopupSettingsProvider.tsx new file mode 100644 index 000000000..986806b86 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/page/PopupSettingsProvider.tsx @@ -0,0 +1,45 @@ +/** + * 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 React, { FC, useMemo } from 'react'; + +interface PopupSettings { + /** + * @default true + */ + isPopupVisibleControlledByURL: boolean; +} + +const PopupSettingsContext = React.createContext(null); + +/** + * Provider component for the popup settings. + * @param props - The popup settings. + */ +export const PopupSettingsProvider: FC = (props) => { + const { isPopupVisibleControlledByURL } = props; + + const value = useMemo(() => { + return { isPopupVisibleControlledByURL }; + }, [isPopupVisibleControlledByURL]); + + return {props.children}; +}; + +/** + * Hook for accessing the popup settings. + * @returns The popup settings. + */ +export const usePopupSettings = () => { + return ( + React.useContext(PopupSettingsContext) || { + isPopupVisibleControlledByURL: true, + } + ); +}; diff --git a/packages/core/client/src/schema-component/antd/page/SubPages.style.ts b/packages/core/client/src/schema-component/antd/page/SubPages.style.ts new file mode 100644 index 000000000..f531ac62c --- /dev/null +++ b/packages/core/client/src/schema-component/antd/page/SubPages.style.ts @@ -0,0 +1,25 @@ +/** + * 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 { createStyles } from 'antd-style'; + +export const useSubPagesStyle = createStyles(({ css, token }: any) => { + return { + container: css` + .ant-tabs-nav { + background: ${token.colorBgContainer}; + padding: 0 ${token.paddingPageVertical}px; + margin-bottom: 0; + } + .ant-tabs-content-holder { + padding: ${token.paddingPageVertical}px; + } + `, + }; +}); diff --git a/packages/core/client/src/schema-component/antd/page/SubPages.tsx b/packages/core/client/src/schema-component/antd/page/SubPages.tsx new file mode 100644 index 000000000..af733fa4a --- /dev/null +++ b/packages/core/client/src/schema-component/antd/page/SubPages.tsx @@ -0,0 +1,279 @@ +/** + * 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 { ISchema, RecursionField, useFieldSchema } from '@formily/react'; +import _ from 'lodash'; +import React, { FC, useCallback, useContext, useEffect, useState } from 'react'; +import { Location, useLocation } from 'react-router-dom'; +import { useNavigateNoUpdate } from '../../../application/CustomRouterContextProvider'; +import { + useCollectionParentRecord, + useCollectionRecord, + useCollectionRecordData, +} from '../../../data-source/collection-record/CollectionRecordProvider'; +import { useAssociationName } from '../../../data-source/collection/AssociationProvider'; +import { useCollectionManager } from '../../../data-source/collection/CollectionManagerProvider'; +import { useCollection } from '../../../data-source/collection/CollectionProvider'; +import { DataBlockProvider } from '../../../data-source/data-block/DataBlockProvider'; +import { useDataBlockRequest } from '../../../data-source/data-block/DataBlockRequestProvider'; +import { useDataSourceKey } from '../../../data-source/data-source/DataSourceProvider'; +import { TreeRecordProvider, useTreeParentRecord } from '../../../modules/blocks/data-blocks/table/TreeRecordProvider'; +import { + VariablePopupRecordProvider, + useCurrentPopupRecord, +} from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider'; +import { ActionContext } from '../action/context'; +import { TabsContextProvider } from '../tabs/context'; +import { PagePopups, useRequestSchema } from './PagePopups'; +import { usePopupSettings } from './PopupSettingsProvider'; +import { useSubPagesStyle } from './SubPages.style'; +import { + PopupParams, + decodePathValue, + encodePathValue, + getPopupParamsFromPath, + getStoredPopupContext, + storePopupContext, + withSearchParams, +} from './pagePopupUtils'; +import { + SubPageContext, + getPopupContextFromActionOrAssociationFieldSchema, + usePopupContextInActionOrAssociationField, +} from './usePopupContextInActionOrAssociationField'; + +export interface SubPageParams extends Omit { + /** sub page uid */ + subpageuid: string; +} + +const SubPageTabsPropsProvider: FC<{ params: SubPageParams }> = (props) => { + const navigate = useNavigateNoUpdate(); + const onTabClick = useCallback((key: string) => { + let pathname = window.location.pathname.split('/tab/')[0]; + if (pathname.endsWith('/')) { + pathname = pathname.slice(0, -1); + } + navigate(`${pathname}/tab/${key}`); + }, []); + + return ( + + {props.children} + + ); +}; + +const TreeRecordProviderInSubPage: FC = (props) => { + const recordData = useCollectionRecordData(); + return {props.children}; +}; + +const SubPageProvider: FC<{ params: SubPageParams; context: SubPageContext | undefined; actionType: string }> = ( + props, +) => { + const { params, context } = props; + + if (!context) { + return null; + } + + const nodes = { + addChild: {props.children}, + '': {props.children}, + }; + + const commonElements = ( + + {nodes[props.actionType]} + + ); + + if (context.parentPopupRecord) { + return ( + + {commonElements} + + ); + } + + return commonElements; +}; + +export const SubPage = () => { + const location = useLocation(); + const { subPageParams, popupParams } = getSubPageParamsAndPopupsParams(getSubPagePath(location)); + const { styles } = useSubPagesStyle(); + const { requestSchema } = useRequestSchema(); + const [actionSchema, setActionSchema] = useState(null); + + useEffect(() => { + const run = async () => { + const stored = getStoredPopupContext(subPageParams.subpageuid); + + if (stored) { + return setActionSchema(stored.schema); + } + + const schema = await requestSchema(subPageParams.subpageuid); + setActionSchema(schema); + }; + run(); + }, [subPageParams.subpageuid]); + + // When the URL changes, this component may be re-rendered, because at this time the Schema is still old, so there may be some issues, so here is a judgment. + if (!actionSchema || actionSchema['x-uid'] !== subPageParams.subpageuid) { + return null; + } + + const subPageSchema = Object.values(actionSchema.properties)[0] as ISchema; + const context = getPopupContextFromActionOrAssociationFieldSchema(actionSchema) as SubPageContext; + const addChild = actionSchema?.['x-component-props']?.addChild; + + return ( +
+ + + {_.isEmpty(popupParams) ? null : } + +
+ ); +}; + +export const getSubPagePathFromParams = (params: SubPageParams) => { + const { subpageuid, tab, filterbytk } = params; + const popupPath = [subpageuid, filterbytk && 'filterbytk', filterbytk, tab && 'tab', tab].filter(Boolean); + + return `/subpages/${popupPath.map((item) => encodePathValue(item)).join('/')}`; +}; + +export const getSubPageParamsFromPath = _.memoize((path: string) => { + const [subPageUid, ...subPageParams] = path.split('/').filter(Boolean); + const result = {}; + + for (let i = 0; i < subPageParams.length; i += 2) { + result[subPageParams[i]] = decodePathValue(subPageParams[i + 1]); + } + + return { + subpageuid: subPageUid, + ...result, + } as SubPageParams; +}); + +export const useNavigateTOSubPage = () => { + const navigate = useNavigateNoUpdate(); + const fieldSchema = useFieldSchema(); + const dataSourceKey = useDataSourceKey(); + const record = useCollectionRecord(); + const parentRecord = useCollectionParentRecord(); + const collection = useCollection(); + const cm = useCollectionManager(); + const association = useAssociationName(); + const { updatePopupContext } = usePopupContextInActionOrAssociationField(); + const { value: parentPopupRecordData, collection: parentPopupRecordCollection } = useCurrentPopupRecord() || {}; + const { isPopupVisibleControlledByURL } = usePopupSettings(); + const { setVisible: setVisibleFromAction } = useContext(ActionContext); + const service = useDataBlockRequest(); + const treeParentRecord = useTreeParentRecord(); + + const navigateToSubPage = useCallback(() => { + if (!fieldSchema['x-uid']) { + return; + } + + if (!isPopupVisibleControlledByURL) { + return setVisibleFromAction?.(true); + } + + const filterByTK = (record?.data || treeParentRecord)?.[collection.getPrimaryKey()]; + const sourceId = parentRecord?.data?.[cm.getCollection(association?.split('.')[0])?.getPrimaryKey()]; + const params = { + subpageuid: fieldSchema['x-uid'], + filterbytk: filterByTK, + }; + + storePopupContext(fieldSchema['x-uid'], { + schema: fieldSchema, + record, + parentRecord, + service, + dataSource: dataSourceKey, + collection: collection.name, + association, + sourceId, + parentPopupRecord: parentPopupRecordData + ? { + collection: parentPopupRecordCollection?.name, + filterByTk: parentPopupRecordData[parentPopupRecordCollection.getPrimaryKey()], + } + : undefined, + }); + + updatePopupContext({ + dataSource: dataSourceKey, + collection: association ? undefined : collection.name, + association: association, + sourceId, + parentPopupRecord: parentPopupRecordData + ? { + collection: parentPopupRecordCollection?.name, + filterByTk: parentPopupRecordData[parentPopupRecordCollection.getPrimaryKey()], + } + : undefined, + }); + + const pathname = getSubPagePathFromParams(params); + navigate(withSearchParams(`/admin${pathname}`)); + }, [ + fieldSchema, + navigate, + dataSourceKey, + record, + parentRecord, + collection, + cm, + association, + parentPopupRecordData, + isPopupVisibleControlledByURL, + service, + ]); + + return { navigateToSubPage }; +}; + +export const getSubPageParamsAndPopupsParams = _.memoize((path: string) => { + const [pagePath, ...popupsPath] = path.split('/popups/'); + const subPageParams = getSubPageParamsFromPath(pagePath); + const popupParams = getPopupParamsFromPath(popupsPath.join('/popups/')); + + return { subPageParams, popupParams }; +}); + +/** + * The reason why we don't use the decoded data returned by useParams here is because we need the raw values. + * @param location + * @returns + */ +export function getSubPagePath(location: Location) { + const [, subPagePath] = location.pathname.split('/admin/subpages/'); + return subPagePath || ''; +} diff --git a/packages/core/client/src/schema-component/antd/page/__tests__/PagePopups.test.tsx b/packages/core/client/src/schema-component/antd/page/__tests__/PagePopups.test.tsx new file mode 100644 index 000000000..a48a4b030 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/page/__tests__/PagePopups.test.tsx @@ -0,0 +1,101 @@ +/** + * 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 { getPopupPath, insertChildToParentSchema } from '../PagePopups'; + +vi.mock('@formily/shared', async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...actual, + uid() { + return 'nestedPopup'; + }, + }; +}); + +describe('insertToPopupSchema', () => { + it('should insert childSchema to parentSchema', () => { + const childSchema = { + type: 'string', + 'x-component': 'Input', + }; + const params: any = { + foo: 'bar', + }; + const context: any = { + bar: 'foo', + }; + const parentSchema = { + type: 'object', + properties: { + popup: { + type: 'void', + properties: {}, + }, + }, + }; + + insertChildToParentSchema(childSchema, { params, context }, parentSchema); + + expect(parentSchema).toEqual({ + type: 'object', + properties: { + popup: { + type: 'void', + properties: { + nestedPopup: { + type: 'void', + 'x-component': 'PagePopupsItemProvider', + 'x-component-props': { + params, + context, + }, + properties: { + popupAction: childSchema, + }, + }, + }, + }, + }, + }); + }); +}); + +describe('getPopupPath', () => { + it('should return the popup path', () => { + const location: any = { + pathname: '/Users/Apple/Projects/nocobase/packages/core/client/src/schema-component/antd/page/popups/nestedPopup', + }; + + const result = getPopupPath(location); + + expect(result).toEqual('nestedPopup'); + }); + + it('should return the nested popup path', () => { + const location: any = { + pathname: + '/Users/Apple/Projects/nocobase/packages/core/client/src/schema-component/antd/page/popups/nestedPopup/abc/def/popups/nestedPopup2/abc2/def2', + }; + + const result = getPopupPath(location); + + expect(result).toEqual('nestedPopup/abc/def/popups/nestedPopup2/abc2/def2'); + }); + + it('should return an empty string if there is no popup path', () => { + const location: any = { + pathname: '/Users/Apple/Projects/nocobase/packages/core/client/src/schema-component/antd/page', + }; + + const result = getPopupPath(location); + + expect(result).toEqual(''); + }); +}); diff --git a/packages/core/client/src/schema-component/antd/page/__tests__/SubPages.test.tsx b/packages/core/client/src/schema-component/antd/page/__tests__/SubPages.test.tsx new file mode 100644 index 000000000..550ee6409 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/page/__tests__/SubPages.test.tsx @@ -0,0 +1,120 @@ +/** + * 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 { + getSubPageParamsAndPopupsParams, + getSubPageParamsFromPath, + getSubPagePath, + getSubPagePathFromParams, +} from '../SubPages'; + +describe('getSubPagePathFromParams', () => { + it('should generate the correct subpage path', () => { + const params = { + subpageuid: 'subPage1', + filterbytk: 'filterbytk1', + tab: 'tab1', + }; + const expectedPath = '/subpages/subPage1/filterbytk/filterbytk1/tab/tab1'; + expect(getSubPagePathFromParams(params)).toBe(expectedPath); + }); + + it('should generate the correct subpage path without optional parameters', () => { + const params = { + subpageuid: 'subPage1', + filterbytk: 'filterbytk1', + }; + const expectedPath = '/subpages/subPage1/filterbytk/filterbytk1'; + expect(getSubPagePathFromParams(params)).toBe(expectedPath); + }); + + it('when exist popups in path', () => { + const params = { + subpageuid: 'subPage1', + filterbytk: 'popups', + tab: 'popups', + }; + const expectedPath = `/subpages/subPage1/filterbytk/${window.btoa('popups')}/tab/${window.btoa('popups')}`; + expect(getSubPagePathFromParams(params)).toBe(expectedPath); + }); +}); + +describe('getSubPageParamsAndPopupsParams', () => { + it('should return the correct subPageParams and popupParams', () => { + const path = + 'subPage1/datasource/datasource1/filterbytk/filterbytk1/popups/popupuid1/key1/value1/popups/popupuid2/key2/value2'; + const expectedSubPageParams = { + subpageuid: 'subPage1', + datasource: 'datasource1', + filterbytk: 'filterbytk1', + }; + const expectedPopupParams = [ + { popupuid: 'popupuid1', key1: 'value1' }, + { popupuid: 'popupuid2', key2: 'value2' }, + ]; + expect(getSubPageParamsAndPopupsParams(path)).toEqual({ + subPageParams: expectedSubPageParams, + popupParams: expectedPopupParams, + }); + }); + + it('should return the correct subPageParams and empty popupParams', () => { + const path = 'subPage1/datasource/datasource1/filterbytk/filterbytk1'; + const expectedSubPageParams = { + subpageuid: 'subPage1', + datasource: 'datasource1', + filterbytk: 'filterbytk1', + }; + const expectedPopupParams: string[] = []; + expect(getSubPageParamsAndPopupsParams(path)).toEqual({ + subPageParams: expectedSubPageParams, + popupParams: expectedPopupParams, + }); + }); +}); + +describe('getSubPageParamsFromPath', () => { + it('should return the correct subPageParams from path without popups', () => { + const path = 'subPage1/datasource/datasource1/filterbytk/filterbytk1'; + const expectedSubPageParams = { + subpageuid: 'subPage1', + datasource: 'datasource1', + filterbytk: 'filterbytk1', + }; + expect(getSubPageParamsFromPath(path)).toEqual(expectedSubPageParams); + }); + + it('when exist popups in path', () => { + const path = `subPage1/datasource/datasource1/filterbytk/${window.btoa('popups')}`; + const expectedSubPageParams = { + subpageuid: 'subPage1', + datasource: 'datasource1', + filterbytk: 'popups', + }; + expect(getSubPageParamsFromPath(path)).toEqual(expectedSubPageParams); + }); +}); + +describe('getSubPagePath', () => { + it('should return the subpage path', () => { + const location: any = { + pathname: '/admin/subpages/subPage1/filterbytk/filterbytk1/tab/tab1', + }; + const expectedPath = 'subPage1/filterbytk/filterbytk1/tab/tab1'; + expect(getSubPagePath(location)).toBe(expectedPath); + }); + + it('should return an empty string if subpage path is not found', () => { + const location: any = { + pathname: '/admin', + }; + const expectedPath = ''; + expect(getSubPagePath(location)).toBe(expectedPath); + }); +}); diff --git a/packages/core/client/src/schema-component/antd/page/__tests__/pagePopupUtils.test.ts b/packages/core/client/src/schema-component/antd/page/__tests__/pagePopupUtils.test.ts new file mode 100644 index 000000000..239d29108 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/page/__tests__/pagePopupUtils.test.ts @@ -0,0 +1,130 @@ +/** + * 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 { getPopupParamsFromPath, getPopupPathFromParams, removeLastPopupPath } from '../pagePopupUtils'; + +describe('getPopupParamsFromPath', () => { + it('should parse the path and return the popup parameters', () => { + const path = 'popupUid/filterbytk/filterByTKValue/tab/tabValue'; + const result = getPopupParamsFromPath(path); + + expect(result).toEqual([ + { + popupuid: 'popupUid', + filterbytk: 'filterByTKValue', + tab: 'tabValue', + }, + ]); + }); + + it('should handle multiple popups in the path', () => { + const path = + 'popupUid1/filterbytk/filterByTKValue1/tab/tabValue1/popups/popupUid2/filterbytk/filterByTKValue2/tab/tabValue2'; + const result = getPopupParamsFromPath(path); + + expect(result).toEqual([ + { + popupuid: 'popupUid1', + filterbytk: 'filterByTKValue1', + tab: 'tabValue1', + }, + { + popupuid: 'popupUid2', + filterbytk: 'filterByTKValue2', + tab: 'tabValue2', + }, + ]); + }); + + it('when exist popups in path', () => { + const path = `popupUid1/filterbytk/${window.btoa('popups')}/tab/${window.btoa( + 'popups', + )}/popups/popupUid2/filterbytk/filterByTKValue2/tab/tabValue2`; + + const result = getPopupParamsFromPath(path); + + expect(result).toEqual([ + { + popupuid: 'popupUid1', + filterbytk: 'popups', + tab: 'popups', + }, + { + popupuid: 'popupUid2', + filterbytk: 'filterByTKValue2', + tab: 'tabValue2', + }, + ]); + }); +}); + +describe('getPopupPathFromParams', () => { + it('should generate the popup path from the parameters', () => { + const params = { + popupuid: 'popupUid', + filterbytk: 'filterByTKValue', + tab: 'tabValue', + }; + const result = getPopupPathFromParams(params); + + expect(result).toBe('/popups/popupUid/filterbytk/filterByTKValue/tab/tabValue'); + }); + + it('should handle optional parameters', () => { + const params = { + popupuid: 'popupUid', + filterbytk: 'filterByTKValue', + tab: 'tabValue', + empty: undefined, + }; + const result = getPopupPathFromParams(params); + + expect(result).toBe('/popups/popupUid/filterbytk/filterByTKValue/tab/tabValue'); + }); + + it('when exist popups in path', () => { + const params = { + popupuid: 'popupUid', + filterbytk: 'popups', + tab: 'popups', + }; + + const result = getPopupPathFromParams(params); + + expect(result).toBe(`/popups/popupUid/filterbytk/${window.btoa('popups')}/tab/${window.btoa('popups')}`); + }); +}); + +describe('removeLastPopupPath', () => { + it('should remove the last popup path from the given path', () => { + const path1 = '/admin/page/popups/popupUid/popups/popupUid2'; + const result1 = removeLastPopupPath(path1); + + expect(result1).toBe('/admin/page/popups/popupUid/'); + + const path2 = '/admin/page/popups/popupUid'; + const result2 = removeLastPopupPath(path2); + + expect(result2).toBe('/admin/page/'); + }); + + it('should handle paths without popups', () => { + const path = '/admin/page'; + const result = removeLastPopupPath(path); + + expect(result).toBe(path); + }); + + it('should handle empty paths', () => { + const path = ''; + const result = removeLastPopupPath(path); + + expect(result).toBe(''); + }); +}); diff --git a/packages/core/client/src/schema-component/antd/page/__tests__/usePopupContextInActionOrAssociationField.test.ts b/packages/core/client/src/schema-component/antd/page/__tests__/usePopupContextInActionOrAssociationField.test.ts new file mode 100644 index 000000000..f9fa14221 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/page/__tests__/usePopupContextInActionOrAssociationField.test.ts @@ -0,0 +1,100 @@ +/** + * 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 { renderHook } from '@testing-library/react-hooks'; +import { PopupContext, usePopupContextInActionOrAssociationField } from '../usePopupContextInActionOrAssociationField'; + +vi.mock('../../../hooks/useDesignable', async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...actual, + useDesignable() { + return { dn: dnMock }; + }, + }; +}); + +vi.mock('@formily/react', async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...actual, + useFieldSchema() { + return fieldSchemaMock; + }, + }; +}); + +let fieldSchemaMock = null; +let dnMock = null; + +describe('usePopupContextInActionOrAssociationField', () => { + test('updatePopupContext should update the x-action-context field in the popup schema', () => { + fieldSchemaMock = { + properties: { + drawer: { + 'x-uid': 'drawer', + }, + }, + 'x-uid': 'fieldSchemaMock', + }; + + dnMock = { + emit: vi.fn(), + }; + + const { result } = renderHook(() => usePopupContextInActionOrAssociationField()); + + const context: PopupContext = { + dataSource: 'dataSource', + collection: 'collection', + association: 'association', + sourceId: 'sourceId', + }; + + result.current.updatePopupContext(context); + + expect(dnMock.emit).toHaveBeenCalledWith('patch', { + schema: { + 'x-uid': fieldSchemaMock['x-uid'], + 'x-action-context': context, + }, + }); + expect(dnMock.emit).toHaveBeenCalledTimes(1); + expect(fieldSchemaMock).toEqual({ + properties: { + drawer: { + 'x-uid': 'drawer', + }, + }, + 'x-action-context': context, + 'x-uid': 'fieldSchemaMock', + }); + + // Updating with the same values again should not trigger emit + result.current.updatePopupContext(context); + expect(dnMock.emit).toHaveBeenCalledTimes(1); + + // It will filter out null and undefined values + result.current.updatePopupContext({ + ...context, + collection: undefined, + sourceId: null, + }); + expect(dnMock.emit).toHaveBeenCalledWith('patch', { + schema: { + 'x-uid': fieldSchemaMock['x-uid'], + 'x-action-context': { + dataSource: 'dataSource', + association: 'association', + }, + }, + }); + expect(dnMock.emit).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/core/client/src/schema-component/antd/page/index.ts b/packages/core/client/src/schema-component/antd/page/index.ts index c87309eaf..b0efef865 100644 --- a/packages/core/client/src/schema-component/antd/page/index.ts +++ b/packages/core/client/src/schema-component/antd/page/index.ts @@ -7,8 +7,9 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -export * from './Page'; export * from './FixedBlock'; -export * from './PageTab.Settings'; -export * from './Page.Settings'; export * from './FixedBlockDesignerItem'; +export * from './Page'; +export * from './Page.Settings'; +export * from './PageTab.Settings'; +export { PopupSettingsProvider } from './PopupSettingsProvider'; diff --git a/packages/core/client/src/schema-component/antd/page/nestedSchemaKeyStorage.ts b/packages/core/client/src/schema-component/antd/page/nestedSchemaKeyStorage.ts new file mode 100644 index 000000000..6bb81239e --- /dev/null +++ b/packages/core/client/src/schema-component/antd/page/nestedSchemaKeyStorage.ts @@ -0,0 +1,20 @@ +/** + * 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 { uid } from '@formily/shared'; + +const randomNestedSchemaKeyStorage: Record = {}; + +export const getRandomNestedSchemaKey = (popupUid: string) => { + return randomNestedSchemaKeyStorage[popupUid] || (randomNestedSchemaKeyStorage[popupUid] = uid()); +}; + +export const deleteRandomNestedSchemaKey = (popupUid: string) => { + return delete randomNestedSchemaKeyStorage[popupUid]; +}; diff --git a/packages/core/client/src/schema-component/antd/page/pagePopupUtils.tsx b/packages/core/client/src/schema-component/antd/page/pagePopupUtils.tsx new file mode 100644 index 000000000..262b3d840 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/page/pagePopupUtils.tsx @@ -0,0 +1,273 @@ +/** + * 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 { ISchema, useFieldSchema } from '@formily/react'; +import _ from 'lodash'; +import { useCallback, useContext } from 'react'; +import { useLocationNoUpdate, useNavigateNoUpdate } from '../../../application'; +import { + CollectionRecord, + useAssociationName, + useCollection, + useCollectionManager, + useCollectionParentRecord, + useCollectionRecord, + useDataBlockRequest, + useDataSourceKey, +} from '../../../data-source'; +import { useCurrentPopupRecord } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider'; +import { ActionContext } from '../action/context'; +import { PopupVisibleProviderContext, usePopupContextAndParams } from './PagePopups'; +import { usePopupSettings } from './PopupSettingsProvider'; +import { PopupContext, usePopupContextInActionOrAssociationField } from './usePopupContextInActionOrAssociationField'; + +export interface PopupParams { + /** popup uid */ + popupuid: string; + /** record id */ + filterbytk?: string; + /** tab uid */ + tab?: string; +} + +export interface PopupContextStorage extends PopupContext { + schema?: ISchema; + record?: CollectionRecord; + parentRecord?: CollectionRecord; + /** used to refresh data for block */ + service?: any; +} + +const popupsContextStorage: Record = {}; + +export const getStoredPopupContext = (popupUid: string) => { + return popupsContextStorage[popupUid]; +}; + +export const storePopupContext = (popupUid: string, params: PopupContextStorage) => { + popupsContextStorage[popupUid] = params; +}; + +export const getPopupParamsFromPath = _.memoize((path: string) => { + const popupPaths = path.split('/popups/'); + return popupPaths.filter(Boolean).map((popupPath) => { + const [popupUid, ...popupParams] = popupPath.split('/').filter(Boolean); + const obj = {}; + + for (let i = 0; i < popupParams.length; i += 2) { + obj[popupParams[i]] = decodePathValue(popupParams[i + 1]); + } + + return { + popupuid: popupUid, + ...obj, + } as PopupParams; + }); +}); + +export const getPopupPathFromParams = (params: PopupParams) => { + const { popupuid: popupUid, tab, filterbytk } = params; + const popupPath = [popupUid, filterbytk && 'filterbytk', filterbytk, tab && 'tab', tab].filter(Boolean); + + return `/popups/${popupPath.map((item) => encodePathValue(item)).join('/')}`; +}; + +export const usePagePopup = () => { + const navigate = useNavigateNoUpdate(); + const location = useLocationNoUpdate(); + const fieldSchema = useFieldSchema(); + const dataSourceKey = useDataSourceKey(); + const record = useCollectionRecord(); + const parentRecord = useCollectionParentRecord(); + const collection = useCollection(); + const cm = useCollectionManager(); + const association = useAssociationName(); + const { visible, setVisible } = useContext(PopupVisibleProviderContext) || { visible: false, setVisible: () => {} }; + const { params: popupParams } = usePopupContextAndParams(); + const service = useDataBlockRequest(); + const { isPopupVisibleControlledByURL } = usePopupSettings(); + const { setVisible: setVisibleFromAction } = useContext(ActionContext); + const { updatePopupContext } = usePopupContextInActionOrAssociationField(); + const { value: parentPopupRecordData, collection: parentPopupRecordCollection } = useCurrentPopupRecord() || {}; + const getSourceId = useCallback( + (_parentRecordData?: Record) => + (_parentRecordData || parentRecord?.data)?.[cm.getCollection(association?.split('.')[0])?.getPrimaryKey()], + [parentRecord, association], + ); + + const getNewPathname = useCallback( + ({ tabKey, popupUid, recordData }: { tabKey?: string; popupUid: string; recordData: Record }) => { + let _collection = collection; + if (association) { + _collection = cm.getCollection(association); + } + const filterByTK = recordData?.[_collection.getPrimaryKey()]; + return getPopupPathFromParams({ + popupuid: popupUid, + filterbytk: filterByTK, + tab: tabKey, + }); + }, + [association, cm, collection, dataSourceKey, parentRecord?.data, association], + ); + + const getPopupContext = useCallback( + (sourceId?: string) => { + const context = { + dataSource: dataSourceKey, + collection: association ? undefined : collection.name, + association, + sourceId: sourceId || getSourceId(), + parentPopupRecord: !_.isEmpty(parentPopupRecordData) + ? { + collection: parentPopupRecordCollection?.name, + filterByTk: parentPopupRecordData[parentPopupRecordCollection.getPrimaryKey()], + } + : undefined, + }; + + return _.omitBy(context, _.isNil) as PopupContext; + }, + [dataSourceKey, collection, association, getSourceId, parentPopupRecordData, parentPopupRecordCollection], + ); + + const openPopup = useCallback( + ({ + recordData, + parentRecordData, + }: { + recordData?: Record; + parentRecordData?: Record; + } = {}) => { + if (!isPopupVisibleControlledByURL) { + return setVisibleFromAction?.(true); + } + + recordData = recordData || record?.data; + const pathname = getNewPathname({ popupUid: fieldSchema['x-uid'], recordData }); + let url = location.pathname; + if (_.last(url) === '/') { + url = url.slice(0, -1); + } + + const sourceId = getSourceId(parentRecordData); + + storePopupContext(fieldSchema['x-uid'], { + schema: fieldSchema, + record: new CollectionRecord({ isNew: false, data: recordData }), + parentRecord: parentRecordData ? new CollectionRecord({ isNew: false, data: parentRecordData }) : parentRecord, + service, + dataSource: dataSourceKey, + collection: collection.name, + association, + sourceId, + parentPopupRecord: parentPopupRecordData + ? { + collection: parentPopupRecordCollection?.name, + filterByTk: parentPopupRecordData[parentPopupRecordCollection.getPrimaryKey()], + } + : undefined, + }); + + updatePopupContext(getPopupContext(sourceId)); + + navigate(withSearchParams(`${url}${pathname}`)); + }, + [ + association, + cm, + collection, + dataSourceKey, + fieldSchema, + getNewPathname, + navigate, + parentRecord, + record, + service, + location, + isPopupVisibleControlledByURL, + parentPopupRecordData, + getSourceId, + getPopupContext, + ], + ); + + const closePopup = useCallback(() => { + if (!isPopupVisibleControlledByURL) { + return setVisibleFromAction?.(false); + } + + navigate(withSearchParams(removeLastPopupPath(location.pathname))); + }, [navigate, location, isPopupVisibleControlledByURL]); + + const changeTab = useCallback( + (key: string) => { + const pathname = getNewPathname({ tabKey: key, popupUid: popupParams?.popupuid, recordData: record?.data }); + let url = removeLastPopupPath(location.pathname); + if (_.last(url) === '/') { + url = url.slice(0, -1); + } + navigate(`${url}${pathname}`); + }, + [getNewPathname, navigate, popupParams?.popupuid, record?.data, location], + ); + + return { + /** + * used to open popup by changing the url + */ + openPopup, + /** + * used to close popup by changing the url + */ + closePopup, + visibleWithURL: visible, + setVisibleWithURL: setVisible, + popupParams, + changeTab, + getPopupContext, + }; +}; + +// e.g. /popups/popupUid/popups/popupUid2 -> /popups/popupUid +export function removeLastPopupPath(path: string) { + if (!path.includes('popups')) { + return path; + } + return path.split('popups').slice(0, -1).join('popups'); +} + +export function withSearchParams(path: string) { + return `${path}${window.location.search}`; +} + +/** + * Prevent problems when "popups" appears in the path + * @param value + * @returns + */ +export function encodePathValue(value: string) { + const encodedValue = encodeURIComponent(value); + if (encodedValue === 'popups') { + return window.btoa(value); + } + return encodedValue; +} + +/** + * Prevent problems when "popups" appears in the path + * @param value + * @returns + */ +export function decodePathValue(value: string) { + if (value === window.btoa('popups')) { + return 'popups'; + } + return decodeURIComponent(value); +} diff --git a/packages/core/client/src/schema-component/antd/page/usePopupContextInActionOrAssociationField.ts b/packages/core/client/src/schema-component/antd/page/usePopupContextInActionOrAssociationField.ts new file mode 100644 index 000000000..5414a5619 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/page/usePopupContextInActionOrAssociationField.ts @@ -0,0 +1,85 @@ +/** + * 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 { ISchema, useFieldSchema } from '@formily/react'; +import _ from 'lodash'; +import { useCallback } from 'react'; +import { useDesignable } from '../../hooks/useDesignable'; + +export interface PopupContext { + dataSource: string; + collection?: string; + association?: string; + sourceId?: string; + /** + * Context for the parent popup record variable + */ + parentPopupRecord?: { + /** collection name */ + collection: string; + filterByTk: string; + }; +} + +export interface SubPageContext extends PopupContext { + /** + * Context for the parent popup record variable + */ + parentPopupRecord: { + /** collection name */ + collection: string; + filterByTk: string; + }; +} + +export const CONTEXT_SCHEMA_KEY = 'x-action-context'; + +/** + * support only in Action or AssociationField, because it depends on a specific schema structure + * @returns + */ +export const usePopupContextInActionOrAssociationField = () => { + const fieldSchema = useFieldSchema(); + const { dn } = useDesignable(); + + const updatePopupContext = useCallback( + (context: PopupContext) => { + context = _.omitBy(context, _.isNil) as PopupContext; + + if (_.isEqual(context, getPopupContextFromActionOrAssociationFieldSchema(fieldSchema))) { + return; + } + + fieldSchema[CONTEXT_SCHEMA_KEY] = context; + + return dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + [CONTEXT_SCHEMA_KEY]: context, + }, + }); + }, + [fieldSchema, dn], + ); + + return { + /** + * update the value of the x-nb-popup-context field in the popup schema + */ + updatePopupContext, + }; +}; + +/** + * @param fieldSchema support only schema of Action or AssociationField, because it depends on a specific schema structure + * @returns + */ +export function getPopupContextFromActionOrAssociationFieldSchema(fieldSchema: ISchema) { + return fieldSchema[CONTEXT_SCHEMA_KEY] as PopupContext; +} diff --git a/packages/core/client/src/schema-component/core/RemoteSchemaComponent.tsx b/packages/core/client/src/schema-component/core/RemoteSchemaComponent.tsx index b2e315264..3fa948fb9 100644 --- a/packages/core/client/src/schema-component/core/RemoteSchemaComponent.tsx +++ b/packages/core/client/src/schema-component/core/RemoteSchemaComponent.tsx @@ -10,11 +10,11 @@ import { createForm } from '@formily/core'; import { Schema } from '@formily/react'; import { Spin } from 'antd'; -import React, { useMemo } from 'react'; -import { useRequest } from '../../api-client'; +import React, { memo, useMemo } from 'react'; import { useSchemaComponentContext } from '../hooks'; import { FormProvider } from './FormProvider'; import { SchemaComponent } from './SchemaComponent'; +import { useRequestSchema } from './useRequestSchema'; export interface RemoteSchemaComponentProps { scope?: any; @@ -42,15 +42,15 @@ const RequestSchemaComponent: React.FC = (props) => schemaTransform = defaultTransform, } = props; const { reset } = useSchemaComponentContext(); + const type = onlyRenderProperties ? 'getProperties' : 'getJsonSchema'; const conf = { - url: `/uiSchemas:${onlyRenderProperties ? 'getProperties' : 'getJsonSchema'}/${uid}`, + url: `/uiSchemas:${type}/${uid}`, }; const form = useMemo(() => createForm(), [uid]); - const { data, loading } = useRequest<{ - data: any; - }>(conf, { - refreshDeps: [uid], - onSuccess(data) { + const { schema, loading } = useRequestSchema({ + uid, + type, + onSuccess: (data) => { onSuccess && onSuccess(data); reset && reset(); }, @@ -62,14 +62,15 @@ const RequestSchemaComponent: React.FC = (props) => return ; } return noForm ? ( - + ) : ( - + ); }; -export const RemoteSchemaComponent: React.FC = (props) => { +export const RemoteSchemaComponent: React.FC = memo((props) => { return props.uid ? : null; -}; +}); +RemoteSchemaComponent.displayName = 'RemoteSchemaComponent'; diff --git a/packages/core/client/src/schema-component/core/SchemaComponent.tsx b/packages/core/client/src/schema-component/core/SchemaComponent.tsx index 3135b5348..cb9782654 100644 --- a/packages/core/client/src/schema-component/core/SchemaComponent.tsx +++ b/packages/core/client/src/schema-component/core/SchemaComponent.tsx @@ -8,10 +8,10 @@ */ import { IRecursionFieldProps, ISchemaFieldProps, RecursionField, Schema } from '@formily/react'; -import React, { useContext, useMemo } from 'react'; +import { useUpdate } from 'ahooks'; +import React, { memo, useContext, useMemo } from 'react'; import { SchemaComponentContext } from '../context'; import { SchemaComponentOptions } from './SchemaComponentOptions'; -import { useUpdate } from 'ahooks'; type SchemaComponentOnChange = { onChange?: (s: Schema) => void; @@ -44,10 +44,10 @@ interface DistributedProps { distributed?: boolean; } -const RecursionSchemaComponent = (props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => { - const { components, scope, schema, distributed, ...others } = props; +const RecursionSchemaComponent = memo((props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => { + const { components, scope, schema: _schema, distributed, ...others } = props; const ctx = useContext(SchemaComponentContext); - const s = useMemo(() => toSchema(schema), [schema]); + const schema = useMemo(() => toSchema(_schema), [_schema]); const refresh = useUpdate(); return ( @@ -60,30 +60,32 @@ const RecursionSchemaComponent = (props: ISchemaFieldProps & SchemaComponentOnCh if (ctx.distributed === false || distributed === false) { ctx.refresh?.(); } - props.onChange?.(s); + props.onChange?.(schema); }, }} > - + ); -}; +}); -const MemoizedSchemaComponent = (props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => { +const MemoizedSchemaComponent = memo((props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => { const { schema, ...others } = props; const s = useMemoizedSchema(schema); return ; -}; +}); -export const SchemaComponent = ( - props: (ISchemaFieldProps | IRecursionFieldProps) & { memoized?: boolean } & SchemaComponentOnChange & - DistributedProps, -) => { - const { memoized, ...others } = props; - if (memoized) { - return ; - } - return ; -}; +export const SchemaComponent = memo( + ( + props: (ISchemaFieldProps | IRecursionFieldProps) & { memoized?: boolean } & SchemaComponentOnChange & + DistributedProps, + ) => { + const { memoized, ...others } = props; + if (memoized) { + return ; + } + return ; + }, +); diff --git a/packages/core/client/src/schema-component/core/useRequestSchema.tsx b/packages/core/client/src/schema-component/core/useRequestSchema.tsx new file mode 100644 index 000000000..deb67b933 --- /dev/null +++ b/packages/core/client/src/schema-component/core/useRequestSchema.tsx @@ -0,0 +1,34 @@ +/** + * 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 { useRequest } from '../../api-client'; + +export const useRequestSchema = ({ + uid, + type = 'getJsonSchema', + onSuccess, +}: { + uid: string; + type?: 'getProperties' | 'getJsonSchema'; + onSuccess?: (data: any) => void; +}) => { + const conf = { + url: `/uiSchemas:${type}/${uid}`, + }; + const { data, loading } = useRequest<{ + data: any; + }>(conf, { + refreshDeps: [uid], + onSuccess(data) { + onSuccess && onSuccess(data); + }, + }); + + return { schema: data?.data, loading }; +}; diff --git a/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx b/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx index 790ab997b..9fcd276c4 100644 --- a/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx +++ b/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx @@ -8,20 +8,19 @@ */ import { DownOutlined } from '@ant-design/icons'; -import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react'; +import { observer, useField, useFieldSchema, useForm } from '@formily/react'; import { Button, Dropdown, MenuProps } from 'antd'; -import React, { useEffect, useMemo, useState, forwardRef, createRef } from 'react'; import { composeRef } from 'rc-util/lib/ref'; -import { useDesignable } from '../../'; -import { useACLRolesCheck, useRecordPkValue, useACLActionParamsContext } from '../../acl/ACLProvider'; -import { - CollectionProvider_deprecated, - useCollection_deprecated, - useCollectionManager_deprecated, -} from '../../collection-manager'; +import React, { createRef, forwardRef, useEffect, useMemo } from 'react'; +import { Collection, useDesignable } from '../../'; +import { useACLActionParamsContext, useACLRolesCheck, useRecordPkValue } from '../../acl/ACLProvider'; +import { useCollectionManager_deprecated, useCollection_deprecated } from '../../collection-manager'; +import { useTreeParentRecord } from '../../modules/blocks/data-blocks/table/TreeRecordProvider'; import { useRecord } from '../../record-provider'; -import { ActionContextProvider, useActionContext, useCompile } from '../../schema-component'; +import { useCompile } from '../../schema-component'; import { linkageAction } from '../../schema-component/antd/action/utils'; +import { useNavigateTOSubPage } from '../../schema-component/antd/page/SubPages'; +import { usePagePopup } from '../../schema-component/antd/page/pagePopupUtils'; import { parseVariables } from '../../schema-component/common/utils/uitls'; import { useLocalVariables, useVariables } from '../../variables'; @@ -66,17 +65,17 @@ function useAclCheckFn() { } const InternalCreateRecordAction = (props: any, ref) => { - const [visible, setVisible] = useState(false); - const collection = useCollection_deprecated(); const fieldSchema = useFieldSchema(); + const openMode = fieldSchema?.['x-component-props']?.['openMode']; const field: any = useField(); - const [currentCollection, setCurrentCollection] = useState(collection.name); - const [currentCollectionDataSource, setCurrentCollectionDataSource] = useState(collection.dataSource); const linkageRules: any[] = fieldSchema?.['x-linkage-rules'] || []; const values = useRecord(); - const ctx = useActionContext(); const variables = useVariables(); const localVariables = useLocalVariables({ currentForm: { values } as any }); + const { openPopup } = usePagePopup(); + const { navigateToSubPage } = useNavigateTOSubPage(); + const treeRecordData = useTreeParentRecord(); + useEffect(() => { field.stateOfLinkageRules = {}; linkageRules @@ -95,26 +94,26 @@ const InternalCreateRecordAction = (props: any, ref) => { }, [field, linkageRules, localVariables, variables]); const internalRef = createRef(); const buttonRef = composeRef(ref, internalRef); + return ( //@ts-ignore
}> { - if (collectionData.name === collection.name) { - ctx?.setVisible(true); - } else { - setVisible(true); + onClick={(collection: Collection) => { + if (openMode === 'page') { + return navigateToSubPage(); + } + + if (treeRecordData) { + openPopup({ + recordData: treeRecordData, + }); + } else { + openPopup(); } - setCurrentCollection(collectionData.name); - setCurrentCollectionDataSource(collectionData.dataSource); }} /> - - - - -
); }; diff --git a/packages/core/client/src/schema-items/OpenModeSchemaItems.tsx b/packages/core/client/src/schema-items/OpenModeSchemaItems.tsx index d00d2c6cb..ddf282d55 100644 --- a/packages/core/client/src/schema-items/OpenModeSchemaItems.tsx +++ b/packages/core/client/src/schema-items/OpenModeSchemaItems.tsx @@ -9,15 +9,17 @@ import { useField, useFieldSchema } from '@formily/react'; import { Select } from 'antd'; -import React from 'react'; +import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { SchemaInitializerItem, SchemaInitializerSelect } from '../application'; import { useDesignable } from '../schema-component'; +import { usePopupSettings } from '../schema-component/antd/page/PopupSettingsProvider'; import { SchemaSettingsSelectItem } from '../schema-settings'; interface Options { openMode?: boolean; openSize?: boolean; + modeOptions?: { label: string; value: string }[]; } export const SchemaInitializerOpenModeSchemaItems: React.FC = (options) => { const { openMode = true, openSize = true } = options; @@ -25,17 +27,29 @@ export const SchemaInitializerOpenModeSchemaItems: React.FC = (options) const field = useField(); const { t } = useTranslation(); const { dn } = useDesignable(); + const { isPopupVisibleControlledByURL } = usePopupSettings(); const openModeValue = fieldSchema?.['x-component-props']?.['openMode'] || 'drawer'; + const modeOptions = useMemo(() => { + if (isPopupVisibleControlledByURL) { + return [ + { label: t('Drawer'), value: 'drawer' }, + { label: t('Dialog'), value: 'modal' }, + { label: t('Page'), value: 'page' }, + ]; + } + + return [ + { label: t('Drawer'), value: 'drawer' }, + { label: t('Dialog'), value: 'modal' }, + ]; + }, [t, isPopupVisibleControlledByURL]); return ( <> {openMode ? ( { field.componentProps.openMode = value; @@ -92,23 +106,40 @@ export const SchemaInitializerOpenModeSchemaItems: React.FC = (options) ); }; -export const SchemaSettingOpenModeSchemaItems: React.FC = (options) => { - const { openMode = true, openSize = true } = options; +export const SchemaSettingOpenModeSchemaItems: React.FC = (props) => { + const { openMode = true, openSize = true, modeOptions } = props; const fieldSchema = useFieldSchema(); const field = useField(); const { t } = useTranslation(); const { dn } = useDesignable(); + const { isPopupVisibleControlledByURL } = usePopupSettings(); const openModeValue = fieldSchema?.['x-component-props']?.['openMode'] || 'drawer'; + const _modeOptions = useMemo(() => { + if (modeOptions) { + return modeOptions; + } + + if (isPopupVisibleControlledByURL) { + return [ + { label: t('Drawer'), value: 'drawer' }, + { label: t('Dialog'), value: 'modal' }, + { label: t('Page'), value: 'page' }, + ]; + } + + return [ + { label: t('Drawer'), value: 'drawer' }, + { label: t('Dialog'), value: 'modal' }, + ]; + }, [modeOptions, t]); + return ( <> {openMode ? ( { field.componentProps.openMode = value; diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx index e652589bf..1758e5591 100644 --- a/packages/core/client/src/schema-settings/SchemaSettings.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx @@ -43,10 +43,9 @@ import React, { } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; -import { Router } from 'react-router-dom'; import { APIClientProvider } from '../api-client/APIClientProvider'; import { useAPIClient } from '../api-client/hooks/useAPIClient'; -import { ApplicationContext, useApp } from '../application'; +import { ApplicationContext, LocationSearchContext, useApp, useLocationSearch } from '../application'; import { BlockContext, BlockRequestContext_deprecated, @@ -746,6 +745,7 @@ export const SchemaSettingsModalItem: FC = (props) const { association } = useDataBlockProps() || {}; const formCtx = useFormBlockContext(); const blockOptions = useBlockContext(); + const locationSearch = useLocationSearch(); // 解决变量`当前对象`值在弹窗中丢失的问题 const { formValue: subFormValue, collection: subFormCollection } = useSubFormValue(); @@ -784,7 +784,7 @@ export const SchemaSettingsModalItem: FC = (props) name="form" getActiveFieldsName={upLevelActiveFields?.getActiveFieldsName} > - + = (props) - + diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useParentPopupVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useParentPopupVariable.ts index 83a7fe395..dfe4e7178 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useParentPopupVariable.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useParentPopupVariable.ts @@ -40,5 +40,6 @@ export const useParentPopupVariable = (props: any = {}) => { /** 当前记录对应的 collection name */ collectionName: collection?.name, dataSource: collection?.dataSource, + defaultValue: undefined, }; }; diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/usePopupVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/usePopupVariable.ts index e8c0dc768..ee50acf84 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/usePopupVariable.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/usePopupVariable.ts @@ -40,5 +40,6 @@ export const usePopupVariable = (props: any = {}) => { /** 当前记录对应的 collection name */ collectionName: collection?.name, dataSource: collection?.dataSource, + defaultValue: undefined, }; }; diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useURLSearchParamsVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useURLSearchParamsVariable.ts index 7cc687fa6..966e55f68 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useURLSearchParamsVariable.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useURLSearchParamsVariable.ts @@ -12,7 +12,7 @@ import _ from 'lodash'; import qs from 'qs'; import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { useLocationSearch } from '../../../application/CustomRouterContextProvider'; import { useFlag } from '../../../flag-provider/hooks/useFlag'; import { Option } from '../type'; import { getLabelWithTooltip } from './useBaseVariable'; @@ -64,9 +64,9 @@ export const useURLSearchParamsCtx = (search: string) => { export const useURLSearchParamsVariable = (props: any = {}) => { const variableName = '$nURLSearchParams'; const { t } = useTranslation(); - const location = useLocation(); + const searchString = useLocationSearch(); const { isVariableParsedInOtherContext } = useFlag(); - const urlSearchParamsCtx = useURLSearchParamsCtx(location.search); + const urlSearchParamsCtx = useURLSearchParamsCtx(searchString); const disabled = useMemo(() => _.isEmpty(urlSearchParamsCtx), [urlSearchParamsCtx]); const urlSearchParamsSettings: Option = useMemo(() => { return { @@ -100,8 +100,7 @@ export const useURLSearchParamsVariable = (props: any = {}) => { /** 变量值 */ urlSearchParamsCtx, /** - * 这里是用于当通过该变量解析出来的值是一个 undefined 时,最终应该返回的值。 - * 默认返回的是 null,这样会导致数据范围中的 filter 条件不会被清除掉,而 URL search params 变量的值为空时,应该清除掉 filter 条件, + * 这里的默认值是 null,这样会导致数据范围中的 filter 条件不会被清除掉,而 URL search params 变量的值为空时,应该清除掉 filter 条件, * 所以这里把 defaultValue 设置为 undefined,这样在解析出来的值是 undefined 时,会返回 undefined,从而清除掉 filter 条件。 */ defaultValue: undefined, diff --git a/packages/core/client/src/schema-templates/BlockTemplateDetails.tsx b/packages/core/client/src/schema-templates/BlockTemplateDetails.tsx index b02d84d63..f73131606 100644 --- a/packages/core/client/src/schema-templates/BlockTemplateDetails.tsx +++ b/packages/core/client/src/schema-templates/BlockTemplateDetails.tsx @@ -10,8 +10,9 @@ import { PageHeader as AntdPageHeader } from '@ant-design/pro-layout'; import { Input, Spin } from 'antd'; import React, { useContext, useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { useAPIClient, useRequest, useSchemaTemplateManager } from '..'; +import { useNavigateNoUpdate } from '../application/CustomRouterContextProvider'; import { RemoteSchemaComponent, SchemaComponentContext } from '../schema-component'; const EditableTitle = (props) => { @@ -68,7 +69,7 @@ const EditableTitle = (props) => { }; export const BlockTemplateDetails = () => { - const navigate = useNavigate(); + const navigate = useNavigateNoUpdate(); const params = useParams(); const key = params?.key; const value = useContext(SchemaComponentContext); diff --git a/packages/core/client/src/user/CurrentUser.tsx b/packages/core/client/src/user/CurrentUser.tsx index a042ad618..ee369aa69 100644 --- a/packages/core/client/src/user/CurrentUser.tsx +++ b/packages/core/client/src/user/CurrentUser.tsx @@ -13,8 +13,8 @@ import { error } from '@nocobase/utils/client'; import { App, Dropdown, Menu, MenuProps } from 'antd'; import React, { createContext, useCallback, useMemo as useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; import { useACLRoleContext, useAPIClient, useCurrentUserContext, useToken } from '..'; +import { useNavigateNoUpdate } from '../application/CustomRouterContextProvider'; import { useChangePassword } from './ChangePassword'; import { useCurrentUserSettingsMenu } from './CurrentUserSettingsMenuProvider'; import { useEditProfile } from './EditProfile'; @@ -48,7 +48,7 @@ export const SettingsMenu: React.FC<{ const { redirectUrl = '' } = props; const { allowAll, snippets } = useACLRoleContext(); const appAllowed = allowAll || snippets?.includes('app'); - const navigate = useNavigate(); + const navigate = useNavigateNoUpdate(); const api = useAPIClient(); const { t } = useTranslation(); const silenceApi = useAPIClient(); diff --git a/packages/core/client/src/user/CurrentUserProvider.tsx b/packages/core/client/src/user/CurrentUserProvider.tsx index 7bcf4855a..6aec3556c 100644 --- a/packages/core/client/src/user/CurrentUserProvider.tsx +++ b/packages/core/client/src/user/CurrentUserProvider.tsx @@ -8,9 +8,10 @@ */ import React, { createContext, useContext, useMemo } from 'react'; -import { Navigate, useLocation } from 'react-router-dom'; +import { Navigate } from 'react-router-dom'; import { useACLRoleContext } from '../acl'; import { ReturnTypeOfUseRequest, useRequest } from '../api-client'; +import { useLocationNoUpdate } from '../application'; import { useAppSpin } from '../application/hooks/useAppSpin'; import { useCompile } from '../schema-component'; @@ -53,7 +54,7 @@ export const CurrentUserProvider = (props) => { export const NavigateIfNotSignIn = ({ children }) => { const result = useCurrentUserContext(); - const { pathname, search } = useLocation(); + const { pathname, search } = useLocationNoUpdate(); const redirect = `?redirect=${pathname}${search}`; if (!result?.data?.data?.id) { return ; diff --git a/packages/core/client/src/variables/hooks/useLocalVariables.tsx b/packages/core/client/src/variables/hooks/useLocalVariables.tsx index 6059164d5..686518f83 100644 --- a/packages/core/client/src/variables/hooks/useLocalVariables.tsx +++ b/packages/core/client/src/variables/hooks/useLocalVariables.tsx @@ -37,11 +37,13 @@ const useLocalVariables = (props?: Props) => { popupRecordCtx, collectionName: collectionNameOfPopupRecord, dataSource: popupDataSource, + defaultValue: defaultValueOfPopupRecord, } = usePopupVariable(); const { parentPopupRecordCtx, collectionName: collectionNameOfParentPopupRecord, dataSource: parentPopupDataSource, + defaultValue: defaultValueOfParentPopupRecord, } = useParentPopupVariable(); const { datetimeCtx } = useDatetimeVariable(); const { currentFormCtx } = useCurrentFormVariable({ form: props?.currentForm }); @@ -98,12 +100,14 @@ const useLocalVariables = (props?: Props) => { ctx: popupRecordCtx, collectionName: collectionNameOfPopupRecord, dataSource: popupDataSource, + defaultValue: defaultValueOfPopupRecord, }, { name: '$nParentPopupRecord', ctx: parentPopupRecordCtx, collectionName: collectionNameOfParentPopupRecord, dataSource: parentPopupDataSource, + defaultValue: defaultValueOfParentPopupRecord, }, { name: '$nForm', @@ -138,12 +142,15 @@ const useLocalVariables = (props?: Props) => { collectionNameOfParentRecord, currentParentRecordDataSource, popupRecordCtx, + parentPopupRecordCtx, collectionNameOfPopupRecord, popupDataSource, datetimeCtx, shouldDisplayCurrentObject, currentObjectCtx, currentCollectionName, + defaultValueOfPopupRecord, + defaultValueOfParentPopupRecord, ]); // 尽量保持返回的值不变,这样可以减少接口的请求次数,因为关系字段会缓存到变量的 ctx 中 }; diff --git a/packages/core/sdk/src/APIClient.ts b/packages/core/sdk/src/APIClient.ts index 7f51e2360..1b0bbcc0e 100644 --- a/packages/core/sdk/src/APIClient.ts +++ b/packages/core/sdk/src/APIClient.ts @@ -347,10 +347,14 @@ export class APIClient { return this.axios.request(config); } - resource(name: string, of?: any, headers?: AxiosRequestHeaders): IResource { + resource(name: string, of?: any, headers?: AxiosRequestHeaders, cancel?: boolean): IResource { const target = {}; const handler = { get: (_: any, actionName: string) => { + if (cancel) { + return; + } + let url = name.split('.').join(`/${encodeURIComponent(of) || '_'}/`); url += `:${actionName}`; const config: AxiosRequestConfig = { url }; diff --git a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/utils.tsx b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/utils.tsx index 74a8dbc12..a6bbbe4fb 100644 --- a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/utils.tsx +++ b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/utils.tsx @@ -16,6 +16,7 @@ import { useCollectionManager_deprecated, useCollection_deprecated, useCompile, + useNavigateNoUpdate, useRemoveGridFormItem, useTableBlockContext, } from '@nocobase/client'; @@ -23,7 +24,6 @@ import { isURL } from '@nocobase/utils/client'; import { App, message } from 'antd'; import { useContext, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; export const useCustomBulkEditFormItemInitializerFields = (options?: any) => { const { name, fields } = useCollection_deprecated(); @@ -83,7 +83,7 @@ export const useCustomizeBulkEditActionProps = () => { const { field, resource, __parent } = useBlockRequestContext(); const expressionScope = useContext(SchemaExpressionScopeContext); const actionContext = useActionContext(); - const navigate = useNavigate(); + const navigate = useNavigateNoUpdate(); const compile = useCompile(); const actionField = useField(); const tableBlockContext = useTableBlockContext(); diff --git a/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/utils.tsx b/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/utils.tsx index f6321577e..9ca8917bf 100644 --- a/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/utils.tsx +++ b/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/utils.tsx @@ -16,13 +16,13 @@ import { useCollection_deprecated, useCompile, useLocalVariables, + useNavigateNoUpdate, useTableBlockContext, useVariables, } from '@nocobase/client'; import { isURL } from '@nocobase/utils/client'; import { App, message } from 'antd'; import { useContext } from 'react'; -import { useNavigate } from 'react-router-dom'; import { useBulkUpdateTranslation } from './locale'; export const useCustomizeBulkUpdateActionProps = () => { @@ -32,7 +32,7 @@ export const useCustomizeBulkUpdateActionProps = () => { const tableBlockContext = useTableBlockContext(); const { rowKey } = tableBlockContext; - const navigate = useNavigate(); + const navigate = useNavigateNoUpdate(); const compile = useCompile(); const { t } = useBulkUpdateTranslation(); const actionField: any = useField(); diff --git a/packages/plugins/@nocobase/plugin-action-custom-request/src/client/hooks/useCustomizeRequestActionProps.ts b/packages/plugins/@nocobase/plugin-action-custom-request/src/client/hooks/useCustomizeRequestActionProps.ts index e000c2824..a550810d5 100644 --- a/packages/plugins/@nocobase/plugin-action-custom-request/src/client/hooks/useCustomizeRequestActionProps.ts +++ b/packages/plugins/@nocobase/plugin-action-custom-request/src/client/hooks/useCustomizeRequestActionProps.ts @@ -8,15 +8,21 @@ */ import { useField, useFieldSchema, useForm } from '@formily/react'; -import { useAPIClient, useActionContext, useCompile, useDataSourceKey, useRecord } from '@nocobase/client'; +import { + useAPIClient, + useActionContext, + useCompile, + useDataSourceKey, + useNavigateNoUpdate, + useRecord, +} from '@nocobase/client'; import { isURL } from '@nocobase/utils/client'; import { App } from 'antd'; import { saveAs } from 'file-saver'; -import { useNavigate } from 'react-router-dom'; export const useCustomizeRequestActionProps = () => { const apiClient = useAPIClient(); - const navigate = useNavigate(); + const navigate = useNavigateNoUpdate(); const actionSchema = useFieldSchema(); const compile = useCompile(); const form = useForm(); diff --git a/packages/plugins/@nocobase/plugin-action-duplicate/src/client/DuplicateAction.Settings.tsx b/packages/plugins/@nocobase/plugin-action-duplicate/src/client/DuplicateAction.Settings.tsx index ac29c6c04..0c12bd75d 100644 --- a/packages/plugins/@nocobase/plugin-action-duplicate/src/client/DuplicateAction.Settings.tsx +++ b/packages/plugins/@nocobase/plugin-action-duplicate/src/client/DuplicateAction.Settings.tsx @@ -12,16 +12,16 @@ import { ISchema, connect, mapProps, useField, useFieldSchema, useForm } from '@ import { ActionDesigner, SchemaSettingOpenModeSchemaItems, - useCollection_deprecated, - useRecord, - SchemaSettingsModalItem, + SchemaSettings, SchemaSettingsItemType, SchemaSettingsLinkageRules, + SchemaSettingsModalItem, useCollectionState, + useCollection_deprecated, useDesignable, + useRecord, useSchemaToolbar, useSyncFromForm, - SchemaSettings, } from '@nocobase/client'; import { Tree as AntdTree } from 'antd'; import { cloneDeep } from 'lodash'; @@ -357,19 +357,19 @@ const schemaSettingsItems: SchemaSettingsItemType[] = [ name: 'openMode', Component: SchemaSettingOpenModeSchemaItems, useComponentProps() { - const fieldSchema = useFieldSchema(); - const isPopupAction = [ - 'create', - 'update', - 'view', - 'customize:popup', - 'duplicate', - 'customize:create', - ].includes(fieldSchema['x-action'] || ''); + const { t } = useTranslation(); + + const modeOptions = useMemo(() => { + return [ + { label: t('Drawer'), value: 'drawer' }, + { label: t('Dialog'), value: 'modal' }, + ]; + }, [t]); return { - openMode: isPopupAction, - openSize: isPopupAction, + openMode: true, + openSize: true, + modeOptions, }; }, }, diff --git a/packages/plugins/@nocobase/plugin-auth/src/client/AuthProvider.tsx b/packages/plugins/@nocobase/plugin-auth/src/client/AuthProvider.tsx index b7b0ea507..9f88e94e0 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/client/AuthProvider.tsx +++ b/packages/plugins/@nocobase/plugin-auth/src/client/AuthProvider.tsx @@ -7,16 +7,15 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { useApp } from '@nocobase/client'; +import { useApp, useLocationSearch } from '@nocobase/client'; import React, { useEffect } from 'react'; -import { useLocation } from 'react-router-dom'; export const AuthProvider: React.FC = (props) => { - const location = useLocation(); + const searchString = useLocationSearch(); const app = useApp(); useEffect(() => { - const params = new URLSearchParams(location.search); + const params = new URLSearchParams(searchString); const authenticator = params.get('authenticator'); const token = params.get('token'); if (token) { diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/components/qrcode-scanner/index.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/components/qrcode-scanner/index.tsx index fd471ff7c..15c46eb4e 100644 --- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/components/qrcode-scanner/index.tsx +++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/components/qrcode-scanner/index.tsx @@ -6,10 +6,10 @@ * 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 { LeftOutlined, FileImageOutlined } from '@ant-design/icons'; -import { Html5Qrcode } from 'html5-qrcode'; -import React, { useState, useEffect, useRef } from 'react'; +import { FileImageOutlined, LeftOutlined } from '@ant-design/icons'; import { useActionContext } from '@nocobase/client'; +import { Html5Qrcode } from 'html5-qrcode'; +import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ScanBox } from './ScanBox'; import { useScanner } from './useScanner'; diff --git a/packages/plugins/@nocobase/plugin-charts/src/client/ChartQueryMetadataProvider.tsx b/packages/plugins/@nocobase/plugin-charts/src/client/ChartQueryMetadataProvider.tsx index 2e43a10e0..e21a6a5ad 100644 --- a/packages/plugins/@nocobase/plugin-charts/src/client/ChartQueryMetadataProvider.tsx +++ b/packages/plugins/@nocobase/plugin-charts/src/client/ChartQueryMetadataProvider.tsx @@ -9,7 +9,7 @@ import { useAPIClient, useRequest } from '@nocobase/client'; import { Spin } from 'antd'; -import React, { createContext, useContext, useEffect } from 'react'; +import React, { createContext, useCallback, useContext, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; export const ChartQueryMetadataContext = createContext({ @@ -18,18 +18,17 @@ export const ChartQueryMetadataContext = createContext({ }); ChartQueryMetadataContext.displayName = 'ChartQueryMetadataContext'; +const options = { + resource: 'chartsQueries', + action: 'listMetadata', + params: { + paginate: false, + sort: ['-id'], + }, +}; + export const ChartQueryMetadataProvider: React.FC = (props) => { const api = useAPIClient(); - - const options = { - resource: 'chartsQueries', - action: 'listMetadata', - params: { - paginate: false, - sort: ['-id'], - }, - }; - const location = useLocation(); const isAdminPage = location.pathname.startsWith('/admin'); @@ -42,26 +41,24 @@ export const ChartQueryMetadataProvider: React.FC = (props) => { ready: !!(isAdminPage && token), }); - const refresh = async () => { + const refresh = useCallback(async () => { const { data } = await api.request(options); service.mutate(data); return data?.data || []; - }; + }, [options, service]); + + const value = useMemo(() => { + return { + refresh, + data: service.data?.data, + }; + }, [service.data?.data, refresh]); if (service.loading) { return ; } - return ( - - {props.children} - - ); + return {props.children}; }; export const useChartQueryMetadataContext = () => { diff --git a/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Map.tsx b/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Map.tsx index 54e4519ff..4239bcba1 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Map.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/components/AMap/Map.tsx @@ -11,16 +11,15 @@ import AMapLoader from '@amap/amap-jsapi-loader'; import '@amap/amap-jsapi-types'; import { SyncOutlined } from '@ant-design/icons'; import { useFieldSchema } from '@formily/react'; -import { css, useApp, useCollection_deprecated } from '@nocobase/client'; +import { css, useApp, useCollection_deprecated, useNavigateNoUpdate } from '@nocobase/client'; import { useMemoizedFn } from 'ahooks'; import { Alert, App, Button, Spin } from 'antd'; import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; import { useMapConfiguration } from '../../hooks'; import { useMapTranslation } from '../../locale'; import { MapEditorType } from '../../types'; -import { Search } from './Search'; import { useMapHeight } from '../hook'; +import { Search } from './Search'; export interface AMapComponentProps { value?: any; onChange?: (value: number[]) => void; @@ -109,7 +108,7 @@ export const AMapComponent = React.forwardRef(); const editor = useRef(null); - const navigate = useNavigate(); + const navigate = useNavigateNoUpdate(); const id = useRef(`nocobase-map-${type || ''}-${Date.now().toString(32)}`); const { modal } = App.useApp(); const height = useMapHeight(); diff --git a/packages/plugins/@nocobase/plugin-map/src/client/components/Configuration.tsx b/packages/plugins/@nocobase/plugin-map/src/client/components/Configuration.tsx index 81adc5d8f..bc93e0a3b 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/components/Configuration.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/components/Configuration.tsx @@ -7,11 +7,10 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { useAPIClient, useCompile } from '@nocobase/client'; +import { useAPIClient, useCompile, useLocationSearch } from '@nocobase/client'; import { useBoolean } from 'ahooks'; import { Button, Card, Form, Input, Tabs, message } from 'antd'; import React, { useEffect, useMemo } from 'react'; -import { useLocation } from 'react-router-dom'; import { MapTypes } from '../constants'; import { MapConfigurationResourceKey, getSSKey, useMapConfiguration } from '../hooks'; import { useMapTranslation } from '../locale'; @@ -108,8 +107,8 @@ const tabList = MapTypes.map((item) => { export const Configuration = () => { const compile = useCompile(); - const location = useLocation(); - const search = new URLSearchParams(location.search); + const searchString = useLocationSearch(); + const search = new URLSearchParams(searchString); return ( diff --git a/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Map.tsx b/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Map.tsx index 142adff9e..f8533792b 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Map.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/components/GoogleMaps/Map.tsx @@ -10,18 +10,17 @@ import { SyncOutlined } from '@ant-design/icons'; import { useFieldSchema } from '@formily/react'; import { Loader } from '@googlemaps/js-api-loader'; -import { css, useAPIClient, useApp, useCollection_deprecated } from '@nocobase/client'; +import { css, useAPIClient, useApp, useCollection_deprecated, useNavigateNoUpdate } from '@nocobase/client'; import { useMemoizedFn } from 'ahooks'; import { Alert, App, Button, Spin } from 'antd'; import React, { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; import { defaultImage } from '../../constants'; import { useMapConfiguration } from '../../hooks'; import { useMapTranslation } from '../../locale'; import { MapEditorType } from '../../types'; +import { useMapHeight } from '../hook'; import { Search } from './Search'; import { getCurrentPosition, getIcon } from './utils'; -import { useMapHeight } from '../hook'; export type OverlayOptions = google.maps.PolygonOptions & google.maps.MarkerOptions & google.maps.PolylineOptions; @@ -125,7 +124,7 @@ export const GoogleMapsComponent = React.forwardRef(); const cleanupOverlayListenersRef = useRef void>>(new Set()); diff --git a/packages/plugins/@nocobase/plugin-mobile-client/src/client/MobileClientProvider.tsx b/packages/plugins/@nocobase/plugin-mobile-client/src/client/MobileClientProvider.tsx index 346dc2539..17e6a15e2 100644 --- a/packages/plugins/@nocobase/plugin-mobile-client/src/client/MobileClientProvider.tsx +++ b/packages/plugins/@nocobase/plugin-mobile-client/src/client/MobileClientProvider.tsx @@ -7,19 +7,20 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import { useNavigateNoUpdate } from '@nocobase/client'; import React, { useEffect } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { isJSBridge } from './core/bridge'; export const MobileClientProvider = React.memo((props) => { const location = useLocation(); - const navigation = useNavigate(); + const navigate = useNavigateNoUpdate(); useEffect(() => { if (isJSBridge() && location.pathname === '/admin') { - navigation('/mobile', { replace: true }); + navigate('/mobile', { replace: true }); } - }, [location.pathname, navigation]); + }, [location.pathname, navigate]); return <>{props.children}; }); diff --git a/packages/plugins/@nocobase/plugin-mobile-client/src/client/router/Application.tsx b/packages/plugins/@nocobase/plugin-mobile-client/src/client/router/Application.tsx index e4d365285..878c7ce31 100644 --- a/packages/plugins/@nocobase/plugin-mobile-client/src/client/router/Application.tsx +++ b/packages/plugins/@nocobase/plugin-mobile-client/src/client/router/Application.tsx @@ -7,7 +7,15 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { ActionContextProvider, AdminProvider, css, cx, RemoteSchemaComponent, useViewport } from '@nocobase/client'; +import { + ActionContextProvider, + AdminProvider, + css, + cx, + PopupSettingsProvider, + RemoteSchemaComponent, + useViewport, +} from '@nocobase/client'; import { DrawerProps, ModalProps } from 'antd'; import React, { useMemo } from 'react'; import { Outlet, useParams } from 'react-router-dom'; @@ -93,35 +101,37 @@ const MApplication: React.FC = (props) => { return ( - - -
- {params.name && !params.name.startsWith('tab_') ? ( - - ) : ( - - {props.children} - - )} - {/* Global action will insert here */} -
-
-
+ + + +
+ {params.name && !params.name.startsWith('tab_') ? ( + + ) : ( + + {props.children} + + )} + {/* Global action will insert here */} +
+
+
+
);