mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-12-02 04:07:50 +08:00
feat: enable direct dialog opening via URL and support for page mode (#4706)
* refactor: optimize page tabs routing * test: add e2e test for page tabs * feat: add popup routing * fix: resolve nested issue * refactor: rename file utils to pagePopupUtils * perf: enhance animation and overall performance * fix: fix filterByTK * fix(sourceId): resolve error when sourceId is undefined * fix: fix List and GridCard * fix: fix params not fresh * fix: fix parent record * fix: resolve the issue on block data not refreshing after popup closure * feat: bind tab with URL in popups * feat(sub-page): enable popup to open in page mode * chore: optimize * feat: support association fields * fix: address the issue of no data in associaiton field * fix: resolve the issue with opening nested dialog in association field * fix: fix the issue of dialog content not refreshing * perf: use useNavigateNoUpdate to replace useNavigate * perf: enhance popups performance by avoiding unnecessary rendering * fix: fix tab page * fix: fix bulk edit action * chore: fix unit test * chore: fix unit tests * fix: fix bug to pass e2e tests * chore: fix build * fix: fix bugs to pass e2e tests * chore: avoid crashing * chore: make e2e tests pass * chore: make e2e tests pass * chore: fix unit tests * fix(multi-app): fix known issues * fix(Duplicate): should no page mode * chore: fix build * fix(mobile): fix known issues * fix: fix open mode of Add new * refactor: rename 'popupUid' to 'popupuid' * refactor: rename 'subPageUid' tp 'subpageuid' * refactor(subpage): simplify configuration of router * fix(variable): refresh data after value change * test: add e2e test for sub page * refactor: refactor and add tests * fix: fix association field * refactor(subPage): avoid blank page occurrences * chore: fix unit tests * fix: correct first-click context setting for association fields * refactor: use Action's uid for subpage * refactor: rename x-nb-popup-context to x-action-context and move it to Action schema * feat: add context during the creation of actions * chore: fix build * chore: make e2e tests pass * fix(addChild): fix context of Add child * fix: avoid loss or query string * fix: avoid including 'popups' in the path * fix: resolve issue with popup variables and add tests * chore(e2e): fix e2e test * fix(sideMenu): resolve the disappearing sidebar issue and add tests * chore(e2e): fix e2e test * fix: should refresh block data after mutiple popups closed * chore: fix e2e test * fix(associationField): fix wrong context * fix: address issue with special characters
This commit is contained in:
parent
44580ff9a8
commit
05cf9986b0
@ -45,7 +45,6 @@ const getRouteUrl = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ACLRolesCheckProvider = (props) => {
|
export const ACLRolesCheckProvider = (props) => {
|
||||||
const route = getRouteUrl(props.children.props);
|
|
||||||
const { setDesignable } = useDesignable();
|
const { setDesignable } = useDesignable();
|
||||||
const { render } = useAppSpin();
|
const { render } = useAppSpin();
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
|
@ -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<NavigateFunction>(null);
|
||||||
|
const LocationNoUpdateContext = React.createContext<Location>(null);
|
||||||
|
export const LocationSearchContext = React.createContext<string>('');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<NavigateNoUpdateContext.Provider value={navigateNoUpdate as NavigateFunction}>
|
||||||
|
{children}
|
||||||
|
</NavigateNoUpdateContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<any>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Object.assign(locationRef.current, location);
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
|
return <LocationNoUpdateContext.Provider value={locationRef.current}>{children}</LocationNoUpdateContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LocationSearchProvider: FC = ({ children }) => {
|
||||||
|
const location = useLocation();
|
||||||
|
return <LocationSearchContext.Provider value={location.search}>{children}</LocationSearchContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<NavigateNoUpdateProvider>
|
||||||
|
<LocationNoUpdateProvider>
|
||||||
|
<LocationSearchProvider>{children}</LocationSearchProvider>
|
||||||
|
</LocationNoUpdateProvider>
|
||||||
|
</NavigateNoUpdateProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -20,6 +20,7 @@ import {
|
|||||||
useRoutes,
|
useRoutes,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import { Application } from './Application';
|
import { Application } from './Application';
|
||||||
|
import { CustomRouterContextProvider } from './CustomRouterContextProvider';
|
||||||
import { BlankComponent, RouterContextCleaner } from './components';
|
import { BlankComponent, RouterContextCleaner } from './components';
|
||||||
|
|
||||||
export interface BrowserRouterOptions extends Omit<BrowserRouterProps, 'children'> {
|
export interface BrowserRouterOptions extends Omit<BrowserRouterProps, 'children'> {
|
||||||
@ -143,10 +144,12 @@ export class RouterManager {
|
|||||||
return (
|
return (
|
||||||
<RouterContextCleaner>
|
<RouterContextCleaner>
|
||||||
<ReactRouter {...opts}>
|
<ReactRouter {...opts}>
|
||||||
<BaseLayout>
|
<CustomRouterContextProvider>
|
||||||
<RenderRoutes />
|
<BaseLayout>
|
||||||
{children}
|
<RenderRoutes />
|
||||||
</BaseLayout>
|
{children}
|
||||||
|
</BaseLayout>
|
||||||
|
</CustomRouterContextProvider>
|
||||||
</ReactRouter>
|
</ReactRouter>
|
||||||
</RouterContextCleaner>
|
</RouterContextCleaner>
|
||||||
);
|
);
|
||||||
|
@ -8,12 +8,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useApp } from './useApp';
|
import { useApp } from './useApp';
|
||||||
|
|
||||||
export const useAppSpin = () => {
|
export const useAppSpin = () => {
|
||||||
const app = useApp();
|
const app = useApp();
|
||||||
|
const renderSpin = useCallback(
|
||||||
|
() => (app?.renderComponent ? app?.renderComponent?.('AppSpin') : React.createElement(Spin)),
|
||||||
|
[app],
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
render: () => (app?.renderComponent ? app?.renderComponent?.('AppSpin') : React.createElement(Spin)),
|
render: renderSpin,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './Application';
|
export * from './Application';
|
||||||
|
export * from './CustomRouterContextProvider';
|
||||||
export * from './Plugin';
|
export * from './Plugin';
|
||||||
export * from './PluginSettingsManager';
|
export * from './PluginSettingsManager';
|
||||||
export * from './RouterManager';
|
export * from './RouterManager';
|
||||||
@ -19,5 +20,8 @@ export * from './schema-initializer';
|
|||||||
export * from './schema-settings';
|
export * from './schema-settings';
|
||||||
export * from './schema-settings/context/SchemaSettingItemContext';
|
export * from './schema-settings/context/SchemaSettingItemContext';
|
||||||
export * from './schema-settings/hooks/useSchemaSettingsRender';
|
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 './schema-toolbar';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
@ -12,4 +12,3 @@ export * from './SchemaSettingsManager';
|
|||||||
export * from './components';
|
export * from './components';
|
||||||
export * from './context/SchemaSettingItemContext';
|
export * from './context/SchemaSettingItemContext';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './utils';
|
|
||||||
|
@ -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';
|
|
@ -20,6 +20,7 @@ import {
|
|||||||
WithoutTableFieldResource,
|
WithoutTableFieldResource,
|
||||||
useCollectionParentRecord,
|
useCollectionParentRecord,
|
||||||
useCollectionRecord,
|
useCollectionRecord,
|
||||||
|
useCollectionRecordData,
|
||||||
useDataBlockProps,
|
useDataBlockProps,
|
||||||
useDataBlockRequest,
|
useDataBlockRequest,
|
||||||
useDataBlockResource,
|
useDataBlockResource,
|
||||||
@ -35,6 +36,7 @@ import {
|
|||||||
import { DataBlockCollector } from '../filter-provider/FilterProvider';
|
import { DataBlockCollector } from '../filter-provider/FilterProvider';
|
||||||
import { useSourceId } from '../modules/blocks/useSourceId';
|
import { useSourceId } from '../modules/blocks/useSourceId';
|
||||||
import { RecordProvider, useRecordIndex } from '../record-provider';
|
import { RecordProvider, useRecordIndex } from '../record-provider';
|
||||||
|
import { usePagePopup } from '../schema-component/antd/page/pagePopupUtils';
|
||||||
import { useAssociationNames } from './hooks';
|
import { useAssociationNames } from './hooks';
|
||||||
import { useDataBlockParentRecord } from './hooks/useDataBlockParentRecord';
|
import { useDataBlockParentRecord } from './hooks/useDataBlockParentRecord';
|
||||||
|
|
||||||
@ -293,11 +295,17 @@ export const useBlockAssociationContext = () => {
|
|||||||
export const useFilterByTk = () => {
|
export const useFilterByTk = () => {
|
||||||
const { resource, __parent } = useBlockRequestContext();
|
const { resource, __parent } = useBlockRequestContext();
|
||||||
const recordIndex = useRecordIndex();
|
const recordIndex = useRecordIndex();
|
||||||
const record = useRecord();
|
const recordData = useCollectionRecordData();
|
||||||
const collection = useCollection_deprecated();
|
const collection = useCollection_deprecated();
|
||||||
const { getCollectionField } = useCollectionManager_deprecated();
|
const { getCollectionField } = useCollectionManager_deprecated();
|
||||||
const assoc = useBlockAssociationContext();
|
const assoc = useBlockAssociationContext();
|
||||||
const withoutTableFieldResource = useContext(WithoutTableFieldResource);
|
const withoutTableFieldResource = useContext(WithoutTableFieldResource);
|
||||||
|
const { popupParams } = usePagePopup();
|
||||||
|
|
||||||
|
if (popupParams?.filterbytk) {
|
||||||
|
return popupParams.filterbytk;
|
||||||
|
}
|
||||||
|
|
||||||
if (!withoutTableFieldResource) {
|
if (!withoutTableFieldResource) {
|
||||||
if (resource instanceof TableFieldResource || __parent?.block === 'TableField') {
|
if (resource instanceof TableFieldResource || __parent?.block === 'TableField') {
|
||||||
return recordIndex;
|
return recordIndex;
|
||||||
@ -306,9 +314,9 @@ export const useFilterByTk = () => {
|
|||||||
|
|
||||||
if (assoc) {
|
if (assoc) {
|
||||||
const association = getCollectionField(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'];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -154,10 +154,8 @@ export const useFormBlockContext = () => {
|
|||||||
export const useFormBlockProps = () => {
|
export const useFormBlockProps = () => {
|
||||||
const ctx = useFormBlockContext();
|
const ctx = useFormBlockContext();
|
||||||
const treeParentRecord = useTreeParentRecord();
|
const treeParentRecord = useTreeParentRecord();
|
||||||
const { fieldSchema } = useActionContext();
|
|
||||||
const addChild = fieldSchema?.['x-component-props']?.addChild;
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (addChild) {
|
if (treeParentRecord) {
|
||||||
ctx.form?.query('parent').take((field) => {
|
ctx.form?.query('parent').take((field) => {
|
||||||
field.disabled = true;
|
field.disabled = true;
|
||||||
field.value = treeParentRecord;
|
field.value = treeParentRecord;
|
||||||
|
@ -159,7 +159,7 @@ export const TableBlockProvider = withDynamicSchemaProps((props) => {
|
|||||||
}
|
}
|
||||||
params['tree'] = true;
|
params['tree'] = true;
|
||||||
} else {
|
} else {
|
||||||
const f = collection.fields.find((f) => f.treeChildren);
|
const f = collection?.fields.find((f) => f.treeChildren);
|
||||||
if (f) {
|
if (f) {
|
||||||
childrenColumnName = f.name;
|
childrenColumnName = f.name;
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,6 @@ import omit from 'lodash/omit';
|
|||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import { ChangeEvent, useCallback, useContext, useEffect, useMemo } from 'react';
|
import { ChangeEvent, useCallback, useContext, useEffect, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useReactToPrint } from 'react-to-print';
|
import { useReactToPrint } from 'react-to-print';
|
||||||
import {
|
import {
|
||||||
AssociationFilter,
|
AssociationFilter,
|
||||||
@ -30,6 +29,7 @@ import {
|
|||||||
useTableBlockContext,
|
useTableBlockContext,
|
||||||
} from '../..';
|
} from '../..';
|
||||||
import { useAPIClient, useRequest } from '../../api-client';
|
import { useAPIClient, useRequest } from '../../api-client';
|
||||||
|
import { useNavigateNoUpdate } from '../../application/CustomRouterContextProvider';
|
||||||
import { useFormBlockContext } from '../../block-provider/FormBlockProvider';
|
import { useFormBlockContext } from '../../block-provider/FormBlockProvider';
|
||||||
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../collection-manager';
|
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../collection-manager';
|
||||||
import { DataBlock, useFilterBlock } from '../../filter-provider/FilterProvider';
|
import { DataBlock, useFilterBlock } from '../../filter-provider/FilterProvider';
|
||||||
@ -206,7 +206,7 @@ export const useCreateActionProps = () => {
|
|||||||
const form = useForm();
|
const form = useForm();
|
||||||
const { field, resource } = useBlockRequestContext();
|
const { field, resource } = useBlockRequestContext();
|
||||||
const { setVisible, setSubmitted, setFormValueChanged } = useActionContext();
|
const { setVisible, setSubmitted, setFormValueChanged } = useActionContext();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigateNoUpdate();
|
||||||
const actionSchema = useFieldSchema();
|
const actionSchema = useFieldSchema();
|
||||||
const actionField = useField();
|
const actionField = useField();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
@ -548,7 +548,7 @@ export const useCustomizeUpdateActionProps = () => {
|
|||||||
const { resource, __parent, service } = useBlockRequestContext();
|
const { resource, __parent, service } = useBlockRequestContext();
|
||||||
const filterByTk = useFilterByTk();
|
const filterByTk = useFilterByTk();
|
||||||
const actionSchema = useFieldSchema();
|
const actionSchema = useFieldSchema();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigateNoUpdate();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
const { modal } = App.useApp();
|
const { modal } = App.useApp();
|
||||||
@ -643,7 +643,7 @@ export const useCustomizeBulkUpdateActionProps = () => {
|
|||||||
const { rowKey } = tableBlockContext;
|
const { rowKey } = tableBlockContext;
|
||||||
const selectedRecordKeys =
|
const selectedRecordKeys =
|
||||||
tableBlockContext.field?.data?.selectedRowKeys ?? expressionScope?.selectedRecordKeys ?? {};
|
tableBlockContext.field?.data?.selectedRowKeys ?? expressionScope?.selectedRecordKeys ?? {};
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigateNoUpdate();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const actionField = useField();
|
const actionField = useField();
|
||||||
@ -754,7 +754,7 @@ export const useCustomizeBulkUpdateActionProps = () => {
|
|||||||
|
|
||||||
export const useCustomizeRequestActionProps = () => {
|
export const useCustomizeRequestActionProps = () => {
|
||||||
const apiClient = useAPIClient();
|
const apiClient = useAPIClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigateNoUpdate();
|
||||||
const filterByTk = useFilterByTk();
|
const filterByTk = useFilterByTk();
|
||||||
const actionSchema = useFieldSchema();
|
const actionSchema = useFieldSchema();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
@ -850,7 +850,7 @@ export const useUpdateActionProps = () => {
|
|||||||
const { field, resource, __parent } = useBlockRequestContext();
|
const { field, resource, __parent } = useBlockRequestContext();
|
||||||
const { setVisible, setSubmitted, setFormValueChanged } = useActionContext();
|
const { setVisible, setSubmitted, setFormValueChanged } = useActionContext();
|
||||||
const actionSchema = useFieldSchema();
|
const actionSchema = useFieldSchema();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigateNoUpdate();
|
||||||
const { fields, getField, name } = useCollection_deprecated();
|
const { fields, getField, name } = useCollection_deprecated();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const actionField = useField();
|
const actionField = useField();
|
||||||
@ -1530,7 +1530,7 @@ export function appendQueryStringToUrl(url: string, queryString: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useLinkActionProps() {
|
export function useLinkActionProps() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigateNoUpdate();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const url = fieldSchema?.['x-component-props']?.['url'];
|
const url = fieldSchema?.['x-component-props']?.['url'];
|
||||||
|
@ -73,7 +73,7 @@ export function useParsedFilter({ filterOption }: { filterOption: any }) {
|
|||||||
equals: _.isEqual,
|
equals: _.isEqual,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}, [JSON.stringify(filterOption)]);
|
}, [JSON.stringify(filterOption), parseFilter, findVariable]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/** 数据范围的筛选参数 */
|
/** 数据范围的筛选参数 */
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* 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 { useLocation } from 'react-router-dom';
|
||||||
import { useAPIClient, useRequest } from '../api-client';
|
import { useAPIClient, useRequest } from '../api-client';
|
||||||
import { useAppSpin } from '../application/hooks/useAppSpin';
|
import { useAppSpin } from '../application/hooks/useAppSpin';
|
||||||
@ -23,26 +23,23 @@ const CollectionHistoryContext = createContext<CollectionHistoryContextValue>({
|
|||||||
});
|
});
|
||||||
CollectionHistoryContext.displayName = 'CollectionHistoryContext';
|
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) => {
|
export const CollectionHistoryProvider: React.FC = (props) => {
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
|
|
||||||
const options = {
|
|
||||||
resource: 'collectionsHistory',
|
|
||||||
action: 'list',
|
|
||||||
params: {
|
|
||||||
paginate: false,
|
|
||||||
appends: ['fields'],
|
|
||||||
filter: {
|
|
||||||
// inherit: false,
|
|
||||||
},
|
|
||||||
sort: ['sort'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
// console.log('location', location);
|
|
||||||
|
|
||||||
const isAdminPage = location.pathname.startsWith('/admin');
|
const isAdminPage = location.pathname.startsWith('/admin');
|
||||||
const token = api.auth.getToken() || '';
|
const token = api.auth.getToken() || '';
|
||||||
const { render } = useAppSpin();
|
const { render } = useAppSpin();
|
||||||
@ -55,26 +52,24 @@ export const CollectionHistoryProvider: React.FC = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 刷新 collecionHistory
|
// 刷新 collecionHistory
|
||||||
const refreshCH = async () => {
|
const refreshCH = useCallback(async () => {
|
||||||
const { data } = await api.request(options);
|
const { data } = await api.request(options);
|
||||||
service.mutate(data);
|
service.mutate(data);
|
||||||
return data?.data || [];
|
return data?.data || [];
|
||||||
};
|
}, [service]);
|
||||||
|
|
||||||
|
const value = useMemo(() => {
|
||||||
|
return {
|
||||||
|
historyCollections: service.data?.data,
|
||||||
|
refreshCH,
|
||||||
|
};
|
||||||
|
}, [refreshCH, service.data?.data]);
|
||||||
|
|
||||||
if (service.loading) {
|
if (service.loading) {
|
||||||
return render();
|
return render();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <CollectionHistoryContext.Provider value={value}>{props.children}</CollectionHistoryContext.Provider>;
|
||||||
<CollectionHistoryContext.Provider
|
|
||||||
value={{
|
|
||||||
historyCollections: service.data?.data,
|
|
||||||
refreshCH,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</CollectionHistoryContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useHistoryCollectionsByNames = (collectionNames: string[]) => {
|
export const useHistoryCollectionsByNames = (collectionNames: string[]) => {
|
||||||
|
@ -7,15 +7,15 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* 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 { useAPIClient, useRequest } from '../api-client';
|
||||||
import { CollectionManagerSchemaComponentProvider } from './CollectionManagerSchemaComponentProvider';
|
import { useAppSpin } from '../application/hooks/useAppSpin';
|
||||||
import { CollectionCategroriesContext } from './context';
|
|
||||||
import { CollectionManagerOptions } from './types';
|
|
||||||
import { CollectionManagerProvider } from '../data-source/collection/CollectionManagerProvider';
|
import { CollectionManagerProvider } from '../data-source/collection/CollectionManagerProvider';
|
||||||
import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider';
|
import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider';
|
||||||
import { useCollectionHistory } from './CollectionHistoryProvider';
|
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
|
* @deprecated use `CollectionManagerProvider` instead
|
||||||
@ -28,18 +28,19 @@ export const CollectionManagerProvider_deprecated: React.FC<CollectionManagerOpt
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const coptions = {
|
||||||
|
url: 'collectionCategories:list',
|
||||||
|
params: {
|
||||||
|
paginate: false,
|
||||||
|
sort: ['sort'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const RemoteCollectionManagerProvider = (props: any) => {
|
export const RemoteCollectionManagerProvider = (props: any) => {
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const dm = useDataSourceManager();
|
const dm = useDataSourceManager();
|
||||||
const { refreshCH } = useCollectionHistory();
|
const { refreshCH } = useCollectionHistory();
|
||||||
|
|
||||||
const coptions = {
|
|
||||||
url: 'collectionCategories:list',
|
|
||||||
params: {
|
|
||||||
paginate: false,
|
|
||||||
sort: ['sort'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const service = useRequest<{
|
const service = useRequest<{
|
||||||
data: any;
|
data: any;
|
||||||
}>(() => {
|
}>(() => {
|
||||||
@ -50,17 +51,18 @@ export const RemoteCollectionManagerProvider = (props: any) => {
|
|||||||
}>(coptions);
|
}>(coptions);
|
||||||
|
|
||||||
const { render } = useAppSpin();
|
const { render } = useAppSpin();
|
||||||
|
const refreshCategory = useCallback(async () => {
|
||||||
|
const { data } = await api.request(coptions);
|
||||||
|
result.mutate(data);
|
||||||
|
return data?.data || [];
|
||||||
|
}, [result]);
|
||||||
|
|
||||||
if (service.loading) {
|
if (service.loading) {
|
||||||
return render();
|
return render();
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshCategory = async () => {
|
|
||||||
const { data } = await api.request(coptions);
|
|
||||||
result.mutate(data);
|
|
||||||
return data?.data || [];
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<CollectionCategroriesProvider service={{ ...result }} refreshCategory={refreshCategory}>
|
<CollectionCategroriesProvider service={result} refreshCategory={refreshCategory}>
|
||||||
<CollectionManagerProvider_deprecated {...props}></CollectionManagerProvider_deprecated>
|
<CollectionManagerProvider_deprecated {...props}></CollectionManagerProvider_deprecated>
|
||||||
</CollectionCategroriesProvider>
|
</CollectionCategroriesProvider>
|
||||||
);
|
);
|
||||||
|
@ -9,21 +9,22 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { SchemaComponentOptions } from '..';
|
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 * 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) => {
|
export const CollectionManagerSchemaComponentProvider: React.FC = (props) => {
|
||||||
return (
|
return (
|
||||||
<SchemaComponentOptions
|
<SchemaComponentOptions scope={scope} components={components}>
|
||||||
scope={{ cm: { ...hooks, useDataSourceFromRAC }, ds }}
|
|
||||||
components={{
|
|
||||||
SubFieldDataSourceProvider_deprecated,
|
|
||||||
DataSourceProvider_deprecated,
|
|
||||||
CollectionProvider_deprecated,
|
|
||||||
ResourceActionProvider,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</SchemaComponentOptions>
|
</SchemaComponentOptions>
|
||||||
);
|
);
|
||||||
|
@ -7,17 +7,17 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
import { CascaderProps } from 'antd';
|
import { CascaderProps } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useCompile, useSchemaComponentContext } from '../../schema-component';
|
import { useCollectionManager } from '../../data-source/collection/CollectionManagerProvider';
|
||||||
import { CollectionFieldOptions_deprecated, CollectionOptions } from '../types';
|
import { DEFAULT_DATA_SOURCE_KEY } from '../../data-source/data-source/DataSourceManager';
|
||||||
import { InheritanceCollectionMixin } from '../mixins/InheritanceCollectionMixin';
|
|
||||||
import { uid } from '@formily/shared';
|
|
||||||
import { useDataSourceManager } from '../../data-source/data-source/DataSourceManagerProvider';
|
import { useDataSourceManager } from '../../data-source/data-source/DataSourceManagerProvider';
|
||||||
import { useDataSource } from '../../data-source/data-source/DataSourceProvider';
|
import { useDataSource } from '../../data-source/data-source/DataSourceProvider';
|
||||||
import { DEFAULT_DATA_SOURCE_KEY } from '../../data-source/data-source/DataSourceManager';
|
import { useCompile, useSchemaComponentContext } from '../../schema-component';
|
||||||
import { useCollectionManager } from '../../data-source/collection/CollectionManagerProvider';
|
import { InheritanceCollectionMixin } from '../mixins/InheritanceCollectionMixin';
|
||||||
|
import { CollectionFieldOptions_deprecated, CollectionOptions } from '../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated use `useCollectionManager` instead
|
* @deprecated use `useCollectionManager` instead
|
||||||
@ -273,9 +273,12 @@ export const useCollectionManager_deprecated = (dataSourceName?: string) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 是否可以作为标题字段
|
// 是否可以作为标题字段
|
||||||
const isTitleField = (field) => {
|
const isTitleField = useCallback(
|
||||||
return getInterface(field.interface)?.titleUsable;
|
(field) => {
|
||||||
};
|
return getInterface(field.interface)?.titleUsable;
|
||||||
|
},
|
||||||
|
[getInterface],
|
||||||
|
);
|
||||||
|
|
||||||
const getParentCollectionFields = useCallback(
|
const getParentCollectionFields = useCallback(
|
||||||
(parentCollection, currentCollection, customDataSource?: string) => {
|
(parentCollection, currentCollection, customDataSource?: string) => {
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* 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 React, { FC, ReactNode, createContext, useContext, useMemo } from 'react';
|
||||||
|
|
||||||
import { ACLCollectionProvider } from '../../acl/ACLProvider';
|
import { ACLCollectionProvider } from '../../acl/ACLProvider';
|
||||||
|
@ -38,7 +38,7 @@ function useCurrentRequest<T>(options: Omit<AllDataBlockProps, 'type'>) {
|
|||||||
}
|
}
|
||||||
const paramsValue = params.filterByTk === undefined ? _.omit(params, 'filterByTk') : params;
|
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]);
|
}, [resource, action, JSON.stringify(params), JSON.stringify(record), requestService]);
|
||||||
|
|
||||||
@ -129,7 +129,16 @@ export const BlockRequestProvider: FC = ({ children }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const memoizedParentRecord = useMemo(() => {
|
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]);
|
}, [parentRequest.data?.data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -137,13 +146,13 @@ export const BlockRequestProvider: FC = ({ children }) => {
|
|||||||
{action !== 'list' ? (
|
{action !== 'list' ? (
|
||||||
<CollectionRecordProvider
|
<CollectionRecordProvider
|
||||||
isNew={action == null}
|
isNew={action == null}
|
||||||
record={currentRequest.data?.data}
|
record={currentRequest.data?.data || record}
|
||||||
parentRecord={memoizedParentRecord}
|
parentRecord={memoizedParentRecord || parentRecord}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</CollectionRecordProvider>
|
</CollectionRecordProvider>
|
||||||
) : (
|
) : (
|
||||||
<CollectionRecordProvider isNew={false} record={null} parentRecord={memoizedParentRecord}>
|
<CollectionRecordProvider isNew={false} record={null} parentRecord={memoizedParentRecord || parentRecord}>
|
||||||
{children}
|
{children}
|
||||||
</CollectionRecordProvider>
|
</CollectionRecordProvider>
|
||||||
)}
|
)}
|
||||||
|
@ -40,7 +40,7 @@ export const DataBlockResourceProvider: FC<{ children?: ReactNode }> = ({ childr
|
|||||||
|
|
||||||
const resource = useMemo(() => {
|
const resource = useMemo(() => {
|
||||||
if (association) {
|
if (association) {
|
||||||
return api.resource(association, sourceIdValue, headers);
|
return api.resource(association, sourceIdValue, headers, !sourceIdValue);
|
||||||
}
|
}
|
||||||
return api.resource(collectionName, undefined, headers);
|
return api.resource(collectionName, undefined, headers);
|
||||||
}, [api, association, collection, sourceIdValue, headers]);
|
}, [api, association, collection, sourceIdValue, headers]);
|
||||||
|
@ -40,7 +40,7 @@ export interface ForeignKeyField {
|
|||||||
type Collection = ReturnType<typeof useCollection_deprecated>;
|
type Collection = ReturnType<typeof useCollection_deprecated>;
|
||||||
|
|
||||||
export interface DataBlock {
|
export interface DataBlock {
|
||||||
/** 唯一标识符,schema 中的 name 值 */
|
/** 唯一标识符,schema 中的 x-uid 值 */
|
||||||
uid: string;
|
uid: string;
|
||||||
/** 用户自行设置的区块名称 */
|
/** 用户自行设置的区块名称 */
|
||||||
title?: string;
|
title?: string;
|
||||||
|
@ -8,7 +8,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, test } from '@nocobase/test/e2e';
|
import { expect, test } from '@nocobase/test/e2e';
|
||||||
import { OneTableWithDelete } from './templates';
|
import { OneTableWithDelete, shouldRefreshBlockDataAfterMultiplePopupsClosed } from './templates';
|
||||||
|
|
||||||
test.describe('action settings', () => {
|
test.describe('action settings', () => {
|
||||||
test('refresh data on action', async ({ page, mockPage, mockRecords }) => {
|
test('refresh data on action', async ({ page, mockPage, mockRecords }) => {
|
||||||
await mockPage(OneTableWithDelete).goto();
|
await mockPage(OneTableWithDelete).goto();
|
||||||
@ -44,4 +45,24 @@ test.describe('action settings', () => {
|
|||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
expect(requestMade).toBeFalsy();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, test } from '@nocobase/test/e2e';
|
import { expect, test } from '@nocobase/test/e2e';
|
||||||
import { oneEmptyTableWithUsers } from './templates';
|
import { PopupAndSubPageWithParams, oneEmptyTableWithUsers } from './templates';
|
||||||
|
|
||||||
test.describe('Link', () => {
|
test.describe('Link', () => {
|
||||||
test('basic', async ({ page, mockPage, mockRecords }) => {
|
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: 'nocobase', exact: true })).toBeVisible();
|
||||||
await expect(page.getByRole('button', { name: users[1].username, 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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -222,3 +222,368 @@ export const oneEmptyTableWithUsers = {
|
|||||||
'x-index': 1,
|
'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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
@ -196,3 +196,780 @@ export const OneTableWithDelete = {
|
|||||||
'x-index': 1,
|
'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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
@ -8,8 +8,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
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';
|
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
||||||
export const CreateChildInitializer = (props) => {
|
export const CreateChildInitializer = (props) => {
|
||||||
|
const { getPopupContext } = usePagePopup();
|
||||||
const schema = {
|
const schema = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: '{{ t("Add child") }}',
|
title: '{{ t("Add child") }}',
|
||||||
@ -63,6 +66,7 @@ export const CreateChildInitializer = (props) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[CONTEXT_SCHEMA_KEY]: getPopupContext(),
|
||||||
};
|
};
|
||||||
return <ActionInitializerItem {...props} schema={schema} />;
|
return <ActionInitializerItem {...props} schema={schema} />;
|
||||||
};
|
};
|
||||||
|
@ -9,9 +9,12 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSchemaInitializerItem } from '../../../application';
|
import { useSchemaInitializerItem } from '../../../application';
|
||||||
|
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
||||||
|
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
||||||
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
||||||
|
|
||||||
export const CreateActionInitializer = () => {
|
export const CreateActionInitializer = () => {
|
||||||
|
const { getPopupContext } = usePagePopup();
|
||||||
const schema = {
|
const schema = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-action': 'create',
|
'x-action': 'create',
|
||||||
@ -65,6 +68,7 @@ export const CreateActionInitializer = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[CONTEXT_SCHEMA_KEY]: getPopupContext(),
|
||||||
};
|
};
|
||||||
const itemConfig = useSchemaInitializerItem();
|
const itemConfig = useSchemaInitializerItem();
|
||||||
return <ActionInitializerItem {...itemConfig} item={itemConfig} schema={schema} />;
|
return <ActionInitializerItem {...itemConfig} item={itemConfig} schema={schema} />;
|
||||||
|
@ -9,9 +9,12 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSchemaInitializerItem } from '../../../application';
|
import { useSchemaInitializerItem } from '../../../application';
|
||||||
|
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
||||||
|
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
||||||
import { BlockInitializer } from '../../../schema-initializer/items';
|
import { BlockInitializer } from '../../../schema-initializer/items';
|
||||||
|
|
||||||
export const PopupActionInitializer = (props) => {
|
export const PopupActionInitializer = (props) => {
|
||||||
|
const { getPopupContext } = usePagePopup();
|
||||||
const schema = {
|
const schema = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: '{{ t("Popup") }}',
|
title: '{{ t("Popup") }}',
|
||||||
@ -58,6 +61,7 @@ export const PopupActionInitializer = (props) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[CONTEXT_SCHEMA_KEY]: getPopupContext(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemConfig = useSchemaInitializerItem();
|
const itemConfig = useSchemaInitializerItem();
|
||||||
|
@ -8,9 +8,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
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';
|
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
||||||
|
|
||||||
export const UpdateActionInitializer = (props) => {
|
export const UpdateActionInitializer = (props) => {
|
||||||
|
const { getPopupContext } = usePagePopup();
|
||||||
const schema = {
|
const schema = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: '{{ t("Edit") }}',
|
title: '{{ t("Edit") }}',
|
||||||
@ -57,6 +60,7 @@ export const UpdateActionInitializer = (props) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[CONTEXT_SCHEMA_KEY]: getPopupContext(),
|
||||||
};
|
};
|
||||||
return <ActionInitializerItem {...props} schema={schema} />;
|
return <ActionInitializerItem {...props} schema={schema} />;
|
||||||
};
|
};
|
||||||
|
@ -8,9 +8,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
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';
|
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
||||||
|
|
||||||
export const ViewActionInitializer = (props) => {
|
export const ViewActionInitializer = (props) => {
|
||||||
|
const { getPopupContext } = usePagePopup();
|
||||||
const schema = {
|
const schema = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: '{{ t("View") }}',
|
title: '{{ t("View") }}',
|
||||||
@ -56,6 +59,7 @@ export const ViewActionInitializer = (props) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[CONTEXT_SCHEMA_KEY]: getPopupContext(),
|
||||||
};
|
};
|
||||||
return <ActionInitializerItem {...props} schema={schema} />;
|
return <ActionInitializerItem {...props} schema={schema} />;
|
||||||
};
|
};
|
||||||
|
@ -37,6 +37,7 @@ test.describe('where single data details block can be added', () => {
|
|||||||
await nocoPage.goto();
|
await nocoPage.goto();
|
||||||
|
|
||||||
// 1.打开弹窗
|
// 1.打开弹窗
|
||||||
|
await page.getByRole('button', { name: '2', exact: true }).getByText('2').hover();
|
||||||
await page.getByRole('button', { name: '2', exact: true }).getByText('2').click();
|
await page.getByRole('button', { name: '2', exact: true }).getByText('2').click();
|
||||||
|
|
||||||
// 2.通过 Current record 创建一个详情区块
|
// 2.通过 Current record 创建一个详情区块
|
||||||
@ -121,7 +122,7 @@ test.describe('configure actions', () => {
|
|||||||
|
|
||||||
// create delete ------------------------------------------------------------------------------------
|
// create delete ------------------------------------------------------------------------------------
|
||||||
await createAction(page, '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
|
// create print
|
||||||
await createAction(page, 'Print');
|
await createAction(page, 'Print');
|
||||||
|
@ -48,13 +48,13 @@ test.describe('association form block', () => {
|
|||||||
test('association table block add new ', async ({ page, mockPage, mockRecord }) => {
|
test('association table block add new ', async ({ page, mockPage, mockRecord }) => {
|
||||||
await mockPage(T3979).goto();
|
await mockPage(T3979).goto();
|
||||||
await mockRecord('general');
|
await mockRecord('general');
|
||||||
await expect(await page.getByLabel('block-item-CardItem-general-')).toBeVisible();
|
await expect(page.getByLabel('block-item-CardItem-general-')).toBeVisible();
|
||||||
// 1. 打开关系字段弹窗
|
// 1. 打开关系字段弹窗
|
||||||
await page.getByLabel('block-item-CardItem-general-').locator('a').click();
|
await page.getByLabel('block-item-CardItem-general-').locator('a').click();
|
||||||
await page.getByLabel('block-item-CardItem-roles-').click();
|
await page.getByLabel('block-item-CardItem-roles-').click();
|
||||||
|
|
||||||
// 2. 提交后,Table 会显示新增的数据
|
// 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. 区块数据表为关系字段的区块
|
// 3. 区块数据表为关系字段的区块
|
||||||
await page
|
await page
|
||||||
@ -64,6 +64,6 @@ test.describe('association form block', () => {
|
|||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'form Form' }).hover();
|
await page.getByRole('menuitem', { name: 'form Form' }).hover();
|
||||||
await page.getByRole('menuitem', { name: 'Current collection' }).click();
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -17,7 +17,15 @@ import {
|
|||||||
test,
|
test,
|
||||||
} from '@nocobase/test/e2e';
|
} from '@nocobase/test/e2e';
|
||||||
import { oneEmptyTableWithUsers } from '../../../details-multi/__e2e__/templatesOfBug';
|
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.describe('set default value', () => {
|
||||||
test('basic fields', async ({ page, mockPage }) => {
|
test('basic fields', async ({ page, mockPage }) => {
|
||||||
@ -203,7 +211,6 @@ test.describe('set default value', () => {
|
|||||||
// https://nocobase.height.app/T-4028/description
|
// https://nocobase.height.app/T-4028/description
|
||||||
// 刷新页面后,默认值应该依然存在
|
// 刷新页面后,默认值应该依然存在
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await page.getByRole('button', { name: 'Add new' }).click();
|
|
||||||
await page
|
await page
|
||||||
.getByTestId('drawer-Action.Container-general-Add record')
|
.getByTestId('drawer-Action.Container-general-Add record')
|
||||||
.getByRole('button', { name: 'Add new' })
|
.getByRole('button', { name: 'Add new' })
|
||||||
@ -363,11 +370,51 @@ test.describe('set default value', () => {
|
|||||||
).toBeVisible();
|
).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 }) => {
|
test('Parent popup record', async ({ page, mockPage }) => {
|
||||||
await mockPage(oneTableWithNestPopups).goto();
|
await mockPage(oneTableWithNestPopups).goto();
|
||||||
|
|
||||||
// 1. 表单字段默认值中使用 `Parent popup record`
|
// 1. 表单字段默认值中使用 `Parent popup record`
|
||||||
await page.getByLabel('action-Action.Link-View').click();
|
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('action-Action.Link-View in popup').click();
|
||||||
await page.getByLabel('schema-initializer-Grid-popup').nth(1).hover();
|
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: '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.getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-users-users.').hover();
|
||||||
await page.getByRole('menuitem', { name: 'Set default value' }).click();
|
await page.getByRole('menuitem', { name: 'Set default value' }).click();
|
||||||
await page.getByLabel('variable-button').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: 'Parent popup record right' }).click();
|
||||||
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
||||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||||
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin');
|
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', () => {
|
test.describe('actions schema settings', () => {
|
||||||
|
@ -48,7 +48,6 @@ test.describe('creation form block schema settings', () => {
|
|||||||
|
|
||||||
// 刷新页面后,显示的应该依然是上次设置的值
|
// 刷新页面后,显示的应该依然是上次设置的值
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await page.getByRole('button', { name: 'Add new' }).click();
|
|
||||||
await runExpect();
|
await runExpect();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -68,7 +67,6 @@ test.describe('creation form block schema settings', () => {
|
|||||||
|
|
||||||
// 刷新页面
|
// 刷新页面
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await page.getByRole('button', { name: 'Add new' }).click();
|
|
||||||
await page.getByLabel('block-item-CardItem-general-form').hover();
|
await page.getByLabel('block-item-CardItem-general-form').hover();
|
||||||
await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover();
|
await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover();
|
||||||
await expect(page.getByRole('menuitem', { name: 'Save as block template' })).not.toBeVisible();
|
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.reload();
|
||||||
await page.getByRole('button', { name: 'Add new' }).click();
|
|
||||||
await page.getByLabel('block-item-CardItem-general-form').hover();
|
await page.getByLabel('block-item-CardItem-general-form').hover();
|
||||||
await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover();
|
await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover();
|
||||||
await expect(page.getByRole('menuitem', { name: 'Save as block template' })).toBeVisible();
|
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.reload();
|
||||||
await page.getByRole('button', { name: 'Add new' }).click();
|
|
||||||
await expect(page.getByLabel('block-item-CardItem-general-form')).not.toBeVisible();
|
await expect(page.getByLabel('block-item-CardItem-general-form')).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -36,6 +36,7 @@ test.describe('where edit form block can be added', () => {
|
|||||||
await nocoPage.goto();
|
await nocoPage.goto();
|
||||||
|
|
||||||
// 1.打开弹窗
|
// 1.打开弹窗
|
||||||
|
await page.getByRole('button', { name: '2', exact: true }).getByText('2').hover();
|
||||||
await page.getByRole('button', { name: '2', exact: true }).getByText('2').click();
|
await page.getByRole('button', { name: '2', exact: true }).getByText('2').click();
|
||||||
|
|
||||||
// 2.创建一个编辑表单区块
|
// 2.创建一个编辑表单区块
|
||||||
|
@ -50,7 +50,6 @@ test.describe('edit form block schema settings', () => {
|
|||||||
|
|
||||||
// 刷新页面后,显示的应该依然是上次设置的值
|
// 刷新页面后,显示的应该依然是上次设置的值
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await page.getByText('Edit', { exact: true }).click();
|
|
||||||
await runExpect();
|
await runExpect();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -81,7 +80,6 @@ test.describe('edit form block schema settings', () => {
|
|||||||
|
|
||||||
// 刷新页面后,设置的值应该依然存在
|
// 刷新页面后,设置的值应该依然存在
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await page.getByText('Edit', { exact: true }).click();
|
|
||||||
await clickOption(page, 'Linkage rules');
|
await clickOption(page, 'Linkage rules');
|
||||||
await runExpect();
|
await runExpect();
|
||||||
});
|
});
|
||||||
@ -104,7 +102,6 @@ test.describe('edit form block schema settings', () => {
|
|||||||
|
|
||||||
// 刷新页面
|
// 刷新页面
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await page.getByText('Edit', { exact: true }).click();
|
|
||||||
await page.getByLabel('block-item-CardItem-general-form').hover();
|
await page.getByLabel('block-item-CardItem-general-form').hover();
|
||||||
await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover();
|
await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover();
|
||||||
await expect(page.getByRole('menuitem', { name: 'Save as block template' })).not.toBeVisible();
|
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.reload();
|
||||||
await page.getByText('Edit', { exact: true }).click();
|
|
||||||
await page.getByLabel('block-item-CardItem-general-form').hover();
|
await page.getByLabel('block-item-CardItem-general-form').hover();
|
||||||
await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover();
|
await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover();
|
||||||
await expect(page.getByRole('menuitem', { name: 'Save as block template' })).toBeVisible();
|
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.reload();
|
||||||
await page.getByText('Edit', { exact: true }).click();
|
|
||||||
await expect(page.getByLabel('block-item-CardItem-general-form')).not.toBeVisible();
|
await expect(page.getByLabel('block-item-CardItem-general-form')).not.toBeVisible();
|
||||||
});
|
});
|
||||||
// https://nocobase.height.app/T-3825
|
// https://nocobase.height.app/T-3825
|
||||||
test('Unsaved changes warning display', async ({ page, mockPage, mockRecord }) => {
|
test('Unsaved changes warning display', async ({ page, mockPage, mockRecord }) => {
|
||||||
await mockPage(T3825).goto();
|
await mockPage(T3825).goto();
|
||||||
await mockRecord('general', { number: 9, formula: 10 });
|
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 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('');
|
||||||
await page.getByRole('spinbutton').fill('10');
|
await page.getByRole('spinbutton').fill('10');
|
||||||
await expect(
|
await expect(
|
||||||
await page
|
page
|
||||||
.getByLabel('block-item-CollectionField-general-form-general.formula-formula')
|
.getByLabel('block-item-CollectionField-general-form-general.formula-formula')
|
||||||
.locator('.nb-read-pretty-input-number')
|
.locator('.nb-read-pretty-input-number'),
|
||||||
.innerText(),
|
).toHaveText('11');
|
||||||
).toBe('11');
|
|
||||||
await page.getByLabel('drawer-Action.Container-general-Edit record-mask').click();
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -426,7 +426,7 @@ export const T3825: PageConfig = {
|
|||||||
_isJSONSchemaObject: true,
|
_isJSONSchemaObject: true,
|
||||||
version: '2.0',
|
version: '2.0',
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: '{{ t("Edit") }}',
|
title: '{{ t("Edit record") }}',
|
||||||
'x-action': 'update',
|
'x-action': 'update',
|
||||||
'x-toolbar': 'ActionSchemaToolbar',
|
'x-toolbar': 'ActionSchemaToolbar',
|
||||||
'x-settings': 'actionSettings:edit',
|
'x-settings': 'actionSettings:edit',
|
||||||
|
@ -16,7 +16,6 @@ import { useAPIClient } from '../../../../api-client';
|
|||||||
import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer';
|
import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer';
|
||||||
import { SchemaInitializerActionModal } from '../../../../application/schema-initializer/components/SchemaInitializerActionModal';
|
import { SchemaInitializerActionModal } from '../../../../application/schema-initializer/components/SchemaInitializerActionModal';
|
||||||
import { SchemaInitializerItem } from '../../../../application/schema-initializer/components/SchemaInitializerItem';
|
import { SchemaInitializerItem } from '../../../../application/schema-initializer/components/SchemaInitializerItem';
|
||||||
import { useSchemaInitializer } from '../../../../application/schema-initializer/context';
|
|
||||||
import { useCollection_deprecated } from '../../../../collection-manager';
|
import { useCollection_deprecated } from '../../../../collection-manager';
|
||||||
import { SelectWithTitle } from '../../../../common/SelectWithTitle';
|
import { SelectWithTitle } from '../../../../common/SelectWithTitle';
|
||||||
import { useDataBlockProps } from '../../../../data-source';
|
import { useDataBlockProps } from '../../../../data-source';
|
||||||
|
@ -17,7 +17,13 @@ import {
|
|||||||
oneTableBlockWithAddNewAndViewAndEditAndBasicFields,
|
oneTableBlockWithAddNewAndViewAndEditAndBasicFields,
|
||||||
test,
|
test,
|
||||||
} from '@nocobase/test/e2e';
|
} from '@nocobase/test/e2e';
|
||||||
import { T3843, oneTableWithColumnFixed, oneTableWithUpdateRecord } from './templatesOfBug';
|
import {
|
||||||
|
T3843,
|
||||||
|
oneTableWithColumnFixed,
|
||||||
|
oneTableWithUpdateRecord,
|
||||||
|
testingOfOpenModeForAddChild,
|
||||||
|
testingWithPageMode,
|
||||||
|
} from './templatesOfBug';
|
||||||
|
|
||||||
const addSomeCustomActions = async (page: Page) => {
|
const addSomeCustomActions = async (page: Page) => {
|
||||||
// 先删除掉之前的 actions
|
// 先删除掉之前的 actions
|
||||||
@ -50,7 +56,7 @@ test.describe('actions schema settings', () => {
|
|||||||
await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-general' }).hover();
|
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 mockPage(oneEmptyTableBlockWithActions).goto();
|
||||||
|
|
||||||
await expectSettingsMenu({
|
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 mockPage(oneEmptyTableBlockWithActions).goto();
|
||||||
|
|
||||||
await showMenu(page);
|
await showMenu(page);
|
||||||
@ -72,7 +78,7 @@ test.describe('actions schema settings', () => {
|
|||||||
await expect(page.getByRole('button', { name: '1234' })).toBeVisible();
|
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 mockPage(oneEmptyTableBlockWithActions).goto();
|
||||||
await showMenu(page);
|
await showMenu(page);
|
||||||
|
|
||||||
@ -85,9 +91,45 @@ test.describe('actions schema settings', () => {
|
|||||||
|
|
||||||
await page.getByRole('button', { name: 'Add new' }).click();
|
await page.getByRole('button', { name: 'Add new' }).click();
|
||||||
await expect(page.getByTestId('modal-Action.Container-general-Add record')).toBeVisible();
|
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 mockPage(oneEmptyTableBlockWithActions).goto();
|
||||||
|
|
||||||
await showMenu(page);
|
await showMenu(page);
|
||||||
@ -116,7 +158,7 @@ test.describe('actions schema settings', () => {
|
|||||||
expect(drawerWidth2).toBeGreaterThanOrEqual(800);
|
expect(drawerWidth2).toBeGreaterThanOrEqual(800);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('delete', async ({ page, mockPage, mockRecord }) => {
|
test('delete', async ({ page, mockPage }) => {
|
||||||
await mockPage(oneEmptyTableBlockWithActions).goto();
|
await mockPage(oneEmptyTableBlockWithActions).goto();
|
||||||
|
|
||||||
await showMenu(page);
|
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();
|
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 mockPage(oneEmptyTableBlockWithActions).goto();
|
||||||
|
|
||||||
await expectSettingsMenu({
|
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', () => {
|
test.describe('popup', () => {
|
||||||
@ -459,7 +885,7 @@ test.describe('actions schema settings', () => {
|
|||||||
await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:addChild-tree').hover();
|
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();
|
const nocoPage = await mockPage(oneEmptyTableWithTreeCollection).waitForInit();
|
||||||
await nocoPage.goto();
|
await nocoPage.goto();
|
||||||
await page.getByLabel('block-item-CardItem-treeCollection-table').hover();
|
await page.getByLabel('block-item-CardItem-treeCollection-table').hover();
|
||||||
@ -511,6 +937,35 @@ test.describe('actions schema settings', () => {
|
|||||||
.getByText('1', { exact: true }),
|
.getByText('1', { exact: true }),
|
||||||
).toBeVisible();
|
).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', () => {
|
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();
|
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 mockPage(oneEmptyTableBlockWithActions).goto();
|
||||||
|
|
||||||
await expectSettingsMenu({
|
await expectSettingsMenu({
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,7 @@ import { useField, useFieldSchema } from '@formily/react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAPIClient } from '../../../../api-client';
|
import { useAPIClient } from '../../../../api-client';
|
||||||
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
|
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
|
||||||
|
import { createSwitchSettingsItem } from '../../../../application/schema-settings/utils/createSwitchSettingsItem';
|
||||||
import { useTableBlockContext } from '../../../../block-provider';
|
import { useTableBlockContext } from '../../../../block-provider';
|
||||||
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../../collection-manager';
|
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../../collection-manager';
|
||||||
import { FilterBlockType } from '../../../../filter-provider/utils';
|
import { FilterBlockType } from '../../../../filter-provider/utils';
|
||||||
@ -20,10 +21,9 @@ import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/Schema
|
|||||||
import { SchemaSettingsConnectDataBlocks } from '../../../../schema-settings/SchemaSettingsConnectDataBlocks';
|
import { SchemaSettingsConnectDataBlocks } from '../../../../schema-settings/SchemaSettingsConnectDataBlocks';
|
||||||
import { SchemaSettingsSortField } from '../../../../schema-settings/SchemaSettingsSortField';
|
import { SchemaSettingsSortField } from '../../../../schema-settings/SchemaSettingsSortField';
|
||||||
import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate';
|
import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate';
|
||||||
import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem';
|
|
||||||
import { setDefaultSortingRulesSchemaSettingsItem } from '../../../../schema-settings/setDefaultSortingRulesSchemaSettingsItem';
|
import { setDefaultSortingRulesSchemaSettingsItem } from '../../../../schema-settings/setDefaultSortingRulesSchemaSettingsItem';
|
||||||
import { setTheDataScopeSchemaSettingsItem } from '../../../../schema-settings/setTheDataScopeSchemaSettingsItem';
|
import { setTheDataScopeSchemaSettingsItem } from '../../../../schema-settings/setTheDataScopeSchemaSettingsItem';
|
||||||
import { createSwitchSettingsItem } from '../../../../application/schema-settings/utils';
|
import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem';
|
||||||
|
|
||||||
export const tableBlockSettings = new SchemaSettings({
|
export const tableBlockSettings = new SchemaSettings({
|
||||||
name: 'blockSettings:table',
|
name: 'blockSettings:table',
|
||||||
|
@ -171,7 +171,7 @@ test.describe('where filter block can be added', () => {
|
|||||||
await connectToOtherBlock('Users #o1nq');
|
await connectToOtherBlock('Users #o1nq');
|
||||||
|
|
||||||
await page.getByLabel('block-item-CardItem-users-filter-form').getByRole('textbox').fill(usersRecords[0].nickname);
|
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');
|
await page.waitForLoadState('networkidle');
|
||||||
for (const record of usersRecords) {
|
for (const record of usersRecords) {
|
||||||
await expect(page.getByLabel('block-item-CardItem-users-details').getByText(record.nickname)).toBeVisible({
|
await expect(page.getByLabel('block-item-CardItem-users-details').getByText(record.nickname)).toBeVisible({
|
||||||
|
53
packages/core/client/src/modules/page/__e2e__/router.test.ts
Normal file
53
packages/core/client/src/modules/page/__e2e__/router.test.ts
Normal file
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -71,7 +71,7 @@ test.describe('page schema settings', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe('tabs schema settings', () => {
|
test.describe('tabs schema settings', () => {
|
||||||
async function showSettings(page: Page) {
|
async function showSettingsOfTab(page: Page) {
|
||||||
await page.getByText('Unnamed').hover();
|
await page.getByText('Unnamed').hover();
|
||||||
await page.getByRole('tab').getByLabel('designer-schema-settings-Page').hover();
|
await page.getByRole('tab').getByLabel('designer-schema-settings-Page').hover();
|
||||||
}
|
}
|
||||||
@ -87,7 +87,7 @@ test.describe('tabs schema settings', () => {
|
|||||||
await mockPage().goto();
|
await mockPage().goto();
|
||||||
await enablePageTabs(page);
|
await enablePageTabs(page);
|
||||||
|
|
||||||
await showSettings(page);
|
await showSettingsOfTab(page);
|
||||||
await page.getByRole('menuitem', { name: 'Edit', exact: true }).click();
|
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').click();
|
||||||
await page.getByLabel('block-item-Input-Tab name').getByRole('textbox').fill('new name of page tab');
|
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 mockPage().goto();
|
||||||
await enablePageTabs(page);
|
await enablePageTabs(page);
|
||||||
|
|
||||||
await showSettings(page);
|
await showSettingsOfTab(page);
|
||||||
await page.getByRole('menuitem', { name: 'Delete', exact: true }).click();
|
await page.getByRole('menuitem', { name: 'Delete', exact: true }).click();
|
||||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||||
|
|
||||||
|
@ -7,3 +7,135 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* 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,
|
||||||
|
};
|
||||||
|
@ -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.getByLabel('schema-initializer-Tabs-').click();
|
||||||
await page.getByRole('textbox').click();
|
await page.getByRole('textbox').click();
|
||||||
await page.getByRole('textbox').fill('test123');
|
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();
|
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.getByLabel('schema-initializer-Tabs-').click();
|
||||||
await page.getByRole('textbox').click();
|
await page.getByRole('textbox').click();
|
||||||
await page.getByRole('textbox').fill('test7');
|
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();
|
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.getByLabel('schema-initializer-Tabs-').click();
|
||||||
await page.getByRole('textbox').click();
|
await page.getByRole('textbox').click();
|
||||||
await page.getByRole('textbox').fill('test8');
|
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();
|
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.getByLabel('schema-initializer-Tabs-').click();
|
||||||
await page.getByRole('textbox').click();
|
await page.getByRole('textbox').click();
|
||||||
await page.getByRole('textbox').fill('test1');
|
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();
|
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.getByLabel('schema-initializer-Tabs-').click();
|
||||||
await page.getByRole('textbox').click();
|
await page.getByRole('textbox').click();
|
||||||
await page.getByRole('textbox').fill('test8');
|
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();
|
await expect(page.getByText('test8')).toBeVisible();
|
||||||
|
|
||||||
|
@ -31,6 +31,9 @@ test.describe('tabs schema settings', () => {
|
|||||||
await page.goto(commonPageUrl);
|
await page.goto(commonPageUrl);
|
||||||
await page.getByRole('button', { name: 'Add new' }).click();
|
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 showSettings(page);
|
||||||
await page.getByRole('menuitem', { name: 'Edit', exact: true }).click();
|
await page.getByRole('menuitem', { name: 'Edit', exact: true }).click();
|
||||||
await page.mouse.move(300, 0);
|
await page.mouse.move(300, 0);
|
||||||
@ -48,6 +51,9 @@ test.describe('tabs schema settings', () => {
|
|||||||
await page.goto(commonPageUrl);
|
await page.goto(commonPageUrl);
|
||||||
await page.getByRole('button', { name: 'Add new' }).click();
|
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 showSettings(page);
|
||||||
await page.getByRole('menuitem', { name: 'Delete', exact: true }).click();
|
await page.getByRole('menuitem', { name: 'Delete', exact: true }).click();
|
||||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||||
|
@ -24,8 +24,10 @@ import { RemoteDocumentTitlePlugin } from '../document-title';
|
|||||||
import { PinnedListPlugin } from '../plugin-manager';
|
import { PinnedListPlugin } from '../plugin-manager';
|
||||||
import { PMPlugin } from '../pm';
|
import { PMPlugin } from '../pm';
|
||||||
import { AdminLayoutPlugin, RouteSchemaComponent } from '../route-switch';
|
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 { 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 { AssociationFilterPlugin, SchemaInitializerPlugin } from '../schema-initializer';
|
||||||
import { SchemaSettingsPlugin } from '../schema-settings';
|
import { SchemaSettingsPlugin } from '../schema-settings';
|
||||||
import { BlockTemplateDetails, BlockTemplatePage } from '../schema-templates';
|
import { BlockTemplateDetails, BlockTemplatePage } from '../schema-templates';
|
||||||
@ -302,6 +304,22 @@ export class NocoBaseBuildInPlugin extends Plugin {
|
|||||||
path: '/admin/:name',
|
path: '/admin/:name',
|
||||||
Component: 'AdminDynamicPage',
|
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() {
|
addComponents() {
|
||||||
|
@ -10,12 +10,11 @@
|
|||||||
import { PageHeader } from '@ant-design/pro-layout';
|
import { PageHeader } from '@ant-design/pro-layout';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { Layout, Menu, Result } from 'antd';
|
import { Layout, Menu, Result } from 'antd';
|
||||||
import _, { get } from 'lodash';
|
|
||||||
import React, { createContext, useCallback, useMemo } from 'react';
|
import React, { createContext, useCallback, useMemo } from 'react';
|
||||||
import { Navigate, Outlet, useLocation, useNavigate, useParams } from 'react-router-dom';
|
import { Navigate, Outlet, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useStyles } from './style';
|
|
||||||
import { ADMIN_SETTINGS_PATH, PluginSettingsPageType, useApp } from '../application';
|
import { ADMIN_SETTINGS_PATH, PluginSettingsPageType, useApp } from '../application';
|
||||||
import { useCompile } from '../schema-component';
|
import { useCompile } from '../schema-component';
|
||||||
|
import { useStyles } from './style';
|
||||||
|
|
||||||
export const SettingsCenterContext = createContext<any>({});
|
export const SettingsCenterContext = createContext<any>({});
|
||||||
SettingsCenterContext.displayName = 'SettingsCenterContext';
|
SettingsCenterContext.displayName = 'SettingsCenterContext';
|
||||||
|
@ -11,8 +11,8 @@ import { css } from '@emotion/css';
|
|||||||
import { useSessionStorageState } from 'ahooks';
|
import { useSessionStorageState } from 'ahooks';
|
||||||
import { App, ConfigProvider, Divider, Layout } from 'antd';
|
import { App, ConfigProvider, Divider, Layout } from 'antd';
|
||||||
import { createGlobalStyle } from 'antd-style';
|
import { createGlobalStyle } from 'antd-style';
|
||||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { FC, createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Link, Outlet, useLocation, useMatch, useNavigate, useParams } from 'react-router-dom';
|
import { Link, Outlet, useMatch, useParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ACLRolesCheckProvider,
|
ACLRolesCheckProvider,
|
||||||
CurrentAppInfoProvider,
|
CurrentAppInfoProvider,
|
||||||
@ -33,11 +33,12 @@ import {
|
|||||||
useSystemSettings,
|
useSystemSettings,
|
||||||
useToken,
|
useToken,
|
||||||
} from '../../../';
|
} from '../../../';
|
||||||
|
import { useLocationNoUpdate, useNavigateNoUpdate } from '../../../application/CustomRouterContextProvider';
|
||||||
import { Plugin } from '../../../application/Plugin';
|
import { Plugin } from '../../../application/Plugin';
|
||||||
import { useAppSpin } from '../../../application/hooks/useAppSpin';
|
import { useAppSpin } from '../../../application/hooks/useAppSpin';
|
||||||
|
import { useMenuTranslation } from '../../../schema-component/antd/menu/locale';
|
||||||
import { Help } from '../../../user/Help';
|
import { Help } from '../../../user/Help';
|
||||||
import { VariablesProvider } from '../../../variables';
|
import { VariablesProvider } from '../../../variables';
|
||||||
import { useMenuTranslation } from '../../../schema-component/antd/menu/locale';
|
|
||||||
|
|
||||||
const filterByACL = (schema, options) => {
|
const filterByACL = (schema, options) => {
|
||||||
const { allowAll, allowMenuItemIds = [] } = options;
|
const { allowAll, allowMenuItemIds = [] } = options;
|
||||||
@ -76,13 +77,13 @@ const useMenuProps = () => {
|
|||||||
|
|
||||||
const MenuEditor = (props) => {
|
const MenuEditor = (props) => {
|
||||||
const { notification } = App.useApp();
|
const { notification } = App.useApp();
|
||||||
const [hasNotice, setHasNotice] = useSessionStorageState('plugin-notice', { defaultValue: false });
|
const [, setHasNotice] = useSessionStorageState('plugin-notice', { defaultValue: false });
|
||||||
const { t } = useMenuTranslation();
|
const { t } = useMenuTranslation();
|
||||||
const { setTitle: _setTitle } = useDocumentTitle();
|
const { setTitle: _setTitle } = useDocumentTitle();
|
||||||
const setTitle = useCallback((title) => _setTitle(t(title)), []);
|
const setTitle = useCallback((title) => _setTitle(t(title)), []);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigateNoUpdate();
|
||||||
const params = useParams<any>();
|
const params = useParams<any>();
|
||||||
const location = useLocation();
|
const location = useLocationNoUpdate();
|
||||||
const isMatchAdmin = useMatch('/admin');
|
const isMatchAdmin = useMatch('/admin');
|
||||||
const isMatchAdminName = useMatch('/admin/:name');
|
const isMatchAdminName = useMatch('/admin/:name');
|
||||||
const defaultSelectedUid = params.name;
|
const defaultSelectedUid = params.name;
|
||||||
@ -91,12 +92,12 @@ const MenuEditor = (props) => {
|
|||||||
const ctx = useACLRoleContext();
|
const ctx = useACLRoleContext();
|
||||||
const [current, setCurrent] = useState(null);
|
const [current, setCurrent] = useState(null);
|
||||||
|
|
||||||
const onSelect = ({ item }) => {
|
const onSelect = useCallback(({ item }) => {
|
||||||
const schema = item.props.schema;
|
const schema = item.props.schema;
|
||||||
setTitle(schema.title);
|
setTitle(schema.title);
|
||||||
setCurrent(schema);
|
setCurrent(schema);
|
||||||
navigate(`/admin/${schema['x-uid']}`);
|
navigate(`/admin/${schema['x-uid']}`);
|
||||||
};
|
}, []);
|
||||||
const { render } = useAppSpin();
|
const { render } = useAppSpin();
|
||||||
const adminSchemaUid = useAdminSchemaUid();
|
const adminSchemaUid = useAdminSchemaUid();
|
||||||
const { data, loading } = useRequest<{
|
const { data, loading } = useRequest<{
|
||||||
@ -154,7 +155,7 @@ const MenuEditor = (props) => {
|
|||||||
sideMenuRef.current.style.display = 'block';
|
sideMenuRef.current.style.display = 'block';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [data?.data, params.name, sideMenuRef]);
|
}, [data?.data, params.name, sideMenuRef, location?.pathname]);
|
||||||
|
|
||||||
const schema = useMemo(() => {
|
const schema = useMemo(() => {
|
||||||
const s = filterByACL(data?.data, ctx);
|
const s = filterByACL(data?.data, ctx);
|
||||||
@ -211,17 +212,16 @@ const MenuEditor = (props) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const scope = useMemo(() => {
|
||||||
|
return { useMenuProps, onSelect, sideMenuRef, defaultSelectedUid };
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return render();
|
return render();
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<SchemaIdContext.Provider value={defaultSelectedUid}>
|
<SchemaIdContext.Provider value={defaultSelectedUid}>
|
||||||
<SchemaComponent
|
<SchemaComponent distributed memoized scope={scope} schema={schema} />
|
||||||
distributed
|
|
||||||
memoized
|
|
||||||
scope={{ useMenuProps, onSelect, sideMenuRef, defaultSelectedUid }}
|
|
||||||
schema={schema}
|
|
||||||
/>
|
|
||||||
</SchemaIdContext.Provider>
|
</SchemaIdContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -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 <Layout.Sider className={sideClass} theme={'light'} ref={props.sideMenuRef}></Layout.Sider>;
|
||||||
|
});
|
||||||
|
InternalAdminSideBar.displayName = 'InternalAdminSideBar';
|
||||||
|
|
||||||
const AdminSideBar = ({ sideMenuRef }) => {
|
const AdminSideBar = ({ sideMenuRef }) => {
|
||||||
const params = useParams<any>();
|
const params = useParams<any>();
|
||||||
if (!params.name) return null;
|
return <InternalAdminSideBar pageUid={params.name} sideMenuRef={sideMenuRef} />;
|
||||||
return (
|
|
||||||
<Layout.Sider
|
|
||||||
className={css`
|
|
||||||
height: 100%;
|
|
||||||
/* position: fixed; */
|
|
||||||
position: relative;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
background: rgba(0, 0, 0, 0);
|
|
||||||
z-index: 100;
|
|
||||||
.ant-layout-sider-children {
|
|
||||||
top: var(--nb-header-height);
|
|
||||||
position: fixed;
|
|
||||||
width: 200px;
|
|
||||||
height: calc(100vh - var(--nb-header-height));
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
theme={'light'}
|
|
||||||
ref={sideMenuRef}
|
|
||||||
></Layout.Sider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AdminDynamicPage = () => {
|
export const AdminDynamicPage = () => {
|
||||||
const params = useParams<{ name?: string }>();
|
return <RouteSchemaComponent />;
|
||||||
return <RouteSchemaComponent schema={params.name} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InternalAdminLayout = () => {
|
export const InternalAdminLayout = () => {
|
||||||
|
@ -11,7 +11,7 @@ import React from 'react';
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { RemoteSchemaComponent } from '../../../';
|
import { RemoteSchemaComponent } from '../../../';
|
||||||
|
|
||||||
export function RouteSchemaComponent(props: any) {
|
export function RouteSchemaComponent() {
|
||||||
const params = useParams<any>();
|
const params = useParams();
|
||||||
return <RemoteSchemaComponent onlyRenderProperties uid={params.name} />;
|
return <RemoteSchemaComponent onlyRenderProperties uid={params.name} />;
|
||||||
}
|
}
|
||||||
|
@ -11,13 +11,12 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea
|
|||||||
import { Drawer } from 'antd';
|
import { Drawer } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
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 { useStyles } from './Action.Drawer.style';
|
||||||
import { useActionContext } from './hooks';
|
import { useActionContext } from './hooks';
|
||||||
import { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer';
|
import { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer';
|
||||||
import { ComposedActionDrawer } from './types';
|
import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types';
|
||||||
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
|
|
||||||
import { ErrorFallback } from '../error-fallback';
|
|
||||||
|
|
||||||
const DrawerErrorFallback: React.FC<FallbackProps> = (props) => {
|
const DrawerErrorFallback: React.FC<FallbackProps> = (props) => {
|
||||||
const { visible, setVisible } = useActionContext();
|
const { visible, setVisible } = useActionContext();
|
||||||
|
@ -11,7 +11,7 @@ import { observer, RecursionField, useField, useFieldSchema, useForm } from '@fo
|
|||||||
import { isPortalInBody } from '@nocobase/utils/client';
|
import { isPortalInBody } from '@nocobase/utils/client';
|
||||||
import { App, Button } from 'antd';
|
import { App, Button } from 'antd';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { default as lodash } from 'lodash';
|
import _, { default as lodash } from 'lodash';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -28,6 +28,10 @@ import { useLocalVariables, useVariables } from '../../../variables';
|
|||||||
import { SortableItem } from '../../common';
|
import { SortableItem } from '../../common';
|
||||||
import { useCompile, useComponent, useDesigner } from '../../hooks';
|
import { useCompile, useComponent, useDesigner } from '../../hooks';
|
||||||
import { useProps } from '../../hooks/useProps';
|
import { useProps } from '../../hooks/useProps';
|
||||||
|
import { PopupVisibleProvider } from '../page/PagePopups';
|
||||||
|
import { usePagePopup } from '../page/pagePopupUtils';
|
||||||
|
import { usePopupSettings } from '../page/PopupSettingsProvider';
|
||||||
|
import { useNavigateTOSubPage } from '../page/SubPages';
|
||||||
import ActionContainer from './Action.Container';
|
import ActionContainer from './Action.Container';
|
||||||
import { ActionDesigner } from './Action.Designer';
|
import { ActionDesigner } from './Action.Designer';
|
||||||
import { ActionDrawer } from './Action.Drawer';
|
import { ActionDrawer } from './Action.Drawer';
|
||||||
@ -36,11 +40,16 @@ import { ActionModal } from './Action.Modal';
|
|||||||
import { ActionPage } from './Action.Page';
|
import { ActionPage } from './Action.Page';
|
||||||
import useStyles from './Action.style';
|
import useStyles from './Action.style';
|
||||||
import { ActionContextProvider } from './context';
|
import { ActionContextProvider } from './context';
|
||||||
import { useA } from './hooks';
|
|
||||||
import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction';
|
import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction';
|
||||||
import { ActionProps, ComposedAction } from './types';
|
import { ActionProps, ComposedAction } from './types';
|
||||||
import { linkageAction, setInitialActionState } from './utils';
|
import { linkageAction, setInitialActionState } from './utils';
|
||||||
|
|
||||||
|
const useA = () => {
|
||||||
|
return {
|
||||||
|
async run() {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const handleError = (err) => console.log(err);
|
const handleError = (err) => console.log(err);
|
||||||
|
|
||||||
export const Action: ComposedAction = withDynamicSchemaProps(
|
export const Action: ComposedAction = withDynamicSchemaProps(
|
||||||
@ -48,7 +57,6 @@ export const Action: ComposedAction = withDynamicSchemaProps(
|
|||||||
const {
|
const {
|
||||||
popover,
|
popover,
|
||||||
confirm,
|
confirm,
|
||||||
openMode: om,
|
|
||||||
containerRefKey,
|
containerRefKey,
|
||||||
component,
|
component,
|
||||||
useAction = useA,
|
useAction = useA,
|
||||||
@ -70,11 +78,13 @@ export const Action: ComposedAction = withDynamicSchemaProps(
|
|||||||
const aclCtx = useACLActionParamsContext();
|
const aclCtx = useACLActionParamsContext();
|
||||||
const { wrapSSR, componentCls, hashId } = useStyles();
|
const { wrapSSR, componentCls, hashId } = useStyles();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { visibleWithURL, setVisibleWithURL } = usePagePopup();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [formValueChanged, setFormValueChanged] = useState(false);
|
const [formValueChanged, setFormValueChanged] = useState(false);
|
||||||
|
const { setSubmitted: setParentSubmitted } = useActionContext();
|
||||||
const Designer = useDesigner();
|
const Designer = useDesigner();
|
||||||
const field = useField<any>();
|
const field = useField<any>();
|
||||||
const { run, element, disabled: disableAction } = useAction(actionCallback);
|
const { run, element, disabled: disableAction } = _.isFunction(useAction) ? useAction(actionCallback) : ({} as any);
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
@ -120,47 +130,12 @@ export const Action: ComposedAction = withDynamicSchemaProps(
|
|||||||
});
|
});
|
||||||
}, [field, linkageRules, localVariables, variables]);
|
}, [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(() => {
|
const buttonStyle = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
...style,
|
...style,
|
||||||
opacity: designable && (field?.data?.hidden || !aclCtx) && 0.1,
|
opacity: designable && (field?.data?.hidden || !aclCtx) && 0.1,
|
||||||
};
|
};
|
||||||
}, [designable, field?.data?.hidden, style]);
|
}, [aclCtx, designable, field?.data?.hidden, style]);
|
||||||
|
|
||||||
const handleMouseEnter = useCallback(
|
const handleMouseEnter = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
@ -168,55 +143,66 @@ export const Action: ComposedAction = withDynamicSchemaProps(
|
|||||||
},
|
},
|
||||||
[onMouseEnter],
|
[onMouseEnter],
|
||||||
);
|
);
|
||||||
const renderButton = () => {
|
|
||||||
if (!designable && (field?.data?.hidden || !aclCtx)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
const buttonProps = {
|
||||||
<SortableItem
|
designable,
|
||||||
role="button"
|
field,
|
||||||
aria-label={getAriaLabel()}
|
aclCtx,
|
||||||
{...others}
|
actionTitle,
|
||||||
onMouseEnter={handleMouseEnter}
|
icon,
|
||||||
loading={field?.data?.loading || loading}
|
loading,
|
||||||
icon={typeof icon === 'string' ? <Icon type={icon} /> : icon}
|
disabled,
|
||||||
disabled={disabled}
|
buttonStyle,
|
||||||
style={buttonStyle}
|
handleMouseEnter,
|
||||||
onClick={handleButtonClick}
|
tarComponent,
|
||||||
component={tarComponent || Button}
|
designerProps,
|
||||||
className={classnames(componentCls, hashId, className, 'nb-action')}
|
componentCls,
|
||||||
type={(props as any).type === 'danger' ? undefined : props.type}
|
hashId,
|
||||||
>
|
className,
|
||||||
{actionTitle}
|
others,
|
||||||
<Designer {...designerProps} />
|
getAriaLabel,
|
||||||
</SortableItem>
|
type: props.type,
|
||||||
);
|
Designer,
|
||||||
|
openMode,
|
||||||
|
onClick,
|
||||||
|
refreshDataBlockRequest,
|
||||||
|
service,
|
||||||
|
fieldSchema,
|
||||||
|
setVisible,
|
||||||
|
run,
|
||||||
|
confirm,
|
||||||
|
modal,
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonElement = renderButton();
|
const buttonElement = RenderButton(buttonProps);
|
||||||
|
|
||||||
// if (!btnHover) {
|
// if (!btnHover) {
|
||||||
// return buttonElement;
|
// return buttonElement;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const result = (
|
const result = (
|
||||||
<ActionContextProvider
|
<PopupVisibleProvider visible={false}>
|
||||||
button={buttonElement}
|
<ActionContextProvider
|
||||||
visible={visible}
|
button={buttonElement}
|
||||||
setVisible={setVisible}
|
visible={visible || visibleWithURL}
|
||||||
formValueChanged={formValueChanged}
|
setVisible={(value) => {
|
||||||
setFormValueChanged={setFormValueChanged}
|
setVisible?.(value);
|
||||||
openMode={openMode}
|
setVisibleWithURL?.(value);
|
||||||
openSize={openSize}
|
}}
|
||||||
containerRefKey={containerRefKey}
|
formValueChanged={formValueChanged}
|
||||||
fieldSchema={fieldSchema}
|
setFormValueChanged={setFormValueChanged}
|
||||||
>
|
openMode={openMode}
|
||||||
{popover && <RecursionField basePath={field.address} onlyRenderProperties schema={fieldSchema} />}
|
openSize={openSize}
|
||||||
{!popover && renderButton()}
|
containerRefKey={containerRefKey}
|
||||||
<VariablePopupRecordProvider>{!popover && props.children}</VariablePopupRecordProvider>
|
fieldSchema={fieldSchema}
|
||||||
{element}
|
setSubmitted={setParentSubmitted}
|
||||||
</ActionContextProvider>
|
>
|
||||||
|
{popover && <RecursionField basePath={field.address} onlyRenderProperties schema={fieldSchema} />}
|
||||||
|
{!popover && <RenderButton {...buttonProps} />}
|
||||||
|
<VariablePopupRecordProvider>{!popover && props.children}</VariablePopupRecordProvider>
|
||||||
|
{element}
|
||||||
|
</ActionContextProvider>
|
||||||
|
</PopupVisibleProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
// fix https://nocobase.height.app/T-3235/description
|
// fix https://nocobase.height.app/T-3235/description
|
||||||
@ -284,3 +270,129 @@ Action.Container = ActionContainer;
|
|||||||
Action.Page = ActionPage;
|
Action.Page = ActionPage;
|
||||||
|
|
||||||
export default Action;
|
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 (
|
||||||
|
<SortableItem
|
||||||
|
role="button"
|
||||||
|
aria-label={getAriaLabel()}
|
||||||
|
{...others}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
loading={field?.data?.loading || loading}
|
||||||
|
icon={typeof icon === 'string' ? <Icon type={icon} /> : icon}
|
||||||
|
disabled={disabled}
|
||||||
|
style={buttonStyle}
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
component={tarComponent || Button}
|
||||||
|
className={classnames(componentCls, hashId, className, 'nb-action')}
|
||||||
|
type={type === 'danger' ? undefined : type}
|
||||||
|
>
|
||||||
|
{actionTitle}
|
||||||
|
<Designer {...designerProps} />
|
||||||
|
</SortableItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -82,7 +82,6 @@ describe('Action', () => {
|
|||||||
expect(document.querySelector('.nb-action-page')).not.toBeInTheDocument();
|
expect(document.querySelector('.nb-action-page')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
await waitFor(async () => {
|
await waitFor(async () => {
|
||||||
await userEvent.click(getByText('Close'));
|
|
||||||
// page
|
// page
|
||||||
await userEvent.click(getByText('Page'));
|
await userEvent.click(getByText('Page'));
|
||||||
await userEvent.click(getByText('Open'));
|
await userEvent.click(getByText('Open'));
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createContext, useEffect, useRef, useState } from 'react';
|
import React, { createContext, useEffect, useRef, useState } from 'react';
|
||||||
import { useActionContext } from './hooks';
|
|
||||||
import { useDataBlockRequest } from '../../../data-source';
|
import { useDataBlockRequest } from '../../../data-source';
|
||||||
import { ActionContextProps } from './types';
|
import { ActionContextProps } from './types';
|
||||||
|
|
||||||
@ -17,11 +16,10 @@ ActionContext.displayName = 'ActionContext';
|
|||||||
|
|
||||||
export const ActionContextProvider: React.FC<ActionContextProps & { value?: ActionContextProps }> = (props) => {
|
export const ActionContextProvider: React.FC<ActionContextProps & { value?: ActionContextProps }> = (props) => {
|
||||||
const [submitted, setSubmitted] = useState(false); //是否有提交记录
|
const [submitted, setSubmitted] = useState(false); //是否有提交记录
|
||||||
const contextProps = useActionContext();
|
|
||||||
const { visible } = { ...props, ...props.value } || {};
|
const { visible } = { ...props, ...props.value } || {};
|
||||||
const isFirstRender = useRef(true); // 使用ref跟踪是否为首次渲染
|
const isFirstRender = useRef(true); // 使用ref跟踪是否为首次渲染
|
||||||
const service = useDataBlockRequest();
|
const service = useDataBlockRequest();
|
||||||
const { setSubmitted: setParentSubmitted } = { ...props, ...props.value, ...contextProps };
|
const { setSubmitted: setParentSubmitted } = { ...props, ...props.value };
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible !== undefined) {
|
if (visible !== undefined) {
|
||||||
if (isFirstRender.current) {
|
if (isFirstRender.current) {
|
||||||
@ -39,7 +37,7 @@ export const ActionContextProvider: React.FC<ActionContextProps & { value?: Acti
|
|||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionContext.Provider value={{ ...contextProps, ...props, ...props?.value, submitted, setSubmitted }}>
|
<ActionContext.Provider value={{ ...props, ...props?.value, submitted, setSubmitted }}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</ActionContext.Provider>
|
</ActionContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -14,12 +14,6 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useIsDetailBlock } from '../../../block-provider/FormBlockProvider';
|
import { useIsDetailBlock } from '../../../block-provider/FormBlockProvider';
|
||||||
import { ActionContext } from './context';
|
import { ActionContext } from './context';
|
||||||
|
|
||||||
export const useA = () => {
|
|
||||||
return {
|
|
||||||
async run() {},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useActionContext = () => {
|
export const useActionContext = () => {
|
||||||
const ctx = useContext(ActionContext);
|
const ctx = useContext(ActionContext);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
import { Field } from '@formily/core';
|
import { Field } from '@formily/core';
|
||||||
import { observer, useField, useFieldSchema } from '@formily/react';
|
import { observer, useField, useFieldSchema } from '@formily/react';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { useCollection, useCollectionManager } from '../../../data-source/collection';
|
import { useCollectionManager } from '../../../data-source/collection';
|
||||||
import { markRecordAsNew } from '../../../data-source/collection-record/isNewRecord';
|
import { markRecordAsNew } from '../../../data-source/collection-record/isNewRecord';
|
||||||
import { useSchemaComponentContext } from '../../hooks';
|
import { useSchemaComponentContext } from '../../hooks';
|
||||||
import { AssociationFieldContext } from './context';
|
import { AssociationFieldContext } from './context';
|
||||||
@ -18,8 +18,7 @@ import { AssociationFieldContext } from './context';
|
|||||||
export const AssociationFieldProvider = observer(
|
export const AssociationFieldProvider = observer(
|
||||||
(props) => {
|
(props) => {
|
||||||
const field = useField<Field>();
|
const field = useField<Field>();
|
||||||
const collection = useCollection();
|
const cm = useCollectionManager();
|
||||||
const dm = useCollectionManager();
|
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
|
|
||||||
// 这里有点奇怪,在 Table 切换显示的组件时,这个组件并不会触发重新渲染,所以增加这个 Hooks 让其重新渲染
|
// 这里有点奇怪,在 Table 切换显示的组件时,这个组件并不会触发重新渲染,所以增加这个 Hooks 让其重新渲染
|
||||||
@ -29,12 +28,12 @@ export const AssociationFieldProvider = observer(
|
|||||||
const allowDissociate = fieldSchema['x-component-props']?.allowDissociate !== false;
|
const allowDissociate = fieldSchema['x-component-props']?.allowDissociate !== false;
|
||||||
|
|
||||||
const collectionField = useMemo(
|
const collectionField = useMemo(
|
||||||
() => collection.getField(fieldSchema['x-collection-field']),
|
() => cm.getCollectionField(fieldSchema['x-collection-field']),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[fieldSchema['x-collection-field'], fieldSchema.name],
|
[fieldSchema['x-collection-field'], fieldSchema.name],
|
||||||
);
|
);
|
||||||
const isFileCollection = useMemo(
|
const isFileCollection = useMemo(
|
||||||
() => dm.getCollection(collectionField?.target)?.template === 'file',
|
() => cm.getCollection(collectionField?.target)?.template === 'file',
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[fieldSchema['x-collection-field']],
|
[fieldSchema['x-collection-field']],
|
||||||
);
|
);
|
||||||
|
@ -7,22 +7,20 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* 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 { toArr } from '@formily/shared';
|
||||||
import React, { Fragment, useRef, useState } from 'react';
|
import React, { Fragment, useRef } from 'react';
|
||||||
import { useDesignable } from '../../';
|
import { useDesignable } from '../../';
|
||||||
import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider';
|
import { useCollectionManager_deprecated } from '../../../collection-manager';
|
||||||
import { CollectionProvider_deprecated, useCollectionManager_deprecated } from '../../../collection-manager';
|
import { useCollectionRecordData } from '../../../data-source/collection-record/CollectionRecordProvider';
|
||||||
import { RecordProvider, useRecord } from '../../../record-provider';
|
|
||||||
import { FormProvider } from '../../core';
|
|
||||||
import { useCompile } from '../../hooks';
|
import { useCompile } from '../../hooks';
|
||||||
import { ActionContextProvider, useActionContext } from '../action';
|
import { useActionContext } from '../action';
|
||||||
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
|
import { usePagePopup } from '../page/pagePopupUtils';
|
||||||
|
import { transformNestedData } from './InternalCascadeSelect';
|
||||||
|
import { ButtonListProps, ReadPrettyInternalViewer, isObject } from './InternalViewer';
|
||||||
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
|
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
|
||||||
import schema from './schema';
|
import schema from './schema';
|
||||||
import { getTabFormatValue, useLabelUiSchema } from './util';
|
import { getTabFormatValue, useLabelUiSchema } from './util';
|
||||||
import { transformNestedData } from './InternalCascadeSelect';
|
|
||||||
import { isObject } from './InternalViewer';
|
|
||||||
|
|
||||||
interface IEllipsisWithTooltipRef {
|
interface IEllipsisWithTooltipRef {
|
||||||
setPopoverVisible: (boolean) => void;
|
setPopoverVisible: (boolean) => void;
|
||||||
@ -34,126 +32,77 @@ const toValue = (value, placeholder) => {
|
|||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ButtonTabList: React.FC<ButtonListProps> = (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<IEllipsisWithTooltipRef>();
|
||||||
|
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 (
|
||||||
|
<Fragment key={`${record?.[fieldNames.value]}_${index}`}>
|
||||||
|
<span>
|
||||||
|
{snapshot ? (
|
||||||
|
text
|
||||||
|
) : enableLink !== false ? (
|
||||||
|
<a
|
||||||
|
onMouseEnter={() => {
|
||||||
|
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}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
text
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{index < arr.length - 1 ? <span style={{ marginRight: 4, color: '#aaa' }}>,</span> : null}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return <>{renderRecords()}</>;
|
||||||
|
};
|
||||||
|
|
||||||
export const ReadPrettyInternalTag: React.FC = observer(
|
export const ReadPrettyInternalTag: React.FC = observer(
|
||||||
(props: any) => {
|
(props: any) => {
|
||||||
const fieldSchema = useFieldSchema();
|
return <ReadPrettyInternalViewer {...props} ButtonList={ButtonTabList} />;
|
||||||
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<IEllipsisWithTooltipRef>();
|
|
||||||
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 (
|
|
||||||
<Fragment key={`${record?.[fieldNames.value]}_${index}`}>
|
|
||||||
<span>
|
|
||||||
{snapshot ? (
|
|
||||||
text
|
|
||||||
) : enableLink !== false ? (
|
|
||||||
<a
|
|
||||||
onMouseEnter={() => {
|
|
||||||
setBtnHover(true);
|
|
||||||
}}
|
|
||||||
onClick={(e) => {
|
|
||||||
setBtnHover(true);
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
if (designable) {
|
|
||||||
insertViewer(schema.Viewer);
|
|
||||||
}
|
|
||||||
setVisible(true);
|
|
||||||
setRecord(record);
|
|
||||||
ellipsisWithTooltipRef?.current?.setPopoverVisible(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
text
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
{index < arr.length - 1 ? <span style={{ marginRight: 4, color: '#aaa' }}>,</span> : null}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const btnElement = (
|
|
||||||
<EllipsisWithTooltip ellipsis={true} ref={ellipsisWithTooltipRef}>
|
|
||||||
{renderRecords()}
|
|
||||||
</EllipsisWithTooltip>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (enableLink === false || !btnHover) {
|
|
||||||
return btnElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderWithoutTableFieldResourceProvider = () => (
|
|
||||||
<WithoutTableFieldResource.Provider value={true}>
|
|
||||||
<FormProvider>
|
|
||||||
<RecursionField
|
|
||||||
schema={fieldSchema}
|
|
||||||
onlyRenderProperties
|
|
||||||
basePath={field.address}
|
|
||||||
filterProperties={(s) => {
|
|
||||||
return s['x-component'] === 'AssociationField.Viewer';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormProvider>
|
|
||||||
</WithoutTableFieldResource.Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderRecordProvider = () => {
|
|
||||||
const collectionFieldNames = fieldSchema?.['x-collection-field']?.split('.');
|
|
||||||
|
|
||||||
return collectionFieldNames && collectionFieldNames.length > 2 ? (
|
|
||||||
<RecordProvider record={record} parent={recordCtx[collectionFieldNames[1]]}>
|
|
||||||
{renderWithoutTableFieldResourceProvider()}
|
|
||||||
</RecordProvider>
|
|
||||||
) : (
|
|
||||||
<RecordProvider record={record} parent={recordCtx}>
|
|
||||||
{renderWithoutTableFieldResourceProvider()}
|
|
||||||
</RecordProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<BlockAssociationContext.Provider value={`${collectionField?.collectionName}.${collectionField?.name}`}>
|
|
||||||
<CollectionProvider_deprecated name={collectionField?.target ?? collectionField?.targetCollection}>
|
|
||||||
{btnElement}
|
|
||||||
<ActionContextProvider
|
|
||||||
value={{ visible, setVisible, openMode: 'drawer', snapshot: collectionField?.interface === 'snapshot' }}
|
|
||||||
>
|
|
||||||
{renderRecordProvider()}
|
|
||||||
</ActionContextProvider>
|
|
||||||
</CollectionProvider_deprecated>
|
|
||||||
</BlockAssociationContext.Provider>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
{ displayName: 'ReadPrettyInternalTag' },
|
{ displayName: 'ReadPrettyInternalTag' },
|
||||||
);
|
);
|
||||||
|
@ -9,18 +9,16 @@
|
|||||||
|
|
||||||
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||||
import { toArr } from '@formily/shared';
|
import { toArr } from '@formily/shared';
|
||||||
import React, { Fragment, useRef, useState } from 'react';
|
import React, { FC, Fragment, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useDesignable } from '../../';
|
import { useDesignable } from '../../';
|
||||||
import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider';
|
import { WithoutTableFieldResource } from '../../../block-provider';
|
||||||
import { CollectionProvider_deprecated, useCollectionManager_deprecated } from '../../../collection-manager';
|
import { useCollectionManager, useCollectionRecordData } from '../../../data-source';
|
||||||
import { Collection } from '../../../data-source';
|
|
||||||
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
|
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
|
||||||
import { RecordProvider, useRecord } from '../../../record-provider';
|
|
||||||
import { FormProvider } from '../../core';
|
|
||||||
import { useCompile } from '../../hooks';
|
import { useCompile } from '../../hooks';
|
||||||
import { ActionContextProvider, useActionContext } from '../action';
|
import { ActionContextProvider, useActionContext } from '../action';
|
||||||
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
|
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
|
||||||
|
import { PopupVisibleProvider } from '../page/PagePopups';
|
||||||
|
import { usePagePopup } from '../page/pagePopupUtils';
|
||||||
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
|
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
|
||||||
import { transformNestedData } from './InternalCascadeSelect';
|
import { transformNestedData } from './InternalCascadeSelect';
|
||||||
import schema from './schema';
|
import schema from './schema';
|
||||||
@ -39,137 +37,153 @@ const toValue = (value, placeholder) => {
|
|||||||
export function isObject(value) {
|
export function isObject(value) {
|
||||||
return typeof value === 'object' && value !== null;
|
return typeof value === 'object' && value !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ButtonListProps {
|
||||||
|
value: any;
|
||||||
|
setBtnHover: any;
|
||||||
|
fieldNames?: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtonLinkList: FC<ButtonListProps> = (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<IEllipsisWithTooltipRef>();
|
||||||
|
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 (
|
||||||
|
<Fragment key={`${record?.id}_${index}`}>
|
||||||
|
<span>
|
||||||
|
{snapshot ? (
|
||||||
|
text
|
||||||
|
) : enableLink !== false ? (
|
||||||
|
<a
|
||||||
|
onMouseEnter={() => {
|
||||||
|
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}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
text
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{index < arr.length - 1 ? <span style={{ marginRight: 4, color: '#aaa' }}>,</span> : null}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return <>{renderRecords()}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ReadPrettyInternalViewerProps {
|
||||||
|
ButtonList: FC<ButtonListProps>;
|
||||||
|
value: any;
|
||||||
|
fieldNames?: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const ReadPrettyInternalViewer: React.FC = observer(
|
export const ReadPrettyInternalViewer: React.FC = observer(
|
||||||
(props: any) => {
|
(props: ReadPrettyInternalViewerProps) => {
|
||||||
|
const { value, ButtonList = ButtonLinkList } = props;
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const recordCtx = useRecord();
|
|
||||||
const { getCollection } = useCollectionManager_deprecated();
|
|
||||||
const { enableLink } = fieldSchema['x-component-props'] || {};
|
const { enableLink } = fieldSchema['x-component-props'] || {};
|
||||||
// value 做了转换,但 props.value 和原来 useField().value 的值不一致
|
// value 做了转换,但 props.value 和原来 useField().value 的值不一致
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const fieldNames = useFieldNames(props);
|
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const insertViewer = useInsertSchema('Viewer');
|
|
||||||
const { options: collectionField } = useAssociationFieldContext();
|
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<IEllipsisWithTooltipRef>();
|
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
|
||||||
const getLabelUiSchema = useLabelUiSchemaV2();
|
const { visibleWithURL, setVisibleWithURL } = usePagePopup();
|
||||||
const [btnHover, setBtnHover] = useState(false);
|
const [btnHover, setBtnHover] = useState(!!visibleWithURL);
|
||||||
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 (
|
|
||||||
<Fragment key={`${record?.id}_${index}`}>
|
|
||||||
<span>
|
|
||||||
{snapshot ? (
|
|
||||||
text
|
|
||||||
) : enableLink !== false ? (
|
|
||||||
<a
|
|
||||||
onMouseEnter={() => {
|
|
||||||
setBtnHover(true);
|
|
||||||
}}
|
|
||||||
onClick={(e) => {
|
|
||||||
setBtnHover(true);
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
if (designable) {
|
|
||||||
insertViewer(schema.Viewer);
|
|
||||||
}
|
|
||||||
setVisible(true);
|
|
||||||
setRecord(record);
|
|
||||||
ellipsisWithTooltipRef?.current?.setPopoverVisible(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
text
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
{index < arr.length - 1 ? <span style={{ marginRight: 4, color: '#aaa' }}>,</span> : null}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const btnElement = (
|
const btnElement = (
|
||||||
<EllipsisWithTooltip ellipsis={true} ref={ellipsisWithTooltipRef}>
|
<EllipsisWithTooltip ellipsis={true} ref={ellipsisWithTooltipRef}>
|
||||||
{renderRecords()}
|
<ButtonList setBtnHover={setBtnHover} value={value} fieldNames={props.fieldNames} />
|
||||||
</EllipsisWithTooltip>
|
</EllipsisWithTooltip>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (enableLink === false || !btnHover) {
|
if (enableLink === false || !btnHover) {
|
||||||
return btnElement;
|
return btnElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderWithoutTableFieldResourceProvider = () => (
|
const renderWithoutTableFieldResourceProvider = () => (
|
||||||
<VariablePopupRecordProvider recordData={record} collection={targetCollection as Collection}>
|
// The recordData here is only provided when the popup is opened, not the current row record
|
||||||
|
<VariablePopupRecordProvider>
|
||||||
<WithoutTableFieldResource.Provider value={true}>
|
<WithoutTableFieldResource.Provider value={true}>
|
||||||
<FormProvider>
|
<RecursionField
|
||||||
<RecursionField
|
schema={fieldSchema}
|
||||||
schema={fieldSchema}
|
onlyRenderProperties
|
||||||
onlyRenderProperties
|
basePath={field.address}
|
||||||
basePath={field.address}
|
filterProperties={(s) => {
|
||||||
filterProperties={(s) => {
|
return s['x-component'] === 'AssociationField.Viewer';
|
||||||
return s['x-component'] === 'AssociationField.Viewer';
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</FormProvider>
|
|
||||||
</WithoutTableFieldResource.Provider>
|
</WithoutTableFieldResource.Provider>
|
||||||
</VariablePopupRecordProvider>
|
</VariablePopupRecordProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderRecordProvider = () => {
|
|
||||||
const collectionFieldNames = fieldSchema?.['x-collection-field']?.split('.');
|
|
||||||
|
|
||||||
return collectionFieldNames && collectionFieldNames.length > 2 ? (
|
|
||||||
<RecordProvider record={record} parent={recordCtx[collectionFieldNames[1]]}>
|
|
||||||
{renderWithoutTableFieldResourceProvider()}
|
|
||||||
</RecordProvider>
|
|
||||||
) : (
|
|
||||||
<RecordProvider record={record} parent={recordCtx}>
|
|
||||||
{renderWithoutTableFieldResourceProvider()}
|
|
||||||
</RecordProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<PopupVisibleProvider visible={false}>
|
||||||
<BlockAssociationContext.Provider value={`${collectionField?.collectionName}.${collectionField?.name}`}>
|
<ActionContextProvider
|
||||||
<CollectionProvider_deprecated name={collectionField?.target ?? collectionField?.targetCollection}>
|
value={{
|
||||||
{btnElement}
|
visible: visible || visibleWithURL,
|
||||||
<ActionContextProvider
|
setVisible: (value) => {
|
||||||
value={{
|
setVisible?.(value);
|
||||||
visible,
|
setVisibleWithURL?.(value);
|
||||||
setVisible,
|
},
|
||||||
openMode: 'drawer',
|
openMode: 'drawer',
|
||||||
snapshot: collectionField?.interface === 'snapshot',
|
snapshot: collectionField?.interface === 'snapshot',
|
||||||
fieldSchema: fieldSchema,
|
fieldSchema: fieldSchema,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{renderRecordProvider()}
|
{btnElement}
|
||||||
</ActionContextProvider>
|
{renderWithoutTableFieldResourceProvider()}
|
||||||
</CollectionProvider_deprecated>
|
</ActionContextProvider>
|
||||||
</BlockAssociationContext.Provider>
|
</PopupVisibleProvider>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ displayName: 'ReadPrettyInternalViewer' },
|
{ displayName: 'ReadPrettyInternalViewer' },
|
||||||
|
@ -185,7 +185,14 @@ export default function useServiceOptions(props) {
|
|||||||
}, [collectionField?.target, action, filter, service]);
|
}, [collectionField?.target, action, filter, service]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFieldNames = (props) => {
|
export const useFieldNames = (
|
||||||
|
props: {
|
||||||
|
fieldNames?: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
} = {},
|
||||||
|
) => {
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const fieldNames =
|
const fieldNames =
|
||||||
fieldSchema['x-component-props']?.['field']?.['uiSchema']?.['x-component-props']?.['fieldNames'] ||
|
fieldSchema['x-component-props']?.['field']?.['uiSchema']?.['x-component-props']?.['fieldNames'] ||
|
||||||
|
@ -7,19 +7,19 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useFieldSchema } from '@formily/react';
|
||||||
import { Button, Result, Typography } from 'antd';
|
import { Button, Result, Typography } from 'antd';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { FallbackProps } from 'react-error-boundary';
|
import { FallbackProps } from 'react-error-boundary';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { ErrorFallbackModal } from './ErrorFallbackModal';
|
|
||||||
import { useAPIClient } from '../../../api-client';
|
import { useAPIClient } from '../../../api-client';
|
||||||
import { useFieldSchema } from '@formily/react';
|
import { useLocationNoUpdate } from '../../../application';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { ErrorFallbackModal } from './ErrorFallbackModal';
|
||||||
|
|
||||||
const { Paragraph, Text, Link } = Typography;
|
const { Paragraph, Text, Link } = Typography;
|
||||||
|
|
||||||
export const useDownloadLogs = (error: any, data: Record<string, any> = {}) => {
|
export const useDownloadLogs = (error: any, data: Record<string, any> = {}) => {
|
||||||
const location = useLocation();
|
const location = useLocationNoUpdate();
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
return {
|
return {
|
||||||
|
@ -32,7 +32,8 @@ const InternalGridCardBlockProvider = (props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!service?.loading) {
|
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]);
|
}, [field.address, form, service?.data?.data, service?.loading]);
|
||||||
|
|
||||||
|
@ -32,7 +32,10 @@ const InternalListBlockProvider = (props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!service?.loading) {
|
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]);
|
}, [field.address, form, service?.data?.data, service?.loading]);
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* 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 { ArrayField } from '@formily/core';
|
||||||
import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
||||||
import { List as AntdList, PaginationProps, theme } from 'antd';
|
import { List as AntdList, PaginationProps, theme } from 'antd';
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
import { TreeSelect } from '@formily/antd-v5';
|
import { TreeSelect } from '@formily/antd-v5';
|
||||||
import { Field, onFieldChange } from '@formily/core';
|
import { Field, onFieldChange } from '@formily/core';
|
||||||
import { ISchema, Schema, useField, useFieldSchema } from '@formily/react';
|
import { ISchema, Schema, useField, useFieldSchema } from '@formily/react';
|
||||||
import React from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { findByUid } from '.';
|
import { findByUid } from '.';
|
||||||
import { createDesignable, useCompile } from '../..';
|
import { createDesignable, useCompile } from '../..';
|
||||||
@ -210,6 +210,8 @@ const InsertMenuItems = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const components = { TreeSelect };
|
||||||
|
|
||||||
export const MenuDesigner = () => {
|
export const MenuDesigner = () => {
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
@ -218,48 +220,58 @@ export const MenuDesigner = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const menuSchema = findMenuSchema(fieldSchema);
|
const menuSchema = findMenuSchema(fieldSchema);
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const onSelect = compile(menuSchema?.['x-component-props']?.['onSelect']);
|
const onSelect = useMemo(
|
||||||
const items = toItems(menuSchema?.properties);
|
() => compile(menuSchema?.['x-component-props']?.['onSelect']),
|
||||||
const effects = (form) => {
|
[menuSchema?.['x-component-props']?.['onSelect']],
|
||||||
onFieldChange('target', (field: Field) => {
|
);
|
||||||
const [, component] = field?.value?.split?.('||') || [];
|
const items = useMemo(() => toItems(menuSchema?.properties), [menuSchema?.properties]);
|
||||||
field.query('position').take((f: Field) => {
|
const effects = useCallback(
|
||||||
f.dataSource =
|
(form) => {
|
||||||
component === 'Menu.SubMenu'
|
onFieldChange('target', (field: Field) => {
|
||||||
? [
|
const [, component] = field?.value?.split?.('||') || [];
|
||||||
{ label: t('Before'), value: 'beforeBegin' },
|
field.query('position').take((f: Field) => {
|
||||||
{ label: t('After'), value: 'afterEnd' },
|
f.dataSource =
|
||||||
{ label: t('Inner'), value: 'beforeEnd' },
|
component === 'Menu.SubMenu'
|
||||||
]
|
? [
|
||||||
: [
|
{ label: t('Before'), value: 'beforeBegin' },
|
||||||
{ label: t('Before'), value: 'beforeBegin' },
|
{ label: t('After'), value: 'afterEnd' },
|
||||||
{ 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',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
[t],
|
||||||
const initialValues = {
|
);
|
||||||
title: field.title,
|
const schema = useMemo(() => {
|
||||||
icon: field.componentProps.icon,
|
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') {
|
if (fieldSchema['x-component'] === 'Menu.URL') {
|
||||||
schema.properties['href'] = {
|
schema.properties['href'] = {
|
||||||
title: t('Link'),
|
title: t('Link'),
|
||||||
@ -268,6 +280,90 @@ export const MenuDesigner = () => {
|
|||||||
};
|
};
|
||||||
initialValues['href'] = field.componentProps.href;
|
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 (
|
return (
|
||||||
<GeneralSchemaDesigner>
|
<GeneralSchemaDesigner>
|
||||||
<SchemaSettingsModalItem
|
<SchemaSettingsModalItem
|
||||||
@ -275,92 +371,22 @@ export const MenuDesigner = () => {
|
|||||||
eventKey="edit"
|
eventKey="edit"
|
||||||
schema={schema as ISchema}
|
schema={schema as ISchema}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={({ title, icon, href }) => {
|
onSubmit={onEditSubmit}
|
||||||
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,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<SchemaSettingsModalItem
|
<SchemaSettingsModalItem
|
||||||
title={t('Move to')}
|
title={t('Move to')}
|
||||||
eventKey="move-to"
|
eventKey="move-to"
|
||||||
components={{ TreeSelect }}
|
components={components}
|
||||||
effects={effects}
|
effects={effects}
|
||||||
schema={
|
schema={modalSchema}
|
||||||
{
|
onSubmit={onMoveToSubmit}
|
||||||
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
|
|
||||||
}
|
|
||||||
onSubmit={({ 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);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<SchemaSettingsDivider />
|
<SchemaSettingsDivider />
|
||||||
<InsertMenuItems eventKey={'insertbeforeBegin'} title={t('Insert before')} insertPosition={'beforeBegin'} />
|
<InsertMenuItems eventKey={'insertbeforeBegin'} title={t('Insert before')} insertPosition={'beforeBegin'} />
|
||||||
<InsertMenuItems eventKey={'insertafterEnd'} title={t('Insert after')} insertPosition={'afterEnd'} />
|
<InsertMenuItems eventKey={'insertafterEnd'} title={t('Insert after')} insertPosition={'afterEnd'} />
|
||||||
<InsertMenuItems eventKey={'insertbeforeEnd'} title={t('Insert inner')} insertPosition={'beforeEnd'} />
|
<InsertMenuItems eventKey={'insertbeforeEnd'} title={t('Insert inner')} insertPosition={'beforeEnd'} />
|
||||||
<SchemaSettingsDivider />
|
<SchemaSettingsDivider />
|
||||||
<SchemaSettingsRemove
|
<SchemaSettingsRemove confirm={removeConfirmTitle} />
|
||||||
confirm={{
|
|
||||||
title: t('Delete menu item'),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</GeneralSchemaDesigner>
|
</GeneralSchemaDesigner>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import { error } from '@nocobase/utils/client';
|
import { error } from '@nocobase/utils/client';
|
||||||
import { Menu as AntdMenu, MenuProps } from 'antd';
|
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 { createPortal } from 'react-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { createDesignable, DndContext, SortableItem, useDesignable, useDesigner } from '../..';
|
import { createDesignable, DndContext, SortableItem, useDesignable, useDesigner } from '../..';
|
||||||
@ -229,47 +229,52 @@ const HeaderMenu = ({
|
|||||||
return result;
|
return result;
|
||||||
}, [children, designable]);
|
}, [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Component />
|
<Component />
|
||||||
<AntdMenu
|
<AntdMenu
|
||||||
{...others}
|
{...others}
|
||||||
className={headerMenuClass}
|
className={headerMenuClass}
|
||||||
onSelect={(info: any) => {
|
onSelect={handleSelect}
|
||||||
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);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
mode={mode === 'mix' ? 'horizontal' : mode}
|
mode={mode === 'mix' ? 'horizontal' : mode}
|
||||||
defaultOpenKeys={defaultOpenKeys}
|
defaultOpenKeys={defaultOpenKeys}
|
||||||
defaultSelectedKeys={defaultSelectedKeys}
|
defaultSelectedKeys={defaultSelectedKeys}
|
||||||
@ -347,12 +352,8 @@ const SideMenu = ({
|
|||||||
mode={'inline'}
|
mode={'inline'}
|
||||||
openKeys={openKeys}
|
openKeys={openKeys}
|
||||||
selectedKeys={selectedKeys}
|
selectedKeys={selectedKeys}
|
||||||
onSelect={(info) => {
|
onSelect={onSelect}
|
||||||
onSelect?.(info);
|
onOpenChange={setOpenKeys}
|
||||||
}}
|
|
||||||
onOpenChange={(openKeys) => {
|
|
||||||
setOpenKeys(openKeys);
|
|
||||||
}}
|
|
||||||
className={sideMenuClass}
|
className={sideMenuClass}
|
||||||
items={items as MenuProps['items']}
|
items={items as MenuProps['items']}
|
||||||
/>
|
/>
|
||||||
@ -496,6 +497,14 @@ export const Menu: ComposedMenu = observer(
|
|||||||
{ displayName: 'Menu' },
|
{ displayName: 'Menu' },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const menuItemTitleStyle = {
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
display: 'inline-block',
|
||||||
|
width: '100%',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
};
|
||||||
|
|
||||||
Menu.Item = observer(
|
Menu.Item = observer(
|
||||||
(props) => {
|
(props) => {
|
||||||
const { t } = useMenuTranslation();
|
const { t } = useMenuTranslation();
|
||||||
@ -521,17 +530,7 @@ Menu.Item = observer(
|
|||||||
removeParentsIfNoChildren={false}
|
removeParentsIfNoChildren={false}
|
||||||
>
|
>
|
||||||
<Icon type={icon} />
|
<Icon type={icon} />
|
||||||
<span
|
<span style={menuItemTitleStyle}>{t(field.title)}</span>
|
||||||
style={{
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
display: 'inline-block',
|
|
||||||
width: '100%',
|
|
||||||
verticalAlign: 'middle',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t(field.title)}
|
|
||||||
</span>
|
|
||||||
<Designer />
|
<Designer />
|
||||||
</SortableItem>
|
</SortableItem>
|
||||||
</FieldContext.Provider>
|
</FieldContext.Provider>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* 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) => {
|
export const useStyles = genStyleHook('nb-page', (token) => {
|
||||||
const { componentCls } = token;
|
const { componentCls } = token;
|
@ -13,13 +13,14 @@ import { FormLayout } from '@formily/antd-v5';
|
|||||||
import { Schema, SchemaOptionsContext, useFieldSchema } from '@formily/react';
|
import { Schema, SchemaOptionsContext, useFieldSchema } from '@formily/react';
|
||||||
import { Button, Tabs } from 'antd';
|
import { Button, Tabs } from 'antd';
|
||||||
import classNames from 'classnames';
|
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 { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { Outlet, useOutletContext, useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { FormDialog } from '..';
|
import { FormDialog } from '..';
|
||||||
import { useStyles as useAClStyles } from '../../../acl/style';
|
import { useStyles as useAClStyles } from '../../../acl/style';
|
||||||
import { useRequest } from '../../../api-client';
|
import { useRequest } from '../../../api-client';
|
||||||
|
import { useNavigateNoUpdate } from '../../../application/CustomRouterContextProvider';
|
||||||
import { useAppSpin } from '../../../application/hooks/useAppSpin';
|
import { useAppSpin } from '../../../application/hooks/useAppSpin';
|
||||||
import { useDocumentTitle } from '../../../document-title';
|
import { useDocumentTitle } from '../../../document-title';
|
||||||
import { useGlobalTheme } from '../../../global-theme';
|
import { useGlobalTheme } from '../../../global-theme';
|
||||||
@ -32,8 +33,8 @@ import { useCompile, useDesignable } from '../../hooks';
|
|||||||
import { useToken } from '../__builtins__';
|
import { useToken } from '../__builtins__';
|
||||||
import { ErrorFallback } from '../error-fallback';
|
import { ErrorFallback } from '../error-fallback';
|
||||||
import FixedBlock from './FixedBlock';
|
import FixedBlock from './FixedBlock';
|
||||||
|
import { useStyles } from './Page.style';
|
||||||
import { PageDesigner, PageTabDesigner } from './PageTabDesigner';
|
import { PageDesigner, PageTabDesigner } from './PageTabDesigner';
|
||||||
import { useStyles } from './style';
|
|
||||||
|
|
||||||
export const Page = (props) => {
|
export const Page = (props) => {
|
||||||
const { children, ...others } = props;
|
const { children, ...others } = props;
|
||||||
@ -44,6 +45,7 @@ export const Page = (props) => {
|
|||||||
const dn = useDesignable();
|
const dn = useDesignable();
|
||||||
const { theme } = useGlobalTheme();
|
const { theme } = useGlobalTheme();
|
||||||
const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer();
|
const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer();
|
||||||
|
const { tabUid, name: pageUid } = useParams();
|
||||||
|
|
||||||
// react18 tab 动画会卡顿,所以第一个 tab 时,动画禁用,后面的 tab 才启用
|
// react18 tab 动画会卡顿,所以第一个 tab 时,动画禁用,后面的 tab 才启用
|
||||||
const [hasMounted, setHasMounted] = useState(false);
|
const [hasMounted, setHasMounted] = useState(false);
|
||||||
@ -62,11 +64,13 @@ export const Page = (props) => {
|
|||||||
const enablePageTabs = fieldSchema['x-component-props']?.enablePageTabs;
|
const enablePageTabs = fieldSchema['x-component-props']?.enablePageTabs;
|
||||||
const hidePageTitle = fieldSchema['x-component-props']?.hidePageTitle;
|
const hidePageTitle = fieldSchema['x-component-props']?.hidePageTitle;
|
||||||
const options = useContext(SchemaOptionsContext);
|
const options = useContext(SchemaOptionsContext);
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const navigate = useNavigateNoUpdate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const activeKey = useMemo(
|
const activeKey = useMemo(
|
||||||
() => searchParams.get('tab') || Object.keys(fieldSchema.properties || {}).shift(),
|
// 处理 searchParams 是为了兼容旧版的 tab 参数
|
||||||
[fieldSchema.properties, searchParams],
|
() => tabUid || searchParams.get('tab') || Object.keys(fieldSchema.properties || {}).shift(),
|
||||||
|
[fieldSchema.properties, searchParams, tabUid],
|
||||||
);
|
);
|
||||||
const [height, setHeight] = useState(0);
|
const [height, setHeight] = useState(0);
|
||||||
const { wrapSSR, hashId, componentCls } = useStyles();
|
const { wrapSSR, hashId, componentCls } = useStyles();
|
||||||
@ -87,9 +91,115 @@ export const Page = (props) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleErrors = (error) => {
|
const handleErrors = useCallback((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
|
const footer = useMemo(() => {
|
||||||
|
return enablePageTabs ? (
|
||||||
|
<DndContext>
|
||||||
|
<Tabs
|
||||||
|
size={'small'}
|
||||||
|
animated={hasMounted}
|
||||||
|
activeKey={activeKey}
|
||||||
|
// 这里的样式是为了保证页面 tabs 标签下面的分割线和页面内容对齐(页面内边距可以通过主题编辑器调节)
|
||||||
|
tabBarStyle={{
|
||||||
|
paddingLeft: token.paddingLG - token.paddingPageHorizontal,
|
||||||
|
paddingRight: token.paddingLG - token.paddingPageHorizontal,
|
||||||
|
marginLeft: token.paddingPageHorizontal - token.paddingLG,
|
||||||
|
marginRight: token.paddingPageHorizontal - token.paddingLG,
|
||||||
|
}}
|
||||||
|
onTabClick={(activeKey) => {
|
||||||
|
setLoading(true);
|
||||||
|
navigate(`/admin/${pageUid}/tabs/${activeKey}`);
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, 50);
|
||||||
|
}}
|
||||||
|
tabBarExtraContent={
|
||||||
|
dn.designable && (
|
||||||
|
<Button
|
||||||
|
aria-label={getAriaLabel('tabs')}
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
className={'addTabBtn'}
|
||||||
|
type={'dashed'}
|
||||||
|
onClick={async () => {
|
||||||
|
const values = await FormDialog(
|
||||||
|
t('Add tab'),
|
||||||
|
() => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
|
||||||
|
<FormLayout layout={'vertical'}>
|
||||||
|
<SchemaComponent
|
||||||
|
schema={{
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
title: t('Tab name'),
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
title: t('Icon'),
|
||||||
|
'x-component': 'IconPicker',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormLayout>
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
theme,
|
||||||
|
).open({
|
||||||
|
initialValues: {},
|
||||||
|
});
|
||||||
|
const { title, icon } = values;
|
||||||
|
dn.insertBeforeEnd({
|
||||||
|
type: 'void',
|
||||||
|
title,
|
||||||
|
'x-icon': icon,
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'page:addBlock',
|
||||||
|
properties: {},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Add tab')}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items={fieldSchema.mapProperties((schema) => {
|
||||||
|
return {
|
||||||
|
label: (
|
||||||
|
<SortableItem
|
||||||
|
id={schema.name as string}
|
||||||
|
schema={schema}
|
||||||
|
className={classNames('nb-action-link', 'designerCss', props.className)}
|
||||||
|
>
|
||||||
|
{schema['x-icon'] && <Icon style={{ marginRight: 8 }} type={schema['x-icon']} />}
|
||||||
|
<span>{schema.title || t('Unnamed')}</span>
|
||||||
|
<PageTabDesigner schema={schema} />
|
||||||
|
</SortableItem>
|
||||||
|
),
|
||||||
|
key: schema.name as string,
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</DndContext>
|
||||||
|
) : null;
|
||||||
|
}, [
|
||||||
|
hasMounted,
|
||||||
|
activeKey,
|
||||||
|
fieldSchema,
|
||||||
|
dn.designable,
|
||||||
|
options.scope,
|
||||||
|
options.components,
|
||||||
|
pageUid,
|
||||||
|
fieldSchema.mapProperties((schema) => schema.title || t('Unnamed')).join(),
|
||||||
|
enablePageTabs,
|
||||||
|
]);
|
||||||
|
|
||||||
return wrapSSR(
|
return wrapSSR(
|
||||||
<div className={`${componentCls} ${hashId} ${aclStyles.styles}`}>
|
<div className={`${componentCls} ${hashId} ${aclStyles.styles}`}>
|
||||||
@ -106,155 +216,94 @@ export const Page = (props) => {
|
|||||||
// 如果标题为空的时候会导致 PageHeader 不渲染,所以这里设置一个空白字符,然后再设置高度为 0
|
// 如果标题为空的时候会导致 PageHeader 不渲染,所以这里设置一个空白字符,然后再设置高度为 0
|
||||||
title={pageHeaderTitle || ' '}
|
title={pageHeaderTitle || ' '}
|
||||||
{...others}
|
{...others}
|
||||||
footer={
|
footer={footer}
|
||||||
enablePageTabs && (
|
|
||||||
<DndContext>
|
|
||||||
<Tabs
|
|
||||||
size={'small'}
|
|
||||||
animated={hasMounted}
|
|
||||||
activeKey={activeKey}
|
|
||||||
// 这里的样式是为了保证页面 tabs 标签下面的分割线和页面内容对齐(页面内边距可以通过主题编辑器调节)
|
|
||||||
tabBarStyle={{
|
|
||||||
paddingLeft: token.paddingLG - token.paddingPageHorizontal,
|
|
||||||
paddingRight: token.paddingLG - token.paddingPageHorizontal,
|
|
||||||
marginLeft: token.paddingPageHorizontal - token.paddingLG,
|
|
||||||
marginRight: token.paddingPageHorizontal - token.paddingLG,
|
|
||||||
}}
|
|
||||||
onTabClick={(activeKey) => {
|
|
||||||
setLoading(true);
|
|
||||||
setSearchParams([['tab', activeKey]]);
|
|
||||||
setTimeout(() => {
|
|
||||||
setLoading(false);
|
|
||||||
}, 50);
|
|
||||||
}}
|
|
||||||
tabBarExtraContent={
|
|
||||||
dn.designable && (
|
|
||||||
<Button
|
|
||||||
aria-label={getAriaLabel('tabs')}
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
className={'addTabBtn'}
|
|
||||||
type={'dashed'}
|
|
||||||
onClick={async () => {
|
|
||||||
const values = await FormDialog(
|
|
||||||
t('Add tab'),
|
|
||||||
() => {
|
|
||||||
return (
|
|
||||||
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
|
|
||||||
<FormLayout layout={'vertical'}>
|
|
||||||
<SchemaComponent
|
|
||||||
schema={{
|
|
||||||
properties: {
|
|
||||||
title: {
|
|
||||||
title: t('Tab name'),
|
|
||||||
'x-component': 'Input',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
title: t('Icon'),
|
|
||||||
'x-component': 'IconPicker',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormLayout>
|
|
||||||
</SchemaComponentOptions>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
theme,
|
|
||||||
).open({
|
|
||||||
initialValues: {},
|
|
||||||
});
|
|
||||||
const { title, icon } = values;
|
|
||||||
dn.insertBeforeEnd({
|
|
||||||
type: 'void',
|
|
||||||
title,
|
|
||||||
'x-icon': icon,
|
|
||||||
'x-component': 'Grid',
|
|
||||||
'x-initializer': 'page:addBlock',
|
|
||||||
properties: {},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('Add tab')}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
items={fieldSchema.mapProperties((schema) => {
|
|
||||||
return {
|
|
||||||
label: (
|
|
||||||
<SortableItem
|
|
||||||
id={schema.name as string}
|
|
||||||
schema={schema}
|
|
||||||
className={classNames('nb-action-link', 'designerCss', props.className)}
|
|
||||||
>
|
|
||||||
{schema['x-icon'] && <Icon style={{ marginRight: 8 }} type={schema['x-icon']} />}
|
|
||||||
<span>{schema.title || t('Unnamed')}</span>
|
|
||||||
<PageTabDesigner schema={schema} />
|
|
||||||
</SortableItem>
|
|
||||||
),
|
|
||||||
key: schema.name as string,
|
|
||||||
};
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</DndContext>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="nb-page-wrapper">
|
<div className="nb-page-wrapper">
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback} onError={handleErrors}>
|
<ErrorBoundary FallbackComponent={ErrorFallback} onError={handleErrors}>
|
||||||
{PageContent(loading, disablePageHeader, enablePageTabs, fieldSchema, activeKey, height, props)}
|
{tabUid ? (
|
||||||
|
// used to match the rout with name "admin.page.tab"
|
||||||
|
<Outlet context={{ loading, disablePageHeader, enablePageTabs, fieldSchema, height, tabUid }} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<PageContent {...{ loading, disablePageHeader, enablePageTabs, fieldSchema, height, activeKey }} />
|
||||||
|
{/* Used to match the route with name "admin.page.popup" */}
|
||||||
|
<Outlet />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PageTabs = () => {
|
||||||
|
const { loading, disablePageHeader, enablePageTabs, fieldSchema, height, tabUid } = useOutletContext<any>();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageContent {...{ loading, disablePageHeader, enablePageTabs, fieldSchema, activeKey: tabUid, height }} />
|
||||||
|
{/* used to match the route with name "admin.page.tab.popup" */}
|
||||||
|
<Outlet />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
Page.displayName = 'Page';
|
Page.displayName = 'Page';
|
||||||
|
|
||||||
function PageContent(
|
const PageContent = memo(
|
||||||
loading: boolean,
|
({
|
||||||
disablePageHeader: any,
|
loading,
|
||||||
enablePageTabs: any,
|
disablePageHeader,
|
||||||
fieldSchema: Schema<any, any, any, any, any, any, any, any, any>,
|
enablePageTabs,
|
||||||
activeKey: string,
|
fieldSchema,
|
||||||
height: number,
|
activeKey,
|
||||||
props: any,
|
height,
|
||||||
): React.ReactNode {
|
}: {
|
||||||
const { token } = useToken();
|
loading: boolean;
|
||||||
const { render } = useAppSpin();
|
disablePageHeader: any;
|
||||||
|
enablePageTabs: any;
|
||||||
|
fieldSchema: Schema<any, any, any, any, any, any, any, any, any>;
|
||||||
|
activeKey: string;
|
||||||
|
height: number;
|
||||||
|
}) => {
|
||||||
|
const { token } = useToken();
|
||||||
|
const { render } = useAppSpin();
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return render();
|
return render();
|
||||||
}
|
}
|
||||||
|
|
||||||
return !disablePageHeader && enablePageTabs ? (
|
return (
|
||||||
fieldSchema.mapProperties((schema) => {
|
<>
|
||||||
if (schema.name !== activeKey) return null;
|
{!disablePageHeader && enablePageTabs ? (
|
||||||
|
fieldSchema.mapProperties((schema) => {
|
||||||
|
if (schema.name !== activeKey) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FixedBlock key={schema.name} height={`calc(${height}px + 46px + ${token.paddingPageVertical}px * 2)`}>
|
<FixedBlock key={schema.name} height={`calc(${height}px + 46px + ${token.paddingPageVertical}px * 2)`}>
|
||||||
<SchemaComponent
|
<SchemaComponent
|
||||||
distributed
|
distributed
|
||||||
schema={
|
schema={
|
||||||
new Schema({
|
new Schema({
|
||||||
properties: {
|
properties: {
|
||||||
[schema.name]: schema,
|
[schema.name]: schema,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</FixedBlock>
|
</FixedBlock>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<FixedBlock height={`calc(${height}px + 46px + ${token.paddingPageVertical}px * 2)`}>
|
<FixedBlock height={`calc(${height}px + 46px + ${token.paddingPageVertical}px * 2)`}>
|
||||||
<div className={`pageWithFixedBlockCss nb-page-content`}>
|
<div className={`pageWithFixedBlockCss nb-page-content`}>
|
||||||
<SchemaComponent schema={fieldSchema} distributed />
|
<SchemaComponent schema={fieldSchema} distributed />
|
||||||
</div>
|
</div>
|
||||||
</FixedBlock>
|
</FixedBlock>
|
||||||
);
|
)}
|
||||||
}
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
PageContent.displayName = 'PageContent';
|
||||||
|
@ -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<PopupsVisibleProviderProps>(null);
|
||||||
|
export const PopupParamsProviderContext = React.createContext<PopupProps>(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<PopupsVisibleProviderProps> = ({ children, visible, setVisible }) => {
|
||||||
|
return (
|
||||||
|
<PopupVisibleProviderContext.Provider value={{ visible, setVisible }}>
|
||||||
|
{children}
|
||||||
|
</PopupVisibleProviderContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PopupParamsProvider: FC<PopupProps> = (props) => {
|
||||||
|
const value = useMemo(() => {
|
||||||
|
return { params: props.params, context: props.context };
|
||||||
|
}, [props.params, props.context]);
|
||||||
|
return <PopupParamsProviderContext.Provider value={value}>{props.children}</PopupParamsProviderContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<TabsContextProvider activeKey={params.tab} onTabClick={onTabClick}>
|
||||||
|
{children}
|
||||||
|
</TabsContextProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<PopupParamsProvider params={params} context={context}>
|
||||||
|
<PopupVisibleProvider visible={visible} setVisible={setVisible}>
|
||||||
|
<DataBlockProvider
|
||||||
|
dataSource={context.dataSource}
|
||||||
|
collection={context.collection}
|
||||||
|
association={context.association}
|
||||||
|
sourceId={context.sourceId}
|
||||||
|
filterByTk={params.filterbytk}
|
||||||
|
// @ts-ignore
|
||||||
|
record={storedContext.record}
|
||||||
|
parentRecord={storedContext.parentRecord}
|
||||||
|
action="get"
|
||||||
|
>
|
||||||
|
{/* Pass the service of the block where the button is located down, to refresh the block's data when the popup is closed */}
|
||||||
|
<BlockRequestContext.Provider value={storedContext.service}>
|
||||||
|
<PopupTabsPropsProvider params={params}>
|
||||||
|
<div style={{ display: 'none' }}>{children}</div>
|
||||||
|
</PopupTabsPropsProvider>
|
||||||
|
</BlockRequestContext.Provider>
|
||||||
|
</DataBlockProvider>
|
||||||
|
</PopupVisibleProvider>
|
||||||
|
</PopupParamsProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<ISchema>(null);
|
||||||
|
const popupPropsRef = useRef<PopupProps[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<PagePopupsItemProvider params={popupPropsRef.current[0].params} context={popupPropsRef.current[0].context}>
|
||||||
|
<SchemaComponent components={components} schema={rootSchema} onlyRenderProperties />;
|
||||||
|
</PagePopupsItemProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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/');
|
||||||
|
};
|
@ -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<PopupSettings>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider component for the popup settings.
|
||||||
|
* @param props - The popup settings.
|
||||||
|
*/
|
||||||
|
export const PopupSettingsProvider: FC<PopupSettings> = (props) => {
|
||||||
|
const { isPopupVisibleControlledByURL } = props;
|
||||||
|
|
||||||
|
const value = useMemo(() => {
|
||||||
|
return { isPopupVisibleControlledByURL };
|
||||||
|
}, [isPopupVisibleControlledByURL]);
|
||||||
|
|
||||||
|
return <PopupSettingsContext.Provider value={value}>{props.children}</PopupSettingsContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for accessing the popup settings.
|
||||||
|
* @returns The popup settings.
|
||||||
|
*/
|
||||||
|
export const usePopupSettings = () => {
|
||||||
|
return (
|
||||||
|
React.useContext(PopupSettingsContext) || {
|
||||||
|
isPopupVisibleControlledByURL: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
@ -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;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
});
|
279
packages/core/client/src/schema-component/antd/page/SubPages.tsx
Normal file
279
packages/core/client/src/schema-component/antd/page/SubPages.tsx
Normal file
@ -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<PopupParams, 'popupuid'> {
|
||||||
|
/** 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 (
|
||||||
|
<TabsContextProvider activeKey={props.params.tab} onTabClick={onTabClick}>
|
||||||
|
{props.children}
|
||||||
|
</TabsContextProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TreeRecordProviderInSubPage: FC = (props) => {
|
||||||
|
const recordData = useCollectionRecordData();
|
||||||
|
return <TreeRecordProvider parent={recordData}>{props.children}</TreeRecordProvider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SubPageProvider: FC<{ params: SubPageParams; context: SubPageContext | undefined; actionType: string }> = (
|
||||||
|
props,
|
||||||
|
) => {
|
||||||
|
const { params, context } = props;
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes = {
|
||||||
|
addChild: <TreeRecordProviderInSubPage>{props.children}</TreeRecordProviderInSubPage>,
|
||||||
|
'': <VariablePopupRecordProvider>{props.children}</VariablePopupRecordProvider>,
|
||||||
|
};
|
||||||
|
|
||||||
|
const commonElements = (
|
||||||
|
<DataBlockProvider
|
||||||
|
dataSource={context.dataSource}
|
||||||
|
collection={context.collection}
|
||||||
|
association={context.association}
|
||||||
|
sourceId={context.sourceId}
|
||||||
|
filterByTk={params.filterbytk}
|
||||||
|
action="get"
|
||||||
|
>
|
||||||
|
<SubPageTabsPropsProvider params={props.params}>{nodes[props.actionType]}</SubPageTabsPropsProvider>
|
||||||
|
</DataBlockProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.parentPopupRecord) {
|
||||||
|
return (
|
||||||
|
<DataBlockProvider
|
||||||
|
dataSource={context.dataSource}
|
||||||
|
collection={context.parentPopupRecord.collection}
|
||||||
|
filterByTk={context.parentPopupRecord.filterByTk}
|
||||||
|
action="get"
|
||||||
|
>
|
||||||
|
<VariablePopupRecordProvider>{commonElements}</VariablePopupRecordProvider>
|
||||||
|
</DataBlockProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<SubPageProvider params={subPageParams} context={context} actionType={addChild ? 'addChild' : ''}>
|
||||||
|
<RecursionField schema={subPageSchema} onlyRenderProperties />
|
||||||
|
{_.isEmpty(popupParams) ? null : <PagePopups paramsList={popupParams} />}
|
||||||
|
</SubPageProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 || '';
|
||||||
|
}
|
@ -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('');
|
||||||
|
});
|
||||||
|
});
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
@ -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('');
|
||||||
|
});
|
||||||
|
});
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
@ -7,8 +7,9 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './Page';
|
|
||||||
export * from './FixedBlock';
|
export * from './FixedBlock';
|
||||||
export * from './PageTab.Settings';
|
|
||||||
export * from './Page.Settings';
|
|
||||||
export * from './FixedBlockDesignerItem';
|
export * from './FixedBlockDesignerItem';
|
||||||
|
export * from './Page';
|
||||||
|
export * from './Page.Settings';
|
||||||
|
export * from './PageTab.Settings';
|
||||||
|
export { PopupSettingsProvider } from './PopupSettingsProvider';
|
||||||
|
@ -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<string, string> = {};
|
||||||
|
|
||||||
|
export const getRandomNestedSchemaKey = (popupUid: string) => {
|
||||||
|
return randomNestedSchemaKeyStorage[popupUid] || (randomNestedSchemaKeyStorage[popupUid] = uid());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteRandomNestedSchemaKey = (popupUid: string) => {
|
||||||
|
return delete randomNestedSchemaKeyStorage[popupUid];
|
||||||
|
};
|
@ -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<string, PopupContextStorage> = {};
|
||||||
|
|
||||||
|
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<string, any>) =>
|
||||||
|
(_parentRecordData || parentRecord?.data)?.[cm.getCollection(association?.split('.')[0])?.getPrimaryKey()],
|
||||||
|
[parentRecord, association],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getNewPathname = useCallback(
|
||||||
|
({ tabKey, popupUid, recordData }: { tabKey?: string; popupUid: string; recordData: Record<string, any> }) => {
|
||||||
|
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<string, any>;
|
||||||
|
parentRecordData?: Record<string, any>;
|
||||||
|
} = {}) => {
|
||||||
|
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);
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -10,11 +10,11 @@
|
|||||||
import { createForm } from '@formily/core';
|
import { createForm } from '@formily/core';
|
||||||
import { Schema } from '@formily/react';
|
import { Schema } from '@formily/react';
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import React, { useMemo } from 'react';
|
import React, { memo, useMemo } from 'react';
|
||||||
import { useRequest } from '../../api-client';
|
|
||||||
import { useSchemaComponentContext } from '../hooks';
|
import { useSchemaComponentContext } from '../hooks';
|
||||||
import { FormProvider } from './FormProvider';
|
import { FormProvider } from './FormProvider';
|
||||||
import { SchemaComponent } from './SchemaComponent';
|
import { SchemaComponent } from './SchemaComponent';
|
||||||
|
import { useRequestSchema } from './useRequestSchema';
|
||||||
|
|
||||||
export interface RemoteSchemaComponentProps {
|
export interface RemoteSchemaComponentProps {
|
||||||
scope?: any;
|
scope?: any;
|
||||||
@ -42,15 +42,15 @@ const RequestSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) =>
|
|||||||
schemaTransform = defaultTransform,
|
schemaTransform = defaultTransform,
|
||||||
} = props;
|
} = props;
|
||||||
const { reset } = useSchemaComponentContext();
|
const { reset } = useSchemaComponentContext();
|
||||||
|
const type = onlyRenderProperties ? 'getProperties' : 'getJsonSchema';
|
||||||
const conf = {
|
const conf = {
|
||||||
url: `/uiSchemas:${onlyRenderProperties ? 'getProperties' : 'getJsonSchema'}/${uid}`,
|
url: `/uiSchemas:${type}/${uid}`,
|
||||||
};
|
};
|
||||||
const form = useMemo(() => createForm(), [uid]);
|
const form = useMemo(() => createForm(), [uid]);
|
||||||
const { data, loading } = useRequest<{
|
const { schema, loading } = useRequestSchema({
|
||||||
data: any;
|
uid,
|
||||||
}>(conf, {
|
type,
|
||||||
refreshDeps: [uid],
|
onSuccess: (data) => {
|
||||||
onSuccess(data) {
|
|
||||||
onSuccess && onSuccess(data);
|
onSuccess && onSuccess(data);
|
||||||
reset && reset();
|
reset && reset();
|
||||||
},
|
},
|
||||||
@ -62,14 +62,15 @@ const RequestSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) =>
|
|||||||
return <Spin />;
|
return <Spin />;
|
||||||
}
|
}
|
||||||
return noForm ? (
|
return noForm ? (
|
||||||
<SchemaComponent memoized components={components} scope={scope} schema={schemaTransform(data?.data || {})} />
|
<SchemaComponent memoized components={components} scope={scope} schema={schemaTransform(schema || {})} />
|
||||||
) : (
|
) : (
|
||||||
<FormProvider form={form}>
|
<FormProvider form={form}>
|
||||||
<SchemaComponent memoized components={components} scope={scope} schema={schemaTransform(data?.data || {})} />
|
<SchemaComponent memoized components={components} scope={scope} schema={schemaTransform(schema || {})} />
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RemoteSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) => {
|
export const RemoteSchemaComponent: React.FC<RemoteSchemaComponentProps> = memo((props) => {
|
||||||
return props.uid ? <RequestSchemaComponent {...props} /> : null;
|
return props.uid ? <RequestSchemaComponent {...props} /> : null;
|
||||||
};
|
});
|
||||||
|
RemoteSchemaComponent.displayName = 'RemoteSchemaComponent';
|
||||||
|
@ -8,10 +8,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { IRecursionFieldProps, ISchemaFieldProps, RecursionField, Schema } from '@formily/react';
|
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 { SchemaComponentContext } from '../context';
|
||||||
import { SchemaComponentOptions } from './SchemaComponentOptions';
|
import { SchemaComponentOptions } from './SchemaComponentOptions';
|
||||||
import { useUpdate } from 'ahooks';
|
|
||||||
|
|
||||||
type SchemaComponentOnChange = {
|
type SchemaComponentOnChange = {
|
||||||
onChange?: (s: Schema) => void;
|
onChange?: (s: Schema) => void;
|
||||||
@ -44,10 +44,10 @@ interface DistributedProps {
|
|||||||
distributed?: boolean;
|
distributed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RecursionSchemaComponent = (props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => {
|
const RecursionSchemaComponent = memo((props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => {
|
||||||
const { components, scope, schema, distributed, ...others } = props;
|
const { components, scope, schema: _schema, distributed, ...others } = props;
|
||||||
const ctx = useContext(SchemaComponentContext);
|
const ctx = useContext(SchemaComponentContext);
|
||||||
const s = useMemo(() => toSchema(schema), [schema]);
|
const schema = useMemo(() => toSchema(_schema), [_schema]);
|
||||||
const refresh = useUpdate();
|
const refresh = useUpdate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -60,30 +60,32 @@ const RecursionSchemaComponent = (props: ISchemaFieldProps & SchemaComponentOnCh
|
|||||||
if (ctx.distributed === false || distributed === false) {
|
if (ctx.distributed === false || distributed === false) {
|
||||||
ctx.refresh?.();
|
ctx.refresh?.();
|
||||||
}
|
}
|
||||||
props.onChange?.(s);
|
props.onChange?.(schema);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SchemaComponentOptions inherit components={components} scope={scope}>
|
<SchemaComponentOptions inherit components={components} scope={scope}>
|
||||||
<RecursionField {...others} schema={s} />
|
<RecursionField {...others} schema={schema} />
|
||||||
</SchemaComponentOptions>
|
</SchemaComponentOptions>
|
||||||
</SchemaComponentContext.Provider>
|
</SchemaComponentContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
const MemoizedSchemaComponent = (props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => {
|
const MemoizedSchemaComponent = memo((props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => {
|
||||||
const { schema, ...others } = props;
|
const { schema, ...others } = props;
|
||||||
const s = useMemoizedSchema(schema);
|
const s = useMemoizedSchema(schema);
|
||||||
return <RecursionSchemaComponent {...others} schema={s} />;
|
return <RecursionSchemaComponent {...others} schema={s} />;
|
||||||
};
|
});
|
||||||
|
|
||||||
export const SchemaComponent = (
|
export const SchemaComponent = memo(
|
||||||
props: (ISchemaFieldProps | IRecursionFieldProps) & { memoized?: boolean } & SchemaComponentOnChange &
|
(
|
||||||
DistributedProps,
|
props: (ISchemaFieldProps | IRecursionFieldProps) & { memoized?: boolean } & SchemaComponentOnChange &
|
||||||
) => {
|
DistributedProps,
|
||||||
const { memoized, ...others } = props;
|
) => {
|
||||||
if (memoized) {
|
const { memoized, ...others } = props;
|
||||||
return <MemoizedSchemaComponent {...others} />;
|
if (memoized) {
|
||||||
}
|
return <MemoizedSchemaComponent {...others} />;
|
||||||
return <RecursionSchemaComponent {...others} />;
|
}
|
||||||
};
|
return <RecursionSchemaComponent {...others} />;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
@ -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 };
|
||||||
|
};
|
@ -8,20 +8,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { DownOutlined } from '@ant-design/icons';
|
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 { Button, Dropdown, MenuProps } from 'antd';
|
||||||
import React, { useEffect, useMemo, useState, forwardRef, createRef } from 'react';
|
|
||||||
import { composeRef } from 'rc-util/lib/ref';
|
import { composeRef } from 'rc-util/lib/ref';
|
||||||
import { useDesignable } from '../../';
|
import React, { createRef, forwardRef, useEffect, useMemo } from 'react';
|
||||||
import { useACLRolesCheck, useRecordPkValue, useACLActionParamsContext } from '../../acl/ACLProvider';
|
import { Collection, useDesignable } from '../../';
|
||||||
import {
|
import { useACLActionParamsContext, useACLRolesCheck, useRecordPkValue } from '../../acl/ACLProvider';
|
||||||
CollectionProvider_deprecated,
|
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../collection-manager';
|
||||||
useCollection_deprecated,
|
import { useTreeParentRecord } from '../../modules/blocks/data-blocks/table/TreeRecordProvider';
|
||||||
useCollectionManager_deprecated,
|
|
||||||
} from '../../collection-manager';
|
|
||||||
import { useRecord } from '../../record-provider';
|
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 { 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 { parseVariables } from '../../schema-component/common/utils/uitls';
|
||||||
import { useLocalVariables, useVariables } from '../../variables';
|
import { useLocalVariables, useVariables } from '../../variables';
|
||||||
|
|
||||||
@ -66,17 +65,17 @@ function useAclCheckFn() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const InternalCreateRecordAction = (props: any, ref) => {
|
const InternalCreateRecordAction = (props: any, ref) => {
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
const collection = useCollection_deprecated();
|
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
|
const openMode = fieldSchema?.['x-component-props']?.['openMode'];
|
||||||
const field: any = useField();
|
const field: any = useField();
|
||||||
const [currentCollection, setCurrentCollection] = useState(collection.name);
|
|
||||||
const [currentCollectionDataSource, setCurrentCollectionDataSource] = useState(collection.dataSource);
|
|
||||||
const linkageRules: any[] = fieldSchema?.['x-linkage-rules'] || [];
|
const linkageRules: any[] = fieldSchema?.['x-linkage-rules'] || [];
|
||||||
const values = useRecord();
|
const values = useRecord();
|
||||||
const ctx = useActionContext();
|
|
||||||
const variables = useVariables();
|
const variables = useVariables();
|
||||||
const localVariables = useLocalVariables({ currentForm: { values } as any });
|
const localVariables = useLocalVariables({ currentForm: { values } as any });
|
||||||
|
const { openPopup } = usePagePopup();
|
||||||
|
const { navigateToSubPage } = useNavigateTOSubPage();
|
||||||
|
const treeRecordData = useTreeParentRecord();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
field.stateOfLinkageRules = {};
|
field.stateOfLinkageRules = {};
|
||||||
linkageRules
|
linkageRules
|
||||||
@ -95,26 +94,26 @@ const InternalCreateRecordAction = (props: any, ref) => {
|
|||||||
}, [field, linkageRules, localVariables, variables]);
|
}, [field, linkageRules, localVariables, variables]);
|
||||||
const internalRef = createRef<HTMLButtonElement | HTMLAnchorElement>();
|
const internalRef = createRef<HTMLButtonElement | HTMLAnchorElement>();
|
||||||
const buttonRef = composeRef(ref, internalRef);
|
const buttonRef = composeRef(ref, internalRef);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
<div ref={buttonRef as React.Ref<HTMLButtonElement>}>
|
<div ref={buttonRef as React.Ref<HTMLButtonElement>}>
|
||||||
<CreateAction
|
<CreateAction
|
||||||
{...props}
|
{...props}
|
||||||
onClick={(collectionData) => {
|
onClick={(collection: Collection) => {
|
||||||
if (collectionData.name === collection.name) {
|
if (openMode === 'page') {
|
||||||
ctx?.setVisible(true);
|
return navigateToSubPage();
|
||||||
} else {
|
}
|
||||||
setVisible(true);
|
|
||||||
|
if (treeRecordData) {
|
||||||
|
openPopup({
|
||||||
|
recordData: treeRecordData,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
openPopup();
|
||||||
}
|
}
|
||||||
setCurrentCollection(collectionData.name);
|
|
||||||
setCurrentCollectionDataSource(collectionData.dataSource);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ActionContextProvider value={{ ...ctx, fieldSchema, visible, setVisible }}>
|
|
||||||
<CollectionProvider_deprecated name={currentCollection} dataSource={currentCollectionDataSource}>
|
|
||||||
<RecursionField schema={fieldSchema} basePath={field.address} onlyRenderProperties />
|
|
||||||
</CollectionProvider_deprecated>
|
|
||||||
</ActionContextProvider>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -9,15 +9,17 @@
|
|||||||
|
|
||||||
import { useField, useFieldSchema } from '@formily/react';
|
import { useField, useFieldSchema } from '@formily/react';
|
||||||
import { Select } from 'antd';
|
import { Select } from 'antd';
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SchemaInitializerItem, SchemaInitializerSelect } from '../application';
|
import { SchemaInitializerItem, SchemaInitializerSelect } from '../application';
|
||||||
import { useDesignable } from '../schema-component';
|
import { useDesignable } from '../schema-component';
|
||||||
|
import { usePopupSettings } from '../schema-component/antd/page/PopupSettingsProvider';
|
||||||
import { SchemaSettingsSelectItem } from '../schema-settings';
|
import { SchemaSettingsSelectItem } from '../schema-settings';
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
openMode?: boolean;
|
openMode?: boolean;
|
||||||
openSize?: boolean;
|
openSize?: boolean;
|
||||||
|
modeOptions?: { label: string; value: string }[];
|
||||||
}
|
}
|
||||||
export const SchemaInitializerOpenModeSchemaItems: React.FC<Options> = (options) => {
|
export const SchemaInitializerOpenModeSchemaItems: React.FC<Options> = (options) => {
|
||||||
const { openMode = true, openSize = true } = options;
|
const { openMode = true, openSize = true } = options;
|
||||||
@ -25,17 +27,29 @@ export const SchemaInitializerOpenModeSchemaItems: React.FC<Options> = (options)
|
|||||||
const field = useField();
|
const field = useField();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
|
const { isPopupVisibleControlledByURL } = usePopupSettings();
|
||||||
const openModeValue = fieldSchema?.['x-component-props']?.['openMode'] || 'drawer';
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{openMode ? (
|
{openMode ? (
|
||||||
<SchemaInitializerSelect
|
<SchemaInitializerSelect
|
||||||
title={t('Open mode')}
|
title={t('Open mode')}
|
||||||
options={[
|
options={modeOptions}
|
||||||
{ label: t('Drawer'), value: 'drawer' },
|
|
||||||
{ label: t('Dialog'), value: 'modal' },
|
|
||||||
]}
|
|
||||||
value={openModeValue}
|
value={openModeValue}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
field.componentProps.openMode = value;
|
field.componentProps.openMode = value;
|
||||||
@ -92,23 +106,40 @@ export const SchemaInitializerOpenModeSchemaItems: React.FC<Options> = (options)
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SchemaSettingOpenModeSchemaItems: React.FC<Options> = (options) => {
|
export const SchemaSettingOpenModeSchemaItems: React.FC<Options> = (props) => {
|
||||||
const { openMode = true, openSize = true } = options;
|
const { openMode = true, openSize = true, modeOptions } = props;
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
|
const { isPopupVisibleControlledByURL } = usePopupSettings();
|
||||||
const openModeValue = fieldSchema?.['x-component-props']?.['openMode'] || 'drawer';
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{openMode ? (
|
{openMode ? (
|
||||||
<SchemaSettingsSelectItem
|
<SchemaSettingsSelectItem
|
||||||
title={t('Open mode')}
|
title={t('Open mode')}
|
||||||
options={[
|
options={_modeOptions}
|
||||||
{ label: t('Drawer'), value: 'drawer' },
|
|
||||||
{ label: t('Dialog'), value: 'modal' },
|
|
||||||
]}
|
|
||||||
value={openModeValue}
|
value={openModeValue}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
field.componentProps.openMode = value;
|
field.componentProps.openMode = value;
|
||||||
|
@ -43,10 +43,9 @@ import React, {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Router } from 'react-router-dom';
|
|
||||||
import { APIClientProvider } from '../api-client/APIClientProvider';
|
import { APIClientProvider } from '../api-client/APIClientProvider';
|
||||||
import { useAPIClient } from '../api-client/hooks/useAPIClient';
|
import { useAPIClient } from '../api-client/hooks/useAPIClient';
|
||||||
import { ApplicationContext, useApp } from '../application';
|
import { ApplicationContext, LocationSearchContext, useApp, useLocationSearch } from '../application';
|
||||||
import {
|
import {
|
||||||
BlockContext,
|
BlockContext,
|
||||||
BlockRequestContext_deprecated,
|
BlockRequestContext_deprecated,
|
||||||
@ -746,6 +745,7 @@ export const SchemaSettingsModalItem: FC<SchemaSettingsModalItemProps> = (props)
|
|||||||
const { association } = useDataBlockProps() || {};
|
const { association } = useDataBlockProps() || {};
|
||||||
const formCtx = useFormBlockContext();
|
const formCtx = useFormBlockContext();
|
||||||
const blockOptions = useBlockContext();
|
const blockOptions = useBlockContext();
|
||||||
|
const locationSearch = useLocationSearch();
|
||||||
|
|
||||||
// 解决变量`当前对象`值在弹窗中丢失的问题
|
// 解决变量`当前对象`值在弹窗中丢失的问题
|
||||||
const { formValue: subFormValue, collection: subFormCollection } = useSubFormValue();
|
const { formValue: subFormValue, collection: subFormCollection } = useSubFormValue();
|
||||||
@ -784,7 +784,7 @@ export const SchemaSettingsModalItem: FC<SchemaSettingsModalItemProps> = (props)
|
|||||||
name="form"
|
name="form"
|
||||||
getActiveFieldsName={upLevelActiveFields?.getActiveFieldsName}
|
getActiveFieldsName={upLevelActiveFields?.getActiveFieldsName}
|
||||||
>
|
>
|
||||||
<Router location={location} navigator={null}>
|
<LocationSearchContext.Provider value={locationSearch}>
|
||||||
<BlockRequestContext_deprecated.Provider value={ctx}>
|
<BlockRequestContext_deprecated.Provider value={ctx}>
|
||||||
<DataSourceApplicationProvider dataSourceManager={dm} dataSource={dataSourceKey}>
|
<DataSourceApplicationProvider dataSourceManager={dm} dataSource={dataSourceKey}>
|
||||||
<AssociationOrCollectionProvider
|
<AssociationOrCollectionProvider
|
||||||
@ -819,7 +819,7 @@ export const SchemaSettingsModalItem: FC<SchemaSettingsModalItemProps> = (props)
|
|||||||
</AssociationOrCollectionProvider>
|
</AssociationOrCollectionProvider>
|
||||||
</DataSourceApplicationProvider>
|
</DataSourceApplicationProvider>
|
||||||
</BlockRequestContext_deprecated.Provider>
|
</BlockRequestContext_deprecated.Provider>
|
||||||
</Router>
|
</LocationSearchContext.Provider>
|
||||||
</FormActiveFieldsProvider>
|
</FormActiveFieldsProvider>
|
||||||
</SubFormProvider>
|
</SubFormProvider>
|
||||||
</FormBlockContext.Provider>
|
</FormBlockContext.Provider>
|
||||||
|
@ -40,5 +40,6 @@ export const useParentPopupVariable = (props: any = {}) => {
|
|||||||
/** 当前记录对应的 collection name */
|
/** 当前记录对应的 collection name */
|
||||||
collectionName: collection?.name,
|
collectionName: collection?.name,
|
||||||
dataSource: collection?.dataSource,
|
dataSource: collection?.dataSource,
|
||||||
|
defaultValue: undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -40,5 +40,6 @@ export const usePopupVariable = (props: any = {}) => {
|
|||||||
/** 当前记录对应的 collection name */
|
/** 当前记录对应的 collection name */
|
||||||
collectionName: collection?.name,
|
collectionName: collection?.name,
|
||||||
dataSource: collection?.dataSource,
|
dataSource: collection?.dataSource,
|
||||||
|
defaultValue: undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -12,7 +12,7 @@ import _ from 'lodash';
|
|||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocationSearch } from '../../../application/CustomRouterContextProvider';
|
||||||
import { useFlag } from '../../../flag-provider/hooks/useFlag';
|
import { useFlag } from '../../../flag-provider/hooks/useFlag';
|
||||||
import { Option } from '../type';
|
import { Option } from '../type';
|
||||||
import { getLabelWithTooltip } from './useBaseVariable';
|
import { getLabelWithTooltip } from './useBaseVariable';
|
||||||
@ -64,9 +64,9 @@ export const useURLSearchParamsCtx = (search: string) => {
|
|||||||
export const useURLSearchParamsVariable = (props: any = {}) => {
|
export const useURLSearchParamsVariable = (props: any = {}) => {
|
||||||
const variableName = '$nURLSearchParams';
|
const variableName = '$nURLSearchParams';
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const searchString = useLocationSearch();
|
||||||
const { isVariableParsedInOtherContext } = useFlag();
|
const { isVariableParsedInOtherContext } = useFlag();
|
||||||
const urlSearchParamsCtx = useURLSearchParamsCtx(location.search);
|
const urlSearchParamsCtx = useURLSearchParamsCtx(searchString);
|
||||||
const disabled = useMemo(() => _.isEmpty(urlSearchParamsCtx), [urlSearchParamsCtx]);
|
const disabled = useMemo(() => _.isEmpty(urlSearchParamsCtx), [urlSearchParamsCtx]);
|
||||||
const urlSearchParamsSettings: Option = useMemo(() => {
|
const urlSearchParamsSettings: Option = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
@ -100,8 +100,7 @@ export const useURLSearchParamsVariable = (props: any = {}) => {
|
|||||||
/** 变量值 */
|
/** 变量值 */
|
||||||
urlSearchParamsCtx,
|
urlSearchParamsCtx,
|
||||||
/**
|
/**
|
||||||
* 这里是用于当通过该变量解析出来的值是一个 undefined 时,最终应该返回的值。
|
* 这里的默认值是 null,这样会导致数据范围中的 filter 条件不会被清除掉,而 URL search params 变量的值为空时,应该清除掉 filter 条件,
|
||||||
* 默认返回的是 null,这样会导致数据范围中的 filter 条件不会被清除掉,而 URL search params 变量的值为空时,应该清除掉 filter 条件,
|
|
||||||
* 所以这里把 defaultValue 设置为 undefined,这样在解析出来的值是 undefined 时,会返回 undefined,从而清除掉 filter 条件。
|
* 所以这里把 defaultValue 设置为 undefined,这样在解析出来的值是 undefined 时,会返回 undefined,从而清除掉 filter 条件。
|
||||||
*/
|
*/
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
|
@ -10,8 +10,9 @@
|
|||||||
import { PageHeader as AntdPageHeader } from '@ant-design/pro-layout';
|
import { PageHeader as AntdPageHeader } from '@ant-design/pro-layout';
|
||||||
import { Input, Spin } from 'antd';
|
import { Input, Spin } from 'antd';
|
||||||
import React, { useContext, useState } from 'react';
|
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 { useAPIClient, useRequest, useSchemaTemplateManager } from '..';
|
||||||
|
import { useNavigateNoUpdate } from '../application/CustomRouterContextProvider';
|
||||||
import { RemoteSchemaComponent, SchemaComponentContext } from '../schema-component';
|
import { RemoteSchemaComponent, SchemaComponentContext } from '../schema-component';
|
||||||
|
|
||||||
const EditableTitle = (props) => {
|
const EditableTitle = (props) => {
|
||||||
@ -68,7 +69,7 @@ const EditableTitle = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const BlockTemplateDetails = () => {
|
export const BlockTemplateDetails = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigateNoUpdate();
|
||||||
const params = useParams<any>();
|
const params = useParams<any>();
|
||||||
const key = params?.key;
|
const key = params?.key;
|
||||||
const value = useContext(SchemaComponentContext);
|
const value = useContext(SchemaComponentContext);
|
||||||
|
@ -13,8 +13,8 @@ import { error } from '@nocobase/utils/client';
|
|||||||
import { App, Dropdown, Menu, MenuProps } from 'antd';
|
import { App, Dropdown, Menu, MenuProps } from 'antd';
|
||||||
import React, { createContext, useCallback, useMemo as useEffect, useState } from 'react';
|
import React, { createContext, useCallback, useMemo as useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useACLRoleContext, useAPIClient, useCurrentUserContext, useToken } from '..';
|
import { useACLRoleContext, useAPIClient, useCurrentUserContext, useToken } from '..';
|
||||||
|
import { useNavigateNoUpdate } from '../application/CustomRouterContextProvider';
|
||||||
import { useChangePassword } from './ChangePassword';
|
import { useChangePassword } from './ChangePassword';
|
||||||
import { useCurrentUserSettingsMenu } from './CurrentUserSettingsMenuProvider';
|
import { useCurrentUserSettingsMenu } from './CurrentUserSettingsMenuProvider';
|
||||||
import { useEditProfile } from './EditProfile';
|
import { useEditProfile } from './EditProfile';
|
||||||
@ -48,7 +48,7 @@ export const SettingsMenu: React.FC<{
|
|||||||
const { redirectUrl = '' } = props;
|
const { redirectUrl = '' } = props;
|
||||||
const { allowAll, snippets } = useACLRoleContext();
|
const { allowAll, snippets } = useACLRoleContext();
|
||||||
const appAllowed = allowAll || snippets?.includes('app');
|
const appAllowed = allowAll || snippets?.includes('app');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigateNoUpdate();
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const silenceApi = useAPIClient();
|
const silenceApi = useAPIClient();
|
||||||
|
@ -8,9 +8,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createContext, useContext, useMemo } from 'react';
|
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 { useACLRoleContext } from '../acl';
|
||||||
import { ReturnTypeOfUseRequest, useRequest } from '../api-client';
|
import { ReturnTypeOfUseRequest, useRequest } from '../api-client';
|
||||||
|
import { useLocationNoUpdate } from '../application';
|
||||||
import { useAppSpin } from '../application/hooks/useAppSpin';
|
import { useAppSpin } from '../application/hooks/useAppSpin';
|
||||||
import { useCompile } from '../schema-component';
|
import { useCompile } from '../schema-component';
|
||||||
|
|
||||||
@ -53,7 +54,7 @@ export const CurrentUserProvider = (props) => {
|
|||||||
|
|
||||||
export const NavigateIfNotSignIn = ({ children }) => {
|
export const NavigateIfNotSignIn = ({ children }) => {
|
||||||
const result = useCurrentUserContext();
|
const result = useCurrentUserContext();
|
||||||
const { pathname, search } = useLocation();
|
const { pathname, search } = useLocationNoUpdate();
|
||||||
const redirect = `?redirect=${pathname}${search}`;
|
const redirect = `?redirect=${pathname}${search}`;
|
||||||
if (!result?.data?.data?.id) {
|
if (!result?.data?.data?.id) {
|
||||||
return <Navigate replace to={`/signin${redirect}`} />;
|
return <Navigate replace to={`/signin${redirect}`} />;
|
||||||
|
@ -37,11 +37,13 @@ const useLocalVariables = (props?: Props) => {
|
|||||||
popupRecordCtx,
|
popupRecordCtx,
|
||||||
collectionName: collectionNameOfPopupRecord,
|
collectionName: collectionNameOfPopupRecord,
|
||||||
dataSource: popupDataSource,
|
dataSource: popupDataSource,
|
||||||
|
defaultValue: defaultValueOfPopupRecord,
|
||||||
} = usePopupVariable();
|
} = usePopupVariable();
|
||||||
const {
|
const {
|
||||||
parentPopupRecordCtx,
|
parentPopupRecordCtx,
|
||||||
collectionName: collectionNameOfParentPopupRecord,
|
collectionName: collectionNameOfParentPopupRecord,
|
||||||
dataSource: parentPopupDataSource,
|
dataSource: parentPopupDataSource,
|
||||||
|
defaultValue: defaultValueOfParentPopupRecord,
|
||||||
} = useParentPopupVariable();
|
} = useParentPopupVariable();
|
||||||
const { datetimeCtx } = useDatetimeVariable();
|
const { datetimeCtx } = useDatetimeVariable();
|
||||||
const { currentFormCtx } = useCurrentFormVariable({ form: props?.currentForm });
|
const { currentFormCtx } = useCurrentFormVariable({ form: props?.currentForm });
|
||||||
@ -98,12 +100,14 @@ const useLocalVariables = (props?: Props) => {
|
|||||||
ctx: popupRecordCtx,
|
ctx: popupRecordCtx,
|
||||||
collectionName: collectionNameOfPopupRecord,
|
collectionName: collectionNameOfPopupRecord,
|
||||||
dataSource: popupDataSource,
|
dataSource: popupDataSource,
|
||||||
|
defaultValue: defaultValueOfPopupRecord,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '$nParentPopupRecord',
|
name: '$nParentPopupRecord',
|
||||||
ctx: parentPopupRecordCtx,
|
ctx: parentPopupRecordCtx,
|
||||||
collectionName: collectionNameOfParentPopupRecord,
|
collectionName: collectionNameOfParentPopupRecord,
|
||||||
dataSource: parentPopupDataSource,
|
dataSource: parentPopupDataSource,
|
||||||
|
defaultValue: defaultValueOfParentPopupRecord,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '$nForm',
|
name: '$nForm',
|
||||||
@ -138,12 +142,15 @@ const useLocalVariables = (props?: Props) => {
|
|||||||
collectionNameOfParentRecord,
|
collectionNameOfParentRecord,
|
||||||
currentParentRecordDataSource,
|
currentParentRecordDataSource,
|
||||||
popupRecordCtx,
|
popupRecordCtx,
|
||||||
|
parentPopupRecordCtx,
|
||||||
collectionNameOfPopupRecord,
|
collectionNameOfPopupRecord,
|
||||||
popupDataSource,
|
popupDataSource,
|
||||||
datetimeCtx,
|
datetimeCtx,
|
||||||
shouldDisplayCurrentObject,
|
shouldDisplayCurrentObject,
|
||||||
currentObjectCtx,
|
currentObjectCtx,
|
||||||
currentCollectionName,
|
currentCollectionName,
|
||||||
|
defaultValueOfPopupRecord,
|
||||||
|
defaultValueOfParentPopupRecord,
|
||||||
]); // 尽量保持返回的值不变,这样可以减少接口的请求次数,因为关系字段会缓存到变量的 ctx 中
|
]); // 尽量保持返回的值不变,这样可以减少接口的请求次数,因为关系字段会缓存到变量的 ctx 中
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -347,10 +347,14 @@ export class APIClient {
|
|||||||
return this.axios.request<T, R, D>(config);
|
return this.axios.request<T, R, D>(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
resource(name: string, of?: any, headers?: AxiosRequestHeaders): IResource {
|
resource(name: string, of?: any, headers?: AxiosRequestHeaders, cancel?: boolean): IResource {
|
||||||
const target = {};
|
const target = {};
|
||||||
const handler = {
|
const handler = {
|
||||||
get: (_: any, actionName: string) => {
|
get: (_: any, actionName: string) => {
|
||||||
|
if (cancel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let url = name.split('.').join(`/${encodeURIComponent(of) || '_'}/`);
|
let url = name.split('.').join(`/${encodeURIComponent(of) || '_'}/`);
|
||||||
url += `:${actionName}`;
|
url += `:${actionName}`;
|
||||||
const config: AxiosRequestConfig = { url };
|
const config: AxiosRequestConfig = { url };
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
useCollectionManager_deprecated,
|
useCollectionManager_deprecated,
|
||||||
useCollection_deprecated,
|
useCollection_deprecated,
|
||||||
useCompile,
|
useCompile,
|
||||||
|
useNavigateNoUpdate,
|
||||||
useRemoveGridFormItem,
|
useRemoveGridFormItem,
|
||||||
useTableBlockContext,
|
useTableBlockContext,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
@ -23,7 +24,6 @@ import { isURL } from '@nocobase/utils/client';
|
|||||||
import { App, message } from 'antd';
|
import { App, message } from 'antd';
|
||||||
import { useContext, useMemo } from 'react';
|
import { useContext, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
export const useCustomBulkEditFormItemInitializerFields = (options?: any) => {
|
export const useCustomBulkEditFormItemInitializerFields = (options?: any) => {
|
||||||
const { name, fields } = useCollection_deprecated();
|
const { name, fields } = useCollection_deprecated();
|
||||||
@ -83,7 +83,7 @@ export const useCustomizeBulkEditActionProps = () => {
|
|||||||
const { field, resource, __parent } = useBlockRequestContext();
|
const { field, resource, __parent } = useBlockRequestContext();
|
||||||
const expressionScope = useContext(SchemaExpressionScopeContext);
|
const expressionScope = useContext(SchemaExpressionScopeContext);
|
||||||
const actionContext = useActionContext();
|
const actionContext = useActionContext();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigateNoUpdate();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const actionField = useField();
|
const actionField = useField();
|
||||||
const tableBlockContext = useTableBlockContext();
|
const tableBlockContext = useTableBlockContext();
|
||||||
|
@ -16,13 +16,13 @@ import {
|
|||||||
useCollection_deprecated,
|
useCollection_deprecated,
|
||||||
useCompile,
|
useCompile,
|
||||||
useLocalVariables,
|
useLocalVariables,
|
||||||
|
useNavigateNoUpdate,
|
||||||
useTableBlockContext,
|
useTableBlockContext,
|
||||||
useVariables,
|
useVariables,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { isURL } from '@nocobase/utils/client';
|
import { isURL } from '@nocobase/utils/client';
|
||||||
import { App, message } from 'antd';
|
import { App, message } from 'antd';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useBulkUpdateTranslation } from './locale';
|
import { useBulkUpdateTranslation } from './locale';
|
||||||
|
|
||||||
export const useCustomizeBulkUpdateActionProps = () => {
|
export const useCustomizeBulkUpdateActionProps = () => {
|
||||||
@ -32,7 +32,7 @@ export const useCustomizeBulkUpdateActionProps = () => {
|
|||||||
const tableBlockContext = useTableBlockContext();
|
const tableBlockContext = useTableBlockContext();
|
||||||
const { rowKey } = tableBlockContext;
|
const { rowKey } = tableBlockContext;
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigateNoUpdate();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { t } = useBulkUpdateTranslation();
|
const { t } = useBulkUpdateTranslation();
|
||||||
const actionField: any = useField();
|
const actionField: any = useField();
|
||||||
|
@ -8,15 +8,21 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useField, useFieldSchema, useForm } from '@formily/react';
|
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 { isURL } from '@nocobase/utils/client';
|
||||||
import { App } from 'antd';
|
import { App } from 'antd';
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
export const useCustomizeRequestActionProps = () => {
|
export const useCustomizeRequestActionProps = () => {
|
||||||
const apiClient = useAPIClient();
|
const apiClient = useAPIClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigateNoUpdate();
|
||||||
const actionSchema = useFieldSchema();
|
const actionSchema = useFieldSchema();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
|
@ -12,16 +12,16 @@ import { ISchema, connect, mapProps, useField, useFieldSchema, useForm } from '@
|
|||||||
import {
|
import {
|
||||||
ActionDesigner,
|
ActionDesigner,
|
||||||
SchemaSettingOpenModeSchemaItems,
|
SchemaSettingOpenModeSchemaItems,
|
||||||
useCollection_deprecated,
|
SchemaSettings,
|
||||||
useRecord,
|
|
||||||
SchemaSettingsModalItem,
|
|
||||||
SchemaSettingsItemType,
|
SchemaSettingsItemType,
|
||||||
SchemaSettingsLinkageRules,
|
SchemaSettingsLinkageRules,
|
||||||
|
SchemaSettingsModalItem,
|
||||||
useCollectionState,
|
useCollectionState,
|
||||||
|
useCollection_deprecated,
|
||||||
useDesignable,
|
useDesignable,
|
||||||
|
useRecord,
|
||||||
useSchemaToolbar,
|
useSchemaToolbar,
|
||||||
useSyncFromForm,
|
useSyncFromForm,
|
||||||
SchemaSettings,
|
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { Tree as AntdTree } from 'antd';
|
import { Tree as AntdTree } from 'antd';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
@ -357,19 +357,19 @@ const schemaSettingsItems: SchemaSettingsItemType[] = [
|
|||||||
name: 'openMode',
|
name: 'openMode',
|
||||||
Component: SchemaSettingOpenModeSchemaItems,
|
Component: SchemaSettingOpenModeSchemaItems,
|
||||||
useComponentProps() {
|
useComponentProps() {
|
||||||
const fieldSchema = useFieldSchema();
|
const { t } = useTranslation();
|
||||||
const isPopupAction = [
|
|
||||||
'create',
|
const modeOptions = useMemo(() => {
|
||||||
'update',
|
return [
|
||||||
'view',
|
{ label: t('Drawer'), value: 'drawer' },
|
||||||
'customize:popup',
|
{ label: t('Dialog'), value: 'modal' },
|
||||||
'duplicate',
|
];
|
||||||
'customize:create',
|
}, [t]);
|
||||||
].includes(fieldSchema['x-action'] || '');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
openMode: isPopupAction,
|
openMode: true,
|
||||||
openSize: isPopupAction,
|
openSize: true,
|
||||||
|
modeOptions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -7,16 +7,15 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* 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 React, { useEffect } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
|
|
||||||
export const AuthProvider: React.FC = (props) => {
|
export const AuthProvider: React.FC = (props) => {
|
||||||
const location = useLocation();
|
const searchString = useLocationSearch();
|
||||||
const app = useApp();
|
const app = useApp();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(searchString);
|
||||||
const authenticator = params.get('authenticator');
|
const authenticator = params.get('authenticator');
|
||||||
const token = params.get('token');
|
const token = params.get('token');
|
||||||
if (token) {
|
if (token) {
|
||||||
|
@ -6,10 +6,10 @@
|
|||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
import { LeftOutlined, FileImageOutlined } from '@ant-design/icons';
|
import { FileImageOutlined, LeftOutlined } from '@ant-design/icons';
|
||||||
import { Html5Qrcode } from 'html5-qrcode';
|
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
|
||||||
import { useActionContext } from '@nocobase/client';
|
import { useActionContext } from '@nocobase/client';
|
||||||
|
import { Html5Qrcode } from 'html5-qrcode';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ScanBox } from './ScanBox';
|
import { ScanBox } from './ScanBox';
|
||||||
import { useScanner } from './useScanner';
|
import { useScanner } from './useScanner';
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user